Refactor chart
This commit is contained in:
105
app/src/components/Chart/Chart.tsx
Normal file
105
app/src/components/Chart/Chart.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import DateFormatter from '../helper/DateFormatter'
|
||||
import NoData from './NoData'
|
||||
import NumberFormatter from '../helper/NumberFormatter'
|
||||
import React, { memo, useCallback } from 'react'
|
||||
import TooltipComponent from './TooltipComponent'
|
||||
import { default as ReactResizeDetector } from 'react-resize-detector'
|
||||
import { emphasize } from '@material-ui/core/styles'
|
||||
import { mapCurveType } from './mapCurveType'
|
||||
import { PlotCurveTypes } from '../../reducers/Charts'
|
||||
import { Point, Tooltip } from './Model'
|
||||
import { Theme, withTheme } from '@material-ui/core'
|
||||
import { useCustomXDomain } from './effects/useCustomXDomain'
|
||||
import { useCustomYDomain } from './effects/useCustomYDomain'
|
||||
import 'react-vis/dist/style.css'
|
||||
const { XYPlot, LineMarkSeries, YAxis, HorizontalGridLines, Hint } = require('react-vis')
|
||||
const abbreviate = require('number-abbreviate')
|
||||
|
||||
export interface Props {
|
||||
data: Array<{ x: number; y: number }>
|
||||
theme: Theme
|
||||
interpolation?: PlotCurveTypes
|
||||
range?: [number?, number?]
|
||||
timeRangeStart?: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
export default withTheme(
|
||||
memo((props: Props) => {
|
||||
const [width, setWidth] = React.useState(300)
|
||||
const [tooltip, setTooltip] = React.useState<Tooltip | undefined>()
|
||||
const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
|
||||
|
||||
const hintFormatter = React.useCallback(
|
||||
(point: any) => [
|
||||
{ title: <b>Time</b>, value: <DateFormatter timeFirst={true} date={new Date(point.x)} /> },
|
||||
{ title: <b>Value</b>, value: <NumberFormatter value={point.y} /> },
|
||||
{ title: <b>Raw</b>, value: <span>{point.y}</span> },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const onMouseLeave = React.useCallback(() => {
|
||||
setTooltip(undefined)
|
||||
}, [])
|
||||
|
||||
const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => {
|
||||
if (!something) {
|
||||
return
|
||||
}
|
||||
setTooltip({ point, value: hintFormatter(point), element: something.event.target as any })
|
||||
}, [])
|
||||
|
||||
const paletteColor =
|
||||
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
|
||||
const color = props.color ? props.color : paletteColor
|
||||
|
||||
const highlightSelectedPoint = useCallback(
|
||||
(point: Point) => {
|
||||
const highlight = tooltip && tooltip.point.x === point.x && tooltip.point.y === point.y
|
||||
return highlight ? emphasize(color, 0.8) : color
|
||||
},
|
||||
[tooltip, color]
|
||||
)
|
||||
|
||||
const formatYAxis = useCallback((num: number) => abbreviate(num), [])
|
||||
|
||||
const xDomain = useCustomXDomain(props)
|
||||
const yDomain = useCustomYDomain(props)
|
||||
|
||||
const data = props.data
|
||||
const hasData = data.length > 0
|
||||
const dummyDomain = [-1, 1]
|
||||
const dummyData = [{ x: -2, y: -2 }]
|
||||
return (
|
||||
<div>
|
||||
<div style={{ height: '150px', width: '100%', position: 'relative' }}>
|
||||
{data.length === 0 ? <NoData /> : null}
|
||||
<XYPlot
|
||||
width={width}
|
||||
height={180}
|
||||
yDomain={hasData ? yDomain : dummyDomain}
|
||||
xDomain={hasData ? xDomain : dummyDomain}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<HorizontalGridLines />
|
||||
<YAxis width={45} tickFormat={formatYAxis} />
|
||||
<LineMarkSeries
|
||||
color={color}
|
||||
colorType="literal"
|
||||
getColor={highlightSelectedPoint}
|
||||
onValueMouseOver={showTooltip}
|
||||
size={3}
|
||||
data={hasData ? data : dummyData}
|
||||
curve={mapCurveType(props.interpolation)}
|
||||
/>
|
||||
<Hint value={{ x: 0, y: 0 }} style={{ pointerEvents: 'none' }}>
|
||||
<TooltipComponent tooltip={tooltip} theme={props.theme} />
|
||||
</Hint>
|
||||
</XYPlot>
|
||||
<ReactResizeDetector handleWidth={true} onResize={detectResize} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
15
app/src/components/Chart/Model.ts
Normal file
15
app/src/components/Chart/Model.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Tooltip {
|
||||
value: Array<TooltipRows>
|
||||
point: Point
|
||||
element: Element | null
|
||||
}
|
||||
|
||||
export interface TooltipRows {
|
||||
title: React.ReactElement
|
||||
value: React.ReactElement
|
||||
}
|
||||
27
app/src/components/Chart/NoData.tsx
Normal file
27
app/src/components/Chart/NoData.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { memo } from 'react'
|
||||
import { Typography } from '@material-ui/core'
|
||||
|
||||
function NoData() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
color: '#ccc',
|
||||
verticalAlign: 'middle',
|
||||
paddingLeft: '30px',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Typography style={{ fontWeight: 'bold' }} variant="h5">
|
||||
No Data
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NoData)
|
||||
61
app/src/components/Chart/TooltipComponent.tsx
Normal file
61
app/src/components/Chart/TooltipComponent.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { memo } from 'react'
|
||||
import { fade } from '@material-ui/core/styles'
|
||||
import { Fade, Grow, Paper, Popper, Theme, Typography, withTheme } from '@material-ui/core'
|
||||
import { Tooltip } from './Model'
|
||||
const { Hint } = require('react-vis')
|
||||
|
||||
function TooltipComponent(props: { tooltip?: Tooltip; theme: Theme }) {
|
||||
const { tooltip } = props
|
||||
return (
|
||||
<Popper
|
||||
style={Boolean(tooltip) ? { transition: 'all 0.1s ease-out' } : undefined}
|
||||
open={Boolean(tooltip)}
|
||||
transition={true}
|
||||
keepMounted={true}
|
||||
placement="top"
|
||||
anchorEl={tooltip && tooltip.element}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: '8px',
|
||||
transition: 'all 0.5s ease',
|
||||
}}
|
||||
>
|
||||
<Fade in={Boolean(tooltip)} timeout={300}>
|
||||
<Grow in={Boolean(tooltip)} timeout={300}>
|
||||
<Paper
|
||||
style={{
|
||||
padding: '4px',
|
||||
marginTop: '-12px',
|
||||
backgroundColor: fade(
|
||||
props.theme.palette.type === 'light'
|
||||
? props.theme.palette.background.paper
|
||||
: props.theme.palette.background.default,
|
||||
0.7
|
||||
),
|
||||
}}
|
||||
>
|
||||
<table style={{ lineHeight: '1.25em' }}>
|
||||
<tbody>
|
||||
{tooltip &&
|
||||
tooltip.value.map((v: any, idx: number) => (
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
<Typography style={{ lineHeight: '1.2' }}>{v.title}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography style={{ lineHeight: '1.2' }}>{v.value}</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Paper>
|
||||
</Grow>
|
||||
</Fade>
|
||||
</div>
|
||||
</Popper>
|
||||
)
|
||||
}
|
||||
|
||||
export default withTheme(memo(TooltipComponent))
|
||||
10
app/src/components/Chart/effects/useCustomXDomain.tsx
Normal file
10
app/src/components/Chart/effects/useCustomXDomain.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Props } from '../Chart'
|
||||
|
||||
export function useCustomXDomain(props: Props): [number, number] | undefined {
|
||||
return useMemo(() => {
|
||||
const lastDataPoint = [...props.data].sort((a, b) => b.x - a.x)[0]
|
||||
const lastDataDate = lastDataPoint ? lastDataPoint.x : Date.now()
|
||||
return props.timeRangeStart ? [Date.now() - props.timeRangeStart, lastDataDate] : undefined
|
||||
}, [props.data, props.timeRangeStart])
|
||||
}
|
||||
39
app/src/components/Chart/effects/useCustomYDomain.tsx
Normal file
39
app/src/components/Chart/effects/useCustomYDomain.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Props } from '../Chart'
|
||||
import { useMemo } from 'react'
|
||||
import { Point } from '../Model'
|
||||
|
||||
export function useCustomYDomain(props: Props) {
|
||||
return useMemo(() => {
|
||||
const data = props.data
|
||||
const calculatedDomain = domainForData(data)
|
||||
const yDomain: [number, number] = props.range
|
||||
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
|
||||
: calculatedDomain
|
||||
|
||||
return yDomain
|
||||
}, [props.data])
|
||||
}
|
||||
|
||||
function domainForData(data: Array<Point>): [number, number] {
|
||||
if (!data[0]) {
|
||||
const defaultDomain: [number, number] = [-1, 1]
|
||||
return defaultDomain
|
||||
}
|
||||
let max = data[0].y
|
||||
let min = data[0].y
|
||||
data.forEach(d => {
|
||||
if (max < d.y) {
|
||||
max = d.y
|
||||
}
|
||||
if (min > d.y) {
|
||||
min = d.y
|
||||
}
|
||||
})
|
||||
if ((max === 1 || max === 0) && (min === 1 || min === 0)) {
|
||||
return [0, 1]
|
||||
}
|
||||
if (min === max) {
|
||||
return [min - 0.5 * min, min + 0.5 * min]
|
||||
}
|
||||
return [min, max]
|
||||
}
|
||||
17
app/src/components/Chart/mapCurveType.tsx
Normal file
17
app/src/components/Chart/mapCurveType.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PlotCurveTypes } from '../../reducers/Charts'
|
||||
export function mapCurveType(type: PlotCurveTypes | undefined) {
|
||||
switch (type) {
|
||||
case 'curve':
|
||||
return 'curveMonotoneX'
|
||||
case 'linear':
|
||||
return 'curveLinear'
|
||||
case 'cubic_basis_spline':
|
||||
return 'curveBasis'
|
||||
case 'step_after':
|
||||
return 'curveStepAfter'
|
||||
case 'step_before':
|
||||
return 'curveStepBefore'
|
||||
default:
|
||||
return 'curveMonotoneX'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user