basalt-ui 1.15.0 → 1.17.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 (91) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/agent/rules/basalt-charts.md +74 -13
  3. package/agent/templates/CLAUDE-block.md.tpl +9 -6
  4. package/dist/charts/cursor/resolve.d.ts +29 -5
  5. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  6. package/dist/charts/cursor/resolve.js +25 -4
  7. package/dist/charts/hooks/useChartCursor.d.ts +8 -1
  8. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  9. package/dist/charts/hooks/useChartCursor.js +6 -2
  10. package/dist/charts/index.d.ts +2 -1
  11. package/dist/charts/index.d.ts.map +1 -1
  12. package/dist/charts/kinds/Bars.d.ts +30 -8
  13. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  14. package/dist/charts/kinds/Bars.js +10 -3
  15. package/dist/charts/kinds/Bars.test.d.ts +2 -0
  16. package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
  17. package/dist/charts/kinds/DualPanel.d.ts +30 -0
  18. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  19. package/dist/charts/kinds/DualPanel.js +82 -12
  20. package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
  21. package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
  22. package/dist/charts/kinds/MultiLine.d.ts +9 -0
  23. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  24. package/dist/charts/kinds/MultiLine.js +45 -33
  25. package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
  26. package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
  27. package/dist/charts/kinds/StackedArea.d.ts +9 -0
  28. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  29. package/dist/charts/kinds/StackedArea.js +16 -1
  30. package/dist/charts/kinds/StackedArea.test.d.ts +2 -0
  31. package/dist/charts/kinds/StackedArea.test.d.ts.map +1 -0
  32. package/dist/charts/kinds/ZonedLine.d.ts +9 -0
  33. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  34. package/dist/charts/kinds/ZonedLine.js +5 -0
  35. package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
  36. package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
  37. package/dist/charts/primitives/CartesianChart.d.ts +45 -6
  38. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  39. package/dist/charts/primitives/CartesianChart.js +29 -12
  40. package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -1
  41. package/dist/charts/primitives/ChartFrame.d.ts +8 -1
  42. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  43. package/dist/charts/primitives/ChartFrame.js +1 -1
  44. package/dist/charts/primitives/ChartLegend.d.ts +3 -0
  45. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  46. package/dist/charts/primitives/ChartLegend.js +4 -0
  47. package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
  48. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  49. package/dist/charts/primitives/ChartTooltip.js +3 -2
  50. package/dist/charts/primitives/ChartTooltip.test.d.ts +2 -0
  51. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +1 -0
  52. package/dist/charts/primitives/XZoneRects.d.ts +22 -8
  53. package/dist/charts/primitives/XZoneRects.d.ts.map +1 -1
  54. package/dist/charts/primitives/XZoneRects.js +39 -2
  55. package/dist/charts/series.d.ts +16 -3
  56. package/dist/charts/series.d.ts.map +1 -1
  57. package/dist/charts/series.js +2 -1
  58. package/dist/charts/utils/domain.d.ts +15 -0
  59. package/dist/charts/utils/domain.d.ts.map +1 -1
  60. package/dist/charts/utils/domain.js +6 -1
  61. package/llms.txt +2 -2
  62. package/package.json +1 -1
  63. package/src/charts/cursor/resolve.test.ts +62 -0
  64. package/src/charts/cursor/resolve.ts +71 -7
  65. package/src/charts/hooks/useChartCursor.ts +12 -1
  66. package/src/charts/index.ts +2 -0
  67. package/src/charts/kinds/Bars.test.tsx +183 -0
  68. package/src/charts/kinds/Bars.tsx +41 -12
  69. package/src/charts/kinds/DualPanel.test.tsx +441 -0
  70. package/src/charts/kinds/DualPanel.tsx +127 -11
  71. package/src/charts/kinds/MultiLine.test.tsx +218 -0
  72. package/src/charts/kinds/MultiLine.tsx +64 -36
  73. package/src/charts/kinds/StackedArea.test.tsx +114 -0
  74. package/src/charts/kinds/StackedArea.tsx +25 -1
  75. package/src/charts/kinds/ZonedLine.test.tsx +147 -0
  76. package/src/charts/kinds/ZonedLine.tsx +14 -0
  77. package/src/charts/primitives/CartesianChart.test.tsx +269 -0
  78. package/src/charts/primitives/CartesianChart.tsx +79 -19
  79. package/src/charts/primitives/ChartFrame.test.tsx +42 -0
  80. package/src/charts/primitives/ChartFrame.tsx +9 -2
  81. package/src/charts/primitives/ChartLegend.test.tsx +30 -0
  82. package/src/charts/primitives/ChartLegend.tsx +7 -0
  83. package/src/charts/primitives/ChartTooltip.test.tsx +23 -0
  84. package/src/charts/primitives/ChartTooltip.tsx +7 -1
  85. package/src/charts/primitives/XZoneRects.test.tsx +60 -0
  86. package/src/charts/primitives/XZoneRects.tsx +75 -10
  87. package/src/charts/series.test.ts +51 -0
  88. package/src/charts/series.ts +18 -6
  89. package/src/charts/utils/domain.test.ts +31 -1
  90. package/src/charts/utils/domain.ts +19 -0
  91. package/src/charts/primitives/CartesianChart.test.ts +0 -99
@@ -7,7 +7,7 @@ import { VX } from "../../tokens/index.js";
7
7
  import { useChartCursor } from "../hooks/useChartCursor.js";
8
8
  import { autoMargin, probeAxisLabels } from "../layout/auto-margin.js";
9
9
  import { deriveTooltipRows } from "../series.js";
10
- import { padAutoLower } from "../utils/domain.js";
10
+ import { padAutoLower, padAutoUpper } from "../utils/domain.js";
11
11
  import { fmtAxisDate } from "../utils/format.js";
12
12
  import { smartTicks, smartTicksEvery } from "../utils/ticks.js";
13
13
  import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric } from "./Axes.js";
@@ -41,8 +41,7 @@ function resolveAxisDomain(cfg, data, series, extraBounds = []) {
41
41
  if (min === Infinity) return [0, cfg?.autoMaxFloor ?? 1];
42
42
  const pad = cfg?.autoPad ?? DEFAULT_AUTO_PAD;
43
43
  const ceil = cfg?.autoMinCeil ?? 0;
44
- const paddedMax = max > 0 ? max * pad : max < 0 ? max / pad : 0;
45
- const upper = Math.max(paddedMax, cfg?.autoMaxFloor ?? -Infinity);
44
+ const upper = padAutoUpper(Math.max(max, cfg?.autoMaxFloor ?? -Infinity), pad);
46
45
  const lower = padAutoLower(Math.min(min, ceil), pad);
47
46
  if (lower === upper) return [Math.min(lower, 0), upper === 0 ? 1 : upper * pad];
48
47
  return [lower, upper];
@@ -66,6 +65,7 @@ function CartesianChart({
66
65
  tooltip,
67
66
  margin: marginOverride,
68
67
  cursorValue,
68
+ cursorResolution,
69
69
  ariaLabel,
70
70
  isPending,
71
71
  children
@@ -99,6 +99,7 @@ function CartesianChart({
99
99
  ...tooltip !== void 0 && { tooltip },
100
100
  ...marginOverride !== void 0 && { marginOverride },
101
101
  ...cursorValue !== void 0 && { cursorValue },
102
+ ...cursorResolution !== void 0 && { cursorResolution },
102
103
  ...ariaLabel !== void 0 && { ariaLabel },
103
104
  plot,
104
105
  highlighted,
@@ -123,6 +124,7 @@ function CartesianPlot({
123
124
  tooltip,
124
125
  marginOverride,
125
126
  cursorValue,
127
+ cursorResolution,
126
128
  ariaLabel,
127
129
  plot,
128
130
  highlighted,
@@ -154,17 +156,19 @@ function CartesianPlot({
154
156
  () => probeAxisLabels({
155
157
  domain: leftDomain,
156
158
  ticks: leftTicks,
159
+ nice: y?.nice ?? false,
157
160
  ...y?.format !== void 0 && { format: y.format }
158
161
  }),
159
- [leftDomain, leftTicks, y?.format]
162
+ [leftDomain, leftTicks, y?.format, y?.nice]
160
163
  );
161
164
  const { labels: rightLabels, format: rightFormat } = useMemo(
162
165
  () => rightDomain === null ? { labels: [], format: (v) => String(v) } : probeAxisLabels({
163
166
  domain: rightDomain,
164
167
  ticks: rightTicks,
168
+ nice: y2?.nice ?? false,
165
169
  ...y2?.format !== void 0 && { format: y2.format }
166
170
  }),
167
- [rightDomain, rightTicks, y2?.format]
171
+ [rightDomain, rightTicks, y2?.format, y2?.nice]
168
172
  );
169
173
  const xLabels = useMemo(() => keys.map(formatX), [keys, formatX]);
170
174
  const margin = useMemo(
@@ -183,12 +187,20 @@ function CartesianPlot({
183
187
  [keys, xMax]
184
188
  );
185
189
  const yScale = useMemo(
186
- () => scaleLinear({ domain: leftDomain, range: [yMax, 0] }),
187
- [leftDomain, yMax]
190
+ () => scaleLinear({
191
+ domain: leftDomain,
192
+ range: [yMax, 0],
193
+ ...y?.nice === true && { nice: true }
194
+ }),
195
+ [leftDomain, yMax, y?.nice]
188
196
  );
189
197
  const y2Scale = useMemo(
190
- () => rightDomain === null ? null : scaleLinear({ domain: rightDomain, range: [yMax, 0] }),
191
- [rightDomain, yMax]
198
+ () => rightDomain === null ? null : scaleLinear({
199
+ domain: rightDomain,
200
+ range: [yMax, 0],
201
+ ...y2?.nice === true && { nice: true }
202
+ }),
203
+ [rightDomain, yMax, y2?.nice]
192
204
  );
193
205
  const tickValues = useMemo(
194
206
  () => xTicks === void 0 ? smartTicks(keys, xMax) : smartTicksEvery(keys, xTicks),
@@ -199,7 +211,8 @@ function CartesianPlot({
199
211
  chartId,
200
212
  getKey: getX,
201
213
  xScale,
202
- marginLeft: margin.left
214
+ marginLeft: margin.left,
215
+ ...cursorResolution !== void 0 && { resolution: cursorResolution }
203
216
  });
204
217
  const svgRef = useRef(null);
205
218
  const tooltipEnabled = tooltip !== false;
@@ -207,6 +220,7 @@ function CartesianPlot({
207
220
  const point = cursor.point;
208
221
  const cursorX = point === null ? null : xScale(getX(point)) ?? null;
209
222
  const badge = tooltipCfg?.label === void 0 || point === null ? null : tooltipCfg.label(point);
223
+ const formatHeader = tooltipCfg?.formatHeader;
210
224
  const tooltipSeries = useMemo(
211
225
  () => visible.map(
212
226
  (s) => s.formatValue === void 0 ? { ...s, formatValue: s.axis === "right" ? rightFormat : leftFormat } : s
@@ -321,11 +335,14 @@ function CartesianPlot({
321
335
  TooltipHeader,
322
336
  {
323
337
  date: getX(point),
338
+ ...formatHeader !== void 0 && {
339
+ format: (key) => formatHeader(key, point)
340
+ },
324
341
  ...badge !== null && { label: badge.text, labelColor: badge.color }
325
342
  }
326
343
  ),
327
344
  /* @__PURE__ */ jsxs(TooltipBody, { children: [
328
- tooltipCfg?.prependRows?.(point),
345
+ tooltipCfg?.prependRows?.(point, { visible, hidden }),
329
346
  rows.map((row) => /* @__PURE__ */ jsx(
330
347
  TooltipRow,
331
348
  {
@@ -338,7 +355,7 @@ function CartesianPlot({
338
355
  },
339
356
  row.key
340
357
  )),
341
- tooltipCfg?.extraRows?.(point)
358
+ tooltipCfg?.extraRows?.(point, { visible, hidden })
342
359
  ] })
343
360
  ] })
344
361
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"CartesianChart.test.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.test.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"CartesianChart.test.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.test.tsx"],"names":[],"mappings":""}
@@ -43,9 +43,16 @@ export type ChartFrameProps = {
43
43
  */
44
44
  isPending?: boolean;
45
45
  /**
46
- * Accessible text alternative for the chart, applied as `aria-label` (+ `role="img"`) on the
46
+ * Accessible text alternative for the chart, applied as `aria-label` (+ `role="group"`) on the
47
47
  * outer container so screen readers announce something other than an unlabeled graphic. Every
48
48
  * kind composing `ChartFrame` should accept and forward this from its own props.
49
+ *
50
+ * MUST stay `role="group"`, never `role="img"`. Per the ARIA spec, every descendant of an
51
+ * `role="img"` element is presentational, which erases the `HoverOverlay`'s `role="slider"` from
52
+ * the accessibility tree entirely — a screen reader announces the label and then the
53
+ * keyboard-scrubbable slider underneath it is simply unreachable, silently, with no error
54
+ * anywhere. `role="group"` announces the same label while keeping descendants exposed. Do not
55
+ * "simplify" this back to `img` — it looks like a no-op refactor and it is not.
49
56
  */
50
57
  ariaLabel?: string;
51
58
  /** Draw the SVG marks given the plot rect that already excludes the legend band, plus the set
@@ -1 +1 @@
1
- {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAOhF;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,GAC5B,gBAAgB,GAAG,KAAK,CAS1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAsE7B"}
1
+ {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAOhF;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,GAC5B,gBAAgB,GAAG,KAAK,CAS1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAsE7B"}
@@ -80,7 +80,7 @@ function ChartFrame({
80
80
  {
81
81
  ref: containerRef,
82
82
  style: outerStyle(fill, vertical),
83
- ...ariaLabel !== void 0 && { role: "img", "aria-label": ariaLabel },
83
+ ...ariaLabel !== void 0 && { role: "group", "aria-label": ariaLabel },
84
84
  ...isPending && { "aria-busy": "true" },
85
85
  children: [
86
86
  legendNode !== null && (placement === "top" || placement === "left") && legendNode,
@@ -10,6 +10,9 @@ export type LegendEntry = {
10
10
  dashed?: boolean;
11
11
  /** bar swatch opacity — honored instead of a hardcoded value, so it cannot lie about the fill. */
12
12
  fillOpacity?: number;
13
+ /** line/splitLine swatch stroke opacity — parity with `fillOpacity` above, so a dimmed
14
+ * companion line's swatch cannot lie about its own opacity either. */
15
+ strokeOpacity?: number;
13
16
  /** Drives `groups` rendering (series → hairline divider → overlay → reference). */
14
17
  role?: SeriesRole;
15
18
  /** Companions folded under this entry via `parent` (`deriveLegend`) — rendered as compact,
@@ -1 +1 @@
1
- {"version":3,"file":"ChartLegend.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartLegend.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE5D,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,CAAA;IAC9C,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB;iFAC6E;IAC7E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;gCAC4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AA4ND;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,WAAkB,EAClB,WAAW,EACX,MAAkB,EAClB,QAAQ,EACR,OAAY,EACZ,SAAoB,EACpB,MAAc,EACd,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C,kCAAkC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,+BAmEA"}
1
+ {"version":3,"file":"ChartLegend.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartLegend.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE5D,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,CAAA;IAC9C,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;0EACsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB;iFAC6E;IAC7E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;gCAC4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAgOD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,WAAkB,EAClB,WAAW,EACX,MAAkB,EAClB,QAAQ,EACR,OAAY,EACZ,SAAoB,EACpB,MAAc,EACd,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C,kCAAkC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,+BAmEA"}
@@ -70,6 +70,7 @@ function LegendSwatch({ item, idPrefix }) {
70
70
  y2: 7,
71
71
  stroke: item.color,
72
72
  strokeWidth: item.strokeWidth ?? 2.5,
73
+ strokeOpacity: item.strokeOpacity ?? 1,
73
74
  strokeDasharray: item.dashed ? VX.dashArray : void 0
74
75
  }
75
76
  ),
@@ -82,6 +83,7 @@ function LegendSwatch({ item, idPrefix }) {
82
83
  y2: 7,
83
84
  stroke: item.secondColor,
84
85
  strokeWidth: item.strokeWidth ?? 2.5,
86
+ strokeOpacity: item.strokeOpacity ?? 1,
85
87
  strokeDasharray: item.dashed ? VX.dashArray : void 0
86
88
  }
87
89
  )
@@ -124,6 +126,7 @@ function LegendSwatch({ item, idPrefix }) {
124
126
  y2: 3,
125
127
  stroke: item.color,
126
128
  strokeWidth: item.strokeWidth ?? 3,
129
+ strokeOpacity: item.strokeOpacity ?? 1,
127
130
  strokeLinecap: "round",
128
131
  strokeDasharray: item.dashed ? VX.dashArray : void 0
129
132
  }
@@ -166,6 +169,7 @@ function LegendChildSwatch({ item }) {
166
169
  y2: 5,
167
170
  stroke: item.color,
168
171
  strokeWidth: item.strokeWidth ?? 2,
172
+ strokeOpacity: item.strokeOpacity ?? 1,
169
173
  strokeDasharray: item.dashed ? VX.dashArray : void 0
170
174
  }
171
175
  ) });
@@ -1,9 +1,14 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /** Tooltip header — shows formatted date + optional right-aligned label with color. */
3
- export declare function TooltipHeader({ date, label, labelColor, }: {
3
+ export declare function TooltipHeader({ date, label, labelColor, format, }: {
4
4
  date: string;
5
5
  label?: string;
6
6
  labelColor?: string;
7
+ /** Overrides `fmtTooltipDate` for the header text. Default: today's `fmtTooltipDate` behavior,
8
+ * unchanged. The seam exists because `fmtTooltipDate` regexes `YYYY-MM-DD` out of the key and
9
+ * builds a LOCAL `Date`, so a UTC ISO domain key names the wrong day next to `formatX`/the
10
+ * tooltip badge, which both resolve locally — a caller with a UTC key needs to supply its own. */
11
+ format?: (date: string) => string;
7
12
  }): import("react").JSX.Element;
8
13
  /** Tooltip row — swatch + label + value. */
9
14
  export declare function TooltipRow({ color, label, value, valueColor, shape, strokeWidth, dashed, }: {
@@ -1 +1 @@
1
- {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,GACX,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,SAAS,CAAA;CACpB,sCA4CA"}
1
+ {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,MAAuB,GACxB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;sGAGkG;IAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,SAAS,CAAA;CACpB,sCA4CA"}
@@ -19,7 +19,8 @@ const TOOLTIP_STYLES = {
19
19
  function TooltipHeader({
20
20
  date,
21
21
  label,
22
- labelColor
22
+ labelColor,
23
+ format = fmtTooltipDate
23
24
  }) {
24
25
  return /* @__PURE__ */ jsxs(
25
26
  "div",
@@ -42,7 +43,7 @@ function TooltipHeader({
42
43
  fontFamily: "var(--basalt-font-mono)",
43
44
  color: VX.ink
44
45
  },
45
- children: fmtTooltipDate(date)
46
+ children: format(date)
46
47
  }
47
48
  ),
48
49
  label !== void 0 && /* @__PURE__ */ jsx(
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ChartTooltip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartTooltip.test.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.test.tsx"],"names":[],"mappings":""}
@@ -11,24 +11,38 @@ export type XZoneSpec = {
11
11
  /** Right edge — a domain key produced by the kind's `getX`. Omit for "end of the plot area". */
12
12
  to?: string;
13
13
  fill: string;
14
+ /**
15
+ * `'center'` (default, unchanged behaviour) resolves a present bound to `xScale(key)` — the
16
+ * point's own center, so a band reads "from this sample to that sample" rather than
17
+ * edge-to-edge. `'edge'` resolves a present `from` to `xScale(from) - step/2` and a present `to`
18
+ * to `xScale(to) + step/2` (`step` read from `xScale.step()`), so the band covers both terminal
19
+ * slots in full — a two-key band widens by exactly one step, and `from === to` renders one step
20
+ * wide instead of being skipped as degenerate. An omitted `from`/`to` still resolves to the plot
21
+ * edge (`xScale.range()[0]`/`[1]`) in both modes, unshifted — omission means "the plot's own
22
+ * edge", not "the first/last sample's edge". A resolved `'edge'` bound is clamped into the plot
23
+ * range so a band anchored at the first or last sample cannot paint outside the plot area.
24
+ */
25
+ align?: 'center' | 'edge';
14
26
  };
15
27
  /**
16
28
  * Vertical zone backgrounds (x-range overlays — twilight bands, a shaded time window) — one
17
29
  * <rect> per zone, spanning y=[0, height] and x=[scale(from), scale(to)]. The x analog of
18
30
  * `ZoneRects`, for a `scalePoint` x-axis instead of a `scaleLinear` y-axis.
19
31
  *
20
- * An omitted `from` resolves to `0` (start of the plot area); an omitted `to` resolves to
21
- * `xScale.range()[1]` (the plot's right edge). A present key resolves to `xScale(key)` — the
22
- * POINT CENTER, so a band reads as "from this sample to that sample", not edge-to-edge.
32
+ * Bound resolution depends on `XZoneSpec.align` (see its JSDoc for the full `'center'`/`'edge'`
33
+ * contract) — default `'center'` resolves a present key to the point's own center; `'edge'`
34
+ * widens by half a step at each present bound so the band covers the terminal slots in full.
23
35
  *
24
36
  * A key that is not in the scale's domain skips that band entirely (renders nothing for it) —
25
- * it is NEVER clamped to a plot edge. Clamping would silently paint a band across the whole
26
- * chart for a typo'd or stale key, which is the exact class of quiet lie the repo's "nothing to
27
- * draw is two different states" doctrine rejects: an unknown key must read as "not drawn", not
28
- * as "the whole plot".
37
+ * it is NEVER clamped to a plot edge, in either mode. Clamping an unknown key would silently
38
+ * paint a band across the whole chart for a typo'd or stale key, which is the exact class of
39
+ * quiet lie the repo's "nothing to draw is two different states" doctrine rejects: an unknown key
40
+ * must read as "not drawn", not as "the whole plot".
29
41
  *
30
42
  * A degenerate resolved range (right edge <= left edge, e.g. an inverted `from`/`to` pair) is
31
- * also skipped — no negative-width rect.
43
+ * also skipped — no negative-width rect. In `'center'` mode `from === to` is degenerate (skipped,
44
+ * today's behaviour); in `'edge'` mode it is NOT — the half-step widening on each side always
45
+ * produces a positive-width, one-step-wide rect.
32
46
  */
33
47
  export declare function XZoneRects({ zones, height, xScale, }: {
34
48
  zones: XZoneSpec[];
@@ -1 +1 @@
1
- {"version":3,"file":"XZoneRects.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/XZoneRects.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;GAIG;AACH,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AAEvD,MAAM,MAAM,SAAS,GAAG;IACtB,iGAAiG;IACjG,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gGAAgG;IAChG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,MAAM,EACN,MAAM,GACP,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,UAAU,CAAA;CACnB,+BAsBA"}
1
+ {"version":3,"file":"XZoneRects.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/XZoneRects.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;GAIG;AACH,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AAEvD,MAAM,MAAM,SAAS,GAAG;IACtB,iGAAiG;IACjG,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gGAAgG;IAChG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC1B,CAAA;AAkCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,MAAM,EACN,MAAM,GACP,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,UAAU,CAAA;CACnB,+BAyCA"}
@@ -1,4 +1,22 @@
1
1
  import { Fragment, jsx } from "react/jsx-runtime";
2
+ function clamp(value, min, max) {
3
+ return Math.min(Math.max(value, min), max);
4
+ }
5
+ function resolveBound({
6
+ key,
7
+ omittedEdge,
8
+ xScale,
9
+ align,
10
+ edgeSign,
11
+ rangeMin,
12
+ rangeMax
13
+ }) {
14
+ if (key === void 0) return omittedEdge;
15
+ const center = xScale(key);
16
+ if (center === void 0) return void 0;
17
+ if (align === "center") return center;
18
+ return clamp(center + edgeSign * (xScale.step() / 2), rangeMin, rangeMax);
19
+ }
2
20
  function XZoneRects({
3
21
  zones,
4
22
  height,
@@ -6,8 +24,27 @@ function XZoneRects({
6
24
  }) {
7
25
  return /* @__PURE__ */ jsx(Fragment, { children: zones.map((z, i) => {
8
26
  const [rangeStart, rangeEnd] = xScale.range();
9
- const xFrom = z.from === void 0 ? rangeStart : xScale(z.from);
10
- const xTo = z.to === void 0 ? rangeEnd : xScale(z.to);
27
+ const rangeMin = Math.min(rangeStart, rangeEnd);
28
+ const rangeMax = Math.max(rangeStart, rangeEnd);
29
+ const align = z.align ?? "center";
30
+ const xFrom = resolveBound({
31
+ key: z.from,
32
+ omittedEdge: rangeStart,
33
+ xScale,
34
+ align,
35
+ edgeSign: -1,
36
+ rangeMin,
37
+ rangeMax
38
+ });
39
+ const xTo = resolveBound({
40
+ key: z.to,
41
+ omittedEdge: rangeEnd,
42
+ xScale,
43
+ align,
44
+ edgeSign: 1,
45
+ rangeMin,
46
+ rangeMax
47
+ });
11
48
  if (xFrom === void 0 || xTo === void 0) return null;
12
49
  if (xTo <= xFrom) return null;
13
50
  return /* @__PURE__ */ jsx(
@@ -52,6 +52,14 @@ export type SeriesStyle = {
52
52
  strokeWidth?: number;
53
53
  /** bar/area: the swatch honors this so it cannot lie. */
54
54
  fillOpacity?: number;
55
+ /**
56
+ * A MARK property — dims the plotted stroke, e.g. a faint moving-average companion line. The
57
+ * legend swatch honors it too (parity with `fillOpacity`, which it already honors — a swatch
58
+ * that lies is the thing the derived-legend rule exists to prevent). The TOOLTIP ROW swatch and
59
+ * the CROSSHAIR DOT deliberately do NOT honor it: those are 12px value-readout chips where a
60
+ * sub-1 opacity reads as a rendering bug, not as data.
61
+ */
62
+ strokeOpacity?: number;
55
63
  /** Which y-axis this series is measured against. Default 'left'. `'right'` is what makes a
56
64
  * chart dual-axis — `CartesianChart` reads it for the crosshair dots and the tooltip. */
57
65
  axis?: 'left' | 'right';
@@ -75,12 +83,17 @@ export type SeriesStyle = {
75
83
  export type ChartSeries<T> = SeriesStyle & {
76
84
  /** null = line gap + skipped tooltip row. */
77
85
  getValue: (d: T) => number | null;
78
- /** Per-series override of the shared tooltip/value formatter. */
79
- formatValue?: (v: number) => string;
80
- /** PR star / status dot. Return null for no marker at that point. */
86
+ /** Per-series override of the shared tooltip/value formatter. Receives the hovered datum
87
+ * alongside the value so a row can read fields beyond the plotted number (e.g. `97.5 kg (92.5 ×
88
+ * 3)`). */
89
+ formatValue?: (v: number, d: T) => string;
90
+ /** PR star / status dot. Return null for no marker at that point. `ring` defaults true (today's
91
+ * punched-out stroke, unchanged); `false` omits the stroke entirely. `fillOpacity` defaults 1. */
81
92
  getMarker?: (d: T) => {
82
93
  color?: string;
83
94
  r?: number;
95
+ fillOpacity?: number;
96
+ ring?: boolean;
84
97
  } | null;
85
98
  };
86
99
  /** A single derived tooltip row, ready for `TooltipRow`. */
@@ -1 +1 @@
1
- {"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ function legendEntryFor(s) {
9
9
  dashed: s.dash === "dashed",
10
10
  ...s.strokeWidth !== void 0 && { strokeWidth: s.strokeWidth },
11
11
  ...s.fillOpacity !== void 0 && { fillOpacity: s.fillOpacity },
12
+ ...s.strokeOpacity !== void 0 && { strokeOpacity: s.strokeOpacity },
12
13
  ...s.role !== void 0 && { role: s.role },
13
14
  ...s.note !== void 0 && { note: s.note }
14
15
  };
@@ -42,7 +43,7 @@ function deriveTooltipRows(series, datum, fallbackFormat) {
42
43
  key: s.key,
43
44
  label: s.label,
44
45
  color: s.color,
45
- value: format(value),
46
+ value: format(value, datum),
46
47
  shape: shapeFor(s.mark),
47
48
  dashed: s.dash === "dashed",
48
49
  ...s.strokeWidth !== void 0 && { strokeWidth: s.strokeWidth }
@@ -17,4 +17,19 @@
17
17
  * pre-existing, still-correct behavior for genuinely negative-swinging metrics.
18
18
  */
19
19
  export declare function padAutoLower(candidate: number, pad: number): number;
20
+ /**
21
+ * Auto y-domain upper-bound padding — the exact mirror of {@link padAutoLower}, sharing its
22
+ * sign-safety: pad AWAY from zero (`upper >= candidate`), never toward it. `candidate` is the
23
+ * caller's `Math.max(dataMax, floor)` — the `autoMaxFloor` clamp has already been applied, so
24
+ * padding runs AFTER the floor, not before it. Applying the floor post-pad (the pre-existing bug)
25
+ * lands a floored axis top exactly on the floor value with zero headroom — a target line pinned to
26
+ * that floor then sits glued to the plot edge instead of getting the same breathing room every
27
+ * other bound gets.
28
+ *
29
+ * - `candidate === 0` stays exactly 0 — never pushed positive.
30
+ * - `candidate > 0` multiplies by `pad`, moving the ceiling further UP (away from zero).
31
+ * - `candidate < 0` divides by `pad`, moving the ceiling UP toward (and past) zero — the mirror of
32
+ * `padAutoLower`'s `candidate > 0` branch.
33
+ */
34
+ export declare function padAutoUpper(candidate: number, pad: number): number;
20
35
  //# sourceMappingURL=domain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../src/charts/utils/domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE"}
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../src/charts/utils/domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE"}
@@ -2,6 +2,11 @@ function padAutoLower(candidate, pad) {
2
2
  if (candidate === 0) return 0;
3
3
  return candidate > 0 ? candidate / pad : candidate * pad;
4
4
  }
5
+ function padAutoUpper(candidate, pad) {
6
+ if (candidate === 0) return 0;
7
+ return candidate > 0 ? candidate * pad : candidate / pad;
8
+ }
5
9
  export {
6
- padAutoLower
10
+ padAutoLower,
11
+ padAutoUpper
7
12
  };
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.15.0
1
+ # basalt-ui v1.17.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.15.0
22
+ Version: 1.17.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -59,3 +59,65 @@ describe('resolveCursorPoint', () => {
59
59
  expect(resolveCursorPoint(i, '2026-08-02')).toBeNull()
60
60
  })
61
61
  })
62
+
63
+ describe("resolveCursorPoint — 'leading' resolution (bucket-keyed domains)", () => {
64
+ const weekly = (days: string[]) => buildDomainIndex(daily(days), (p) => p.date, 'leading')
65
+
66
+ test('resolves an exact key, same as nearest', () => {
67
+ const i = weekly(['01', '08', '15'])
68
+ expect(resolveCursorPoint(i, '2026-08-08')?.date).toBe('2026-08-08')
69
+ })
70
+
71
+ test("a target anywhere inside a bucket resolves to that bucket's LEADING key, not the nearest one", () => {
72
+ // The bug this replaces: a weekly chart keyed by the Monday of each week (step 7 days),
73
+ // hovered from a daily sibling landing in the back half of the bucket, used to resolve to the
74
+ // FOLLOWING week under nearest-match. Every back-half day must resolve to the bucket it is
75
+ // actually inside.
76
+ const i = weekly(['01', '08', '15'])
77
+ expect(resolveCursorPoint(i, '2026-08-02')?.date).toBe('2026-08-01')
78
+ expect(resolveCursorPoint(i, '2026-08-05')?.date).toBe('2026-08-01')
79
+ expect(resolveCursorPoint(i, '2026-08-07')?.date).toBe('2026-08-01')
80
+ expect(resolveCursorPoint(i, '2026-08-09')?.date).toBe('2026-08-08')
81
+ expect(resolveCursorPoint(i, '2026-08-14')?.date).toBe('2026-08-08')
82
+ })
83
+
84
+ test('a key BEFORE the first bucket resolves to null — no bucket contains it', () => {
85
+ // Deliberately NOT 'nearest''s one-step tolerance. The first key is the leading edge of the
86
+ // first bucket, so anything earlier is outside every bucket; snapping it onto the first one
87
+ // would paint a crosshair on a bucket that provably does not contain the hovered key.
88
+ const i = weekly(['02', '09', '16'])
89
+ expect(resolveCursorPoint(i, '2026-08-01')).toBeNull()
90
+ expect(resolveCursorPoint(i, '2026-08-02')?.date).toBe('2026-08-02')
91
+ })
92
+
93
+ test('the final bucket ends at last + step — that instant is already outside it', () => {
94
+ const i = weekly(['01', '08', '15']) // step = 7, so the last bucket covers [Aug 15, Aug 22)
95
+ expect(resolveCursorPoint(i, '2026-08-21')?.date).toBe('2026-08-15') // last day inside
96
+ expect(resolveCursorPoint(i, '2026-08-22')).toBeNull() // exactly last + step, exclusive
97
+ expect(resolveCursorPoint(i, '2026-08-23')).toBeNull()
98
+ })
99
+
100
+ test('returns null well beyond the domain, so unrelated charts never sync', () => {
101
+ const i = weekly(['01', '08', '15'])
102
+ expect(resolveCursorPoint(i, '2026-09-30')).toBeNull()
103
+ })
104
+
105
+ test('categorical domains fall back to exact match only, same as nearest', () => {
106
+ const points = [{ date: 'Direct' }, { date: 'Referral' }]
107
+ const i = buildDomainIndex(points, (p) => p.date, 'leading')
108
+ expect(resolveCursorPoint(i, 'Direct')?.date).toBe('Direct')
109
+ expect(resolveCursorPoint(i, 'Organic')).toBeNull()
110
+ })
111
+
112
+ test('a single-point chart resolves only its own key', () => {
113
+ const i = weekly(['01'])
114
+ expect(resolveCursorPoint(i, '2026-08-01')?.date).toBe('2026-08-01')
115
+ expect(resolveCursorPoint(i, '2026-08-02')).toBeNull()
116
+ })
117
+
118
+ test('an explicit resolution argument overrides the index-built default', () => {
119
+ // Built as 'nearest', resolved as 'leading' — the per-call argument wins.
120
+ const i = index(daily(['01', '08', '15']))
121
+ expect(resolveCursorPoint(i, '2026-08-05', 'leading')?.date).toBe('2026-08-01')
122
+ })
123
+ })