@pond-ts/charts 0.58.0 → 0.60.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/API.md +580 -0
- package/CHANGELOG.md +339 -1
- package/dist/AreaChart.d.ts +53 -1
- package/dist/AreaChart.js +16 -3
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +88 -73
- package/dist/BarList.d.ts +22 -0
- package/dist/BarList.js +42 -9
- package/dist/ChartContainer.d.ts +175 -3
- package/dist/ChartContainer.js +190 -11
- package/dist/ChartRow.js +92 -2
- package/dist/Layers.js +14 -4
- package/dist/XAxis.js +19 -14
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +5 -3
- package/dist/area.d.ts +43 -1
- package/dist/area.js +122 -5
- package/dist/bars.d.ts +10 -3
- package/dist/bars.js +13 -9
- package/dist/context.d.ts +46 -8
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/format.d.ts +16 -1
- package/dist/format.js +17 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -0
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/theme.d.ts +80 -4
- package/dist/theme.js +3 -0
- package/dist/use-band-ladder.d.ts +30 -0
- package/dist/use-band-ladder.js +81 -0
- package/dist/useChartFrame.d.ts +122 -0
- package/dist/useChartFrame.js +155 -0
- package/dist/useChartLegend.d.ts +8 -0
- package/dist/viewport.d.ts +35 -2
- package/dist/viewport.js +53 -6
- package/dist/yticks.d.ts +8 -1
- package/dist/yticks.js +109 -1
- package/package.json +6 -5
package/dist/viewport.js
CHANGED
|
@@ -70,8 +70,26 @@ function roundRange(lo, hi) {
|
|
|
70
70
|
* is snapped to whole milliseconds ({@link roundRange}) — `dt` comes from a pixel
|
|
71
71
|
* delta through `xScale.invert()`, so it is fractional by construction.
|
|
72
72
|
*/
|
|
73
|
-
export function panRange(range, dt) {
|
|
74
|
-
|
|
73
|
+
export function panRange(range, dt, options = {}) {
|
|
74
|
+
const { log = false, snap = !log } = options;
|
|
75
|
+
if (log) {
|
|
76
|
+
// On a log axis a pixel drag is a RATIO, not an offset. `dt` arrives as a
|
|
77
|
+
// domain delta from `xScale.invert`, which on a log scale is meaningless as
|
|
78
|
+
// an addend: adding 100s near the 1s end walks off the plot, and near the
|
|
79
|
+
// 3h end barely moves. Convert it to the fraction of the visible decades it
|
|
80
|
+
// represents and shift by that instead, so a drag of N pixels moves the
|
|
81
|
+
// same visual distance wherever it starts.
|
|
82
|
+
const [lo, hi] = range;
|
|
83
|
+
if (!(lo > 0) || !(hi > lo))
|
|
84
|
+
return [lo, hi];
|
|
85
|
+
const span = hi - lo;
|
|
86
|
+
const f = span > 0 ? dt / span : 0; // fraction of the window dragged
|
|
87
|
+
const k = Math.exp(Math.log(hi / lo) * f); // …as a ratio over the decades
|
|
88
|
+
return [lo * k, hi * k];
|
|
89
|
+
}
|
|
90
|
+
const lo = range[0] + dt;
|
|
91
|
+
const hi = range[1] + dt;
|
|
92
|
+
return snap ? roundRange(lo, hi) : [lo, hi];
|
|
75
93
|
}
|
|
76
94
|
/**
|
|
77
95
|
* Zoom `range` around `pivot` (ms) by `factor` — `< 1` zooms in, `> 1` out, with
|
|
@@ -85,15 +103,44 @@ export function panRange(range, dt) {
|
|
|
85
103
|
* lands on the 1 ms floor the snap guarantees, which is the finest view this
|
|
86
104
|
* model has.
|
|
87
105
|
*/
|
|
88
|
-
export function zoomRange(range, pivot, factor, minDuration = 1) {
|
|
106
|
+
export function zoomRange(range, pivot, factor, minDuration = 1, options = {}) {
|
|
107
|
+
const { log = false, snap = !log } = options;
|
|
108
|
+
if (log) {
|
|
109
|
+
// The same arithmetic, done in log space — which is where a log axis is
|
|
110
|
+
// linear. Zooming a log domain multiplicatively is what keeps the pivot
|
|
111
|
+
// under the cursor; doing it additively (as the linear branch does) drags
|
|
112
|
+
// the value under the pointer sideways, which is the one thing zoom must
|
|
113
|
+
// never do.
|
|
114
|
+
//
|
|
115
|
+
// `minDuration` is read as a minimum RATIO between the ends rather than a
|
|
116
|
+
// minimum difference, because a span on a log axis is a number of decades.
|
|
117
|
+
const [lo0, hi0] = range;
|
|
118
|
+
if (!(lo0 > 0) || !(hi0 > lo0) || !(pivot > 0))
|
|
119
|
+
return [lo0, hi0];
|
|
120
|
+
const L = Math.log(lo0);
|
|
121
|
+
const H = Math.log(hi0);
|
|
122
|
+
const P = Math.min(H, Math.max(L, Math.log(pivot)));
|
|
123
|
+
let l = P - (P - L) * factor;
|
|
124
|
+
let h = P + (H - P) * factor;
|
|
125
|
+
const floor = Math.log(Math.max(minDuration, 1 + 1e-9)); // ratio → decades
|
|
126
|
+
if (h - l < floor) {
|
|
127
|
+
const frac = H - L > 0 ? (P - L) / (H - L) : 0.5;
|
|
128
|
+
l = P - floor * frac;
|
|
129
|
+
h = P + floor * (1 - frac);
|
|
130
|
+
}
|
|
131
|
+
return [Math.exp(l), Math.exp(h)];
|
|
132
|
+
}
|
|
89
133
|
const lo = pivot - (pivot - range[0]) * factor;
|
|
90
134
|
const hi = pivot + (range[1] - pivot) * factor;
|
|
91
|
-
if (hi - lo >= minDuration)
|
|
92
|
-
return roundRange(lo, hi);
|
|
135
|
+
if (hi - lo >= minDuration) {
|
|
136
|
+
return snap ? roundRange(lo, hi) : [lo, hi];
|
|
137
|
+
}
|
|
93
138
|
// Floor reached: hold the pivot's fractional position, set span = minDuration.
|
|
94
139
|
const span = range[1] - range[0];
|
|
95
140
|
const frac = span > 0 ? (pivot - range[0]) / span : 0.5;
|
|
96
|
-
|
|
141
|
+
const flo = pivot - minDuration * frac;
|
|
142
|
+
const fhi = pivot + minDuration * (1 - frac);
|
|
143
|
+
return snap ? roundRange(flo, fhi) : [flo, fhi];
|
|
97
144
|
}
|
|
98
145
|
/**
|
|
99
146
|
* Pan a range on a **trading-time** axis: shift both endpoints by the same
|
package/dist/yticks.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ interface TickableScale {
|
|
|
23
23
|
domain(): number[];
|
|
24
24
|
/** Present on d3's `scaleLog` and on no other continuous scale. */
|
|
25
25
|
base?: () => number;
|
|
26
|
+
/** Present on d3's `scaleSymlog` and on no other continuous scale — the
|
|
27
|
+
* linear window's half-width, i.e. the knee ([PND-SYMLOG]). */
|
|
28
|
+
constant?: () => number;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* The y tick **values** a `<YAxis>`'s labels and the row's gridlines draw —
|
|
@@ -54,11 +57,15 @@ interface TickableScale {
|
|
|
54
57
|
* d3's within-decade selection (2,3,…9 × 10ⁿ) is the right answer and is well
|
|
55
58
|
* behaved — so that case defers to the scale.
|
|
56
59
|
*
|
|
60
|
+
* Used by **both** axes. Nothing in here was ever y-specific — it detects log
|
|
61
|
+
* and symlog structurally and defers to the scale for everything else — and
|
|
62
|
+
* `<ChartContainer xScale="log">` made that concrete ([PND-XLOG]).
|
|
63
|
+
*
|
|
57
64
|
* Log detection is structural: `base()` exists on `scaleLog` and on no other
|
|
58
65
|
* continuous scale. The alternative is threading the axis kind down to the
|
|
59
66
|
* gridline site, which has no `AxisSpec` in scope — the same localized-shape
|
|
60
67
|
* approach `resolveBarBaseline` takes to read `.domain()`.
|
|
61
68
|
*/
|
|
62
|
-
export declare function
|
|
69
|
+
export declare function tickValues(scale: TickableScale, count: number): number[];
|
|
63
70
|
export {};
|
|
64
71
|
//# sourceMappingURL=yticks.d.ts.map
|
package/dist/yticks.js
CHANGED
|
@@ -55,12 +55,18 @@ export function resolveYTickCount(height, explicit) {
|
|
|
55
55
|
* d3's within-decade selection (2,3,…9 × 10ⁿ) is the right answer and is well
|
|
56
56
|
* behaved — so that case defers to the scale.
|
|
57
57
|
*
|
|
58
|
+
* Used by **both** axes. Nothing in here was ever y-specific — it detects log
|
|
59
|
+
* and symlog structurally and defers to the scale for everything else — and
|
|
60
|
+
* `<ChartContainer xScale="log">` made that concrete ([PND-XLOG]).
|
|
61
|
+
*
|
|
58
62
|
* Log detection is structural: `base()` exists on `scaleLog` and on no other
|
|
59
63
|
* continuous scale. The alternative is threading the axis kind down to the
|
|
60
64
|
* gridline site, which has no `AxisSpec` in scope — the same localized-shape
|
|
61
65
|
* approach `resolveBarBaseline` takes to read `.domain()`.
|
|
62
66
|
*/
|
|
63
|
-
export function
|
|
67
|
+
export function tickValues(scale, count) {
|
|
68
|
+
if (typeof scale.constant === 'function')
|
|
69
|
+
return symlogTickValues(scale, count);
|
|
64
70
|
if (typeof scale.base !== 'function')
|
|
65
71
|
return scale.ticks(count);
|
|
66
72
|
const domain = scale.domain();
|
|
@@ -80,4 +86,106 @@ export function yTickValues(scale, count) {
|
|
|
80
86
|
out.push(10 ** e);
|
|
81
87
|
return out;
|
|
82
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Tick values for a **symlog** axis — linear through zero, logarithmic beyond
|
|
91
|
+
* ([PND-SYMLOG]).
|
|
92
|
+
*
|
|
93
|
+
* **This is the feature, not a refinement of it.** d3's `scaleSymlog` supplies
|
|
94
|
+
* the transform but its `ticks()` is `linearish` — evenly spaced in *value*. On
|
|
95
|
+
* a ±1M domain with a 20k knee that yields `-1M, -500k, 0, 500k, 1M`: **nothing
|
|
96
|
+
* at all below the knee**, which is the region a symlog axis exists to reveal.
|
|
97
|
+
* The mapping does spread that region generously (0→250px, 20k→294px,
|
|
98
|
+
* 100k→364px on a 500px range), so such a chart is readable-but-unlabelled —
|
|
99
|
+
* confidently gridded on the one part of the scale that isn't the point. Owning
|
|
100
|
+
* the ladder is therefore inseparable from owning the transform.
|
|
101
|
+
*
|
|
102
|
+
* The ladder, and why each piece is there:
|
|
103
|
+
*
|
|
104
|
+
* - **Zero, always.** It is the axis's centre of symmetry and the one value a
|
|
105
|
+
* symlog scale is chosen to keep visible.
|
|
106
|
+
* - **The knee, ±`constant`.** Where the reading changes from linear to
|
|
107
|
+
* logarithmic. Unlabelled, a reader has no way to know which régime a given
|
|
108
|
+
* gap belongs to, and the same pixel distance means different things either
|
|
109
|
+
* side of it.
|
|
110
|
+
* - **Decades beyond the knee, mirrored.** What a log plot is conventionally
|
|
111
|
+
* gridded on, thinned by the same "every `k`th power of ten" rule the log path
|
|
112
|
+
* above uses, so it degrades predictably as the row shrinks instead of
|
|
113
|
+
* exploding.
|
|
114
|
+
* - **Bounds are NOT labelled.** A data-derived bound is rarely round, so
|
|
115
|
+
* printing it puts an arbitrary number next to a decade — the noise a log grid
|
|
116
|
+
* exists to avoid.
|
|
117
|
+
*
|
|
118
|
+
* Below one decade of span past the knee there is nothing to grid
|
|
119
|
+
* logarithmically, so it defers to `scale.ticks(count)` — which is linear, and
|
|
120
|
+
* correct, because inside the knee symlog *is* linear.
|
|
121
|
+
*
|
|
122
|
+
* **Clip first, then thin — never the other way round.** A pan/zoom (or explicit
|
|
123
|
+
* bounds) can leave a window that contains *none* of the ideal ladder:
|
|
124
|
+
* `[510_000, 990_000]` with a 19_800 knee excludes zero, both knees, and its one
|
|
125
|
+
* candidate decade, so an order that thinned a symmetric ladder and clipped
|
|
126
|
+
* afterwards handed **`[]`** to the labels and the gridlines — an axis with no
|
|
127
|
+
* ticks at all, which reads as a rendering failure rather than as a scale. The
|
|
128
|
+
* budget is likewise spent on what *survives* the domain, not on an ideal
|
|
129
|
+
* two-sided ladder, so an asymmetric window is not thinned as if it were twice
|
|
130
|
+
* its size. If nothing survives, the linear ticks are the honest answer.
|
|
131
|
+
*
|
|
132
|
+
* Detection is structural, matching the log path's use of `base()`: `constant()`
|
|
133
|
+
* exists on `scaleSymlog` and on no other continuous scale.
|
|
134
|
+
*/
|
|
135
|
+
function symlogTickValues(scale, count) {
|
|
136
|
+
const domain = scale.domain();
|
|
137
|
+
const lo = Math.min(domain[0], domain[domain.length - 1]);
|
|
138
|
+
const hi = Math.max(domain[0], domain[domain.length - 1]);
|
|
139
|
+
const knee = Math.abs(scale.constant?.() ?? 1);
|
|
140
|
+
const maxAbs = Math.max(Math.abs(lo), Math.abs(hi));
|
|
141
|
+
// Finiteness is checked, not assumed: an explicit `max={Infinity}` reaches here
|
|
142
|
+
// intact, and `floor(log10(Infinity))` is `Infinity` — which made the decade
|
|
143
|
+
// loop's `e += step` a no-op and hung the render in a `for` that could never
|
|
144
|
+
// end. d3's own linear ticks return `[]` on such a domain, so deferring is both
|
|
145
|
+
// safe and the truthful answer for a domain with no finite extent.
|
|
146
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || !Number.isFinite(knee))
|
|
147
|
+
return scale.ticks(count);
|
|
148
|
+
if (!(knee > 0) || !(maxAbs > knee) || !(hi > lo))
|
|
149
|
+
return scale.ticks(count);
|
|
150
|
+
// The ladder starts at the first decade **at least half a decade above the
|
|
151
|
+
// knee** (`× √10`), not merely above it. `ceil(log10(knee))` alone puts a
|
|
152
|
+
// decade arbitrarily close to the knee tick whenever the knee lands just under
|
|
153
|
+
// a power of ten — a data-derived `maxAbs` of 4.95e6 gives a 99k knee and a
|
|
154
|
+
// 100k decade, two ticks a few pixels apart whose labels round to the *same
|
|
155
|
+
// string*. Since the knee itself is always drawn, dropping that decade loses
|
|
156
|
+
// no information; a chart that prints one number twice at two positions is
|
|
157
|
+
// reporting something false about the scale.
|
|
158
|
+
const firstExp = Math.ceil(Math.log10(knee) + 0.5);
|
|
159
|
+
const lastExp = Math.floor(Math.log10(maxAbs));
|
|
160
|
+
const inDomain = (v) => v >= lo && v <= hi;
|
|
161
|
+
// Zero and ±knee are the fixed part of the ladder — kept whole, never thinned,
|
|
162
|
+
// because they are what distinguishes a symlog axis from a log one.
|
|
163
|
+
const fixed = [0, knee, -knee].filter(inDomain);
|
|
164
|
+
// Every decade past the knee that the domain actually contains, in ascending
|
|
165
|
+
// magnitude, each carrying whichever of ±10^e survived.
|
|
166
|
+
const rungs = [];
|
|
167
|
+
for (let e = firstExp; e <= lastExp; e += 1) {
|
|
168
|
+
const pair = [10 ** e, -(10 ** e)].filter(inDomain);
|
|
169
|
+
if (pair.length > 0)
|
|
170
|
+
rungs.push(pair);
|
|
171
|
+
}
|
|
172
|
+
// No rung survives ⇒ there is no logarithmic region to grid, so the linear
|
|
173
|
+
// ticks are the answer — the same reasoning as the knee-swallows-the-domain
|
|
174
|
+
// guard above, and the case that used to produce an empty axis. Note this also
|
|
175
|
+
// covers a window sitting *between* the knee and the first decade.
|
|
176
|
+
if (rungs.length === 0)
|
|
177
|
+
return scale.ticks(count);
|
|
178
|
+
// Thin the SURVIVING rungs to what is left of the budget after the fixed
|
|
179
|
+
// ticks. Stepping by magnitude keeps a rung's ± pair together, so the grid
|
|
180
|
+
// stays symmetric wherever the domain is.
|
|
181
|
+
const budget = Math.max(2, count);
|
|
182
|
+
const total = rungs.reduce((n, pair) => n + pair.length, 0);
|
|
183
|
+
const room = Math.max(1, budget - fixed.length);
|
|
184
|
+
const step = Math.max(1, Math.ceil(total / room));
|
|
185
|
+
const out = new Set(fixed);
|
|
186
|
+
for (let i = 0; i < rungs.length; i += step)
|
|
187
|
+
for (const v of rungs[i])
|
|
188
|
+
out.add(v);
|
|
189
|
+
return [...out].sort((a, b) => a - b);
|
|
190
|
+
}
|
|
83
191
|
//# sourceMappingURL=yticks.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"CHANGELOG.md"
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"API.md"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "tsc -p tsconfig.json",
|
|
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
|
+
"prepack": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && cp ../../API.md ./API.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
|
|
32
33
|
"test": "npm run test:type && npm run test:runtime",
|
|
33
34
|
"test:type": "tsc -p tsconfig.types.json",
|
|
34
35
|
"test:runtime": "vitest run",
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
|
-
"@pond-ts/react": "^0.
|
|
42
|
-
"pond-ts": "^0.
|
|
42
|
+
"@pond-ts/react": "^0.60.0",
|
|
43
|
+
"pond-ts": "^0.60.0",
|
|
43
44
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|