From 4af9ec346c322e4c2adc118be3c6fe27d5f65141 Mon Sep 17 00:00:00 2001 From: Robert Knight <95928279+microbit-robert@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:33:24 +0000 Subject: [PATCH] Add graph line settings to aid accessibility (#548) * Use foundation fork of Smoothie to support line style * Add graph line style settings * Add graph line thickness setting * Add preview graph to settings dialog --------- Co-authored-by: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> --- lang/ui.en.json | 26 ++++++++++++- package-lock.json | 12 +++--- package.json | 2 +- src/components/LiveGraph.tsx | 42 ++++++++++++++++---- src/components/RecordingGraph.tsx | 28 +++++++++++--- src/components/SettingsDialog.tsx | 61 +++++++++++++++++++++++++++++- src/hooks/use-graph-line-styles.ts | 31 +++++++++++++++ src/messages/ui.en.json | 42 +++++++++++++++++++- src/recording-graph.ts | 23 ++++++++--- src/settings.tsx | 13 +++++++ src/utils/preview-graph-data.ts | 23 +++++++++++ 11 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 src/hooks/use-graph-line-styles.ts create mode 100644 src/utils/preview-graph-data.ts diff --git a/lang/ui.en.json b/lang/ui.en.json index 392860796..f64ddb862 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -731,6 +731,30 @@ "defaultMessage": "Default (red, blue, green)", "description": "Graph colour scheme option" }, + "graph-line-scheme": { + "defaultMessage": "Graph line style", + "description": "Graph line scheme setting label" + }, + "graph-line-scheme-accessible": { + "defaultMessage": "Accessible lines (solid, dashed, dots)", + "description": "Graph line scheme option" + }, + "graph-line-scheme-solid": { + "defaultMessage": "Solid lines", + "description": "Graph line scheme option" + }, + "graph-line-weight": { + "defaultMessage": "Graph line thickness", + "description": "Graph line weight setting label" + }, + "graph-line-weight-default": { + "defaultMessage": "Default", + "description": "Graph line weight option" + }, + "graph-line-weight-thick": { + "defaultMessage": "Thick", + "description": "Graph line weight option" + }, "help-label": { "defaultMessage": "Help", "description": "Help icon aria label" @@ -1659,4 +1683,4 @@ "defaultMessage": "unplug and replug the USB cable", "description": "WebUSB error dialog" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 923553a76..ea237c1c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@microbit/makecode-embed": "^0.0.0-alpha.7", "@microbit/microbit-connection": "^0.0.0-alpha.29", "@microbit/ml-header-generator": "^0.4.3", + "@microbit/smoothie": "^1.37.0-microbit.2", "@tensorflow/tfjs": "^4.20.0", "@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-usb": "^1.0.6", @@ -34,7 +35,6 @@ "react-intl": "^6.6.8", "react-router": "^6.24.0", "react-router-dom": "^6.24.0", - "smoothie": "^1.36.1", "zustand": "^4.5.5" }, "devDependencies": { @@ -4387,6 +4387,11 @@ "resolved": "https://registry.npmjs.org/@microbit/ml-header-generator/-/ml-header-generator-0.4.3.tgz", "integrity": "sha512-aMdo074VvHr4Ol1ctx8zvvaqX/FjOBD7bv2I+CLyV051OeZ24PdYB6FMf5nH6ULJVyUgKGNjIadAxRbieaPauA==" }, + "node_modules/@microbit/smoothie": { + "version": "1.37.0-microbit.2", + "resolved": "https://registry.npmjs.org/@microbit/smoothie/-/smoothie-1.37.0-microbit.2.tgz", + "integrity": "sha512-Km5Zr4jUipFneBj9jTXm+HuC+WvDdXrBA/cTOIZi7WnBAL6JeYDQeIFht70Jfs+2sBFEp1dGQZ+x6t1HdjsX6w==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12502,11 +12507,6 @@ "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true }, - "node_modules/smoothie": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.36.1.tgz", - "integrity": "sha512-499Vr2od6TicP8s7ykcyTfddh/6n11BB41G9RE7gqQRyfoPIAYotUTzwAxQpAfOdVOb+BvcG2qla+hjyqwe+PA==" - }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", diff --git a/package.json b/package.json index b559344cb..b584d1d3d 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@microbit/makecode-embed": "^0.0.0-alpha.7", "@microbit/microbit-connection": "^0.0.0-alpha.29", "@microbit/ml-header-generator": "^0.4.3", + "@microbit/smoothie": "^1.37.0-microbit.2", "@tensorflow/tfjs": "^4.20.0", "@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-usb": "^1.0.6", @@ -81,7 +82,6 @@ "react-intl": "^6.6.8", "react-router": "^6.24.0", "react-router-dom": "^6.24.0", - "smoothie": "^1.36.1", "zustand": "^4.5.5" } } diff --git a/src/components/LiveGraph.tsx b/src/components/LiveGraph.tsx index 9e7d9aea8..dfd4d7121 100644 --- a/src/components/LiveGraph.tsx +++ b/src/components/LiveGraph.tsx @@ -2,7 +2,7 @@ import { HStack, usePrevious } from "@chakra-ui/react"; import { useSize } from "@chakra-ui/react-use-size"; import { AccelerometerDataEvent } from "@microbit/microbit-connection"; import { useEffect, useMemo, useRef, useState } from "react"; -import { SmoothieChart, TimeSeries } from "smoothie"; +import { SmoothieChart, TimeSeries } from "@microbit/smoothie"; import { useConnectActions } from "../connect-actions-hooks"; import { ConnectionStatus } from "../connect-status-hooks"; import { useConnectionStage } from "../connection-stage-hooks"; @@ -10,6 +10,7 @@ import { useGraphColors } from "../hooks/use-graph-colors"; import { maxAccelerationScaleForGraphs } from "../mlConfig"; import { useSettings, useStore } from "../store"; import LiveGraphLabels from "./LiveGraphLabels"; +import { useGraphLineStyles } from "../hooks/use-graph-line-styles"; export const smoothenDataPoint = (curr: number, next: number) => { // TODO: Factor out so that recording graph can do the same @@ -20,13 +21,16 @@ export const smoothenDataPoint = (curr: number, next: number) => { const LiveGraph = () => { const { isConnected, status } = useConnectionStage(); const connectActions = useConnectActions(); - const [{ graphColorScheme }] = useSettings(); + const [{ graphColorScheme, graphLineScheme, graphLineWeight }] = + useSettings(); const colors = useGraphColors(graphColorScheme); + const lineStyles = useGraphLineStyles(graphLineScheme); const canvasRef = useRef(null); + // When we update the chart we re-run the effect that syncs it with the connection state. const [chart, setChart] = useState(undefined); - const lineWidth = 2; + const lineWidth = graphLineWeight === "default" ? 2 : 3; const liveGraphContainerRef = useRef(null); const { width, height } = useSize(liveGraphContainerRef) ?? { @@ -58,9 +62,21 @@ const LiveGraph = () => { enableDpiScaling: false, }); - smoothieChart.addTimeSeries(lineX, { lineWidth, strokeStyle: colors.x }); - smoothieChart.addTimeSeries(lineY, { lineWidth, strokeStyle: colors.y }); - smoothieChart.addTimeSeries(lineZ, { lineWidth, strokeStyle: colors.z }); + smoothieChart.addTimeSeries(lineX, { + lineWidth, + strokeStyle: colors.x, + lineDash: lineStyles.x, + }); + smoothieChart.addTimeSeries(lineY, { + lineWidth, + strokeStyle: colors.y, + lineDash: lineStyles.y, + }); + smoothieChart.addTimeSeries(lineZ, { + lineWidth, + strokeStyle: colors.z, + lineDash: lineStyles.z, + }); smoothieChart.addTimeSeries(recordLines, { lineWidth: 3, @@ -73,7 +89,19 @@ const LiveGraph = () => { return () => { smoothieChart.stop(); }; - }, [colors.x, colors.y, colors.z, lineX, lineY, lineZ, recordLines]); + }, [ + colors.x, + colors.y, + colors.z, + lineStyles.x, + lineStyles.y, + lineStyles.z, + lineWidth, + lineX, + lineY, + lineZ, + recordLines, + ]); useEffect(() => { if (isConnected || status === ConnectionStatus.ReconnectingAutomatically) { diff --git a/src/components/RecordingGraph.tsx b/src/components/RecordingGraph.tsx index 0456e3181..44c101ee0 100644 --- a/src/components/RecordingGraph.tsx +++ b/src/components/RecordingGraph.tsx @@ -8,30 +8,45 @@ import { registerables, } from "chart.js"; import { useEffect, useRef } from "react"; +import { useGraphColors } from "../hooks/use-graph-colors"; +import { useGraphLineStyles } from "../hooks/use-graph-line-styles"; import { XYZData } from "../model"; import { getConfig as getRecordingChartConfig } from "../recording-graph"; -import { useGraphColors } from "../hooks/use-graph-colors"; import { useSettings } from "../store"; interface RecordingGraphProps extends BoxProps { data: XYZData; + responsive?: boolean; } -const RecordingGraph = ({ data, children, ...rest }: RecordingGraphProps) => { - const [{ graphColorScheme }] = useSettings(); +const RecordingGraph = ({ + data, + responsive = false, + children, + ...rest +}: RecordingGraphProps) => { + const [{ graphColorScheme, graphLineScheme, graphLineWeight }] = + useSettings(); const canvasRef = useRef(null); const colors = useGraphColors(graphColorScheme); + const lineStyles = useGraphLineStyles(graphLineScheme); useEffect(() => { Chart.unregister(...registerables); Chart.register([LinearScale, LineController, PointElement, LineElement]); const chart = new Chart( canvasRef.current?.getContext("2d") ?? new HTMLCanvasElement(), - getRecordingChartConfig(data, colors) + getRecordingChartConfig( + data, + responsive, + colors, + lineStyles, + graphLineWeight + ) ); return () => { chart.destroy(); }; - }, [colors, data]); + }, [colors, data, graphLineWeight, lineStyles, responsive]); return ( { position="relative" {...rest} > - + {/* canvas dimensions must account for parent border width */} + {children} ); diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index dfd0743b9..cc586b72f 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -13,17 +13,26 @@ import { ModalOverlay, } from "@chakra-ui/modal"; import { + AspectRatio, FormControl, FormHelperText, + Text, useDisclosure, VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { defaultSettings, graphColorSchemeOptions } from "../settings"; +import { + defaultSettings, + graphColorSchemeOptions, + graphLineSchemeOptions, + graphLineWeightOptions, +} from "../settings"; import { useSettings } from "../store"; -import SelectFormControl, { createOptions } from "./SelectFormControl"; +import { previewGraphData } from "../utils/preview-graph-data"; import { ConfirmDialog } from "./ConfirmDialog"; +import RecordingGraph from "./RecordingGraph"; +import SelectFormControl, { createOptions } from "./SelectFormControl"; interface SettingsDialogProps { isOpen: boolean; @@ -64,6 +73,16 @@ export const SettingsDialog = ({ "graph-color-scheme", intl ), + graphLineScheme: createOptions( + graphLineSchemeOptions, + "graph-line-scheme", + intl + ), + graphLineWeight: createOptions( + graphLineWeightOptions, + "graph-line-weight", + intl + ), }; }, [intl]); return ( @@ -107,6 +126,44 @@ export const SettingsDialog = ({ }) } /> + + setSettings({ + ...settings, + graphLineScheme, + }) + } + /> + + setSettings({ + ...settings, + graphLineWeight, + }) + } + /> + + Graph preview + + + +