diff --git a/app/src/components/Chart/Chart.tsx b/app/src/components/Chart/Chart.tsx new file mode 100644 index 0000000..a438680 --- /dev/null +++ b/app/src/components/Chart/Chart.tsx @@ -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() + const detectResize = React.useCallback(newWidth => setWidth(newWidth), []) + + const hintFormatter = React.useCallback( + (point: any) => [ + { title: Time, value: }, + { title: Value, value: }, + { title: Raw, value: {point.y} }, + ], + [] + ) + + 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 ( +
+
+ {data.length === 0 ? : null} + + + + + + + + + +
+
+ ) + }) +) diff --git a/app/src/components/Chart/Model.ts b/app/src/components/Chart/Model.ts new file mode 100644 index 0000000..6ff4269 --- /dev/null +++ b/app/src/components/Chart/Model.ts @@ -0,0 +1,15 @@ +export interface Point { + x: number + y: number +} + +export interface Tooltip { + value: Array + point: Point + element: Element | null +} + +export interface TooltipRows { + title: React.ReactElement + value: React.ReactElement +} diff --git a/app/src/components/Chart/NoData.tsx b/app/src/components/Chart/NoData.tsx new file mode 100644 index 0000000..7ce1ff9 --- /dev/null +++ b/app/src/components/Chart/NoData.tsx @@ -0,0 +1,27 @@ +import React, { memo } from 'react' +import { Typography } from '@material-ui/core' + +function NoData() { + return ( +
+ + No Data + +
+ ) +} + +export default memo(NoData) diff --git a/app/src/components/Chart/TooltipComponent.tsx b/app/src/components/Chart/TooltipComponent.tsx new file mode 100644 index 0000000..f9c65fa --- /dev/null +++ b/app/src/components/Chart/TooltipComponent.tsx @@ -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 ( + +
+ + + + + + {tooltip && + tooltip.value.map((v: any, idx: number) => ( + + + + + ))} + +
+ {v.title} + + {v.value} +
+
+
+
+
+
+ ) +} + +export default withTheme(memo(TooltipComponent)) diff --git a/app/src/components/Sidebar/useCustomXDomain.tsx b/app/src/components/Chart/effects/useCustomXDomain.tsx similarity index 91% rename from app/src/components/Sidebar/useCustomXDomain.tsx rename to app/src/components/Chart/effects/useCustomXDomain.tsx index e5fe3e0..c79206f 100644 --- a/app/src/components/Sidebar/useCustomXDomain.tsx +++ b/app/src/components/Chart/effects/useCustomXDomain.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { Props } from './PlotHistory' +import { Props } from '../Chart' export function useCustomXDomain(props: Props): [number, number] | undefined { return useMemo(() => { diff --git a/app/src/components/Chart/effects/useCustomYDomain.tsx b/app/src/components/Chart/effects/useCustomYDomain.tsx new file mode 100644 index 0000000..e28f674 --- /dev/null +++ b/app/src/components/Chart/effects/useCustomYDomain.tsx @@ -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): [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] +} diff --git a/app/src/components/Chart/mapCurveType.tsx b/app/src/components/Chart/mapCurveType.tsx new file mode 100644 index 0000000..d9b565e --- /dev/null +++ b/app/src/components/Chart/mapCurveType.tsx @@ -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' + } +} diff --git a/app/src/components/ChartPanel/ChartWithTreeNode.tsx b/app/src/components/ChartPanel/ChartWithTreeNode.tsx index 4f910c4..c93f29d 100644 --- a/app/src/components/ChartPanel/ChartWithTreeNode.tsx +++ b/app/src/components/ChartPanel/ChartWithTreeNode.tsx @@ -1,6 +1,6 @@ import * as q from '../../../../backend/src/Model' import React from 'react' -import Chart from './Chart' +import TopicChart from './TopicChart' import { ChartParameters } from '../../reducers/Charts' import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode' @@ -16,5 +16,5 @@ export function ChartWithTreeNode(props: Props) { } const treeNode = usePollingToFetchTreeNode(tree, parameters.topic) - return + return } diff --git a/app/src/components/ChartPanel/Chart.tsx b/app/src/components/ChartPanel/TopicChart.tsx similarity index 98% rename from app/src/components/ChartPanel/Chart.tsx rename to app/src/components/ChartPanel/TopicChart.tsx index 09f9d51..2847457 100644 --- a/app/src/components/ChartPanel/Chart.tsx +++ b/app/src/components/ChartPanel/TopicChart.tsx @@ -51,7 +51,7 @@ function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode) { return messageHistory } -function Chart(props: Props) { +function TopicChart(props: Props) { const { parameters, treeNode } = props const [frozenHistory, setFrozenHistory] = React.useState() const messageHistory = useMessageSubscriptionToUpdate(treeNode) @@ -108,4 +108,4 @@ const mapDispatchToProps = (dispatch: any) => { export default connect( undefined, mapDispatchToProps -)(Chart) +)(TopicChart) diff --git a/app/src/components/Sidebar/HistoryDrawer.tsx b/app/src/components/Sidebar/HistoryDrawer.tsx index 7468891..1919490 100644 --- a/app/src/components/Sidebar/HistoryDrawer.tsx +++ b/app/src/components/Sidebar/HistoryDrawer.tsx @@ -23,6 +23,8 @@ interface State { } class HistoryDrawer extends React.Component { + private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' }) + constructor(props: any) { super(props) this.state = { @@ -40,8 +42,6 @@ class HistoryDrawer extends React.Component { event.stopPropagation() } - private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' }) - public renderHistory() { const style = (element: HistoryItem) => ({ backgroundColor: element.selected diff --git a/app/src/components/Sidebar/PlotHistory.tsx b/app/src/components/Sidebar/PlotHistory.tsx deleted file mode 100644 index eb9fb37..0000000 --- a/app/src/components/Sidebar/PlotHistory.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import DateFormatter from '../helper/DateFormatter' -import NumberFormatter from '../helper/NumberFormatter' -import React, { useCallback, useState } from 'react' -import { default as ReactResizeDetector } from 'react-resize-detector' -import { emphasize, fade } from '@material-ui/core/styles' -import { Paper, Popper, Theme, Typography, withTheme } from '@material-ui/core' -import { PlotCurveTypes } from '../../reducers/Charts' -import { useCustomXDomain } from './useCustomXDomain' -import 'react-vis/dist/style.css' -const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = 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 -} - -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' - } -} - -function useToggle(initialState: boolean): [boolean, () => void, (value: boolean) => void] { - const [value, setValue] = useState(initialState) - const toggle = useCallback(() => { - setValue(!value) - }, [value]) - - return [value, toggle, setValue] -} - -interface Point { - x: any - y: any -} - -export default withTheme((props: Props) => { - const [hintStaysOpen, toggleHintStaysOpen, setStaysOpen] = useToggle(false) - const [width, setWidth] = React.useState(300) - const [tooltip, setTooltip] = React.useState<{ value: any; point: Point; element: any } | undefined>() - const detectResize = React.useCallback(newWidth => setWidth(newWidth), []) - - const hintFormatter = React.useCallback((point: any) => { - return [ - { title: Time, value: }, - { title: Value, value: }, - { title: Raw, value: point.y }, - ] - }, []) - - const onMouseLeave = React.useCallback(() => { - setStaysOpen(false) - setTooltip(undefined) - }, []) - - const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => { - if (!something) { - return - } - setTooltip({ point, value: hintFormatter(point), element: something.event.target }) - }, []) - - 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 getAnchorElement = useCallback(() => tooltip && tooltip.element, [tooltip]) - const formatYAxis = useCallback((num: number) => abbreviate(num), []) - - const xDomain = useCustomXDomain(props) - - return React.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 - - const hasData = data.length > 0 - const dummyDomain = [-1, 1] - const dummyData = [{ x: -2, y: -2 }] - return ( -
-
- {data.length === 0 ? : null} - - - - - - -
- - - - {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] - 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] -} diff --git a/app/src/components/TopicPlot.tsx b/app/src/components/TopicPlot.tsx index a401405..e76a179 100644 --- a/app/src/components/TopicPlot.tsx +++ b/app/src/components/TopicPlot.tsx @@ -1,7 +1,7 @@ import * as dotProp from 'dot-prop' import * as q from '../../../backend/src/Model' import * as React from 'react' -import PlotHistory from './Sidebar/PlotHistory' +import PlotHistory from './Chart/Chart' import { Base64Message } from '../../../backend/src/Model/Base64Message' import { toPlottableValue } from './Sidebar/CodeDiff/util' import { PlotCurveTypes } from '../reducers/Charts' diff --git a/app/src/components/Tree/TreeNode/styles.ts b/app/src/components/Tree/TreeNode/styles.ts index 3ab4605..01944f8 100644 --- a/app/src/components/Tree/TreeNode/styles.ts +++ b/app/src/components/Tree/TreeNode/styles.ts @@ -46,8 +46,8 @@ export const styles = (theme: Theme) => { display: 'inline-block' as 'inline-block', whiteSpace: 'nowrap' as 'nowrap', height: '14px', - padding: '0 4px', - margin: '1px 0px 2px 0px', + padding: '1px 4px 0 4px', + margin: '1px 0px', '&:hover': { backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light, },