@pond-ts/charts 0.31.0 → 0.31.1
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/CHANGELOG.md +12 -2
- package/README.md +45 -204
- package/package.json +2 -2
- package/dist/annotations.d.ts +0 -110
- package/dist/annotations.js +0 -459
- package/dist/chip.d.ts +0 -23
- package/dist/chip.js +0 -43
package/CHANGELOG.md
CHANGED
|
@@ -8,8 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
|
|
|
8
8
|
them all. Pre-1.0: minor bumps may include new features and type-level changes;
|
|
9
9
|
patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.31.
|
|
12
|
-
[0.31.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...HEAD
|
|
12
|
+
[0.31.1]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.1
|
|
13
|
+
[0.31.0]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...3c4e8bd
|
|
13
14
|
[0.30.0]: https://github.com/pjm17971/pond-ts/compare/v0.29.0...v0.30.0
|
|
14
15
|
[0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
|
|
15
16
|
[0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
|
|
@@ -24,6 +25,15 @@ patch bumps are strictly additive.
|
|
|
24
25
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
25
26
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
26
27
|
|
|
28
|
+
## [0.31.1] — 2026-06-28
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **`@pond-ts/charts` and `@pond-ts/fit` now ship their own README** on npm.
|
|
33
|
+
0.31.0 inadvertently published the `pond-ts` core README on every package
|
|
34
|
+
(each `prepack` copied the repo-root README); charts and fit now carry their
|
|
35
|
+
own. No code or API changes.
|
|
36
|
+
|
|
27
37
|
## [0.31.0] — 2026-06-28
|
|
28
38
|
|
|
29
39
|
First published release of **`@pond-ts/charts`** and **`@pond-ts/fit`** (both were
|
package/README.md
CHANGED
|
@@ -1,228 +1,69 @@
|
|
|
1
|
-
# pond-ts
|
|
1
|
+
# @pond-ts/charts
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**React charts for [pond-ts](https://www.npmjs.com/package/pond-ts) time series.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**pond-ts** is the TypeScript-first successor to
|
|
10
|
-
[pondjs](https://github.com/esnet/pond), rewritten from scratch with a
|
|
11
|
-
focus on type safety, composability, and the live-streaming patterns
|
|
12
|
-
that pondjs never grew.
|
|
5
|
+
A composable charting layer built directly on pond-ts series: a canvas data
|
|
6
|
+
plane for the heavy line/area/bar drawing, with SVG overlays for axes, cursors,
|
|
7
|
+
and interaction. Line, area, bar, scatter, and box charts; **time and value
|
|
8
|
+
x-axes** inferred from the data; an interactive cursor; and theming.
|
|
13
9
|
|
|
14
10
|
```sh
|
|
15
|
-
npm install pond-ts
|
|
16
|
-
npm install @pond-ts/react # React hooks (optional)
|
|
11
|
+
npm install @pond-ts/charts pond-ts @pond-ts/react
|
|
17
12
|
```
|
|
18
13
|
|
|
19
|
-
-
|
|
20
|
-
off it. `event.get('cpu')` returns `number | undefined` straight from
|
|
21
|
-
the schema; no `as` casts.
|
|
22
|
-
- **Batch + streaming with the same vocabulary** — `filter`, `map`,
|
|
23
|
-
`aggregate`, `rolling`, `diff`, `rate`, `fill`, `cumulative`,
|
|
24
|
-
`sample`, `reduce` all exist on both `TimeSeries` and `LiveSeries`.
|
|
25
|
-
- **Multi-entity by construction** — `partitionBy('host')` routes per
|
|
26
|
-
entity; `rolling` / `aggregate` / `fill` / `sample` over a partitioned
|
|
27
|
-
view all become per-entity automatically.
|
|
28
|
-
- **Bounded-memory streaming** — retention policies, eviction-aware
|
|
29
|
-
views, and sampling decouple downstream window length
|
|
30
|
-
from event rate at firehose loads (up to 500k events/sec on a
|
|
31
|
-
single node.js instance.)
|
|
32
|
-
- **Triggers** — for control of rolling emission cadences. Synchronised
|
|
33
|
-
partitioned rolling fires across partitions on every boundary.
|
|
34
|
-
- **Typed column extraction** — `series.column('cpu')` returns a
|
|
35
|
-
schema-narrowed typed column with single-pass reductions
|
|
36
|
-
(`min`/`max`/`sum`/`mean`/`stdev`/`median`/`percentile`/`minMax`),
|
|
37
|
-
index downsampling (`bin`), and a zero-copy `toFloat64Array()` for
|
|
38
|
-
canvas / WebGL draw loops — no per-event allocation on the hot path.
|
|
39
|
-
- **No legacy baggage**
|
|
40
|
-
|
|
41
|
-
## Quick start: batch
|
|
14
|
+
`pond-ts`, `@pond-ts/react`, and `react` (18 or 19) are peer dependencies.
|
|
42
15
|
|
|
43
|
-
|
|
44
|
-
import { Sequence, TimeSeries } from 'pond-ts';
|
|
16
|
+
## Quick start
|
|
45
17
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{ name: 'requests', kind: 'number' },
|
|
50
|
-
{ name: 'host', kind: 'string' },
|
|
51
|
-
] as const;
|
|
18
|
+
```tsx
|
|
19
|
+
import { TimeSeries } from 'pond-ts';
|
|
20
|
+
import { ChartContainer, ChartRow, Layers, LineChart } from '@pond-ts/charts';
|
|
52
21
|
|
|
53
|
-
const
|
|
22
|
+
const series = new TimeSeries({
|
|
54
23
|
name: 'cpu',
|
|
55
|
-
schema
|
|
24
|
+
schema: [
|
|
25
|
+
{ name: 'time', kind: 'time' },
|
|
26
|
+
{ name: 'cpu', kind: 'number' },
|
|
27
|
+
] as const,
|
|
56
28
|
rows: [
|
|
57
|
-
[
|
|
58
|
-
[
|
|
59
|
-
[
|
|
60
|
-
['2025-01-01T00:03:00Z', 0.48, 128, 'host1'],
|
|
61
|
-
['2025-01-01T00:04:00Z', 0.63, 166, 'host3'],
|
|
29
|
+
[1717200000000, 50],
|
|
30
|
+
[1717200060000, 62],
|
|
31
|
+
[1717200120000, 48],
|
|
62
32
|
],
|
|
63
33
|
});
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The full batch surface (`align`, `rolling`, `smooth`, `groupBy`, `join`,
|
|
79
|
-
`reduce`, `diff`, `rate`, `fill`, `dedupe`, `materialize`, `sample`,
|
|
80
|
-
`partitionBy`, `pivotByGroup`, …) follows the same shape: TimeSeries
|
|
81
|
-
in, TimeSeries out, schema preserved.
|
|
82
|
-
|
|
83
|
-
## Quick start: live (streaming)
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { LiveSeries, Sequence } from 'pond-ts';
|
|
87
|
-
|
|
88
|
-
// 1. Same schema; this is a live append buffer with retention.
|
|
89
|
-
const live = new LiveSeries({
|
|
90
|
-
name: 'cpu',
|
|
91
|
-
schema,
|
|
92
|
-
retention: { maxAge: '10m' }, // keep only the last 10 minutes
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// 2. Push as events arrive. Each push is validated against the schema.
|
|
96
|
-
live.push([Date.now(), 0.45, 128, 'api-1']);
|
|
97
|
-
|
|
98
|
-
// 3. Compose live views — incremental, push-driven, eviction-aware.
|
|
99
|
-
const recentAvg = live.rolling('5m', { cpu: 'avg' });
|
|
100
|
-
recentAvg.on('event', (e) => render(e.get('cpu')));
|
|
101
|
-
|
|
102
|
-
// 4. Snapshot to a TimeSeries for batch analytics at any time.
|
|
103
|
-
const snap = live.toTimeSeries();
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
The full live surface (`filter`, `map`, `select`, `window`, `aggregate`,
|
|
107
|
-
`rolling`, `reduce`, `diff`, `rate`, `pctChange`, `fill`, `cumulative`,
|
|
108
|
-
`sample`) is incremental — events flow, views emit, retention bounds
|
|
109
|
-
memory.
|
|
110
|
-
|
|
111
|
-
## Quick start: multi-entity
|
|
112
|
-
|
|
113
|
-
`partitionBy` routes events into per-key buffers. Every stateful
|
|
114
|
-
operator downstream of `partitionBy` runs per-partition automatically:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
const perHost = cpu
|
|
118
|
-
.partitionBy('host')
|
|
119
|
-
.rolling('5m', { cpu: 'avg', cpu_sd: 'stdev' });
|
|
120
|
-
|
|
121
|
-
// .collect() fans the per-partition outputs back into a flat TimeSeries
|
|
122
|
-
// with the partition key auto-injected as a column.
|
|
123
|
-
const flat = perHost.collect();
|
|
35
|
+
export function CpuChart() {
|
|
36
|
+
return (
|
|
37
|
+
<ChartContainer width={640}>
|
|
38
|
+
<ChartRow height={240}>
|
|
39
|
+
<Layers>
|
|
40
|
+
<LineChart series={series} column="cpu" as="cpu" />
|
|
41
|
+
</Layers>
|
|
42
|
+
</ChartRow>
|
|
43
|
+
</ChartContainer>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
124
46
|
```
|
|
125
47
|
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
48
|
+
The container **infers the x-axis from the series** — hand it a `ValueSeries`
|
|
49
|
+
(`series.byValue('distance')`) and the same `<LineChart>` plots against distance
|
|
50
|
+
instead of time, with no axis-type prop.
|
|
129
51
|
|
|
130
|
-
##
|
|
52
|
+
## What's in the box
|
|
131
53
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.partitionBy('host')
|
|
140
|
-
.sample({ stride: 10 })
|
|
141
|
-
.rolling('5m', { cpu_avg: 'avg', cpu_sd: 'stdev' });
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
For visualization, the snapshot side ships reservoir sampling too —
|
|
145
|
-
single-pass Algorithm R, sorted by key, fixed point count regardless of
|
|
146
|
-
source size:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
const points = series.sample({ reservoir: { size: 500 } }).toRows();
|
|
150
|
-
// 500 uncorrelated points drawn uniformly from the source.
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Performance
|
|
154
|
-
|
|
155
|
-
pond-ts is faster on **every** comparable operation, with no regressions —
|
|
156
|
-
a **~17x** geometric-mean speedup across the measurable ops, plus a handful
|
|
157
|
-
of transforms (`select` / `rename`) that are **effectively instant** (O(1)
|
|
158
|
-
column rebinds, below the timer's resolution). The advantage grows with data
|
|
159
|
-
size.
|
|
160
|
-
|
|
161
|
-
| Category | Speedup (N=16k) | Notes |
|
|
162
|
-
| ----------------- | ---------------------------------------------------- | --------------------------------------------- |
|
|
163
|
-
| **Rate** | ~120x | Single columnar walk vs Pipeline |
|
|
164
|
-
| **Fill** | 77–87x | Single columnar pass vs Pipeline per strategy |
|
|
165
|
-
| **Aggregation** | 57–82x | O(N+B) bucketing vs O(N×B) Pipeline |
|
|
166
|
-
| **Statistics** | 18–80x | Typed-array reduce vs ImmutableJS iteration |
|
|
167
|
-
| **Alignment** | 42x | Forward cursor vs repeated binary search |
|
|
168
|
-
| **Construction** | 13x | Columnar intake vs ImmutableJS wrapping |
|
|
169
|
-
| **Chained** | 8x | Derived constructors vs per-step Pipeline |
|
|
170
|
-
| **Transforms** | `select`/`rename` instant; `collapse` 30x; `map` ~4x | Column reshapes vs Pipeline |
|
|
171
|
-
| **Event access** | 6x | Array indexing vs ImmutableJS `get()` |
|
|
172
|
-
| **Serialization** | 4x | Lightweight columnar representation |
|
|
173
|
-
|
|
174
|
-
See the [full benchmark results](website/docs/reference/benchmarks.mdx)
|
|
175
|
-
for detailed numbers. Run locally:
|
|
176
|
-
|
|
177
|
-
```sh
|
|
178
|
-
npm run build && node packages/core/bench/vs-pondjs.cjs
|
|
179
|
-
```
|
|
54
|
+
- **Charts** — `LineChart`, `AreaChart`, `BarChart`, `ScatterChart`, `BoxPlot`,
|
|
55
|
+
`BandChart`.
|
|
56
|
+
- **Layout** — `ChartContainer` / `ChartRow` / `Layers` composition, with
|
|
57
|
+
`YAxis`, `XAxis`, and `TimeAxis`.
|
|
58
|
+
- **Interaction** — a cursor system (staffed flag, per-row cursor modes, value
|
|
59
|
+
readouts).
|
|
60
|
+
- **Theming** — `defaultTheme` and `estelaTheme`.
|
|
180
61
|
|
|
181
62
|
## Documentation
|
|
182
63
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
-
|
|
186
|
-
— five-minute walkthrough with batch, live, and React examples.
|
|
187
|
-
- **[Concepts](https://pjm17971.github.io/pond-ts/docs/start-here/concepts)**
|
|
188
|
-
— temporal keys, sequences, windowing, partitioning, triggers, late
|
|
189
|
-
data.
|
|
190
|
-
- **[Transforms reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/transforms/queries)**
|
|
191
|
-
— every batch operator (queries, aggregation, alignment, rolling,
|
|
192
|
-
smoothing, sampling, cleaning, reshape, anomaly detection).
|
|
193
|
-
- **[Live reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/live/live-series)**
|
|
194
|
-
— `LiveSeries`, live transforms, triggering.
|
|
195
|
-
- **[How-to guides](https://pjm17971.github.io/pond-ts/docs/how-to-guides)**
|
|
196
|
-
— building a dashboard, ingesting messy data.
|
|
197
|
-
- **[API reference (auto-generated)](https://pjm17971.github.io/pond-ts/generated-api/core/)**
|
|
198
|
-
— TypeDoc output, every public class and method.
|
|
199
|
-
- **[CHANGELOG](./CHANGELOG.md)** — what shipped in each release.
|
|
200
|
-
|
|
201
|
-
## Examples
|
|
202
|
-
|
|
203
|
-
- **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
|
|
204
|
-
— a working React dashboard that streams synthetic per-host CPU /
|
|
205
|
-
request metrics, computes per-host rolling baselines, flags anomalies
|
|
206
|
-
against ±σ bands, and renders everything as live line and bar charts
|
|
207
|
-
(~600 lines of TypeScript). Walked through end-to-end in
|
|
208
|
-
[Building a dashboard](website/docs/how-to-guides/dashboard-guide.mdx).
|
|
209
|
-
|
|
210
|
-
## Develop
|
|
211
|
-
|
|
212
|
-
The repo is an npm-workspaces monorepo with two published packages
|
|
213
|
-
(`pond-ts`, `@pond-ts/react`). Node 18+ for runtime; Node 20+ for the
|
|
214
|
-
docs site (Docusaurus).
|
|
215
|
-
|
|
216
|
-
```sh
|
|
217
|
-
npm install # one-time, hoists deps for both packages
|
|
218
|
-
npm run build # build both packages
|
|
219
|
-
npm test # runtime + type-level tests on both packages
|
|
220
|
-
npm run format # prettier write across the repo
|
|
221
|
-
npm run verify # format check + build + test (CI parity)
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
`packages/core/` is the `pond-ts` package; `packages/react/` is
|
|
225
|
-
`@pond-ts/react`. Docs live in `website/`.
|
|
64
|
+
Guides, the component reference, and live examples live at
|
|
65
|
+
**<https://pjm17971.github.io/pond-ts/>**. Source and issues:
|
|
66
|
+
[github.com/pjm17971/pond-ts](https://github.com/pjm17971/pond-ts).
|
|
226
67
|
|
|
227
68
|
## License
|
|
228
69
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc -p tsconfig.json",
|
|
31
|
-
"prepack": "cp ../../
|
|
31
|
+
"prepack": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
|
|
32
32
|
"test": "npm run test:type && npm run test:runtime",
|
|
33
33
|
"test:type": "tsc -p tsconfig.types.json",
|
|
34
34
|
"test:runtime": "vitest run",
|
package/dist/annotations.d.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { type AnnotationSpec } from './context.js';
|
|
2
|
-
/**
|
|
3
|
-
* Greedy left→right lane packing for the **top-flag** labels (markers + regions): a
|
|
4
|
-
* label that would overlap the one to its left drops to the next free lane below,
|
|
5
|
-
* so close-in-x labels stack instead of colliding (and a dragged label slides under
|
|
6
|
-
* its neighbour). Returns slot-key → lane (0 = top). Baselines, whose labels anchor
|
|
7
|
-
* at the left at their own y, don't participate.
|
|
8
|
-
*/
|
|
9
|
-
export declare function computeLabelLanes(annotations: readonly AnnotationSpec[], toPixel: (axisX: number) => number): Map<symbol, number>;
|
|
10
|
-
export interface MarkerProps {
|
|
11
|
-
/** x position in axis units — epoch ms on a time axis, the value on a value
|
|
12
|
-
* axis. (The generalisation of the mockup's "time line": a mark at an x, time
|
|
13
|
-
* or value.) */
|
|
14
|
-
at: number;
|
|
15
|
-
/** Chip label; omit to auto-label with the shared x formatter (the axis's). */
|
|
16
|
-
label?: string;
|
|
17
|
-
/** Stable consumer id — a click reports it via the container's
|
|
18
|
-
* `onSelectAnnotation`, so the consumer can track which mark is selected. */
|
|
19
|
-
id?: string;
|
|
20
|
-
/** Controlled selection — brightens to the front (level 1). Handles are an
|
|
21
|
-
* edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
|
|
22
|
-
selected?: boolean;
|
|
23
|
-
/** Whether the mark responds to hover + selection (default `true`). When
|
|
24
|
-
* `false` it's inert background context — drawn at the back (level 3) always,
|
|
25
|
-
* no hover, no select, no edit. */
|
|
26
|
-
selectable?: boolean;
|
|
27
|
-
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
28
|
-
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
29
|
-
hovered?: boolean;
|
|
30
|
-
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
31
|
-
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
32
|
-
* other marks stay static. Independent of the container's global
|
|
33
|
-
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
34
|
-
* `editingId` and sets `editing={editingId === id}`). */
|
|
35
|
-
editing?: boolean;
|
|
36
|
-
/** Make the marker **editable** (in edit mode): dragging its line reports the
|
|
37
|
-
* new `at` (controlled — wire it back to `at`). The whole line moves. */
|
|
38
|
-
onChange?: (at: number) => void;
|
|
39
|
-
}
|
|
40
|
-
/** A vertical line at an x position (a time, a distance, a lap boundary). */
|
|
41
|
-
export declare function Marker({ at, label, id, selected, selectable, hovered, editing, onChange, }: MarkerProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
-
export interface BaselineProps {
|
|
43
|
-
/** y value in the linked axis's units. */
|
|
44
|
-
value: number;
|
|
45
|
-
/** Which `<YAxis>` (by id) to measure against; omit for the row's default axis. */
|
|
46
|
-
axis?: string;
|
|
47
|
-
/** Chip label; omit to format `value` with that axis's formatter. */
|
|
48
|
-
label?: string;
|
|
49
|
-
/** Stable consumer id — a click reports it via `onSelectAnnotation`. */
|
|
50
|
-
id?: string;
|
|
51
|
-
/** Controlled selection — brightens to the front (level 1). Handles are an
|
|
52
|
-
* edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
|
|
53
|
-
selected?: boolean;
|
|
54
|
-
/** Whether the baseline responds to hover + selection (default `true`). When
|
|
55
|
-
* `false` it's inert background context — drawn at the back (level 3) always. */
|
|
56
|
-
selectable?: boolean;
|
|
57
|
-
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
58
|
-
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
59
|
-
hovered?: boolean;
|
|
60
|
-
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
61
|
-
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
62
|
-
* other marks stay static. Independent of the container's global
|
|
63
|
-
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
64
|
-
* `editingId` and sets `editing={editingId === id}`). */
|
|
65
|
-
editing?: boolean;
|
|
66
|
-
/** Make the baseline **editable** (in edit mode): dragging it vertically reports
|
|
67
|
-
* the new `value` (controlled — wire it back to `value`). */
|
|
68
|
-
onChange?: (value: number) => void;
|
|
69
|
-
}
|
|
70
|
-
/** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
|
|
71
|
-
* Its label anchors at the left, at the line's height. */
|
|
72
|
-
export declare function Baseline({ value, axis, label, id, selected, selectable, hovered, editing, onChange, }: BaselineProps): import("react/jsx-runtime").JSX.Element | null;
|
|
73
|
-
export interface RegionProps {
|
|
74
|
-
/** Start x in axis units (time or value). */
|
|
75
|
-
from: number;
|
|
76
|
-
/** End x in axis units. */
|
|
77
|
-
to: number;
|
|
78
|
-
/** Chip label; omit to auto-label `from–to` with the shared x formatter. */
|
|
79
|
-
label?: string;
|
|
80
|
-
/** Stable consumer id — a click (or double-click outside edit) reports it via
|
|
81
|
-
* `onSelectAnnotation`. */
|
|
82
|
-
id?: string;
|
|
83
|
-
/** Controlled selection — brightens to the front (level 1; the body too). Edge
|
|
84
|
-
* handles are an edit-mode hover affordance, not a selection cue. Ignored if not
|
|
85
|
-
* `selectable`. */
|
|
86
|
-
selected?: boolean;
|
|
87
|
-
/** Whether the region responds to hover + selection (default `true`). When
|
|
88
|
-
* `false` it's inert background context — drawn at the back (level 3) always,
|
|
89
|
-
* and the double-click hit-test skips it. */
|
|
90
|
-
selectable?: boolean;
|
|
91
|
-
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
92
|
-
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
93
|
-
hovered?: boolean;
|
|
94
|
-
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
95
|
-
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
96
|
-
* other marks stay static. Independent of the container's global
|
|
97
|
-
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
98
|
-
* `editingId` and sets `editing={editingId === id}`). */
|
|
99
|
-
editing?: boolean;
|
|
100
|
-
/** Make the region **editable** (in edit mode): drag the body to move it (both
|
|
101
|
-
* edges shift), drag an edge to resize. Reports the new `{ from, to }`. */
|
|
102
|
-
onChange?: (next: {
|
|
103
|
-
from: number;
|
|
104
|
-
to: number;
|
|
105
|
-
}) => void;
|
|
106
|
-
}
|
|
107
|
-
/** A shaded span over an x range — a lap, a zone, a selected interval. Its label
|
|
108
|
-
* flies as a flag off the left edge. */
|
|
109
|
-
export declare function Region({ from, to, label, id, selected, selectable, hovered, editing, onChange, }: RegionProps): import("react/jsx-runtime").JSX.Element;
|
|
110
|
-
//# sourceMappingURL=annotations.d.ts.map
|
package/dist/annotations.js
DELETED
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { ContainerContext, RowContext, } from './context.js';
|
|
4
|
-
import { flagChipStyle, flagChipX } from './chip.js';
|
|
5
|
-
import { useSlotKey } from './use-slot-key.js';
|
|
6
|
-
/**
|
|
7
|
-
* User-authored **annotations** — marks you place *on* a chart, in a register
|
|
8
|
-
* deliberately distinct from the data: `<Region>` (a shaded x-span), `<Baseline>`
|
|
9
|
-
* (a horizontal value line), and `<Marker>` (a vertical x line). All three render
|
|
10
|
-
* in the theme's turquoise {@link ChartTheme.annotation} register so a placed mark
|
|
11
|
-
* never reads as data ("the data stays foam; the marks you place are turquoise").
|
|
12
|
-
*
|
|
13
|
-
* They are children of `<Layers>` (so they share the plot's coordinate space) and
|
|
14
|
-
* paint an SVG overlay above the data canvas + below the cursor. Each label is a
|
|
15
|
-
* **flag** (the cursor value flag's shape). **Brightness encodes depth** — a mark
|
|
16
|
-
* draws at one of three {@link ChartTheme.annotation | depth} levels (forward =
|
|
17
|
-
* brightest); `selectable={false}` pins it at the back as inert context.
|
|
18
|
-
*
|
|
19
|
-
* **Three interaction modes** (all controlled — the consumer holds the ids):
|
|
20
|
-
* - **Inspect-select** (any mode): a single click selects a mark
|
|
21
|
-
* ({@link ContainerFrame.onSelectAnnotation}); `hovered`/`onHoverAnnotation` sync
|
|
22
|
-
* hover both ways (e.g. with a legend); both come **forward** (selected = level 1,
|
|
23
|
-
* hover = level 2).
|
|
24
|
-
* - **Single-annotation edit** (any mode): a double-click requests edit of just that
|
|
25
|
-
* mark ({@link ContainerFrame.onEditAnnotation}); the consumer sets its `editing`
|
|
26
|
-
* prop → it gains always-on handles and becomes draggable while the rest stay
|
|
27
|
-
* static. An empty plot click exits (fires `onSelectAnnotation(null)`).
|
|
28
|
-
* - **Global edit** ({@link ContainerFrame.editAnnotations}): the data cursor steps
|
|
29
|
-
* aside and *every* mark with an `onChange` is editable, handles on hover; armed
|
|
30
|
-
* {@link ContainerFrame.creating | create tools} draw new marks.
|
|
31
|
-
*
|
|
32
|
-
* Editing is controlled: a `<Region>` body drags to **move**, its edges **resize**;
|
|
33
|
-
* a `<Marker>`/`<Baseline>` drags whole — each reports via `onChange`. An edit hit
|
|
34
|
-
* area **claims the gesture** (pointer capture + `stopPropagation`) so a drag never
|
|
35
|
-
* starts a pan. Marks register with the container
|
|
36
|
-
* ({@link ContainerFrame.annotations}), which draws each mark's **guide** across the
|
|
37
|
-
* other rows and lets a drag **snap** to other marks' x-positions.
|
|
38
|
-
*/
|
|
39
|
-
/** Fallback when a theme defines no `annotation` token — a neutral turquoise. */
|
|
40
|
-
const DEFAULT_ANNOTATION = {
|
|
41
|
-
color: '#14b8a6',
|
|
42
|
-
fillOpacity: 0.1,
|
|
43
|
-
depth: [1, 0.7, 0.4],
|
|
44
|
-
};
|
|
45
|
-
/** Handle-pill geometry (px) — long axis vs short axis. */
|
|
46
|
-
const HANDLE_LONG = 18;
|
|
47
|
-
const HANDLE_SHORT = 6;
|
|
48
|
-
/** How wide a line's invisible grab area is, each side (px). */
|
|
49
|
-
const HIT_PAD = 5;
|
|
50
|
-
/** How wide a region edge's resize grab area is (px) — sits over the body. */
|
|
51
|
-
const EDGE_GRAB = 8;
|
|
52
|
-
/** Flag-chip top offset (px from the row top) — shared so labels align. */
|
|
53
|
-
const FLAG_TOP = 2;
|
|
54
|
-
/** Depth level (1 = forward/brightest … 3 = back/dimmest) → an index into the
|
|
55
|
-
* theme's `depth` ramp. Brighter reads as more forward, i.e. more attention.
|
|
56
|
-
*
|
|
57
|
-
* A mark's LINES (marker/baseline line, region edges) and a region's BODY fill
|
|
58
|
-
* can sit at different levels: in edit mode the lines come fully forward (level
|
|
59
|
-
* 1) while a region body stays one step back (level 2), so the edges read as the
|
|
60
|
-
* grabbable thing. A non-`selectable` mark is inert background context — always
|
|
61
|
-
* level 3, ignoring hover + selection. */
|
|
62
|
-
function lineLevel(selectable, editing, hovering, selected) {
|
|
63
|
-
if (!selectable)
|
|
64
|
-
return 3;
|
|
65
|
-
if (selected)
|
|
66
|
-
return 1;
|
|
67
|
-
if (editing)
|
|
68
|
-
return 1; // edit mode brings the structural lines forward
|
|
69
|
-
if (hovering)
|
|
70
|
-
return 2;
|
|
71
|
-
return 3;
|
|
72
|
-
}
|
|
73
|
-
/** Region body-fill level — like {@link lineLevel} but one step back in edit mode. */
|
|
74
|
-
function bodyLevel(selectable, editing, hovering, selected) {
|
|
75
|
-
if (!selectable)
|
|
76
|
-
return 3;
|
|
77
|
-
if (selected)
|
|
78
|
-
return 1;
|
|
79
|
-
if (editing || hovering)
|
|
80
|
-
return 2;
|
|
81
|
-
return 3;
|
|
82
|
-
}
|
|
83
|
-
/** Region body-fill opacity multiplier by depth level (the fill is subtle so the
|
|
84
|
-
* data reads through; it lifts as the region comes forward). */
|
|
85
|
-
const FILL_MULT = [1.6, 1.3, 1];
|
|
86
|
-
/** Pick the value for a depth level (1–3) from a three-stop ramp. Indexes by a
|
|
87
|
-
* literal so the lookup is total (no out-of-range `undefined`). */
|
|
88
|
-
function rampAt(ramp, level) {
|
|
89
|
-
return level === 1 ? ramp[0] : level === 2 ? ramp[1] : ramp[2];
|
|
90
|
-
}
|
|
91
|
-
/** The full-plot overlay each annotation paints into — above the data canvas,
|
|
92
|
-
* below the cursor. Inert to the pointer by default; an edit-mode hit area opts
|
|
93
|
-
* back in to `pointerEvents: auto` for itself only. */
|
|
94
|
-
const overlayStyle = {
|
|
95
|
-
position: 'absolute',
|
|
96
|
-
top: 0,
|
|
97
|
-
left: 0,
|
|
98
|
-
pointerEvents: 'none',
|
|
99
|
-
};
|
|
100
|
-
/** Read the container + row frames an annotation needs, or throw if misplaced. */
|
|
101
|
-
function useAnnotationFrame(name) {
|
|
102
|
-
const container = useContext(ContainerContext);
|
|
103
|
-
if (container === null) {
|
|
104
|
-
throw new Error(`<${name}> must be rendered inside a <ChartContainer>`);
|
|
105
|
-
}
|
|
106
|
-
const row = useContext(RowContext);
|
|
107
|
-
if (row === null) {
|
|
108
|
-
throw new Error(`<${name}> must be rendered inside a <ChartRow>`);
|
|
109
|
-
}
|
|
110
|
-
const ann = container.theme.annotation ?? DEFAULT_ANNOTATION;
|
|
111
|
-
return { container, row, ann };
|
|
112
|
-
}
|
|
113
|
-
/** Register this annotation with the container (so it can draw the mark's guide on
|
|
114
|
-
* other rows, order regions, and serve snap targets), keyed by the caller's stable
|
|
115
|
-
* per-instance slot key; unregister on unmount. `xs` should be memoised by the
|
|
116
|
-
* caller so the effect only re-runs when the position actually moves. */
|
|
117
|
-
function useRegisterAnnotation(container, key, id, rowKey, kind, xs, selected, selectable, editing, label) {
|
|
118
|
-
const { registerAnnotation, unregisterAnnotation } = container;
|
|
119
|
-
useEffect(() => () => unregisterAnnotation(key), [unregisterAnnotation, key]);
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
registerAnnotation(key, {
|
|
122
|
-
key,
|
|
123
|
-
id,
|
|
124
|
-
kind,
|
|
125
|
-
rowKey,
|
|
126
|
-
xs,
|
|
127
|
-
selected,
|
|
128
|
-
selectable,
|
|
129
|
-
editing,
|
|
130
|
-
label,
|
|
131
|
-
});
|
|
132
|
-
}, [
|
|
133
|
-
registerAnnotation,
|
|
134
|
-
key,
|
|
135
|
-
id,
|
|
136
|
-
kind,
|
|
137
|
-
rowKey,
|
|
138
|
-
xs,
|
|
139
|
-
selected,
|
|
140
|
-
selectable,
|
|
141
|
-
editing,
|
|
142
|
-
label,
|
|
143
|
-
]);
|
|
144
|
-
}
|
|
145
|
-
/** Vertical px between stacked label lanes. */
|
|
146
|
-
const LANE_H = 22;
|
|
147
|
-
/** Rough chip-width model for overlap detection (monospace-ish: chars × width +
|
|
148
|
-
* padding) and the min px gap kept between two labels sharing a lane. */
|
|
149
|
-
const LABEL_CHAR_W = 7;
|
|
150
|
-
const LABEL_PAD = 16;
|
|
151
|
-
const LANE_GAP = 6;
|
|
152
|
-
/**
|
|
153
|
-
* Greedy left→right lane packing for the **top-flag** labels (markers + regions): a
|
|
154
|
-
* label that would overlap the one to its left drops to the next free lane below,
|
|
155
|
-
* so close-in-x labels stack instead of colliding (and a dragged label slides under
|
|
156
|
-
* its neighbour). Returns slot-key → lane (0 = top). Baselines, whose labels anchor
|
|
157
|
-
* at the left at their own y, don't participate.
|
|
158
|
-
*/
|
|
159
|
-
export function computeLabelLanes(annotations, toPixel) {
|
|
160
|
-
const flags = annotations
|
|
161
|
-
.filter((a) => (a.kind === 'marker' || a.kind === 'region') &&
|
|
162
|
-
a.label.length > 0 &&
|
|
163
|
-
a.xs.length > 0)
|
|
164
|
-
.map((a) => {
|
|
165
|
-
const ax = a.kind === 'region' ? Math.min(a.xs[0], a.xs[1]) : a.xs[0];
|
|
166
|
-
return {
|
|
167
|
-
key: a.key,
|
|
168
|
-
left: toPixel(ax),
|
|
169
|
-
width: a.label.length * LABEL_CHAR_W + LABEL_PAD,
|
|
170
|
-
};
|
|
171
|
-
})
|
|
172
|
-
.sort((p, q) => p.left - q.left);
|
|
173
|
-
const laneEnds = []; // right-px of the last label placed in each lane
|
|
174
|
-
const lanes = new Map();
|
|
175
|
-
for (const f of flags) {
|
|
176
|
-
let lane = 0;
|
|
177
|
-
while (lane < laneEnds.length && laneEnds[lane] + LANE_GAP > f.left) {
|
|
178
|
-
lane += 1;
|
|
179
|
-
}
|
|
180
|
-
laneEnds[lane] = f.left + f.width;
|
|
181
|
-
lanes.set(f.key, lane);
|
|
182
|
-
}
|
|
183
|
-
return lanes;
|
|
184
|
-
}
|
|
185
|
-
/** A mark's hover state, synced both ways with the consumer. The effective hover
|
|
186
|
-
* is the local pointer hover **OR** the controlled `hovered` prop (so a legend row
|
|
187
|
-
* can light the mark remotely); `reportHover` mirrors local pointer enter/leave out
|
|
188
|
-
* to {@link ContainerFrame.onHoverAnnotation} by `id` (so the mark can light the
|
|
189
|
-
* legend). A mark with no `id` keeps its hover purely local. */
|
|
190
|
-
function useAnnotationHover(container, id, hovered) {
|
|
191
|
-
const [selfHover, setSelfHover] = useState(false);
|
|
192
|
-
const hovering = selfHover || hovered === true;
|
|
193
|
-
const reportHover = (h) => {
|
|
194
|
-
setSelfHover(h);
|
|
195
|
-
if (id !== undefined)
|
|
196
|
-
container.onHoverAnnotation?.(h ? id : null);
|
|
197
|
-
};
|
|
198
|
-
return { hovering, reportHover };
|
|
199
|
-
}
|
|
200
|
-
/** Pixel radius within which a drag snaps to a guideline (another mark's x). */
|
|
201
|
-
const SNAP_PX = 6;
|
|
202
|
-
/**
|
|
203
|
-
* Snap a dragged plot-pixel `px` to the nearest **guideline** — another
|
|
204
|
-
* annotation's x — within {@link SNAP_PX}. Returns that guideline's **axis** value
|
|
205
|
-
* to snap to, or `null` if none is near (the caller keeps the raw position).
|
|
206
|
-
* Excludes the dragging mark's own `key`, and reads the same registry the guides
|
|
207
|
-
* draw from, so a drag visibly clicks onto the lines you can see.
|
|
208
|
-
*/
|
|
209
|
-
function snapToGuides(container, selfKey, px) {
|
|
210
|
-
let best = null;
|
|
211
|
-
let bestDist = SNAP_PX;
|
|
212
|
-
for (const a of container.annotations) {
|
|
213
|
-
if (a.key === selfKey)
|
|
214
|
-
continue;
|
|
215
|
-
for (const tx of a.xs) {
|
|
216
|
-
const d = Math.abs(container.xScale(tx) - px);
|
|
217
|
-
if (d < bestDist) {
|
|
218
|
-
bestDist = d;
|
|
219
|
-
best = tx;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return best;
|
|
224
|
-
}
|
|
225
|
-
/** A label chip — the cursor value flag's shape (shared {@link flagChipStyle}:
|
|
226
|
-
* filled, no outline) with text in the annotation register. */
|
|
227
|
-
function Chip({ theme, color, style, children, }) {
|
|
228
|
-
return (_jsx("div", { style: { ...flagChipStyle(theme), color, ...style }, children: children }));
|
|
229
|
-
}
|
|
230
|
-
/** A non-interactive handle pill, shown on hover (edit mode) / when selected. */
|
|
231
|
-
function Pill({ cx, cy, w, h, color, }) {
|
|
232
|
-
return (_jsx("rect", { x: cx - w / 2, y: cy - h / 2, width: w, height: h, rx: 3, fill: color, style: { pointerEvents: 'none' } }));
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* A transparent hit rect over a selectable mark. It **always** reports hover
|
|
236
|
-
* (`onHover`, → level 2 even with edit off), and a **single click that didn't drag
|
|
237
|
-
* selects** the mark (`onSelect`) in *any* mode — the inspect-select — while a
|
|
238
|
-
* **double-click edits** it (`onEdit` — the consumer flips it into single-annotation
|
|
239
|
-
* edit). Both clicks `stopPropagation` so they never reach the plot's data-select /
|
|
240
|
-
* deselect. Only when `editable` does it claim the drag gesture (`stopPropagation` +
|
|
241
|
-
* guarded pointer capture, so a drag never starts a pan) and report the pointer's
|
|
242
|
-
* **plot-pixel** position on press (`onDragStart`) / move (`onDrag`). With editing
|
|
243
|
-
* off, press / move bubble so a pan reads straight through.
|
|
244
|
-
*/
|
|
245
|
-
function DragArea({ x, y, w, h, cursor, editable, onHover, onSelect, onEdit, onDragStart, onDrag, }) {
|
|
246
|
-
const dragging = useRef(false);
|
|
247
|
-
// Tracks whether this press became a drag (moved past a few px) — a click that
|
|
248
|
-
// didn't drag selects instead of edits. Tracked in *both* modes so a pan-drag
|
|
249
|
-
// started on the mark (edit off) doesn't fire a spurious select on release.
|
|
250
|
-
const moved = useRef(false);
|
|
251
|
-
const downAt = useRef(null);
|
|
252
|
-
const at = (e) => {
|
|
253
|
-
const svg = e.currentTarget.ownerSVGElement;
|
|
254
|
-
if (svg === null)
|
|
255
|
-
return [0, 0];
|
|
256
|
-
const r = svg.getBoundingClientRect();
|
|
257
|
-
return [e.clientX - r.left, e.clientY - r.top];
|
|
258
|
-
};
|
|
259
|
-
return (_jsx("rect", { x: x, y: y, width: Math.max(w, 1), height: Math.max(h, 1), fill: "transparent", style: { pointerEvents: 'auto', cursor }, onPointerEnter: () => onHover(true), onPointerLeave: () => {
|
|
260
|
-
if (!dragging.current)
|
|
261
|
-
onHover(false);
|
|
262
|
-
}, onPointerDown: (e) => {
|
|
263
|
-
const p = at(e);
|
|
264
|
-
moved.current = false;
|
|
265
|
-
downAt.current = p; // tracked in both modes for the click/drag guard
|
|
266
|
-
if (!editable)
|
|
267
|
-
return; // edit off: let it bubble (pan reads through)
|
|
268
|
-
e.stopPropagation(); // claim the gesture — don't let the plot start a pan
|
|
269
|
-
dragging.current = true;
|
|
270
|
-
onDragStart?.(p[0], p[1]);
|
|
271
|
-
try {
|
|
272
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
/* ignore (synthetic / already-released pointer) */
|
|
276
|
-
}
|
|
277
|
-
}, onPointerMove: (e) => {
|
|
278
|
-
const [px, py] = at(e);
|
|
279
|
-
const d = downAt.current;
|
|
280
|
-
if (d !== null && Math.hypot(px - d[0], py - d[1]) > 3) {
|
|
281
|
-
moved.current = true;
|
|
282
|
-
}
|
|
283
|
-
if (!editable || !dragging.current)
|
|
284
|
-
return;
|
|
285
|
-
e.stopPropagation();
|
|
286
|
-
onDrag(px, py);
|
|
287
|
-
}, onPointerUp: (e) => {
|
|
288
|
-
if (!dragging.current)
|
|
289
|
-
return;
|
|
290
|
-
e.stopPropagation();
|
|
291
|
-
try {
|
|
292
|
-
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
/* ignore */
|
|
296
|
-
}
|
|
297
|
-
dragging.current = false;
|
|
298
|
-
onHover(false);
|
|
299
|
-
},
|
|
300
|
-
// Click (no drag) selects; double-click edits. Stop both so a mark click
|
|
301
|
-
// never reaches the plot's data-select / deselect.
|
|
302
|
-
onClick: (e) => {
|
|
303
|
-
e.stopPropagation();
|
|
304
|
-
if (!moved.current)
|
|
305
|
-
onSelect?.();
|
|
306
|
-
}, onDoubleClick: (e) => {
|
|
307
|
-
e.stopPropagation();
|
|
308
|
-
onEdit?.();
|
|
309
|
-
} }));
|
|
310
|
-
}
|
|
311
|
-
/** A vertical line at an x position (a time, a distance, a lap boundary). */
|
|
312
|
-
export function Marker({ at, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
|
|
313
|
-
const { container, row, ann } = useAnnotationFrame('Marker');
|
|
314
|
-
const selfKey = useSlotKey();
|
|
315
|
-
const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
|
|
316
|
-
// Draggable right now: global edit mode OR this mark's single-edit flag, and no
|
|
317
|
-
// tool armed, and it has an onChange to report to.
|
|
318
|
-
const editable = (container.editAnnotations || editing) &&
|
|
319
|
-
container.creating === null &&
|
|
320
|
-
onChange !== undefined;
|
|
321
|
-
const xs = useMemo(() => [at], [at]);
|
|
322
|
-
const text = label ?? container.formatTime(at);
|
|
323
|
-
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'marker', xs, selected, selectable, editing, text);
|
|
324
|
-
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
325
|
-
const select = id !== undefined && container.creating === null
|
|
326
|
-
? () => container.onSelectAnnotation?.(id)
|
|
327
|
-
: undefined;
|
|
328
|
-
const edit = id !== undefined && container.creating === null
|
|
329
|
-
? () => container.onEditAnnotation?.(id)
|
|
330
|
-
: undefined;
|
|
331
|
-
const x = container.xScale(at);
|
|
332
|
-
const h = row.height;
|
|
333
|
-
const opacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
|
|
334
|
-
const showHandle = editable && (editing || hovering);
|
|
335
|
-
const lane = container.labelLanes.get(selfKey) ?? 0;
|
|
336
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("line", { x1: x, y1: 0, x2: x, y2: h, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: x, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })), selectable && (_jsx(DragArea, { x: x - HIT_PAD, y: 0, w: 2 * HIT_PAD, h: h, cursor: editing ? 'ew-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.(snapToGuides(container, selfKey, px) ??
|
|
337
|
-
+container.xScale.invert(px)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
338
|
-
top: `${FLAG_TOP + lane * LANE_H}px`,
|
|
339
|
-
...flagChipX(x, container.plotWidth),
|
|
340
|
-
}, children: text })] }));
|
|
341
|
-
}
|
|
342
|
-
/** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
|
|
343
|
-
* Its label anchors at the left, at the line's height. */
|
|
344
|
-
export function Baseline({ value, axis, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
|
|
345
|
-
const { container, row, ann } = useAnnotationFrame('Baseline');
|
|
346
|
-
const selfKey = useSlotKey();
|
|
347
|
-
const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
|
|
348
|
-
// Draggable right now: global edit mode OR this mark's single-edit flag, and no
|
|
349
|
-
// tool armed, and it has an onChange to report to.
|
|
350
|
-
const editable = (container.editAnnotations || editing) &&
|
|
351
|
-
container.creating === null &&
|
|
352
|
-
onChange !== undefined;
|
|
353
|
-
// A horizontal line casts no vertical guide — register with no xs (still
|
|
354
|
-
// tracked for ordering / future use). No snap target either (the guidelines are
|
|
355
|
-
// vertical; a baseline drags vertically).
|
|
356
|
-
const xs = useMemo(() => [], []);
|
|
357
|
-
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'baseline', xs, selected, selectable, editing, label ?? '');
|
|
358
|
-
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
359
|
-
const select = id !== undefined && container.creating === null
|
|
360
|
-
? () => container.onSelectAnnotation?.(id)
|
|
361
|
-
: undefined;
|
|
362
|
-
const edit = id !== undefined && container.creating === null
|
|
363
|
-
? () => container.onEditAnnotation?.(id)
|
|
364
|
-
: undefined;
|
|
365
|
-
const axisId = axis ?? row.defaultAxisId;
|
|
366
|
-
const yScale = row.yScales.get(axisId);
|
|
367
|
-
// The axis may not have resolved yet (a layer mounts before its <YAxis>); skip
|
|
368
|
-
// until its scale exists rather than guessing a domain.
|
|
369
|
-
if (yScale === undefined)
|
|
370
|
-
return null;
|
|
371
|
-
const y = yScale(value);
|
|
372
|
-
const w = container.plotWidth;
|
|
373
|
-
const opacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
|
|
374
|
-
const showHandle = editable && (editing || hovering);
|
|
375
|
-
const fmt = row.formats.get(axisId);
|
|
376
|
-
const text = label ?? (fmt ? fmt(value) : String(value));
|
|
377
|
-
// Handle pill near the right end (clears the left-anchored label).
|
|
378
|
-
const handleX = w - 14;
|
|
379
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: { top: `${y}px`, left: '2px', transform: 'translateY(-50%)' }, children: text })] }));
|
|
380
|
-
}
|
|
381
|
-
/** A shaded span over an x range — a lap, a zone, a selected interval. Its label
|
|
382
|
-
* flies as a flag off the left edge. */
|
|
383
|
-
export function Region({ from, to, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
|
|
384
|
-
const { container, row, ann } = useAnnotationFrame('Region');
|
|
385
|
-
const selfKey = useSlotKey();
|
|
386
|
-
const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
|
|
387
|
-
// Draggable right now: global edit mode OR this mark's single-edit flag, and no
|
|
388
|
-
// tool armed, and it has an onChange to report to.
|
|
389
|
-
const editable = (container.editAnnotations || editing) &&
|
|
390
|
-
container.creating === null &&
|
|
391
|
-
onChange !== undefined;
|
|
392
|
-
const xs = useMemo(() => [from, to], [from, to]);
|
|
393
|
-
const text = label ?? `${container.formatTime(from)}–${container.formatTime(to)}`;
|
|
394
|
-
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'region', xs, selected, selectable, editing, text);
|
|
395
|
-
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
396
|
-
const select = id !== undefined && container.creating === null
|
|
397
|
-
? () => container.onSelectAnnotation?.(id)
|
|
398
|
-
: undefined;
|
|
399
|
-
const edit = id !== undefined && container.creating === null
|
|
400
|
-
? () => container.onEditAnnotation?.(id)
|
|
401
|
-
: undefined;
|
|
402
|
-
const xa = container.xScale(from);
|
|
403
|
-
const xb = container.xScale(to);
|
|
404
|
-
const left = Math.min(xa, xb);
|
|
405
|
-
const spanW = Math.abs(xb - xa);
|
|
406
|
-
const h = row.height;
|
|
407
|
-
// The edges (lines) come fully forward in edit mode; the body fill sits one step
|
|
408
|
-
// back (so the edges read as the grabbable thing). Both jump to level 1 selected.
|
|
409
|
-
const edgeOpacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
|
|
410
|
-
const fillOpacity = ann.fillOpacity *
|
|
411
|
-
rampAt(FILL_MULT, bodyLevel(selectable, editable, hovering, selected));
|
|
412
|
-
const showHandles = editable && (editing || hovering);
|
|
413
|
-
const lane = container.labelLanes.get(selfKey) ?? 0;
|
|
414
|
-
// Body move-drag: capture the start position + pointer on press, then move by
|
|
415
|
-
// the TOTAL delta from there, so the *raw* position accumulates from a fixed
|
|
416
|
-
// origin. Snap is applied only to the output — never fed back into this
|
|
417
|
-
// accumulator — so once you drag past SNAP_PX the region releases cleanly
|
|
418
|
-
// instead of re-snapping on every small move.
|
|
419
|
-
const dragRef = useRef(null);
|
|
420
|
-
const edge = (atX) => (_jsx("line", { x1: atX, y1: 0, x2: atX, y2: h, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, shapeRendering: "crispEdges" }));
|
|
421
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("rect", { x: left, y: 0, width: spanW, height: h, fill: ann.color, opacity: fillOpacity }), edge(xa), edge(xb), showHandles && (_jsxs(_Fragment, { children: [_jsx(Pill, { cx: xa, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color }), _jsx(Pill, { cx: xb, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })] })), selectable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: left, y: 0, w: spanW, h: h, cursor: editing ? 'grab' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragStart: (px) => {
|
|
422
|
-
dragRef.current = { from, to, startPx: px };
|
|
423
|
-
}, onDrag: (px) => {
|
|
424
|
-
const s = dragRef.current;
|
|
425
|
-
if (s === null)
|
|
426
|
-
return;
|
|
427
|
-
// Raw position = start + TOTAL pointer delta (snap-independent),
|
|
428
|
-
// so dragging past SNAP_PX escapes a snapped edge.
|
|
429
|
-
const delta = +container.xScale.invert(px) -
|
|
430
|
-
+container.xScale.invert(s.startPx);
|
|
431
|
-
let nf = s.from + delta;
|
|
432
|
-
let nt = s.to + delta;
|
|
433
|
-
// Snap whichever edge lands near a guideline, keeping the width —
|
|
434
|
-
// output only, so the raw drift above can pull free of it.
|
|
435
|
-
const sf = snapToGuides(container, selfKey, container.xScale(nf));
|
|
436
|
-
const st = snapToGuides(container, selfKey, container.xScale(nt));
|
|
437
|
-
if (sf !== null) {
|
|
438
|
-
nt += sf - nf;
|
|
439
|
-
nf = sf;
|
|
440
|
-
}
|
|
441
|
-
else if (st !== null) {
|
|
442
|
-
nf += st - nt;
|
|
443
|
-
nt = st;
|
|
444
|
-
}
|
|
445
|
-
onChange?.({ from: nf, to: nt });
|
|
446
|
-
} }), editable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: xa - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.({
|
|
447
|
-
from: snapToGuides(container, selfKey, px) ??
|
|
448
|
-
+container.xScale.invert(px),
|
|
449
|
-
to,
|
|
450
|
-
}) }), _jsx(DragArea, { x: xb - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.({
|
|
451
|
-
from,
|
|
452
|
-
to: snapToGuides(container, selfKey, px) ??
|
|
453
|
-
+container.xScale.invert(px),
|
|
454
|
-
}) })] }))] }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
455
|
-
top: `${FLAG_TOP + lane * LANE_H}px`,
|
|
456
|
-
...flagChipX(left, container.plotWidth),
|
|
457
|
-
}, children: text })] }));
|
|
458
|
-
}
|
|
459
|
-
//# sourceMappingURL=annotations.js.map
|
package/dist/chip.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react';
|
|
2
|
-
import type { ChartTheme } from './theme.js';
|
|
3
|
-
/**
|
|
4
|
-
* The shared **chip** look — one source of truth for the cursor's value flag and
|
|
5
|
-
* the annotation labels, so a placed label and a cursor flag read as the same
|
|
6
|
-
* object. A filled panel (the theme's `chip.background`) with crisp tabular text
|
|
7
|
-
* and **no outline** — the fill delineates it (a border read as a hard edge on a
|
|
8
|
-
* dark ground). The caller layers on `color` (the series / annotation hue) and the
|
|
9
|
-
* position (`top` + `left`/`right`).
|
|
10
|
-
*
|
|
11
|
-
* Note: on a light theme whose `chip.background` doesn't contrast with the plot
|
|
12
|
-
* ground, the borderless chip needs another delineator (a subtle shadow, or a
|
|
13
|
-
* contrasting chip background) — a token to settle before this ships.
|
|
14
|
-
*/
|
|
15
|
-
export declare function flagChipStyle(theme: ChartTheme): CSSProperties;
|
|
16
|
-
/**
|
|
17
|
-
* Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
|
|
18
|
-
* `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
|
|
19
|
-
* in-plot. Shared by the cursor value flag and the annotation labels so a chip
|
|
20
|
-
* sits **identically** relative to its pole in both.
|
|
21
|
-
*/
|
|
22
|
-
export declare function flagChipX(x: number, plotWidth: number): CSSProperties;
|
|
23
|
-
//# sourceMappingURL=chip.d.ts.map
|
package/dist/chip.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared **chip** look — one source of truth for the cursor's value flag and
|
|
3
|
-
* the annotation labels, so a placed label and a cursor flag read as the same
|
|
4
|
-
* object. A filled panel (the theme's `chip.background`) with crisp tabular text
|
|
5
|
-
* and **no outline** — the fill delineates it (a border read as a hard edge on a
|
|
6
|
-
* dark ground). The caller layers on `color` (the series / annotation hue) and the
|
|
7
|
-
* position (`top` + `left`/`right`).
|
|
8
|
-
*
|
|
9
|
-
* Note: on a light theme whose `chip.background` doesn't contrast with the plot
|
|
10
|
-
* ground, the borderless chip needs another delineator (a subtle shadow, or a
|
|
11
|
-
* contrasting chip background) — a token to settle before this ships.
|
|
12
|
-
*/
|
|
13
|
-
export function flagChipStyle(theme) {
|
|
14
|
-
return {
|
|
15
|
-
position: 'absolute',
|
|
16
|
-
background: theme.chip?.background,
|
|
17
|
-
borderRadius: '3px',
|
|
18
|
-
padding: '0 4px',
|
|
19
|
-
fontFamily: theme.font.family,
|
|
20
|
-
fontSize: `${theme.font.size}px`,
|
|
21
|
-
fontVariantNumeric: 'tabular-nums',
|
|
22
|
-
whiteSpace: 'nowrap',
|
|
23
|
-
pointerEvents: 'none',
|
|
24
|
-
lineHeight: 1.5,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/** Gap (px) between a flag chip and its pole — the cursor staff or an annotation's
|
|
28
|
-
* line — so the chip floats just beside the pole rather than sitting on it. */
|
|
29
|
-
const FLAG_GAP = 4;
|
|
30
|
-
/** Past this fraction of the plot, a flag flips to the left of its pole to stay in. */
|
|
31
|
-
const FLAG_FLIP = 0.85;
|
|
32
|
-
/**
|
|
33
|
-
* Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
|
|
34
|
-
* `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
|
|
35
|
-
* in-plot. Shared by the cursor value flag and the annotation labels so a chip
|
|
36
|
-
* sits **identically** relative to its pole in both.
|
|
37
|
-
*/
|
|
38
|
-
export function flagChipX(x, plotWidth) {
|
|
39
|
-
return x > plotWidth * FLAG_FLIP
|
|
40
|
-
? { right: `${plotWidth - x + FLAG_GAP}px` }
|
|
41
|
-
: { left: `${x + FLAG_GAP}px` };
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=chip.js.map
|