1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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,
}));
}
}
|