diff options
| author | grothedev <grothedev@gmail.com> | 2026-05-29 21:49:20 -0400 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2026-05-29 21:49:20 -0400 |
| commit | 6196004b51a6850909c154f5402ff4858eab479a (patch) | |
| tree | 126b8bb1600d0a656e0df016e25d08c390f3540e /web-timeplot/src/app/create-app.js | |
| parent | 27dc5849c3eaf4824d79938e7077abdbe2c82e24 (diff) | |
mv web stuff to root project dirHEADprototypeframeworkmain
Diffstat (limited to 'web-timeplot/src/app/create-app.js')
| -rw-r--r-- | web-timeplot/src/app/create-app.js | 449 |
1 files changed, 0 insertions, 449 deletions
diff --git a/web-timeplot/src/app/create-app.js b/web-timeplot/src/app/create-app.js deleted file mode 100644 index 4f4f0fc..0000000 --- a/web-timeplot/src/app/create-app.js +++ /dev/null @@ -1,449 +0,0 @@ -import { EventBus } from '../core/event-bus.js'; -import { Store, createInitialState } from '../core/store.js'; -import { TimeController } from '../core/time-controller.js'; -import { PlotBuffer } from '../plot/plot-buffer.js'; -import { TimeplotView } from '../plot/timeplot-view.js'; -import { SourceRegistry } from '../data/source-registry.js'; -import { parseReplayCsv } from '../data/parse-replay-csv.js'; -import { PanelManager } from '../ui/panel-manager.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function buildDeltaPoints(points) { - if (points.length < 2) { - return []; - } - - const derived = []; - for (let index = 1; index < points.length; index += 1) { - const previous = points[index - 1]; - const current = points[index]; - const deltaTime = Math.max(1, current.timeMs - previous.timeMs); - derived.push({ - ...current, - value: (current.value - previous.value) / deltaTime * 1000, - sourceId: `${current.sourceId}:delta`, - }); - } - - return derived; -} - -function buildSmoothedPoints(points, windowSize = 5) { - if (points.length === 0) { - return []; - } - - const smoothed = []; - for (let index = 0; index < points.length; index += 1) { - const start = Math.max(0, index - windowSize + 1); - const windowPoints = points.slice(start, index + 1); - const average = windowPoints.reduce((sum, point) => sum + point.value, 0) / windowPoints.length; - smoothed.push({ - ...points[index], - value: average, - sourceId: `${points[index].sourceId}:smooth`, - }); - } - - return smoothed; -} - -function transformPoints(points, transform) { - switch (transform) { - case 'delta': - return buildDeltaPoints(points); - case 'smooth': - return buildSmoothedPoints(points); - case 'raw': - default: - return points; - } -} - -function describeTransform(transform) { - switch (transform) { - case 'delta': - return 'Δvalue / second'; - case 'smooth': - return 'moving average'; - case 'raw': - default: - return 'raw signal'; - } -} - -function deriveValueRange(points, fallbackRange) { - if (points.length === 0) { - return fallbackRange; - } - - let min = Infinity; - let max = -Infinity; - for (const point of points) { - min = Math.min(min, point.value); - max = Math.max(max, point.value); - } - - const maxAbs = Math.max(Math.abs(min), Math.abs(max), 0.1); - return { - min: -maxAbs, - max: maxAbs, - }; -} - -function pickActiveHover(primaryCandidate, secondaryCandidate) { - if (!primaryCandidate && !secondaryCandidate) { - return null; - } - - if (primaryCandidate && !secondaryCandidate) { - return primaryCandidate; - } - - if (!primaryCandidate && secondaryCandidate) { - return secondaryCandidate; - } - - return primaryCandidate.lastPointerEventAt >= secondaryCandidate.lastPointerEventAt - ? primaryCandidate - : secondaryCandidate; -} - -export async function createApp(root) { - const bus = new EventBus(); - const store = new Store(createInitialState()); - const timeController = new TimeController(store); - const sourceBuffers = new Map(Object.keys(store.getState().sources).map((sourceKey) => [sourceKey, new PlotBuffer(store.getState().plot.maxPoints)])); - let sourceRegistry; - - const syncBuffersFromState = () => { - const state = store.getState(); - for (const sourceKey of Object.keys(state.sources)) { - if (!sourceBuffers.has(sourceKey)) { - sourceBuffers.set(sourceKey, new PlotBuffer(state.plot.maxPoints)); - } - sourceBuffers.get(sourceKey).maxPoints = state.plot.maxPoints; - } - - for (const sourceKey of Array.from(sourceBuffers.keys())) { - if (!state.sources[sourceKey]) { - sourceBuffers.delete(sourceKey); - } - } - }; - - const clearSourceBuffer = (sourceKey) => { - sourceBuffers.get(sourceKey)?.clear(); - }; - - const getGraphPoints = (state, graphId) => { - const graphConfig = state.graphs[graphId]; - const sourceBuffer = sourceBuffers.get(graphConfig.sourceKey); - const basePoints = sourceBuffer - ? sourceBuffer.getVisiblePoints(state.time.plotTimeMs, state.plot.windowDurationMs) - : []; - const transformedPoints = transformPoints(basePoints, graphConfig.transform); - return { - graphConfig, - points: transformedPoints, - range: deriveValueRange(transformedPoints, state.plot.valueRange), - }; - }; - - const actions = { - togglePause: () => timeController.togglePause(), - setSpeed: (speed) => timeController.setSpeed(speed), - resetScene: () => { - timeController.reset(); - sourceBuffers.forEach((plotBuffer) => plotBuffer.clear()); - sourceRegistry.reset(); - }, - togglePanel: (panelId) => { - store.setState((state) => ({ - ...state, - panels: { - ...state.panels, - [panelId]: { - ...state.panels[panelId], - visible: !state.panels[panelId].visible, - }, - }, - })); - }, - updateSource: (sourceKey, field, value) => { - store.setState((state) => ({ - ...state, - sources: { - ...state.sources, - [sourceKey]: { - ...state.sources[sourceKey], - [field]: value, - ...(field === 'type' - ? { - loadError: value === 'csv-replay' && state.sources[sourceKey].dataset.length === 0 - ? (state.sources[sourceKey].dataFileName - ? `Reload ${state.sources[sourceKey].dataFileName} to restore replay data` - : 'Load a CSV file to begin replay') - : '', - wsStatus: value === 'websocket' ? state.sources[sourceKey].wsStatus : 'idle', - wsStatusDetail: value === 'websocket' ? state.sources[sourceKey].wsStatusDetail : '', - } - : {}), - }, - }, - })); - sourceRegistry.syncFromState(); - syncBuffersFromState(); - - if (field === 'type' || field === 'wsUrl' || field === 'wsReconnectMs') { - clearSourceBuffer(sourceKey); - sourceRegistry.reset(); - } - }, - loadSourceFile: async (sourceKey, file) => { - try { - const state = store.getState(); - const sampleRateHz = state.sources[sourceKey]?.sampleRateHz ?? 60; - const text = await file.text(); - const { points, metadata } = parseReplayCsv(text, { sampleRateHz }); - - clearSourceBuffer(sourceKey); - store.setState((currentState) => ({ - ...currentState, - sources: { - ...currentState.sources, - [sourceKey]: { - ...currentState.sources[sourceKey], - type: 'csv-replay', - dataset: points, - dataFileName: file.name, - datasetPointCount: metadata.pointCount, - datasetDurationMs: metadata.durationMs, - loadError: '', - wsStatus: 'idle', - wsStatusDetail: '', - }, - }, - })); - sourceRegistry.syncFromState(); - sourceRegistry.reset(); - } catch (error) { - store.setState((currentState) => ({ - ...currentState, - sources: { - ...currentState.sources, - [sourceKey]: { - ...currentState.sources[sourceKey], - loadError: error instanceof Error ? error.message : String(error), - }, - }, - })); - } - }, - updatePlot: (field, value) => { - store.setState((state) => ({ - ...state, - plot: { - ...state.plot, - [field]: value, - }, - })); - - if (field === 'maxPoints') { - buffer.maxPoints = clamp(value, 200, 4000); - sourceBuffers.forEach((plotBuffer) => { - plotBuffer.maxPoints = clamp(value, 200, 4000); - }); - } - }, - updateGraph: (graphId, field, value) => { - store.setState((state) => ({ - ...state, - graphs: { - ...state.graphs, - [graphId]: { - ...state.graphs[graphId], - [field]: value, - }, - }, - })); - }, - }; - - const panelManager = new PanelManager({ root, store, actions }); - const elements = panelManager.mount(); - - const plotView = new TimeplotView({ - host: elements.primaryCanvasHost, - panelId: 'primary', - title: 'Primary signal', - subtitle: null, - showReadouts: true, - lineColor: 0x9fd1ff, - pointColor: 0xe7f2ff, - }); - - const secondaryPlotView = new TimeplotView({ - host: elements.secondaryCanvasHost, - panelId: 'secondary', - title: 'Secondary signal', - subtitle: null, - showReadouts: false, - lineColor: 0xffc46b, - pointColor: 0xffe1b0, - }); - - const renderer = await plotView.init(); - await secondaryPlotView.init(); - store.patch({ - app: { - ...store.getState().app, - renderer, - }, - }); - - sourceRegistry = new SourceRegistry(store, bus); - - bus.on('data:point', (point) => { - sourceBuffers.get(point.sourceId)?.addPoint(point); - }); - - const keyHandler = (event) => { - if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement) { - return; - } - - if (event.code === 'Space') { - event.preventDefault(); - actions.togglePause(); - return; - } - - if (event.key === '[') { - actions.setSpeed(store.getState().time.speed - 0.1); - return; - } - - if (event.key === ']') { - actions.setSpeed(store.getState().time.speed + 0.1); - return; - } - - if (event.key.toLowerCase() === 'g') { - actions.updatePlot('showGrid', !store.getState().plot.showGrid); - } - }; - - window.addEventListener('keydown', keyHandler); - - plotView.app.ticker.add(() => { - timeController.tick(); - sourceRegistry.syncFromState(); - syncBuffersFromState(); - sourceRegistry.update(store.getState().time.plotTimeMs); - - const state = store.getState(); - const primaryGraph = getGraphPoints(state, 'primary'); - const secondaryGraph = getGraphPoints(state, 'secondary'); - - plotView.panelTitle = state.graphs.primary.title; - plotView.panelSubtitle = `${state.sources[state.graphs.primary.sourceKey].label} · ${describeTransform(state.graphs.primary.transform)} · time ↓`; - secondaryPlotView.panelTitle = state.graphs.secondary.title; - secondaryPlotView.panelSubtitle = `${state.sources[state.graphs.secondary.sourceKey].label} · ${describeTransform(state.graphs.secondary.transform)} · time ↓`; - - const primaryState = { - ...state, - plot: { - ...state.plot, - valueRange: primaryGraph.range, - }, - }; - - const secondaryState = { - ...state, - plot: { - ...state.plot, - valueRange: secondaryGraph.range, - }, - }; - - plotView.render(primaryState, primaryGraph.points); - secondaryPlotView.render(secondaryState, secondaryGraph.points); - - const primaryHover = plotView.getHoverCandidate(); - const secondaryHover = secondaryPlotView.getHoverCandidate(); - const activeHover = pickActiveHover(primaryHover, secondaryHover); - - if (!activeHover) { - plotView.clearHover(); - secondaryPlotView.clearHover(); - store.setState((currentState) => ({ - ...currentState, - plot: { - ...currentState.plot, - hoveredPoint: null, - tooltip: { - ...currentState.plot.tooltip, - visible: false, - point: null, - linkedPoint: null, - }, - }, - })); - panelManager.sync(store.getState(), { - primary: primaryGraph.points.length, - secondary: secondaryGraph.points.length, - }); - return; - } - - const primaryLinkedPoint = plotView.findNearestScreenPointByTime(activeHover.point.timeMs); - const secondaryLinkedPoint = secondaryPlotView.findNearestScreenPointByTime(activeHover.point.timeMs); - - plotView.renderLinkedHover(primaryLinkedPoint); - secondaryPlotView.renderLinkedHover(secondaryLinkedPoint); - - const activePanelLabel = activeHover.panelId === 'secondary' - ? state.graphs.secondary.title - : state.graphs.primary.title; - const linkedPoint = activeHover.panelId === 'secondary' ? primaryLinkedPoint : secondaryLinkedPoint; - const linkedPanelLabel = activeHover.panelId === 'secondary' - ? state.graphs.primary.title - : state.graphs.secondary.title; - - store.setState((currentState) => ({ - ...currentState, - plot: { - ...currentState.plot, - hoveredPoint: activeHover.point, - tooltip: { - ...currentState.plot.tooltip, - visible: true, - panelId: activeHover.panelId, - panelLabel: activePanelLabel, - x: activeHover.x, - y: activeHover.y, - point: activeHover.point, - linkedPoint, - linkedPanelLabel, - }, - }, - })); - - panelManager.sync(store.getState(), { - primary: primaryGraph.points.length, - secondary: secondaryGraph.points.length, - }); - }); - - return { - destroy() { - window.removeEventListener('keydown', keyHandler); - plotView.destroy(); - secondaryPlotView.destroy(); - }, - }; -} |
