diff --git a/gui/package.json b/gui/package.json index 376770e7f2..58fc4f9cc1 100644 --- a/gui/package.json +++ b/gui/package.json @@ -26,6 +26,7 @@ "@tweenjs/tween.js": "^25.0.0", "@twemoji/svg": "^15.0.0", "browser-fs-access": "^0.35.0", + "chart.js": "^4.5.0", "classnames": "^2.5.1", "flatbuffers": "22.10.26", "intl-pluralrules": "^2.0.1", @@ -33,6 +34,7 @@ "jotai": "^2.12.2", "prompts": "^2.4.2", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-error-boundary": "^4.0.13", "react-helmet": "^6.1.0", diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 3cf68761ad..22e10c0e39 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -390,6 +390,10 @@ tracker-settings-update-blocked = Update not available. No other releases availa tracker-settings-update-available = { $versionName } is now available tracker-settings-update = Update now tracker-settings-update-title = Firmware version +tracker-settings-graph-acceleration-title = Tracker Acceleration +tracker-settings-graph-position-title = Tracker Position +tracker-settings-graph-show-title = Show Tracker Graph +tracker-settings-graph-hide-title = Hide Tracker Graph ## Tracker part card info tracker-part_card-no_name = No name diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 84c869f554..ca1884556a 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -61,6 +61,16 @@ import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate'; import { ConnectionLost } from './components/onboarding/pages/ConnectionLost'; import { VRCWarningsPage } from './components/vrc/VRCWarningsPage'; import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/StayAlignedSetup'; +import { + Chart as ChartJS, + Title, + Tooltip, + Legend, + CategoryScale, + LinearScale, + PointElement, + LineElement, +} from 'chart.js'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -73,6 +83,16 @@ function Layout() { const { isMobile } = useBreakpoint('mobile'); useDiscordPresence(); + ChartJS.register( + Title, + Tooltip, + Legend, + CategoryScale, + LinearScale, + PointElement, + LineElement + ); + return ( <> diff --git a/gui/src/components/tracker/TrackerGraph.tsx b/gui/src/components/tracker/TrackerGraph.tsx new file mode 100644 index 0000000000..9cf42db030 --- /dev/null +++ b/gui/src/components/tracker/TrackerGraph.tsx @@ -0,0 +1,177 @@ +import { useLocalization } from '@fluent/react'; +import { useEffect, useState } from 'react'; +import { Line } from 'react-chartjs-2'; +import { TrackerDataT } from 'solarxr-protocol'; +import { Button } from '@/components/commons/Button'; +import { useConfig } from '@/hooks/config'; + +export function TrackerGraph({ tracker }: { tracker: TrackerDataT }) { + const { l10n } = useLocalization(); + const { config } = useConfig(); + + type AxisData = { + x: number; + y: number; + time: number; + }; + + type ChartData = { + x: AxisData[]; + y: AxisData[]; + z: AxisData[]; + }; + + const [chartData, setChartData] = useState({ + x: [], + y: [], + z: [], + }); + + const [showTrackerGraph, setShowTrackerGraph] = useState(false); + + const secondDuration = 60; + + useEffect(() => { + if (!showTrackerGraph) { + return; + } + + const newValue = tracker.info?.isImu + ? tracker.linearAcceleration + : tracker.position; + if (!newValue) { + return; + } + + const currentTime = new Date().getTime() / 1000; + const startTime = currentTime - secondDuration; + + const updateData = (data: AxisData[], newSample: number) => { + const remapped = data + .filter((value) => value.time >= startTime) + .map((value) => ({ ...value, x: value.time - startTime })); + remapped.push({ + time: currentTime, + x: secondDuration, + y: newSample, + }); + return remapped; + }; + + const newData = { + x: updateData(chartData.x, newValue.x), + y: updateData(chartData.y, newValue.y), + z: updateData(chartData.z, newValue.z), + }; + setChartData(newData); + }, [tracker]); + + useEffect(() => { + if (!showTrackerGraph) { + setChartData({ x: [], y: [], z: [] }); + } + }, [showTrackerGraph]); + + return ( + <> + + {showTrackerGraph && ( +
+ `"${font}"`).join(','), + size: config?.textSize, + }, + plugins: { + title: { + display: true, + text: l10n.getString( + tracker?.info?.isImu + ? 'tracker-settings-graph-acceleration-title' + : 'tracker-settings-graph-position-title' + ), + color: 'white', + }, + tooltip: { + mode: 'index', + intersect: false, + animation: false, + callbacks: { + title: () => '', + }, + }, + legend: { + labels: { + color: 'white', + }, + }, + }, + scales: { + x: { + type: 'linear', + min: 0, + max: secondDuration, + ticks: { + color: 'white', + }, + }, + y: { + min: -4, + max: 4, + ticks: { + color: 'white', + }, + }, + }, + elements: { + point: { + radius: 0, + }, + }, + parsing: false, + normalized: true, + maintainAspectRatio: false, + }} + data={{ + labels: ['X', 'Y', 'Z'], + datasets: [ + { + label: 'X', + data: chartData.x, + borderColor: 'rgb(200, 50, 50)', + backgroundColor: 'rgb(200, 100, 100)', + }, + { + label: 'Y', + data: chartData.y, + borderColor: 'rgb(50, 200, 50)', + backgroundColor: 'rgb(100, 200, 100)', + }, + { + label: 'Z', + data: chartData.z, + borderColor: 'rgb(50, 50, 200)', + backgroundColor: 'rgb(100, 100, 200)', + }, + ], + }} + id="tracker-graph" + /> +
+ )} + + ); +} diff --git a/gui/src/components/tracker/TrackerSettings.tsx b/gui/src/components/tracker/TrackerSettings.tsx index 4f0489cce0..20ab85665f 100644 --- a/gui/src/components/tracker/TrackerSettings.tsx +++ b/gui/src/components/tracker/TrackerSettings.tsx @@ -40,6 +40,7 @@ import semver from 'semver'; import { useSetAtom } from 'jotai'; import { ignoredTrackersAtom } from '@/store/app-store'; import { checkForUpdate } from '@/hooks/firmware-update'; +import { TrackerGraph } from './TrackerGraph'; const rotationsLabels: [Quaternion, string][] = [ [rotationToQuatMap.BACK, 'tracker-rotation-back'], @@ -505,6 +506,7 @@ export function TrackerSettingsPage() { )} + {tracker && } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ffeb45faa..ff2cbf5e56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: browser-fs-access: specifier: ^0.35.0 version: 0.35.0 + chart.js: + specifier: ^4.5.0 + version: 4.5.0 classnames: specifier: ^2.5.1 version: 2.5.1 @@ -104,6 +107,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-chartjs-2: + specifier: ^5.3.0 + version: 5.3.0(chart.js@4.5.0)(react@18.3.1) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) @@ -720,6 +726,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@mediapipe/tasks-vision@0.10.8': resolution: {integrity: sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==} @@ -1780,6 +1789,10 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2470,6 +2483,7 @@ packages: got-fetch@5.1.10: resolution: {integrity: sha512-Gwj/A2htjvLEcY07PKDItv0WCPEs3dV2vWeZ+9TVBSKSTuWEZ4oXaMD0ZAOsajwx2orahQWN4HI0MfRyWSZsbg==} engines: {node: '>=14.0.0'} + deprecated: please use built-in fetch in nodejs peerDependencies: got: ^12.0.0 @@ -3542,6 +3556,12 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + react-chartjs-2@5.3.0: + resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-composer@5.0.3: resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} peerDependencies: @@ -4959,6 +4979,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@kurkle/color@0.3.4': {} + '@mediapipe/tasks-vision@0.10.8': {} '@mgit-at/typescript-flatbuffers-codegen@0.1.3': @@ -6072,6 +6094,10 @@ snapshots: character-reference-invalid@2.0.1: {} + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -8159,6 +8185,11 @@ snapshots: quick-lru@5.1.1: {} + react-chartjs-2@5.3.0(chart.js@4.5.0)(react@18.3.1): + dependencies: + chart.js: 4.5.0 + react: 18.3.1 + react-composer@5.0.3(react@18.3.1): dependencies: prop-types: 15.8.1