diff --git a/app/src/components/ChartPanel/Chart.tsx b/app/src/components/ChartPanel/Chart.tsx index 6028582..09f9d51 100644 --- a/app/src/components/ChartPanel/Chart.tsx +++ b/app/src/components/ChartPanel/Chart.tsx @@ -8,6 +8,7 @@ import { connect } from 'react-redux' import { Paper } from '@material-ui/core' import ChartTitle from './ChartTitle' import { ChartActions } from './ChartActions' +import { RingBuffer } from '../../../../backend/src/Model' const throttle = require('lodash.throttle') interface Props { @@ -84,18 +85,14 @@ function Chart(props: Props) { /> - {messageHistory ? ( - - ) : ( - No data - )} + (1)} + dotPath={parameters.dotPath} + /> ) } diff --git a/app/src/components/Sidebar/PlotHistory.tsx b/app/src/components/Sidebar/PlotHistory.tsx index d05ae85..7af100e 100644 --- a/app/src/components/Sidebar/PlotHistory.tsx +++ b/app/src/components/Sidebar/PlotHistory.tsx @@ -1,13 +1,17 @@ -import React from 'react' 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 { PlotCurveTypes } from '../../reducers/Charts' -import { Theme, withTheme } 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 { Theme, Typography, withTheme, Popper, Paper } from '@material-ui/core' 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 { 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) => { + const [hintStaysOpen, toggleHintStaysOpen, setStaysOpen] = useToggle(false) 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 hintFormatter = React.useCallback((point: any) => { return [ - { title: Time, value: }, - { title: Value, value: point.y }, + { title: Time, value: }, + { title: Value, value: }, + { title: Raw, value: point.y }, ] }, []) 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) => { - setTooltip({ value }) + const showTooltip = React.useCallback((value: any, something: { event: MouseEvent }) => { + 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) return React.useMemo(() => { @@ -70,28 +105,100 @@ export default withTheme((props: Props) => { color = props.color } + const hasData = data.length > 0 + const dummyDomain = [-1, 1] + const dummyData = [{ x: -2, y: -2 }] return ( -
- - - - abbreviate(num)} /> - - {tooltip.value ? : null} - - +
+
+ {data.length === 0 ? : null} + + + abbreviate(num)} /> + + + +
+ + + + {tooltip && + tooltip.value.map((v: any, idx: number) => ( + + + + + ))} + +
+ {v.title} + + {v.value} +
+
+
+
+
+
+ +
) }, [width, props.data, tooltip, props.interpolation, props.range, props.color, props.theme, xDomain]) }) +function NoData() { + return ( +
+ + No Data + +
+ ) +} + function domainForData(data: Array<{ x: number; y: number }>): [number, number] { if (!data[0]) { const defaultDomain: [number, number] = [-1, 1] diff --git a/app/src/components/Sidebar/useCustomXDomain.tsx b/app/src/components/Sidebar/useCustomXDomain.tsx new file mode 100644 index 0000000..e5fe3e0 --- /dev/null +++ b/app/src/components/Sidebar/useCustomXDomain.tsx @@ -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]) +} diff --git a/app/src/components/TopicPlot.tsx b/app/src/components/TopicPlot.tsx index e81c838..a401405 100644 --- a/app/src/components/TopicPlot.tsx +++ b/app/src/components/TopicPlot.tsx @@ -51,9 +51,13 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH function TopicPlot(props: Props) { const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined - const data = props.dotPath - ? nodeDotPathToHistory(startOffset, props.history, props.dotPath) - : nodeToHistory(startOffset, props.history) + const data = React.useMemo( + () => + props.dotPath + ? nodeDotPathToHistory(startOffset, props.history, props.dotPath) + : nodeToHistory(startOffset, props.history), + [props.history.last(), startOffset, props.dotPath] + ) return ( { private localizedDate(locale: string) { return moment(this.props.date) .locale(locale) - .format('L LTS') + .format(this.props.timeFirst ? 'LTS L' : 'L LTS') } private unitForInterval(milliseconds: number) { diff --git a/app/src/components/helper/NumberFormatter.tsx b/app/src/components/helper/NumberFormatter.tsx new file mode 100644 index 0000000..05f6cc4 --- /dev/null +++ b/app/src/components/helper/NumberFormatter.tsx @@ -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 ( + + {formatted} + + ) + } catch { + // localization fail, use fallback + } + + return props.value +} + +const mapStateToProps = (state: AppState) => { + return { + locale: state.settings.get('timeLocale'), + } +} + +export default connect(mapStateToProps)(NumberFormatter) diff --git a/backend/src/Model/RingBuffer.ts b/backend/src/Model/RingBuffer.ts index 5a83ec3..94a9580 100644 --- a/backend/src/Model/RingBuffer.ts +++ b/backend/src/Model/RingBuffer.ts @@ -35,6 +35,7 @@ export class RingBuffer { private compact() { this.items = this.toArray() + this.start = 0 this.end = this.items.length }