summaryrefslogtreecommitdiff
path: root/web-timeplot/src/app/create-app.js
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2026-05-29 21:49:20 -0400
committergrothedev <grothedev@gmail.com>2026-05-29 21:49:20 -0400
commit6196004b51a6850909c154f5402ff4858eab479a (patch)
tree126b8bb1600d0a656e0df016e25d08c390f3540e /web-timeplot/src/app/create-app.js
parent27dc5849c3eaf4824d79938e7077abdbe2c82e24 (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.js449
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();
- },
- };
-}