summaryrefslogtreecommitdiff
path: root/web-timeplot/src/core
diff options
context:
space:
mode:
authorThomas Grothe <grothe.tr@gmail.com>2026-04-30 00:53:13 -0400
committerThomas Grothe <grothe.tr@gmail.com>2026-04-30 00:53:13 -0400
commit73d75835e18a33c7f6c1b09bbcef93b16a7a9bfa (patch)
treee079c6c45416333e29cf900831c07619a87d5c39 /web-timeplot/src/core
parenta1c95e72bea26f554eb05916d6fc584927367159 (diff)
redo timeplot web
Diffstat (limited to 'web-timeplot/src/core')
-rw-r--r--web-timeplot/src/core/event-bus.js32
-rw-r--r--web-timeplot/src/core/store.js95
-rw-r--r--web-timeplot/src/core/time-controller.js80
3 files changed, 207 insertions, 0 deletions
diff --git a/web-timeplot/src/core/event-bus.js b/web-timeplot/src/core/event-bus.js
new file mode 100644
index 0000000..192eb6d
--- /dev/null
+++ b/web-timeplot/src/core/event-bus.js
@@ -0,0 +1,32 @@
+export class EventBus {
+ constructor() {
+ this.listeners = new Map();
+ }
+
+ on(eventName, listener) {
+ if (!this.listeners.has(eventName)) {
+ this.listeners.set(eventName, new Set());
+ }
+
+ const listeners = this.listeners.get(eventName);
+ listeners.add(listener);
+
+ return () => {
+ listeners.delete(listener);
+ if (listeners.size === 0) {
+ this.listeners.delete(eventName);
+ }
+ };
+ }
+
+ emit(eventName, payload) {
+ const listeners = this.listeners.get(eventName);
+ if (!listeners) {
+ return;
+ }
+
+ for (const listener of listeners) {
+ listener(payload);
+ }
+ }
+}
diff --git a/web-timeplot/src/core/store.js b/web-timeplot/src/core/store.js
new file mode 100644
index 0000000..9989e5f
--- /dev/null
+++ b/web-timeplot/src/core/store.js
@@ -0,0 +1,95 @@
+function clonePanelState(panels) {
+ return Object.fromEntries(Object.entries(panels).map(([key, value]) => [key, { ...value }]));
+}
+
+export function createInitialState() {
+ return {
+ app: {
+ title: 'TimePlot',
+ renderer: 'pending',
+ },
+ time: {
+ realNowMs: Date.now(),
+ realElapsedMs: 0,
+ plotTimeMs: 0,
+ speed: 1,
+ paused: false,
+ },
+ plot: {
+ showGrid: true,
+ showPoints: true,
+ windowDurationMs: 20000,
+ maxPoints: 1600,
+ valueRange: {
+ min: -1.6,
+ max: 1.6,
+ },
+ hoveredPoint: null,
+ tooltip: {
+ visible: false,
+ x: 0,
+ y: 0,
+ point: null,
+ },
+ },
+ source: {
+ activeId: 'synthetic-wave',
+ preset: 'telemetry',
+ sampleRateHz: 60,
+ amplitude: 1,
+ noise: 0.08,
+ },
+ panels: {
+ status: { title: 'Status', visible: true },
+ source: { title: 'Data Source', visible: true },
+ config: { title: 'Config', visible: true },
+ help: { title: 'Help', visible: false },
+ },
+ };
+}
+
+export class Store {
+ constructor(initialState = createInitialState()) {
+ this.state = initialState;
+ this.listeners = new Set();
+ }
+
+ getState() {
+ return this.state;
+ }
+
+ subscribe(listener) {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ setState(updater) {
+ const nextState = typeof updater === 'function' ? updater(this.state) : updater;
+ this.state = nextState;
+ for (const listener of this.listeners) {
+ listener(this.state);
+ }
+ }
+
+ patch(partial) {
+ this.setState((state) => ({
+ ...state,
+ ...partial,
+ time: partial.time ? { ...state.time, ...partial.time } : state.time,
+ plot: partial.plot
+ ? {
+ ...state.plot,
+ ...partial.plot,
+ valueRange: partial.plot.valueRange
+ ? { ...state.plot.valueRange, ...partial.plot.valueRange }
+ : state.plot.valueRange,
+ tooltip: partial.plot.tooltip
+ ? { ...state.plot.tooltip, ...partial.plot.tooltip }
+ : state.plot.tooltip,
+ }
+ : state.plot,
+ source: partial.source ? { ...state.source, ...partial.source } : state.source,
+ panels: partial.panels ? clonePanelState({ ...state.panels, ...partial.panels }) : state.panels,
+ }));
+ }
+}
diff --git a/web-timeplot/src/core/time-controller.js b/web-timeplot/src/core/time-controller.js
new file mode 100644
index 0000000..7cd57c7
--- /dev/null
+++ b/web-timeplot/src/core/time-controller.js
@@ -0,0 +1,80 @@
+export class TimeController {
+ constructor(store) {
+ this.store = store;
+ this.lastFrameTime = performance.now();
+ }
+
+ tick(now = performance.now()) {
+ const deltaMs = now - this.lastFrameTime;
+ this.lastFrameTime = now;
+
+ this.store.setState((state) => {
+ const realElapsedMs = state.time.realElapsedMs + deltaMs;
+ const plotDeltaMs = state.time.paused ? 0 : deltaMs * state.time.speed;
+
+ return {
+ ...state,
+ time: {
+ ...state.time,
+ realNowMs: Date.now(),
+ realElapsedMs,
+ plotTimeMs: Math.max(0, state.time.plotTimeMs + plotDeltaMs),
+ },
+ };
+ });
+
+ return deltaMs;
+ }
+
+ togglePause() {
+ this.store.setState((state) => ({
+ ...state,
+ time: {
+ ...state.time,
+ paused: !state.time.paused,
+ },
+ }));
+ }
+
+ setPaused(paused) {
+ this.store.setState((state) => ({
+ ...state,
+ time: {
+ ...state.time,
+ paused,
+ },
+ }));
+ }
+
+ setSpeed(speed) {
+ const clampedSpeed = Math.max(0.1, Math.min(12, speed));
+ this.store.setState((state) => ({
+ ...state,
+ time: {
+ ...state.time,
+ speed: clampedSpeed,
+ },
+ }));
+ }
+
+ reset() {
+ this.store.setState((state) => ({
+ ...state,
+ time: {
+ ...state.time,
+ realElapsedMs: 0,
+ plotTimeMs: 0,
+ },
+ plot: {
+ ...state.plot,
+ hoveredPoint: null,
+ tooltip: {
+ ...state.plot.tooltip,
+ visible: false,
+ point: null,
+ },
+ },
+ }));
+ this.lastFrameTime = performance.now();
+ }
+}