@pond-ts/charts 0.57.0 → 0.59.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.
Files changed (85) hide show
  1. package/API.md +576 -0
  2. package/CHANGELOG.md +1213 -1
  3. package/dist/AreaChart.d.ts +12 -1
  4. package/dist/AreaChart.js +131 -13
  5. package/dist/BarChart.d.ts +56 -7
  6. package/dist/BarChart.js +263 -39
  7. package/dist/BarList.d.ts +85 -5
  8. package/dist/BarList.js +25 -4
  9. package/dist/BoxList.d.ts +70 -3
  10. package/dist/BoxList.js +21 -7
  11. package/dist/BoxPlot.d.ts +2 -1
  12. package/dist/BoxPlot.js +101 -9
  13. package/dist/Candlestick.d.ts +13 -1
  14. package/dist/Candlestick.js +89 -3
  15. package/dist/ChartContainer.d.ts +36 -48
  16. package/dist/ChartContainer.js +465 -59
  17. package/dist/ChartRow.d.ts +9 -2
  18. package/dist/ChartRow.js +176 -14
  19. package/dist/HeatMap.d.ts +176 -0
  20. package/dist/HeatMap.js +344 -0
  21. package/dist/Layers.d.ts +5 -1
  22. package/dist/Layers.js +1014 -253
  23. package/dist/Legend.js +8 -4
  24. package/dist/LineChart.d.ts +18 -1
  25. package/dist/LineChart.js +165 -4
  26. package/dist/ListTable.d.ts +30 -3
  27. package/dist/ListTable.js +381 -23
  28. package/dist/ScatterChart.d.ts +3 -2
  29. package/dist/ScatterChart.js +68 -4
  30. package/dist/XAxis.js +40 -22
  31. package/dist/YAxis.d.ts +58 -2
  32. package/dist/YAxis.js +3 -1
  33. package/dist/area.d.ts +34 -1
  34. package/dist/area.js +88 -1
  35. package/dist/bars.d.ts +67 -6
  36. package/dist/bars.js +250 -35
  37. package/dist/box.d.ts +2 -2
  38. package/dist/box.js +158 -40
  39. package/dist/brush.d.ts +142 -0
  40. package/dist/brush.js +179 -0
  41. package/dist/child-index.d.ts +27 -0
  42. package/dist/child-index.js +57 -0
  43. package/dist/context.d.ts +870 -39
  44. package/dist/cursors.d.ts +161 -0
  45. package/dist/cursors.js +503 -0
  46. package/dist/data.d.ts +38 -0
  47. package/dist/data.js +43 -0
  48. package/dist/decimate.d.ts +78 -1
  49. package/dist/decimate.js +157 -0
  50. package/dist/format.d.ts +15 -0
  51. package/dist/format.js +16 -1
  52. package/dist/heat.d.ts +163 -0
  53. package/dist/heat.js +659 -0
  54. package/dist/index.d.ts +13 -4
  55. package/dist/index.js +27 -0
  56. package/dist/line.d.ts +137 -0
  57. package/dist/line.js +328 -0
  58. package/dist/ohlc.d.ts +16 -1
  59. package/dist/ohlc.js +93 -4
  60. package/dist/range.d.ts +14 -1
  61. package/dist/range.js +24 -3
  62. package/dist/scatter.d.ts +17 -9
  63. package/dist/scatter.js +221 -33
  64. package/dist/select.d.ts +13 -5
  65. package/dist/select.js +14 -6
  66. package/dist/selection-fixtures.d.ts +174 -0
  67. package/dist/selection-fixtures.js +569 -0
  68. package/dist/selection-stories.d.ts +73 -0
  69. package/dist/selection-stories.js +301 -0
  70. package/dist/selectors.d.ts +316 -0
  71. package/dist/selectors.js +391 -0
  72. package/dist/span.d.ts +122 -0
  73. package/dist/span.js +203 -0
  74. package/dist/sweep.d.ts +154 -0
  75. package/dist/sweep.js +282 -0
  76. package/dist/theme.d.ts +510 -5
  77. package/dist/theme.js +217 -41
  78. package/dist/tracker.d.ts +6 -0
  79. package/dist/tracker.js +6 -0
  80. package/dist/tradingAxis.fixture.d.ts +78 -0
  81. package/dist/tradingAxis.fixture.js +215 -0
  82. package/dist/useChartLegend.js +18 -3
  83. package/dist/yticks.d.ts +3 -0
  84. package/dist/yticks.js +104 -0
  85. package/package.json +6 -5
@@ -48,8 +48,10 @@ export function buildChartLegend(container, cursor, rowKey) {
48
48
  label: spec.label,
49
49
  swatch: spec.swatch,
50
50
  ...(spec.id !== undefined ? { id: spec.id } : {}),
51
- selected: spec.id !== undefined && container.selected?.id === spec.id,
52
- hovered: spec.id !== undefined && container.hovered?.id === spec.id,
51
+ selected: spec.id !== undefined &&
52
+ container.selected.some((m) => m.id === spec.id),
53
+ hovered: spec.id !== undefined &&
54
+ container.hovered.some((m) => m.id === spec.id),
53
55
  };
54
56
  const last = rows[rows.length - 1];
55
57
  if (last !== undefined && last.rowKey === spec.rowKey) {
@@ -79,7 +81,20 @@ export function buildChartLegend(container, cursor, rowKey) {
79
81
  select: (row) => {
80
82
  if (row.id === undefined)
81
83
  return;
82
- container.select(container.selected?.id === row.id ? null : seriesSelectInfo(row));
84
+ // Toggle: clicking an already-selected row reports `null`, which a
85
+ // consumer reads as "clear".
86
+ //
87
+ // **Known limitation ([PND-MULTISEL]).** With a multi-member set that
88
+ // clears *everything*, not just this row — the callback's vocabulary is
89
+ // one hit, and "remove this one member" has no representation in it. A
90
+ // legend click also carries no modifiers (see below), so ⌘-click-adds
91
+ // can't be driven from a legend either. Both want the legend row to hand
92
+ // over its own event + identity; that is a legend-surface change rather
93
+ // than part of this wave, and is recorded in the plan. Single-selection
94
+ // behaviour — the only shipped behaviour before this — is unchanged.
95
+ container.select(container.selected.some((m) => m.id === row.id)
96
+ ? null
97
+ : seriesSelectInfo(row));
83
98
  },
84
99
  };
85
100
  }
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 —
package/dist/yticks.js CHANGED
@@ -61,6 +61,8 @@ export function resolveYTickCount(height, explicit) {
61
61
  * approach `resolveBarBaseline` takes to read `.domain()`.
62
62
  */
63
63
  export function yTickValues(scale, count) {
64
+ if (typeof scale.constant === 'function')
65
+ return symlogTickValues(scale, count);
64
66
  if (typeof scale.base !== 'function')
65
67
  return scale.ticks(count);
66
68
  const domain = scale.domain();
@@ -80,4 +82,106 @@ export function yTickValues(scale, count) {
80
82
  out.push(10 ** e);
81
83
  return out;
82
84
  }
85
+ /**
86
+ * Tick values for a **symlog** axis — linear through zero, logarithmic beyond
87
+ * ([PND-SYMLOG]).
88
+ *
89
+ * **This is the feature, not a refinement of it.** d3's `scaleSymlog` supplies
90
+ * the transform but its `ticks()` is `linearish` — evenly spaced in *value*. On
91
+ * a ±1M domain with a 20k knee that yields `-1M, -500k, 0, 500k, 1M`: **nothing
92
+ * at all below the knee**, which is the region a symlog axis exists to reveal.
93
+ * The mapping does spread that region generously (0→250px, 20k→294px,
94
+ * 100k→364px on a 500px range), so such a chart is readable-but-unlabelled —
95
+ * confidently gridded on the one part of the scale that isn't the point. Owning
96
+ * the ladder is therefore inseparable from owning the transform.
97
+ *
98
+ * The ladder, and why each piece is there:
99
+ *
100
+ * - **Zero, always.** It is the axis's centre of symmetry and the one value a
101
+ * symlog scale is chosen to keep visible.
102
+ * - **The knee, ±`constant`.** Where the reading changes from linear to
103
+ * logarithmic. Unlabelled, a reader has no way to know which régime a given
104
+ * gap belongs to, and the same pixel distance means different things either
105
+ * side of it.
106
+ * - **Decades beyond the knee, mirrored.** What a log plot is conventionally
107
+ * gridded on, thinned by the same "every `k`th power of ten" rule the log path
108
+ * above uses, so it degrades predictably as the row shrinks instead of
109
+ * exploding.
110
+ * - **Bounds are NOT labelled.** A data-derived bound is rarely round, so
111
+ * printing it puts an arbitrary number next to a decade — the noise a log grid
112
+ * exists to avoid.
113
+ *
114
+ * Below one decade of span past the knee there is nothing to grid
115
+ * logarithmically, so it defers to `scale.ticks(count)` — which is linear, and
116
+ * correct, because inside the knee symlog *is* linear.
117
+ *
118
+ * **Clip first, then thin — never the other way round.** A pan/zoom (or explicit
119
+ * bounds) can leave a window that contains *none* of the ideal ladder:
120
+ * `[510_000, 990_000]` with a 19_800 knee excludes zero, both knees, and its one
121
+ * candidate decade, so an order that thinned a symmetric ladder and clipped
122
+ * afterwards handed **`[]`** to the labels and the gridlines — an axis with no
123
+ * ticks at all, which reads as a rendering failure rather than as a scale. The
124
+ * budget is likewise spent on what *survives* the domain, not on an ideal
125
+ * two-sided ladder, so an asymmetric window is not thinned as if it were twice
126
+ * its size. If nothing survives, the linear ticks are the honest answer.
127
+ *
128
+ * Detection is structural, matching the log path's use of `base()`: `constant()`
129
+ * exists on `scaleSymlog` and on no other continuous scale.
130
+ */
131
+ function symlogTickValues(scale, count) {
132
+ const domain = scale.domain();
133
+ const lo = Math.min(domain[0], domain[domain.length - 1]);
134
+ const hi = Math.max(domain[0], domain[domain.length - 1]);
135
+ const knee = Math.abs(scale.constant?.() ?? 1);
136
+ const maxAbs = Math.max(Math.abs(lo), Math.abs(hi));
137
+ // Finiteness is checked, not assumed: an explicit `max={Infinity}` reaches here
138
+ // intact, and `floor(log10(Infinity))` is `Infinity` — which made the decade
139
+ // loop's `e += step` a no-op and hung the render in a `for` that could never
140
+ // end. d3's own linear ticks return `[]` on such a domain, so deferring is both
141
+ // safe and the truthful answer for a domain with no finite extent.
142
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || !Number.isFinite(knee))
143
+ return scale.ticks(count);
144
+ if (!(knee > 0) || !(maxAbs > knee) || !(hi > lo))
145
+ return scale.ticks(count);
146
+ // The ladder starts at the first decade **at least half a decade above the
147
+ // knee** (`× √10`), not merely above it. `ceil(log10(knee))` alone puts a
148
+ // decade arbitrarily close to the knee tick whenever the knee lands just under
149
+ // a power of ten — a data-derived `maxAbs` of 4.95e6 gives a 99k knee and a
150
+ // 100k decade, two ticks a few pixels apart whose labels round to the *same
151
+ // string*. Since the knee itself is always drawn, dropping that decade loses
152
+ // no information; a chart that prints one number twice at two positions is
153
+ // reporting something false about the scale.
154
+ const firstExp = Math.ceil(Math.log10(knee) + 0.5);
155
+ const lastExp = Math.floor(Math.log10(maxAbs));
156
+ const inDomain = (v) => v >= lo && v <= hi;
157
+ // Zero and ±knee are the fixed part of the ladder — kept whole, never thinned,
158
+ // because they are what distinguishes a symlog axis from a log one.
159
+ const fixed = [0, knee, -knee].filter(inDomain);
160
+ // Every decade past the knee that the domain actually contains, in ascending
161
+ // magnitude, each carrying whichever of ±10^e survived.
162
+ const rungs = [];
163
+ for (let e = firstExp; e <= lastExp; e += 1) {
164
+ const pair = [10 ** e, -(10 ** e)].filter(inDomain);
165
+ if (pair.length > 0)
166
+ rungs.push(pair);
167
+ }
168
+ // No rung survives ⇒ there is no logarithmic region to grid, so the linear
169
+ // ticks are the answer — the same reasoning as the knee-swallows-the-domain
170
+ // guard above, and the case that used to produce an empty axis. Note this also
171
+ // covers a window sitting *between* the knee and the first decade.
172
+ if (rungs.length === 0)
173
+ return scale.ticks(count);
174
+ // Thin the SURVIVING rungs to what is left of the budget after the fixed
175
+ // ticks. Stepping by magnitude keeps a rung's ± pair together, so the grid
176
+ // stays symmetric wherever the domain is.
177
+ const budget = Math.max(2, count);
178
+ const total = rungs.reduce((n, pair) => n + pair.length, 0);
179
+ const room = Math.max(1, budget - fixed.length);
180
+ const step = Math.max(1, Math.ceil(total / room));
181
+ const out = new Set(fixed);
182
+ for (let i = 0; i < rungs.length; i += step)
183
+ for (const v of rungs[i])
184
+ out.add(v);
185
+ return [...out].sort((a, b) => a - b);
186
+ }
83
187
  //# sourceMappingURL=yticks.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.57.0",
3
+ "version": "0.59.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.57.0",
42
- "pond-ts": "^0.57.0",
42
+ "@pond-ts/react": "^0.59.0",
43
+ "pond-ts": "^0.59.0",
43
44
  "react": "^18.0.0 || ^19.0.0"
44
45
  },
45
46
  "devDependencies": {