From fe5d3ecb93c0f5182ad6b7d1f841bd1071296212 Mon Sep 17 00:00:00 2001 From: Ludwig Richter Date: Mon, 1 Mar 2021 05:00:13 +0100 Subject: [PATCH] feat(widget): Add hold on hover, custom tooltip, area chart and more configuration options --- package-lock.json | 117 ++++++++++++----- package.json | 10 +- src/model/sample-user-config.ts | 69 +++++++--- .../components/custom-tooltip.tsx | 31 +++++ src/widgets/graph-widget/components/graph.tsx | 120 ++++++++++++++++++ src/widgets/graph-widget/graph.tsx | 49 ------- src/widgets/graph-widget/hooks/index.ts | 2 + .../graph-widget/hooks/use-callbacks.ts | 10 +- .../graph-widget/hooks/use-data-hold.ts | 18 +++ src/widgets/graph-widget/hooks/use-data.ts | 7 +- src/widgets/graph-widget/lib/extract-value.ts | 8 +- src/widgets/graph-widget/lib/round-to.ts | 3 + .../graph-widget/model/chart-connection.ts | 47 ++++++- .../graph-widget/model/widget-props.ts | 22 ++++ src/widgets/graph-widget/widget.tsx | 13 +- 15 files changed, 408 insertions(+), 118 deletions(-) create mode 100644 src/widgets/graph-widget/components/custom-tooltip.tsx create mode 100644 src/widgets/graph-widget/components/graph.tsx delete mode 100644 src/widgets/graph-widget/graph.tsx create mode 100644 src/widgets/graph-widget/hooks/use-data-hold.ts create mode 100644 src/widgets/graph-widget/lib/round-to.ts diff --git a/package-lock.json b/package-lock.json index 507d2e8..d78add8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4695,14 +4695,14 @@ } }, "@wuespace/telestion-client-cli": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-cli/-/telestion-client-cli-0.11.0.tgz", - "integrity": "sha512-wJVQ6/Ccq68wzM6fdX1BSNNLSn+O7yvtrbaCiq8kzzGVn0CWPdupmsnD/C0VaxTzfBao3FkIcguD1eK/CpmKvA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-cli/-/telestion-client-cli-0.11.1.tgz", + "integrity": "sha512-9sjJHHreMLO2QepRJi5HRJ4lkaYE4GDzpx6LaErds0Pz8Oqn0g88e0sFnQmB6WSeKWz7T9CJ+d7tSNbLfBqqxQ==", "dev": true, "requires": { "@craco/craco": "^6.0.0", "@fliegwerk/logsemts": "^0.4.0-0", - "@wuespace/telestion-client-template": "^0.11.0", + "@wuespace/telestion-client-template": "^0.11.1", "chalk": "^4.1.0", "change-case": "^4.1.2", "clui": "^0.3.6", @@ -4740,44 +4740,44 @@ } }, "@wuespace/telestion-client-common": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-common/-/telestion-client-common-0.11.0.tgz", - "integrity": "sha512-fil7aqkKW9sFoSobAtR7brLJguhm/AXC6ROqt3Ih8+uBjRH+qIWR5r/VivTZK6fBY7ipqxG185G++THSR8LBsw==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-common/-/telestion-client-common-0.11.1.tgz", + "integrity": "sha512-aCyZsm8DiSzoCagNh6oGLVnBcx79vf+O05zLbwwoxH6yPGahH3m9ka3h0xNfiT51/7Cy6qRKW/X0tqel4ohgaw==", "requires": { - "@wuespace/telestion-client-core": "^0.11.0", - "@wuespace/telestion-client-prop-types": "^0.11.0", - "@wuespace/telestion-client-types": "^0.11.0", + "@wuespace/telestion-client-core": "^0.11.1", + "@wuespace/telestion-client-prop-types": "^0.11.1", + "@wuespace/telestion-client-types": "^0.11.1", "prop-types": "^15.7.2", "react-error-boundary": "^3.1.0", "zustand": "^3.2.0" } }, "@wuespace/telestion-client-core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-core/-/telestion-client-core-0.11.0.tgz", - "integrity": "sha512-nQ6ZX7aWWGuwfu1B6Gfj/A6cKDA769WVOfuTaF+emvuZmm1vCoKtTeHYMb5sj9G2MPLuEib7nAQbX77KlzWwIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-core/-/telestion-client-core-0.11.1.tgz", + "integrity": "sha512-WQ9JbtCEPDPEQdP6zajjU7dOQVcLikECY5+U486zBtdtzd4/HsGTRsQyobNP3jAVq6IiqbWD9hLqlz9Dbk35GQ==", "requires": { "@fliegwerk/logsemts": "^0.4.0-0", - "@wuespace/telestion-client-prop-types": "^0.11.0", - "@wuespace/telestion-client-types": "^0.11.0", - "@wuespace/vertx-event-bus": "^0.11.0", + "@wuespace/telestion-client-prop-types": "^0.11.1", + "@wuespace/telestion-client-types": "^0.11.1", + "@wuespace/vertx-event-bus": "^0.11.1", "prop-types": "^15.7.2", "zustand": "^3.2.0" } }, "@wuespace/telestion-client-prop-types": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-prop-types/-/telestion-client-prop-types-0.11.0.tgz", - "integrity": "sha512-zomNEcbii/mOv81fq3MSI5jB0R5ALr3hjz3b8pB8jwLx4YJQtxjv+7oSQngTSgsWF4nIRWsgv3hCKHPUi7s09g==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-prop-types/-/telestion-client-prop-types-0.11.1.tgz", + "integrity": "sha512-koGzZJRVVVCnhKDU1ykvqwqM9a0Jg10CMoKjtcOc940G7dskCNwPHytIzMJ/3S5PXiSLfl3E0ZjTehuLI/ts5A==", "requires": { - "@wuespace/vertx-event-bus": "^0.11.0", + "@wuespace/vertx-event-bus": "^0.11.1", "prop-types": "^15.7.2" } }, "@wuespace/telestion-client-template": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-template/-/telestion-client-template-0.11.0.tgz", - "integrity": "sha512-6xYJ5u5B5S7+yYigLXkRrIdk7fDT1au5L9OaTfHkHVGIzhFcvDtBM+hpbgzK83cHuZANaXb/U1UrnIYEuGsQ4w==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-template/-/telestion-client-template-0.11.1.tgz", + "integrity": "sha512-MjZzpTiiQuM6cjUGjj9n9jEAPIuvvV1QoDOY/TC1bH3gie1v/Sg9T33DWDp3k2PpTm+00FHd9TuFWM73sit5Pg==", "dev": true, "requires": { "@adobe/react-spectrum": "^3.6.0", @@ -4785,31 +4785,82 @@ "@spectrum-icons/illustrations": "^3.2.0", "@spectrum-icons/ui": "^3.2.0", "@spectrum-icons/workflow": "^3.2.0", - "@wuespace/telestion-client-common": "^0.11.0", - "@wuespace/telestion-client-core": "^0.11.0", - "@wuespace/telestion-client-prop-types": "^0.11.0", + "@wuespace/telestion-client-common": "^0.11.1", + "@wuespace/telestion-client-core": "^0.11.1", + "@wuespace/telestion-client-prop-types": "^0.11.1", "electron": "^11.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "zustand": "^3.2.0" + }, + "dependencies": { + "@wuespace/telestion-client-common": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-common/-/telestion-client-common-0.11.1.tgz", + "integrity": "sha512-aCyZsm8DiSzoCagNh6oGLVnBcx79vf+O05zLbwwoxH6yPGahH3m9ka3h0xNfiT51/7Cy6qRKW/X0tqel4ohgaw==", + "dev": true, + "requires": { + "@wuespace/telestion-client-core": "^0.11.1", + "@wuespace/telestion-client-prop-types": "^0.11.1", + "@wuespace/telestion-client-types": "^0.11.1", + "prop-types": "^15.7.2", + "react-error-boundary": "^3.1.0", + "zustand": "^3.2.0" + } + }, + "@wuespace/telestion-client-core": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-core/-/telestion-client-core-0.11.1.tgz", + "integrity": "sha512-WQ9JbtCEPDPEQdP6zajjU7dOQVcLikECY5+U486zBtdtzd4/HsGTRsQyobNP3jAVq6IiqbWD9hLqlz9Dbk35GQ==", + "dev": true, + "requires": { + "@fliegwerk/logsemts": "^0.4.0-0", + "@wuespace/telestion-client-prop-types": "^0.11.1", + "@wuespace/telestion-client-types": "^0.11.1", + "@wuespace/vertx-event-bus": "^0.11.1", + "prop-types": "^15.7.2", + "zustand": "^3.2.0" + } + }, + "@wuespace/telestion-client-prop-types": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-prop-types/-/telestion-client-prop-types-0.11.1.tgz", + "integrity": "sha512-koGzZJRVVVCnhKDU1ykvqwqM9a0Jg10CMoKjtcOc940G7dskCNwPHytIzMJ/3S5PXiSLfl3E0ZjTehuLI/ts5A==", + "dev": true, + "requires": { + "@wuespace/vertx-event-bus": "^0.11.1", + "prop-types": "^15.7.2" + } + }, + "@wuespace/vertx-event-bus": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/vertx-event-bus/-/vertx-event-bus-0.11.1.tgz", + "integrity": "sha512-rTxpqOH54mo+w4fTmSmgviRUmKB3ZbeM7Rtv1hA3gWvRHTEFeqPhlofxVlbpZp6NRTQyXVyoHpOlti4iiHEOxA==", + "dev": true, + "requires": { + "@fliegwerk/logsemts": "^0.4.0-0", + "@wuespace/telestion-client-types": "^0.11.1", + "sockjs-client": "^1.5.0" + } + } } }, "@wuespace/telestion-client-types": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-types/-/telestion-client-types-0.11.0.tgz", - "integrity": "sha512-ibVVpB458pZXPZnAGXHfSJHNXHhhs7cbsrJFUgSRPjHfTjb+xnQpNIOvfDq4GZ85Vg0BvsYqWTzOlA5yaDQvOA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/telestion-client-types/-/telestion-client-types-0.11.1.tgz", + "integrity": "sha512-Fg6iZyOUZkWrqgRSGbRpzpNEDsejqNdLaP8FlwPrMONExmpI066ZaDUNz4CqXPLF+F1rxo0g5ObuBygPE1wuog==", "requires": { "@types/react": "^17.0.0" } }, "@wuespace/vertx-event-bus": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wuespace/vertx-event-bus/-/vertx-event-bus-0.11.0.tgz", - "integrity": "sha512-q5DKFO27VCRCQxuBh+yAhUgF7F7Nl5p96WiWrhnM3vk0zo+wcRPQXj7bc2xJmCC8j4s6BeETeAoPtW18nO/txA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@wuespace/vertx-event-bus/-/vertx-event-bus-0.11.1.tgz", + "integrity": "sha512-rTxpqOH54mo+w4fTmSmgviRUmKB3ZbeM7Rtv1hA3gWvRHTEFeqPhlofxVlbpZp6NRTQyXVyoHpOlti4iiHEOxA==", "requires": { "@fliegwerk/logsemts": "^0.4.0-0", - "@wuespace/telestion-client-types": "^0.11.0", + "@wuespace/telestion-client-types": "^0.11.1", "sockjs-client": "^1.5.0" } }, diff --git a/package.json b/package.json index 98fd91a..1dc5d42 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "@spectrum-icons/illustrations": "^3.2.1", "@spectrum-icons/ui": "^3.2.0", "@spectrum-icons/workflow": "^3.2.0", - "@wuespace/telestion-client-common": "^0.11.0", - "@wuespace/telestion-client-core": "^0.11.0", - "@wuespace/telestion-client-prop-types": "^0.11.0", + "@wuespace/telestion-client-common": "^0.11.1", + "@wuespace/telestion-client-core": "^0.11.1", + "@wuespace/telestion-client-prop-types": "^0.11.1", "@wuespace/vertx-mock-server": "^0.11.0", "electron": "^11.3.0", "react": "^17.0.1", @@ -51,8 +51,8 @@ "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", "@types/recharts": "^1.8.19", - "@wuespace/telestion-client-cli": "^0.11.0", - "@wuespace/telestion-client-types": "^0.11.0", + "@wuespace/telestion-client-cli": "^0.11.1", + "@wuespace/telestion-client-types": "^0.11.1", "husky": "^4.3.8", "prettier": "^2.2.1", "pretty-quick": "^3.1.0", diff --git a/src/model/sample-user-config.ts b/src/model/sample-user-config.ts index f0d8cf5..f81760a 100644 --- a/src/model/sample-user-config.ts +++ b/src/model/sample-user-config.ts @@ -4,6 +4,9 @@ import { WidgetProps } from '../widgets/graph-widget/model'; const accLineGraph: WidgetProps = { isArea: false, + isCartesianGrid: false, + isHoldOnHover: true, + maxDataSamples: 20, connections: [ { channel: NineDOF, @@ -11,20 +14,17 @@ const accLineGraph: WidgetProps = { { key: 'result[0].acc.x', title: 'Accelerometer X', - color: '#d21800', - isDotted: true + color: '#d21800' }, { key: 'result[0].acc.y', title: 'Accelerometer Y', - color: '#00ec05', - isDotted: true + color: '#00ec05' }, { key: 'result[0].acc.z', title: 'Accelerometer Z', - color: '#0075ff', - isDotted: true + color: '#0075ff' } ] } @@ -33,6 +33,9 @@ const accLineGraph: WidgetProps = { const gyroLineGraph: WidgetProps = { isArea: false, + isCartesianGrid: false, + isHoldOnHover: true, + maxDataSamples: 20, connections: [ { channel: NineDOF, @@ -40,20 +43,17 @@ const gyroLineGraph: WidgetProps = { { key: 'result[0].gyro.x', title: 'Gyroscope X', - color: '#d21800', - isDotted: true + color: '#d21800' }, { key: 'result[0].gyro.y', title: 'Gyroscope Y', - color: '#00ec05', - isDotted: true + color: '#00ec05' }, { key: 'result[0].gyro.z', title: 'Gyroscope Z', - color: '#0075ff', - isDotted: true + color: '#0075ff' } ] } @@ -62,6 +62,9 @@ const gyroLineGraph: WidgetProps = { const magLineGraph: WidgetProps = { isArea: false, + isCartesianGrid: false, + isHoldOnHover: true, + maxDataSamples: 20, connections: [ { channel: NineDOF, @@ -69,20 +72,43 @@ const magLineGraph: WidgetProps = { { key: 'result[0].mag.x', title: 'Magnetometer X', - color: '#d21800', - isDotted: true + color: '#d21800' }, { key: 'result[0].mag.y', title: 'Magnetometer Y', - color: '#00ec05', - isDotted: true + color: '#00ec05' }, { key: 'result[0].mag.z', title: 'Magnetometer Z', - color: '#0075ff', - isDotted: true + color: '#0075ff' + } + ] + } + ] +}; + +const detailsGraph: WidgetProps = { + isArea: true, + isCartesianGrid: true, + isHoldOnHover: true, + maxDataSamples: 60, + connections: [ + { + channel: NineDOF, + descriptors: [ + { + key: 'result[0].gyro.z', + title: 'Gyroscope Z', + color: '#6c18ba' + }, + { + key: 'result[0].acc.x', + title: 'Accelerometer X', + color: '#ec9401', + isDotted: false, + strokeWidth: 2 } ] } @@ -123,6 +149,13 @@ export const userConfig: UserConfig = { height: 4, title: 'Magnetometer', initialProps: magLineGraph + }, + { + widgetName: 'graphWidget', + width: 12, + height: 4, + title: 'Details', + initialProps: detailsGraph } ] } diff --git a/src/widgets/graph-widget/components/custom-tooltip.tsx b/src/widgets/graph-widget/components/custom-tooltip.tsx new file mode 100644 index 0000000..7ba3029 --- /dev/null +++ b/src/widgets/graph-widget/components/custom-tooltip.tsx @@ -0,0 +1,31 @@ +import { Heading, View } from '@adobe/react-spectrum'; +import { roundTo } from '../lib/round-to'; + +export interface CustomTooltipProps { + active: boolean; + payload: Array; + label: number; +} + +export function CustomTooltip({ active, label, payload }: CustomTooltipProps) { + if (active && payload && payload.length) { + return ( + + + Time: {roundTo(label, 2)}s + + {payload.map(({ name, value }) => ( +
+ {name}: {roundTo(value, 4)} +
+ ))} +
+ ); + } + + return null; +} diff --git a/src/widgets/graph-widget/components/graph.tsx b/src/widgets/graph-widget/components/graph.tsx new file mode 100644 index 0000000..328df0e --- /dev/null +++ b/src/widgets/graph-widget/components/graph.tsx @@ -0,0 +1,120 @@ +import { + ResponsiveContainer, + Line, + LineChart, + CartesianGrid, + AreaChart, + XAxis, + YAxis, + Tooltip, + Legend, + Area +} from 'recharts'; + +import { DataSample, DataSetDescriptor } from '../model'; +import { roundTo } from '../lib/round-to'; +import { useDataHold, useDarkColorScheme } from '../hooks'; + +import { CustomTooltip } from './custom-tooltip'; + +const xTickFormatter = (value: number) => `${roundTo(value, 2)}`; + +const lineElements = { + Chart: LineChart, + DataRenderer: Line +}; + +const areaElements = { + Chart: AreaChart, + DataRenderer: Area +}; + +export interface GraphOptions { + isArea: boolean; + isCartesianGrid: boolean; + isHoldOnHover: boolean; +} + +export interface GraphProps { + data: DataSample[]; + descriptors: DataSetDescriptor[]; + options: GraphOptions; +} + +export function Graph({ data, descriptors, options }: GraphProps) { + const { isArea, isCartesianGrid, isHoldOnHover } = options; + const isDark = useDarkColorScheme(); + const [displayed, hold, unHold] = useDataHold(data); + + const { Chart, DataRenderer } = isArea ? areaElements : lineElements; + + return ( + + + {isArea && ( + + {descriptors.map((descriptor, index) => ( + + + + + ))} + + )} + {isCartesianGrid && ( + + )} + + + {/*// @ts-ignore*/} + } /> + + + {descriptors.map((descriptor, index) => ( + // @ts-ignore + + ))} + + + ); +} diff --git a/src/widgets/graph-widget/graph.tsx b/src/widgets/graph-widget/graph.tsx deleted file mode 100644 index 6f8f0ba..0000000 --- a/src/widgets/graph-widget/graph.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - ResponsiveContainer, - Line, - LineChart, - CartesianGrid, - AreaChart, - XAxis, - YAxis, - Tooltip, - Legend -} from 'recharts'; -import { DataSample, DataSetDescriptor } from './model'; -import { useDarkColorScheme } from './hooks/use-dark-color-scheme'; - -export interface GraphProps { - data: DataSample[]; - descriptors: DataSetDescriptor[]; - isArea: boolean; -} - -export function Graph({ data, descriptors, isArea }: GraphProps) { - const isDark = useDarkColorScheme(); - - return ( - - {isArea ? ( - {/* TODO: Implement area chart */} - ) : ( - - - - - - - {descriptors.map(descriptor => ( - - ))} - - )} - - ); -} diff --git a/src/widgets/graph-widget/hooks/index.ts b/src/widgets/graph-widget/hooks/index.ts index 5fe84d9..271fe50 100644 --- a/src/widgets/graph-widget/hooks/index.ts +++ b/src/widgets/graph-widget/hooks/index.ts @@ -1 +1,3 @@ export * from './use-data'; +export * from './use-data-hold'; +export * from './use-dark-color-scheme'; diff --git a/src/widgets/graph-widget/hooks/use-callbacks.ts b/src/widgets/graph-widget/hooks/use-callbacks.ts index b86a442..816bc0c 100644 --- a/src/widgets/graph-widget/hooks/use-callbacks.ts +++ b/src/widgets/graph-widget/hooks/use-callbacks.ts @@ -13,7 +13,8 @@ export type ChartCallback = readonly [ChannelAddress, Callback]; export function useCallbacks( connections: ChartConnection[], - setData: SetData + setData: SetData, + maxDataSamples: number ): ChartCallback[] { const [initialDate] = useState(new Date()); const [error, setError] = useState(); @@ -26,7 +27,10 @@ export function useCallbacks( // build current time diff from start const time = (new Date().getTime() - initialDate.getTime()) / 1000; const dataSample = buildDataSample(descriptors, message, time); - setData(prevState => [...prevState.slice(-20), dataSample]); + setData(prevState => [ + ...prevState.slice(-maxDataSamples), + dataSample + ]); } catch (err) { setError(err); } @@ -34,7 +38,7 @@ export function useCallbacks( return [channel, callback] as ChartCallback; }), - [connections, initialDate, setData] + [connections, initialDate, maxDataSamples, setData] ); if (error) throw error; diff --git a/src/widgets/graph-widget/hooks/use-data-hold.ts b/src/widgets/graph-widget/hooks/use-data-hold.ts new file mode 100644 index 0000000..0538900 --- /dev/null +++ b/src/widgets/graph-widget/hooks/use-data-hold.ts @@ -0,0 +1,18 @@ +import { DataSample } from '../model'; +import { useCallback, useEffect, useState } from 'react'; + +export type DataHoldState = readonly [DataSample[], () => void, () => void]; + +export function useDataHold(data: DataSample[]): DataHoldState { + const [displayed, setDisplayed] = useState(data); + const [isHold, setHold] = useState(false); + + useEffect(() => { + if (!isHold) setDisplayed(data); + }, [data, isHold]); + + const hold = useCallback(() => setHold(true), []); + const unHold = useCallback(() => setHold(false), []); + + return [displayed, hold, unHold]; +} diff --git a/src/widgets/graph-widget/hooks/use-data.ts b/src/widgets/graph-widget/hooks/use-data.ts index 73239f9..da218cd 100644 --- a/src/widgets/graph-widget/hooks/use-data.ts +++ b/src/widgets/graph-widget/hooks/use-data.ts @@ -11,11 +11,14 @@ const selector: StateSelector< EventBusState['eventBus'] > = state => state.eventBus; -export function useData(connections: ChartConnection[]) { +export function useData( + connections: ChartConnection[], + maxDataSamples: number +) { const eventBus = useEventBus(selector); const [data, setData] = useState([]); - const callbacks = useCallbacks(connections, setData); + const callbacks = useCallbacks(connections, setData, maxDataSamples); if (!eventBus) { throw new TypeError( diff --git a/src/widgets/graph-widget/lib/extract-value.ts b/src/widgets/graph-widget/lib/extract-value.ts index c8b4b69..62a0eaf 100644 --- a/src/widgets/graph-widget/lib/extract-value.ts +++ b/src/widgets/graph-widget/lib/extract-value.ts @@ -16,7 +16,13 @@ export function extractValue(obj: JsonSerializable, key: string): number { ); } - reduce = reduce[accessors[i]]; + const value = reduce[accessors[i]]; + + if (typeof value === 'undefined') { + throw new TypeError(`Accessor ${accessors[i]} is undefined.`); + } + + reduce = value; } if (typeof reduce !== 'number') { diff --git a/src/widgets/graph-widget/lib/round-to.ts b/src/widgets/graph-widget/lib/round-to.ts new file mode 100644 index 0000000..5fd32ec --- /dev/null +++ b/src/widgets/graph-widget/lib/round-to.ts @@ -0,0 +1,3 @@ +export function roundTo(num: number, digits: number): number { + return Math.round(num * 10 ** digits) / 10 ** digits; +} diff --git a/src/widgets/graph-widget/model/chart-connection.ts b/src/widgets/graph-widget/model/chart-connection.ts index b47009c..0415556 100644 --- a/src/widgets/graph-widget/model/chart-connection.ts +++ b/src/widgets/graph-widget/model/chart-connection.ts @@ -1,6 +1,21 @@ import { JsonSerializable } from '@wuespace/telestion-client-types'; -export interface ChartConnection extends Record { +export type SimpleCurveType = + | 'basis' + | 'basisClosed' + | 'basisOpen' + | 'linear' + | 'linearClosed' + | 'natural' + | 'monotoneX' + | 'monotoneY' + | 'monotone' + | 'step' + | 'stepBefore' + | 'stepAfter'; + +export interface ChartConnection + extends Record { /** * The channel to subscribe to and receive data from. */ @@ -12,11 +27,12 @@ export interface ChartConnection extends Record { descriptors: DataSetDescriptor[]; } -export interface DataSetDescriptor extends Record { +export interface DataSetDescriptor + extends Record { /** * The title of the data set. */ - title: string; + title?: string; /** * The key from the received message object which should be displayed. @@ -28,8 +44,31 @@ export interface DataSetDescriptor extends Record { */ color: string; + /** + * The stroke width in pixels. + * + * Defaults to `1`. + */ + strokeWidth?: number; + + /** + * The type of interpolation to use. + * + * Defaults to `'monotone'`. + */ + interpolation?: SimpleCurveType; + /** * When `true` data points are displayed. + * + * Defaults to `false`. + */ + isDotted?: boolean; + + /** + * When `true` the rendered line is dashed. + * + * Defaults to `false`. */ - isDotted: boolean; + isDashed?: boolean; } diff --git a/src/widgets/graph-widget/model/widget-props.ts b/src/widgets/graph-widget/model/widget-props.ts index e86d35e..dab0e7a 100644 --- a/src/widgets/graph-widget/model/widget-props.ts +++ b/src/widgets/graph-widget/model/widget-props.ts @@ -2,6 +2,28 @@ import { GenericProps } from '@wuespace/telestion-client-types'; import { ChartConnection } from './chart-connection'; export interface WidgetProps extends GenericProps { + /** + * When `true` the rendered chart lines have colored areas below. + */ isArea: boolean; + + /** + * Render a cartesian grid in the background. + */ + isCartesianGrid: boolean; + + /** + * When `true` the graph holds when hovering over it. + */ + isHoldOnHover: boolean; + + /** + * The number of data samples to display before removing the old ones. + */ + maxDataSamples: number; + + /** + * Connections to get data from. + */ connections: ChartConnection[]; } diff --git a/src/widgets/graph-widget/widget.tsx b/src/widgets/graph-widget/widget.tsx index cf40940..1dfc3d0 100644 --- a/src/widgets/graph-widget/widget.tsx +++ b/src/widgets/graph-widget/widget.tsx @@ -4,14 +4,17 @@ import { Heading, View, Flex } from '@adobe/react-spectrum'; import { DataSetDescriptor, WidgetProps } from './model'; import { useData } from './hooks'; -import { Graph } from './graph'; +import { Graph } from './components/graph'; export function Widget({ title, isArea, + isCartesianGrid, + isHoldOnHover, + maxDataSamples, connections }: BaseRendererProps) { - const data = useData(connections); + const data = useData(connections, maxDataSamples); const descriptors = useMemo( () => connections.reduce( @@ -33,7 +36,11 @@ export function Widget({ {title} - +