Refactor charts

This commit is contained in:
Thomas Nordquist
2019-07-11 22:05:02 +02:00
parent 05867dab48
commit df3e0fc047
8 changed files with 204 additions and 45 deletions

View File

@@ -8,6 +8,7 @@ import { connect } from 'react-redux'
import { Paper } from '@material-ui/core' import { Paper } from '@material-ui/core'
import ChartTitle from './ChartTitle' import ChartTitle from './ChartTitle'
import { ChartActions } from './ChartActions' import { ChartActions } from './ChartActions'
import { RingBuffer } from '../../../../backend/src/Model'
const throttle = require('lodash.throttle') const throttle = require('lodash.throttle')
interface Props { interface Props {
@@ -84,18 +85,14 @@ function Chart(props: Props) {
/> />
</div> </div>
</div> </div>
{messageHistory ? (
<TopicPlot <TopicPlot
color={props.parameters.color} color={props.parameters.color}
interpolation={props.parameters.interpolation} interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined} timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined} range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={frozenHistory || messageHistory} history={frozenHistory || messageHistory || new RingBuffer<q.Message>(1)}
dotPath={parameters.dotPath} dotPath={parameters.dotPath}
/> />
) : (
<span>No data</span>
)}
</Paper> </Paper>
) )
} }

View File

@@ -1,13 +1,17 @@
import React from 'react'
import DateFormatter from '../helper/DateFormatter' import DateFormatter from '../helper/DateFormatter'
import NumberFormatter from '../helper/NumberFormatter'
import Portal from '@material-ui/core/Portal'
import React, { useRef, useCallback, useState } from 'react'
import { default as ReactResizeDetector } from 'react-resize-detector' import { default as ReactResizeDetector } from 'react-resize-detector'
import { PlotCurveTypes } from '../../reducers/Charts' import { PlotCurveTypes } from '../../reducers/Charts'
import { Theme, withTheme } from '@material-ui/core' import { Theme, Typography, withTheme, Popper, Paper } from '@material-ui/core'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines } = require('react-vis')
const abbreviate = require('number-abbreviate')
import Portal from '@material-ui/core/Portal'
import { useCustomXDomain } from './useCustomXDomain' import { useCustomXDomain } from './useCustomXDomain'
import 'react-vis/dist/style.css'
import { number } from 'prop-types'
import { fade } from '@material-ui/core/styles'
import { toggleAdvancedSettings } from '../../actions/ConnectionManager'
const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines, Highlight, MouseEvent } = require('react-vis')
const abbreviate = require('number-abbreviate')
export interface Props { export interface Props {
data: Array<{ x: number; y: number }> data: Array<{ x: number; y: number }>
@@ -35,26 +39,57 @@ function mapCurveType(type: PlotCurveTypes | undefined) {
} }
} }
function useToggle(initialState: boolean): [boolean, () => void, (value: boolean) => void] {
const [value, setValue] = useState(initialState)
const toggle = useCallback(() => {
setValue(!value)
}, [value])
return [value, toggle, setValue]
}
export default withTheme((props: Props) => { export default withTheme((props: Props) => {
const [hintStaysOpen, toggleHintStaysOpen, setStaysOpen] = useToggle(false)
const [width, setWidth] = React.useState(300) const [width, setWidth] = React.useState(300)
const [tooltip, setTooltip] = React.useState({ value: undefined }) const [tooltip, setTooltip] = React.useState<{ value: any; element: any } | undefined>()
const detectResize = React.useCallback(newWidth => setWidth(newWidth), []) const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
const hintFormatter = React.useCallback((point: any) => { const hintFormatter = React.useCallback((point: any) => {
return [ return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> }, { title: <b>Time</b>, value: <DateFormatter timeFirst={true} date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y }, { title: <b>Value</b>, value: <NumberFormatter value={point.y} /> },
{ title: <b>Raw</b>, value: point.y },
] ]
}, []) }, [])
const hideTooltip = React.useCallback(() => { const hideTooltip = React.useCallback(() => {
setTooltip({ value: undefined }) if (!hintStaysOpen) {
setTooltip(undefined)
}
}, [hintStaysOpen])
const onMouseLeave = React.useCallback(() => {
setStaysOpen(false)
setTooltip(undefined)
}, []) }, [])
const showTooltip = React.useCallback((value: any) => { const showTooltip = React.useCallback((value: any, something: { event: MouseEvent }) => {
setTooltip({ value }) if (!something) {
return
}
setTooltip({ value: hintFormatter(value), element: something.event.target })
}, []) }, [])
const onValueClick = React.useCallback(
(value: any, something: { event: MouseEvent }) => {
toggleHintStaysOpen()
console.log('onValueClick')
showTooltip(value, something)
},
[toggleHintStaysOpen]
)
const xDomain = useCustomXDomain(props) const xDomain = useCustomXDomain(props)
return React.useMemo(() => { return React.useMemo(() => {
@@ -70,28 +105,100 @@ export default withTheme((props: Props) => {
color = props.color color = props.color
} }
const hasData = data.length > 0
const dummyDomain = [-1, 1]
const dummyData = [{ x: -2, y: -2 }]
return ( return (
<div style={{ height: '150px', overflow: 'hidden' }}> <div>
<XYPlot width={width} height={180} yDomain={yDomain} xDomain={xDomain}> <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 /> <HorizontalGridLines />
<XAxis />
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} /> <YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
<LineMarkSeries <LineMarkSeries
color={color} color={color}
onValueMouseOver={showTooltip} onValueMouseOver={showTooltip}
onValueMouseOut={hideTooltip} onValueMouseOut={hideTooltip}
onValueClick={onValueClick}
size={3} size={3}
data={data} data={hasData ? data : dummyData}
curve={mapCurveType(props.interpolation)} curve={mapCurveType(props.interpolation)}
/> />
{tooltip.value ? <Hint format={hintFormatter} value={tooltip.value} /> : null} <Hint value={{}}>
<Popper open={Boolean(tooltip)} placement="top" anchorEl={tooltip && tooltip.element}>
<div
style={{
paddingBottom: '8px',
}}
>
<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>
</div>
</Popper>
</Hint>
</XYPlot> </XYPlot>
<ReactResizeDetector handleWidth={true} onResize={detectResize} /> <ReactResizeDetector handleWidth={true} onResize={detectResize} />
</div> </div>
</div>
) )
}, [width, props.data, tooltip, props.interpolation, props.range, props.color, props.theme, xDomain]) }, [width, props.data, tooltip, props.interpolation, props.range, props.color, props.theme, xDomain])
}) })
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>
)
}
function domainForData(data: Array<{ x: number; y: number }>): [number, number] { function domainForData(data: Array<{ x: number; y: number }>): [number, number] {
if (!data[0]) { if (!data[0]) {
const defaultDomain: [number, number] = [-1, 1] const defaultDomain: [number, number] = [-1, 1]

View File

@@ -0,0 +1,10 @@
import { useMemo } from 'react'
import { Props } from './PlotHistory'
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])
}

View File

@@ -51,9 +51,13 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
function TopicPlot(props: Props) { function TopicPlot(props: Props) {
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = props.dotPath const data = React.useMemo(
() =>
props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath) ? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history) : nodeToHistory(startOffset, props.history),
[props.history.last(), startOffset, props.dotPath]
)
return ( return (
<PlotHistory <PlotHistory

View File

@@ -1,5 +1,5 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import React, { useCallback } from 'react' import React from 'react'
import TreeNode from './TreeNode' import TreeNode from './TreeNode'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'

View File

@@ -5,6 +5,7 @@ import { connect } from 'react-redux'
interface Props { interface Props {
date: Date date: Date
timeFirst?: boolean
overrideLocale?: string overrideLocale?: string
locale?: string locale?: string
intervalSince?: Date intervalSince?: Date
@@ -30,7 +31,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
private localizedDate(locale: string) { private localizedDate(locale: string) {
return moment(this.props.date) return moment(this.props.date)
.locale(locale) .locale(locale)
.format('L LTS') .format(this.props.timeFirst ? 'LTS L' : 'L LTS')
} }
private unitForInterval(milliseconds: number) { private unitForInterval(milliseconds: number) {

View File

@@ -0,0 +1,39 @@
import React from 'react'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { Tooltip } from '@material-ui/core'
function NumberFormatter(props: { locale: string; value: number; grouping?: boolean }) {
let formatter: Intl.NumberFormat | undefined
const formatterOptions = { useGrouping: Boolean(props.grouping) }
const defaultFormatter = Intl.NumberFormat(undefined, formatterOptions)
try {
formatter = Intl.NumberFormat(props.locale, formatterOptions)
} catch {
// locale unknown
}
try {
const formatted = (formatter || defaultFormatter).format(props.value)
return (
<Tooltip title={props.value}>
<span>{formatted}</span>
</Tooltip>
)
} catch {
// localization fail, use fallback
}
return <span>props.value</span>
}
const mapStateToProps = (state: AppState) => {
return {
locale: state.settings.get('timeLocale'),
}
}
export default connect(mapStateToProps)(NumberFormatter)

View File

@@ -35,6 +35,7 @@ export class RingBuffer<T extends Lengthwise> {
private compact() { private compact() {
this.items = this.toArray() this.items = this.toArray()
this.start = 0
this.end = this.items.length this.end = this.items.length
} }