blazeplot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/ROADMAP.md +263 -0
- package/dist/core/DataCursor.d.ts +9 -0
- package/dist/core/DataCursor.d.ts.map +1 -0
- package/dist/core/MinMaxPyramid.d.ts +15 -0
- package/dist/core/MinMaxPyramid.d.ts.map +1 -0
- package/dist/core/RingBuffer.d.ts +24 -0
- package/dist/core/RingBuffer.d.ts.map +1 -0
- package/dist/core/SeriesStore.d.ts +22 -0
- package/dist/core/SeriesStore.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/types.d.ts +33 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +669 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction/AxisController.d.ts +12 -0
- package/dist/interaction/AxisController.d.ts.map +1 -0
- package/dist/interaction/Camera2D.d.ts +31 -0
- package/dist/interaction/Camera2D.d.ts.map +1 -0
- package/dist/interaction/InputController.d.ts +23 -0
- package/dist/interaction/InputController.d.ts.map +1 -0
- package/dist/interaction/index.d.ts +5 -0
- package/dist/interaction/index.d.ts.map +1 -0
- package/dist/interaction/types.d.ts +18 -0
- package/dist/interaction/types.d.ts.map +1 -0
- package/dist/render/ReglBackend.d.ts +22 -0
- package/dist/render/ReglBackend.d.ts.map +1 -0
- package/dist/render/Renderer.d.ts +20 -0
- package/dist/render/Renderer.d.ts.map +1 -0
- package/dist/render/ShaderPrograms.d.ts +11 -0
- package/dist/render/ShaderPrograms.d.ts.map +1 -0
- package/dist/render/WebGL2Resources.d.ts +3 -0
- package/dist/render/WebGL2Resources.d.ts.map +1 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/types.d.ts +38 -0
- package/dist/render/types.d.ts.map +1 -0
- package/dist/ui/Axis.d.ts +3 -0
- package/dist/ui/Axis.d.ts.map +1 -0
- package/dist/ui/Chart.d.ts +55 -0
- package/dist/ui/Chart.d.ts.map +1 -0
- package/dist/ui/Grid.d.ts +3 -0
- package/dist/ui/Grid.d.ts.map +1 -0
- package/dist/ui/Legend.d.ts +3 -0
- package/dist/ui/Legend.d.ts.map +1 -0
- package/dist/ui/Tooltip.d.ts +3 -0
- package/dist/ui/Tooltip.d.ts.map +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BlazePlot contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# BlazePlot
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/blazeplot)
|
|
4
|
+
[](https://www.npmjs.com/package/blazeplot)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/Federicocervelli/blazeplot/actions)
|
|
7
|
+
|
|
8
|
+
Real-time LOD time series rendering engine for the browser.
|
|
9
|
+
|
|
10
|
+
BlazePlot is GPU-native plotting engineered for high-frequency streaming data where standard charting libraries (Chart.js, ECharts, uPlot) fall over. Instead of drawing every sample, it keeps a resident ring buffer of millions of points, builds a min/max LOD pyramid, and renders only what each pixel needs — so frame cost is `O(pixels)`, not `O(samples)`.
|
|
11
|
+
|
|
12
|
+
Built on WebGL2 + [regl](https://github.com/regl-project/regl). No Canvas2D, no SVG, no DOM layout.
|
|
13
|
+
|
|
14
|
+
## Target
|
|
15
|
+
|
|
16
|
+
- **10M+** resident points per series
|
|
17
|
+
- **60 Hz** smooth append + render
|
|
18
|
+
- **Zero allocations** in the frame loop
|
|
19
|
+
- **Multi-series** with independent buffers and LOD
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun install blazeplot
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<canvas id="chart" style="width:100%;height:400px"></canvas>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { Chart } from "blazeplot";
|
|
35
|
+
|
|
36
|
+
const canvas = document.getElementById("chart");
|
|
37
|
+
const chart = new Chart(canvas);
|
|
38
|
+
|
|
39
|
+
const series = chart.addSeries(
|
|
40
|
+
{ mode: "line", capacity: 1_000_000, downsample: "minmax" },
|
|
41
|
+
{ color: [0.3, 0.6, 1.0, 1.0] },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
chart.setViewport({ xMin: 0, xMax: 1000, yMin: -2, yMax: 2 });
|
|
45
|
+
chart.start();
|
|
46
|
+
|
|
47
|
+
// Append data periodically
|
|
48
|
+
function push() {
|
|
49
|
+
const n = 256;
|
|
50
|
+
const xs = new Float64Array(n);
|
|
51
|
+
const ys = new Float32Array(n);
|
|
52
|
+
for (let i = 0; i < n; i++) {
|
|
53
|
+
xs[i] = t++;
|
|
54
|
+
ys[i] = Math.sin(t * 0.01) * 0.5 + Math.random() * 0.01;
|
|
55
|
+
}
|
|
56
|
+
series.append(xs, ys);
|
|
57
|
+
requestAnimationFrame(push);
|
|
58
|
+
}
|
|
59
|
+
let t = 0;
|
|
60
|
+
push();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
| | |
|
|
66
|
+
|---|---|
|
|
67
|
+
| **LOD downsampling** | Min/max pyramid automatically selects the right detail level for the visible range — sparse viewports show raw points, dense viewports show vertical min/max segments. |
|
|
68
|
+
| **Streaming append** | Fixed-capacity ring buffer wraps silently. No re-allocation. No memory growth. |
|
|
69
|
+
| **Pan & zoom** | Pointer/touch pan and wheel zoom via `Camera2D`. Customizable viewport policies (e.g. live-follow X while Y is free). |
|
|
70
|
+
| **Grid lines** | Data-anchored grid rendered as WebGL line lists. |
|
|
71
|
+
| **Multi-series** | Independent buffers, styles, and visibility per series. |
|
|
72
|
+
| **No DOM** | No axis DOM elements, no SVG overlay. The canvas owns everything. |
|
|
73
|
+
| **ResizeObserver** | Automatic DPR-aware canvas sizing. |
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `Chart`
|
|
78
|
+
|
|
79
|
+
| Signature | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `new Chart(canvas, options?)` | Create a chart from an HTML canvas element. |
|
|
82
|
+
| `chart.addSeries(config, style?)` | Add a data series. Returns `SeriesStore`. |
|
|
83
|
+
| `chart.removeSeries(series)` | Remove a previously added series. |
|
|
84
|
+
| `chart.setViewport({ xMin, xMax, yMin, yMax })` | Set the visible data range. |
|
|
85
|
+
| `chart.resize(dpr?)` | Resize the canvas to match its CSS size × DPR. |
|
|
86
|
+
| `chart.start()` | Start the render loop (rAF). |
|
|
87
|
+
| `chart.stop()` | Stop the render loop. |
|
|
88
|
+
| `chart.getFrameStats(target?)` | Copy per-frame benchmark counters into a reusable object. |
|
|
89
|
+
| `chart.dispose()` | Dispose GPU resources, observers, and input handlers. |
|
|
90
|
+
|
|
91
|
+
### `ChartOptions`
|
|
92
|
+
|
|
93
|
+
| Property | Default | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `viewportPolicy?` | — | Custom pan/zoom/viewport behavior hooks. |
|
|
96
|
+
| `grid?` | `true` | Show grid lines. |
|
|
97
|
+
| `gridStyle?` | `{ color: [0.22,0.30,0.44,0.45] }` | Grid line color and width. |
|
|
98
|
+
|
|
99
|
+
### `ChartFrameStats`
|
|
100
|
+
|
|
101
|
+
| Field | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `fps` | Instantaneous render-loop FPS. |
|
|
104
|
+
| `frameMs` | Milliseconds spent in `render()`. |
|
|
105
|
+
| `pointsRendered` | Number of vertices drawn this frame. |
|
|
106
|
+
| `drawCalls` | Number of GPU draw calls this frame. |
|
|
107
|
+
| `uploadBytes` | Bytes uploaded to GPU this frame. |
|
|
108
|
+
| `renderMode` | `"none"` / `"raw"` / `"minmax"` / `"mixed"`. |
|
|
109
|
+
|
|
110
|
+
### `SeriesStore`
|
|
111
|
+
|
|
112
|
+
| Signature | Description |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `series.append(xs, ys)` | Append typed arrays of X (Float64) and Y (Float32) values. |
|
|
115
|
+
| `series.clear()` | Clear all data and reset LOD state. |
|
|
116
|
+
| `series.setVisible(v)` | Toggle visibility. |
|
|
117
|
+
| `series.visible` | Current visibility state. |
|
|
118
|
+
| `series.length` | Number of samples buffered. |
|
|
119
|
+
|
|
120
|
+
### `SeriesConfig`
|
|
121
|
+
|
|
122
|
+
| Property | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `mode` | `"line"` / `"envelope"` / `"scatter"` (envelope and scatter roadmap-only). |
|
|
125
|
+
| `capacity` | Ring buffer capacity (samples). |
|
|
126
|
+
| `downsample` | `"minmax"` (the only LOD strategy). |
|
|
127
|
+
|
|
128
|
+
### `SeriesStyle`
|
|
129
|
+
|
|
130
|
+
| Property | Default | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `color` | `[0.3, 0.6, 1.0, 1.0]` | RGBA float color. |
|
|
133
|
+
| `lineWidth` | `1` | Line width in pixels. |
|
|
134
|
+
|
|
135
|
+
### `ViewportPolicy`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
interface ViewportPolicy {
|
|
139
|
+
beforePan?(camera: Camera2D, intent: PanIntent): PanIntent | null;
|
|
140
|
+
beforeZoom?(camera: Camera2D, intent: ZoomIntent): ZoomIntent | null;
|
|
141
|
+
beforeRender?(camera: Camera2D): void;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Built-in data types: `Viewport`, `PanIntent`, `ZoomIntent`, `ZoomAxis`.
|
|
146
|
+
|
|
147
|
+
### Lower-level primitives
|
|
148
|
+
|
|
149
|
+
`Camera2D`, `RingBuffer`, `MinMaxPyramid`, `AxisController` are exported for advanced use cases (custom pipelines, worker threads, offscreen rendering).
|
|
150
|
+
|
|
151
|
+
## How it works
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Data stream ──► RingBuffer (resident, wraps at capacity)
|
|
155
|
+
│
|
|
156
|
+
▼
|
|
157
|
+
MinMaxPyramid (full rebuild today, incremental roadmap)
|
|
158
|
+
│
|
|
159
|
+
▼
|
|
160
|
+
SeriesStore.query() ──► LODView (buckets for visible range)
|
|
161
|
+
│
|
|
162
|
+
▼
|
|
163
|
+
Renderer (regl / WebGL2)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The render loop decides per-frame:
|
|
167
|
+
- **Few visible samples** → raw line strip from ring buffer
|
|
168
|
+
- **Many visible samples** → min/max vertical segments from the pyramid
|
|
169
|
+
|
|
170
|
+
## Architecture
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
src/
|
|
174
|
+
core/ # Data engine — no UI, no GPU
|
|
175
|
+
render/ # GPU abstraction + regl backend
|
|
176
|
+
interaction/ # Camera, input, axis ticks
|
|
177
|
+
ui/ # Orchestrator (Chart)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
bun install
|
|
184
|
+
bun run dev # Vite dev server → preview/
|
|
185
|
+
bun run build # Package build (JS + declarations)
|
|
186
|
+
bun run build:js # JS-only build
|
|
187
|
+
bun test # Core data-structure tests
|
|
188
|
+
bun run typecheck # TypeScript strict check
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Package build
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
bun run build
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Output:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
dist/index.js ES module
|
|
201
|
+
dist/index.d.ts TypeScript declarations
|
|
202
|
+
dist/index.js.map Source map
|
|
203
|
+
dist/*.d.ts.map Declaration maps
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# BlazePlot — Roadmap
|
|
2
|
+
|
|
3
|
+
**BlazePlot is a real-time LOD time series rendering engine, not a plotting library.**
|
|
4
|
+
|
|
5
|
+
Target: 10M points resident, 60 Hz append/update, pan/zoom fluid, render O(pixel) not O(samples), zero allocations in frame loop, multi-series, line/envelope/scatter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
core/ # Data engine — no UI, no GPU
|
|
14
|
+
render/ # GPU abstraction + regl V1 backend
|
|
15
|
+
interaction/ # Camera, input, axis ticks
|
|
16
|
+
ui/ # Orchestrator (Chart)
|
|
17
|
+
tests/ # bun test — RingBuffer, MinMaxPyramid, Camera2D
|
|
18
|
+
preview/ # Dev preview harness, detached from package build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The core split: **data engine** and **renderer** are separate. A `SeriesStore` owns a `RingBuffer` + `MinMaxPyramid`. The renderer reads LOD views, never raw data.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
interface GpuBackend { … } # abstract GPU
|
|
25
|
+
class ReglBackend { … } # V1 implementation
|
|
26
|
+
class FutureWebGPUBackend { …} # V3
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Camera2D** is the canonical viewport model (data-space xMin/xMax/yMin/yMax). Scale/offset for shader uniforms are derived getters. `ViewportPlanner` was removed — its pan/zoom live on `Camera2D` directly.
|
|
30
|
+
|
|
31
|
+
Package output is detached from the preview app:
|
|
32
|
+
- `bun run dev` serves `preview/`.
|
|
33
|
+
- `bun run build` emits `dist/index.js` and `dist/index.d.ts` from `src/index.ts`.
|
|
34
|
+
- `preview/` is excluded from npm package contents.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Phase 1 — Vertical slice: line on screen
|
|
39
|
+
|
|
40
|
+
**Status: in progress**
|
|
41
|
+
|
|
42
|
+
Get one end-to-end path working: append → visible raw extraction → GPU upload → draw.
|
|
43
|
+
|
|
44
|
+
- [x] WebGL2 context + regl init
|
|
45
|
+
- [x] Canvas resize + DPR handling
|
|
46
|
+
- [x] Streaming append with debug overlay
|
|
47
|
+
- [x] `ReglBackend` — createBuffer, updateBuffer, createProgram, cached draw commands
|
|
48
|
+
- [x] Raw line strip draw via regl
|
|
49
|
+
- [x] Wire `Chart.render()`: clear → copy visible raw range → upload → draw
|
|
50
|
+
- [x] **Benchmark overlay**: fps, ms/frame, points rendered, draw calls, upload bytes
|
|
51
|
+
|
|
52
|
+
This is the shortest path to seeing data on screen. Benchmarking the full pipeline comes after.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Phase 2 — Core data engine
|
|
57
|
+
|
|
58
|
+
**Status: basic scaffold implemented, correctness tests passing**
|
|
59
|
+
|
|
60
|
+
- [x] `RingBuffer` — append-only, Float64Array x / Float32Array y, logical index access, ring-wrap aware search
|
|
61
|
+
- [x] `MinMaxPyramid` — min/max per level (bucket size 2), correct higher-level aggregation, ring-wrap aware builds, `query()` returns `LODView`
|
|
62
|
+
- [x] `SeriesStore` — buffer + pyramid + style, dirty tracking
|
|
63
|
+
- [x] `Camera2D` — viewport model with pan, zoom, setViewport, clip/screen transforms
|
|
64
|
+
- [x] `DataCursor` — binary search by timestamp
|
|
65
|
+
- [x] Tests for `RingBuffer`, `MinMaxPyramid`, and `Camera2D` (bun test runner)
|
|
66
|
+
- [ ] **Incremental pyramid update** — current: full rebuild on every `build()`. Target: O(log N) per append, updating only closed buckets in the chain. This is the core competitive advantage — must be designed before release.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Phase 2.5 — Worker pipeline
|
|
71
|
+
|
|
72
|
+
**Status: not started**
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
producer thread / worker ──► SharedArrayBuffer ──► downsampling worker ──► main thread
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- [ ] Ingest worker — receives data, writes to ring buffer via SharedArrayBuffer
|
|
79
|
+
- [ ] Downsample worker — incremental pyramid update off main thread
|
|
80
|
+
- [ ] Main thread — reads coherent snapshot, uploads visible range to GPU
|
|
81
|
+
- [ ] `OffscreenCanvas` optional path
|
|
82
|
+
|
|
83
|
+
Data structures have correctness coverage, but the incremental pyramid API must be finalized before moving them off main thread.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Phase 3 — regl renderer (full)
|
|
88
|
+
|
|
89
|
+
**Status: not started**
|
|
90
|
+
|
|
91
|
+
- [x] `ReglBackend` — createBuffer, updateBuffer (subdata), createProgram, draw command cache
|
|
92
|
+
- [ ] Persistent buffer pool (no Float32Array allocs per frame)
|
|
93
|
+
- [x] Raw line strip for few visible points
|
|
94
|
+
- [x] `MinMaxSegmentRenderer` — vertical min/max segments for dense viewports
|
|
95
|
+
- [ ] Instanced draw for segment mode
|
|
96
|
+
- [x] Camera transform as uniforms (scale/offset getters on Camera2D)
|
|
97
|
+
- [ ] Two shader modes: `line.vert/frag` and `segment.vert/frag`
|
|
98
|
+
- [x] `Renderer.drawMinMaxSegments`
|
|
99
|
+
- [ ] Draw call batching per shader mode
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Phase 4 — Interaction
|
|
104
|
+
|
|
105
|
+
**Status: implemented (camera + input)**
|
|
106
|
+
|
|
107
|
+
- [x] `Camera2D` — viewport model with pan, zoom, setViewport
|
|
108
|
+
- [x] `InputController` — pointer pan, wheel zoom, touch via Pointer Events
|
|
109
|
+
- [x] `ViewportPolicy` — transforms pan/zoom intents and can update camera before render
|
|
110
|
+
- [x] Preview synced-X policy — X stays live-followed while wheel zoom/pan affect Y only
|
|
111
|
+
- [x] Camera uniforms propagated to shaders per frame
|
|
112
|
+
- [ ] LOD re-query on pan/zoom (viewport change → new LODView)
|
|
113
|
+
- [x] `AxisController` — smart tick generation and label formatting
|
|
114
|
+
- [ ] Axis tick rendering (smart tick count, label formatting)
|
|
115
|
+
- [x] Grid line rendering
|
|
116
|
+
|
|
117
|
+
Camera modifies `Camera2D`, renderer reads it. No direct data access from interaction layer.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Phase 5 — Multi-series
|
|
122
|
+
|
|
123
|
+
**Status: data model ready**
|
|
124
|
+
|
|
125
|
+
- [x] `Chart.addSeries()` supports multiple stores
|
|
126
|
+
- [x] Each `SeriesStore` has independent buffer + pyramid + style
|
|
127
|
+
- [ ] Batched draw calls (same shader → one draw per series group)
|
|
128
|
+
- [ ] Shared X axis optional, independent Y per series
|
|
129
|
+
- [x] Color/style per-series
|
|
130
|
+
- [x] Series visibility toggle
|
|
131
|
+
|
|
132
|
+
Limit: solid lines only, no markers, no antialias, no spline, no fill.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Phase 6 — Public API
|
|
137
|
+
|
|
138
|
+
**Status: basic shape exists**
|
|
139
|
+
|
|
140
|
+
- [x] `new Chart(canvas)`
|
|
141
|
+
- [x] `new Chart(canvas, { viewportPolicy })`
|
|
142
|
+
- [x] `chart.addSeries(config, style)`
|
|
143
|
+
- [x] `chart.setViewport({ xMin, xMax, yMin, yMax })`
|
|
144
|
+
- [x] `chart.start()` / `chart.stop()`
|
|
145
|
+
- [x] `chart.resize()` — handle container resize with DPR
|
|
146
|
+
- [x] `series.append(x, y)` — accepts typed arrays
|
|
147
|
+
- [x] `series.clear()`
|
|
148
|
+
- [x] `chart.removeSeries(series)`
|
|
149
|
+
- [ ] Axis labels / tick rendering
|
|
150
|
+
- [x] Grid
|
|
151
|
+
- [ ] Legend
|
|
152
|
+
- [ ] Tooltip / hit testing
|
|
153
|
+
- [ ] Export image
|
|
154
|
+
- [ ] Theme system
|
|
155
|
+
- [x] ResizeObserver integration
|
|
156
|
+
|
|
157
|
+
Package status:
|
|
158
|
+
- [x] `exports`, `main`, `module`, and `types` point at `dist/`
|
|
159
|
+
- [x] Vite library build from `src/index.ts`
|
|
160
|
+
- [x] Declaration emit from `src/` only via Vite d.ts plugin
|
|
161
|
+
- [x] `bun pm pack --dry-run` includes package files only
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Downsampler — LOD engine
|
|
166
|
+
|
|
167
|
+
**Status: basic pyramid built, not incremental**
|
|
168
|
+
|
|
169
|
+
Current: `MinMaxPyramid.build()` does a full bottom-up rebuild. Target:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
raw level: x: Float64Array, y: Float32Array
|
|
173
|
+
level 1: minY/maxY per bucket of 2
|
|
174
|
+
level 2: minY/maxY per bucket of 4
|
|
175
|
+
level 3: minY/maxY per bucket of 8
|
|
176
|
+
…
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Incremental append**: each append updates only the chain of closed buckets. Query is O(buckets in viewport).
|
|
180
|
+
|
|
181
|
+
Planned incremental design:
|
|
182
|
+
- Raw samples are addressed by monotonically increasing logical sample index, not physical ring position.
|
|
183
|
+
- Level `n` bucket width is `bucketSize ** (n + 1)` raw samples.
|
|
184
|
+
- Appending a sample updates only level 0 while its bucket is open.
|
|
185
|
+
- When a bucket closes, its min/max pair is propagated upward as one input sample for the next level.
|
|
186
|
+
- Higher levels never read raw Y values; they combine child min/max pairs.
|
|
187
|
+
- Ring wrap invalidates buckets whose covered logical index range was overwritten.
|
|
188
|
+
- Query receives a visible logical index range from x-search and maps that range to bucket indices using the selected level width.
|
|
189
|
+
|
|
190
|
+
**Query**: `samples_per_pixel = visible_samples / plotWidthPx`, pick `level = max(0, ceil(log2(samples_per_pixel)) - 1)`, return min/max pairs.
|
|
191
|
+
|
|
192
|
+
**Render decision**:
|
|
193
|
+
- few points → raw line strip
|
|
194
|
+
- many points → vertical min/max segments
|
|
195
|
+
- very many → envelope mesh
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Backend strategy
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
V1: WebGL2 + regl ← CURRENT
|
|
203
|
+
V2: Backend abstraction ← In place (GpuBackend interface)
|
|
204
|
+
V3: WebGPU backend ← Future
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
regl is the V1 backend, not the architecture. The `GpuBackend` interface decouples core from GPU.
|
|
208
|
+
|
|
209
|
+
regl rules for V1:
|
|
210
|
+
- Persistent buffers (no re-create per frame)
|
|
211
|
+
- Precompiled regl commands
|
|
212
|
+
- `subdata` on small ranges
|
|
213
|
+
- Batched draw calls
|
|
214
|
+
- Simple shaders, camera in uniforms
|
|
215
|
+
- WebGL2 required (no fallback)
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Overflow semantics
|
|
220
|
+
|
|
221
|
+
**Status: undecided**
|
|
222
|
+
|
|
223
|
+
`RingBuffer` wraps silently at capacity. For streaming this is usually correct, but it should be explicit. Options:
|
|
224
|
+
- Ring-buffer with wrap notification
|
|
225
|
+
- Fixed capacity with error on overflow
|
|
226
|
+
- Auto-growing buffer (breaks streaming contract)
|
|
227
|
+
|
|
228
|
+
Deferred until we have a concrete use case.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## What we're NOT doing (V1)
|
|
233
|
+
|
|
234
|
+
- SVG / Canvas2D fallback
|
|
235
|
+
- Spline interpolation
|
|
236
|
+
- Complex fill (gradient, area below)
|
|
237
|
+
- Markers / point symbols
|
|
238
|
+
- Antialias perfection
|
|
239
|
+
- Recalculating axes in render loop
|
|
240
|
+
- Per-series draw call without batching
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Competitive advantage
|
|
245
|
+
|
|
246
|
+
Not WebGL. The core differentiator:
|
|
247
|
+
|
|
248
|
+
> Incremental min/max pyramid + zero-allocation render loop + Camera2D viewport model
|
|
249
|
+
|
|
250
|
+
Never render 10M points. Render `plotWidthPx * 2` (2k–8k vertices).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Future / difficult
|
|
255
|
+
|
|
256
|
+
- Multi-chart sync
|
|
257
|
+
- Multiple Y axes
|
|
258
|
+
- Spectrogram / heatmap
|
|
259
|
+
- Large scatter ( > 1M points )
|
|
260
|
+
- OHLC / candlestick
|
|
261
|
+
- FFT / waterfall
|
|
262
|
+
- Out-of-core data ( > RAM)
|
|
263
|
+
- WebGPU backend
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataCursor.d.ts","sourceRoot":"","sources":["../../src/core/DataCursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAA2B;IAE1C,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAK9B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;CAYjC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LODView, Viewport } from './types.js';
|
|
2
|
+
import { RingBuffer } from './RingBuffer.js';
|
|
3
|
+
export declare class MinMaxPyramid {
|
|
4
|
+
readonly bucketSize: number;
|
|
5
|
+
private levels;
|
|
6
|
+
private levelLengths;
|
|
7
|
+
private levelSampleWidths;
|
|
8
|
+
constructor(bucketSize?: number);
|
|
9
|
+
build(source: RingBuffer): void;
|
|
10
|
+
query(_viewport: Viewport, pixelWidth: number, xRange: {
|
|
11
|
+
start: number;
|
|
12
|
+
length: number;
|
|
13
|
+
}): LODView;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=MinMaxPyramid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MinMaxPyramid.d.ts","sourceRoot":"","sources":["../../src/core/MinMaxPyramid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C,qBAAa,aAAa;IAKZ,QAAQ,CAAC,UAAU,EAAE,MAAM;IAJvC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,iBAAiB,CAAc;gBAElB,UAAU,GAAE,MAAU;IAS3C,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAgD/B,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;CAsCnG"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TimeRange } from './types.js';
|
|
2
|
+
export declare class RingBuffer {
|
|
3
|
+
readonly capacity: number;
|
|
4
|
+
private _length;
|
|
5
|
+
private _head;
|
|
6
|
+
private readonly xData;
|
|
7
|
+
private readonly yData;
|
|
8
|
+
constructor(capacity: number);
|
|
9
|
+
get length(): number;
|
|
10
|
+
get range(): TimeRange | null;
|
|
11
|
+
push(x: number, y: number): void;
|
|
12
|
+
get(index: number): {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
} | null;
|
|
16
|
+
getX(index: number): number;
|
|
17
|
+
getY(index: number): number;
|
|
18
|
+
lowerBoundX(x: number): number;
|
|
19
|
+
upperBoundX(x: number): number;
|
|
20
|
+
clear(): void;
|
|
21
|
+
private logicalToPhysical;
|
|
22
|
+
private assertValidIndex;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=RingBuffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RingBuffer.d.ts","sourceRoot":"","sources":["../../src/core/RingBuffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,qBAAa,UAAU;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAa;IAE1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAEzB,QAAQ,EAAE,MAAM;IAU5B,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,KAAK,IAAI,SAAS,GAAG,IAAI,CAG5B;IAED,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAOhC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAKnD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAK3B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAK3B,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAW9B,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAW9B,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;CAKzB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SeriesConfig, SeriesStyle, LODView, Viewport } from './types.js';
|
|
2
|
+
export declare class SeriesStore {
|
|
3
|
+
readonly config: SeriesConfig;
|
|
4
|
+
readonly style: SeriesStyle;
|
|
5
|
+
private readonly buffer;
|
|
6
|
+
private readonly pyramid;
|
|
7
|
+
private _dirty;
|
|
8
|
+
private _visible;
|
|
9
|
+
constructor(config: SeriesConfig, style: SeriesStyle);
|
|
10
|
+
get dirty(): boolean;
|
|
11
|
+
get length(): number;
|
|
12
|
+
get visible(): boolean;
|
|
13
|
+
setVisible(visible: boolean): void;
|
|
14
|
+
append(x: ArrayLike<number>, y: ArrayLike<number>): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
rebuildPyramid(): void;
|
|
17
|
+
query(viewport: Viewport, pixelWidth: number): LODView;
|
|
18
|
+
visibleSampleCount(viewport: Viewport): number;
|
|
19
|
+
copyRawVisible(viewport: Viewport, target: Float32Array, maxPoints: number): number;
|
|
20
|
+
copyMinMaxVisible(viewport: Viewport, target: Float32Array, maxSegments: number): number;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=SeriesStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeriesStore.d.ts","sourceRoot":"","sources":["../../src/core/SeriesStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI/E,qBAAa,WAAW;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IAExC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAiB;gBAErB,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW;IAOpD,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,MAAM,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI;IAQxD,KAAK,IAAI,IAAI;IAMb,cAAc,IAAI,IAAI;IAMtB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAetD,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM;IAM9C,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAmBnF,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;CAqCzF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Viewport, LODBucket, LODView, TimeRange, SeriesStyle, SeriesMode, SeriesConfig } from './types.js';
|
|
2
|
+
export { RingBuffer } from './RingBuffer.js';
|
|
3
|
+
export { MinMaxPyramid } from './MinMaxPyramid.js';
|
|
4
|
+
export { SeriesStore } from './SeriesStore.js';
|
|
5
|
+
export { DataCursor } from './DataCursor.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface Viewport {
|
|
2
|
+
readonly xMin: number;
|
|
3
|
+
readonly xMax: number;
|
|
4
|
+
readonly yMin: number;
|
|
5
|
+
readonly yMax: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LODBucket {
|
|
8
|
+
readonly xStart: number;
|
|
9
|
+
readonly xEnd: number;
|
|
10
|
+
readonly minY: number;
|
|
11
|
+
readonly maxY: number;
|
|
12
|
+
}
|
|
13
|
+
export interface LODView {
|
|
14
|
+
readonly buckets: Float32Array;
|
|
15
|
+
readonly bucketCount: number;
|
|
16
|
+
readonly level: number;
|
|
17
|
+
readonly samplesPerPixel: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TimeRange {
|
|
20
|
+
readonly start: number;
|
|
21
|
+
readonly end: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SeriesStyle {
|
|
24
|
+
readonly color: readonly [number, number, number, number];
|
|
25
|
+
readonly lineWidth: number;
|
|
26
|
+
}
|
|
27
|
+
export type SeriesMode = "line" | "envelope" | "scatter";
|
|
28
|
+
export interface SeriesConfig {
|
|
29
|
+
readonly mode: SeriesMode;
|
|
30
|
+
readonly capacity: number;
|
|
31
|
+
readonly downsample: "minmax";
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;CAC/B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Chart } from './ui/Chart.js';
|
|
2
|
+
export type { ChartFrameStats, ChartOptions } from './ui/Chart.js';
|
|
3
|
+
export { SeriesStore } from './core/SeriesStore.js';
|
|
4
|
+
export { RingBuffer } from './core/RingBuffer.js';
|
|
5
|
+
export { MinMaxPyramid } from './core/MinMaxPyramid.js';
|
|
6
|
+
export { Camera2D } from './interaction/Camera2D.js';
|
|
7
|
+
export { AxisController } from './interaction/AxisController.js';
|
|
8
|
+
export type { Viewport, LODBucket, LODView, TimeRange, SeriesStyle, SeriesMode, SeriesConfig } from './core/types.js';
|
|
9
|
+
export type { PanIntent, ZoomAxis, ZoomIntent, ViewportPolicy } from './interaction/types.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtH,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC"}
|