@astryxdesign/charts 0.6.2-canary.2b2113d → 0.6.2-canary.2fedb82

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChartGrid.d.ts","sourceRoot":"","sources":["../src/ChartGrid.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAiB,EACjB,QAAgB,EAChB,SAAa,GACd,EAAE,cAAc,+BAmEhB"}
1
+ {"version":3,"file":"ChartGrid.d.ts","sourceRoot":"","sources":["../src/ChartGrid.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAiB,EACjB,QAAgB,EAChB,SAAa,GACd,EAAE,cAAc,+BAmEhB"}
package/dist/ChartGrid.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @file ChartGrid.tsx (v2)
5
+ * @input Chart context scales and dimensions, guide visibility, and tick density
5
6
  * @output Grid lines behind chart marks
6
7
  * @position Child of Chart v2; reads scales from chart context
7
8
  */
@@ -2,10 +2,10 @@
2
2
 
3
3
  /**
4
4
  * @file ChartSwatch.tsx
5
+ * @input Caller-owned CSS color and optional square or line shape
5
6
  * @output Color swatch primitive for legends, tooltips, and standalone use
6
- * @position Composable primitive — used by ChartLegend (and future
7
- * ChartTooltip), or independently anywhere a series indicator
8
- * is needed.
7
+ * @position Composable primitive — used by ChartLegend and ChartTooltip, or
8
+ * independently anywhere a series indicator is needed.
9
9
  *
10
10
  * @example
11
11
  * ```
@@ -39,8 +39,8 @@ export interface ChartTooltipProps {
39
39
  * Composable grouped tooltip for Chart.
40
40
  *
41
41
  * Subscribes to the chart's pointer event stream and only re-renders when the
42
- * hovered data index changes (not on every pointer move). Card position is
43
- * updated imperatively via a ref to keep pointer-move cost ~free.
42
+ * hovered data index changes (not on every pointer move). Card coordinates retain
43
+ * their previous state when the resolved point and browser geometry are unchanged.
44
44
  */
45
45
  export declare function ChartTooltip({ series, render, hoverIndicator, showHoverDots, placement, }: ChartTooltipProps): import("react").JSX.Element;
46
46
  //# sourceMappingURL=ChartTooltip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../src/ChartTooltip.tsx"],"names":[],"mappings":"AA0BA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAaf,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAC,SAAS,EAAoB,MAAM,SAAS,CAAC;AAG1D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,SAAS,CAAC;IAC5E;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AA2DD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAW,EACX,MAAM,EACN,cAAqB,EACrB,aAAoB,EACpB,SAAkB,GACnB,EAAE,iBAAiB,+BA4QnB"}
1
+ {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../src/ChartTooltip.tsx"],"names":[],"mappings":"AA8BA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAYf,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAC,SAAS,EAAoB,MAAM,SAAS,CAAC;AAG1D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,SAAS,CAAC;IAC5E;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAwDD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAW,EACX,MAAM,EACN,cAAqB,EACrB,aAAoB,EACpB,SAAkB,GACnB,EAAE,iBAAiB,+BAqUnB"}
@@ -17,7 +17,11 @@
17
17
  *
18
18
  * // Composable — drop into the chart's children for full control
19
19
  * <Chart>
20
- * <ChartTooltip hoverIndicator={false} placement="top" />
20
+ * <ChartTooltip
21
+ * series={series}
22
+ * hoverIndicator={false}
23
+ * placement="top"
24
+ * />
21
25
  * </Chart>
22
26
  * ```
23
27
  */
@@ -29,13 +33,25 @@ import { createPortal } from 'react-dom';
29
33
  import * as stylex from '@stylexjs/stylex';
30
34
  import '@astryxdesign/core/theme/tokens.stylex';
31
35
  import { colorVars, radiusVars, shadowVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
32
- import { Text } from '@astryxdesign/core';
33
- import { VStack, HStack } from '@astryxdesign/core';
36
+ import { HStack, Text, useLayer, VStack } from '@astryxdesign/core';
34
37
  import { useChart } from "./ChartContext.js";
35
38
  import { ChartSwatch, swatchVariantForType } from "./ChartSwatch.js";
36
39
  import { deriveTooltipSeriesValues, shouldRenderHoverDot } from "./tooltip.js";
37
40
  import { isBandScale, xPixel } from "./utils.js";
38
41
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
42
+ const styles = {
43
+ card: {
44
+ kWkggS: "x1prclbq",
45
+ kaIpWk: "x1hviunn",
46
+ k8WAf4: "xce4md1",
47
+ kg3NbH: "xrrkdod",
48
+ kGVxlE: "x14hfi27",
49
+ kfzvcC: "x47corl",
50
+ khDVqt: "xuxw1ft",
51
+ $$css: true
52
+ }
53
+ };
54
+
39
55
  /** Default tooltip body — used when `render` is not provided. */
40
56
  const DefaultTooltipContent = /*#__PURE__*/memo(function DefaultTooltipContent({
41
57
  xValue,
@@ -75,8 +91,8 @@ const DefaultTooltipContent = /*#__PURE__*/memo(function DefaultTooltipContent({
75
91
  * Composable grouped tooltip for Chart.
76
92
  *
77
93
  * Subscribes to the chart's pointer event stream and only re-renders when the
78
- * hovered data index changes (not on every pointer move). Card position is
79
- * updated imperatively via a ref to keep pointer-move cost ~free.
94
+ * hovered data index changes (not on every pointer move). Card coordinates retain
95
+ * their previous state when the resolved point and browser geometry are unchanged.
80
96
  */
81
97
  export function ChartTooltip({
82
98
  series = [],
@@ -97,20 +113,44 @@ export function ChartTooltip({
97
113
  height
98
114
  } = useChart();
99
115
  const [hoveredIndex, setHoveredIndex] = useState(null);
116
+ const [cardPosition, setCardPosition] = useState({
117
+ x: 0,
118
+ y: 0
119
+ });
120
+ const [portalTarget, setPortalTarget] = useState(null);
100
121
  const cardRef = useRef(null);
122
+ const lastPointerEventRef = useRef(null);
123
+ const {
124
+ hide: hideLayer,
125
+ isOpen: isLayerOpen,
126
+ render: renderLayer,
127
+ show: showLayer
128
+ } = useLayer({
129
+ mode: 'fixed'
130
+ });
101
131
  // Whether the card is currently suppressed (no content, or a custom `render`
102
- // opted out by returning null). positionCard reads this so a same-index
103
- // pointer move — which repositions the card without a React commit — can't
104
- // re-reveal a card the visibility effect below has hidden.
105
- const cardHiddenRef = useRef(false);
132
+ // opted out by returning null). Start closed; once present content opens the
133
+ // Layer, a content-bearing index change keeps it open while React swaps the
134
+ // content. positionCard also reads this so a same-index pointer move cannot
135
+ // re-reveal a suppressed card before the visibility effect confirms content.
136
+ const cardHiddenRef = useRef(true);
137
+
138
+ // The Layer host must remain in the chart's nearest HTML subtree so nested
139
+ // Theme and MediaTheme scopes keep applying. The portal crosses only the SVG
140
+ // boundary; browser top-layer promotion remains Layer's responsibility.
141
+ useEffect(() => {
142
+ const nextTarget = svgRef.current?.parentElement ?? null;
143
+ setPortalTarget(current => current === nextTarget ? current : nextTarget);
144
+ }, [svgRef, width]);
106
145
 
107
146
  // Stable Set of resolved series keys — used by deriveTooltipSeriesValues.
108
147
  // Recomputed only when the resolved map identity changes (per layout pass).
109
148
  const resolvedKeys = useMemo(() => new Set(resolved.keys()), [resolved]);
110
149
 
111
150
  // ─── Card positioning ──────────────────────────────────────────────────
112
- // Imperative — the card is portaled, and we don't want pointer-move to
113
- // trigger React renders.
151
+ // Layer owns top-layer promotion. Pointer moves update coordinates only when
152
+ // the hovered point or browser geometry changes, so moves within one point do
153
+ // not force a React commit.
114
154
  const positionCard = useCallback(e => {
115
155
  const card = cardRef.current;
116
156
  const svg = svgRef.current;
@@ -118,14 +158,19 @@ export function ChartTooltip({
118
158
  return;
119
159
  }
120
160
  if (!svg || !e.nearest || cardHiddenRef.current) {
121
- card.style.display = 'none';
161
+ hideLayer();
162
+ return;
163
+ }
164
+ showLayer();
165
+ const layerHost = card.parentElement;
166
+ if (!layerHost) {
122
167
  return;
123
168
  }
124
169
  const svgRect = svg.getBoundingClientRect();
125
170
  const screenX = svgRect.left + margin.left + e.nearest.px;
126
171
  const screenY = svgRect.top + margin.top;
127
- const cardWidth = card.offsetWidth;
128
- const cardHeight = card.offsetHeight;
172
+ const cardWidth = layerHost.offsetWidth;
173
+ const cardHeight = layerHost.offsetHeight;
129
174
  const viewportWidth = window.innerWidth;
130
175
  const viewportHeight = window.innerHeight;
131
176
  const gap = 8;
@@ -150,27 +195,31 @@ export function ChartTooltip({
150
195
  }
151
196
  }
152
197
 
153
- // Clamp to the viewport so the card never clips off-screen — it's
154
- // portaled and position:fixed, so this keeps it fully visible for every
155
- // placement (pinned placements and the near-edge `auto` flip alike).
198
+ // Clamp the card coordinates to the viewport. Oversized custom content
199
+ // keeps its caller-owned dimensions and may extend past the far edge.
156
200
  x = Math.max(gap, Math.min(x, viewportWidth - cardWidth - gap));
157
201
  y = Math.max(gap, Math.min(y, viewportHeight - cardHeight - gap));
158
- card.style.left = `${x}px`;
159
- card.style.top = `${y}px`;
160
- card.style.display = 'block';
161
- }, [svgRef, margin, placement]);
202
+ setCardPosition(current => current.x === x && current.y === y ? current : {
203
+ x,
204
+ y
205
+ });
206
+ }, [svgRef, margin, placement, hideLayer, showLayer]);
162
207
 
163
208
  // ─── Subscribe to pointer events ───────────────────────────────────────
164
209
  useEffect(() => {
165
210
  let currentIndex = null;
166
211
  const unsub = onPointer(e => {
212
+ lastPointerEventRef.current = e;
167
213
  const newIndex = e.nearest?.dataIndex ?? null;
168
214
  if (newIndex !== currentIndex) {
169
215
  currentIndex = newIndex;
170
- // Optimistically treat the new index as showable; the visibility effect
171
- // re-hides it after commit if there's no content (e.g. custom `render`
172
- // returned null). This keeps the card appearing on the first move.
173
- cardHiddenRef.current = false;
216
+ // Keep a closed/suppressed card closed until React resolves content.
217
+ // Do not preemptively close an already-visible card for another index:
218
+ // the visibility effect closes absent content after commit, while
219
+ // content-bearing transitions keep the same Layer continuously open.
220
+ if (newIndex == null) {
221
+ cardHiddenRef.current = true;
222
+ }
174
223
  setHoveredIndex(newIndex);
175
224
  }
176
225
  positionCard(e);
@@ -269,24 +318,31 @@ export function ChartTooltip({
269
318
  });
270
319
  }, [hoverIndicator, hoveredIndex, series, xScale, data, xKey, width, height, margin.top]);
271
320
 
272
- // Single source of truth for whether the card has anything to show. It's
273
- // hidden when there's no hover, when the hovered index is stale/out-of-range
274
- // (e.g. `data` shrank while hovering, so `datum` is undefined), or when a
275
- // custom `render` opts out by returning null — the documented contract, which
276
- // still shows the band highlight + dots. The decision is stored in a ref so
277
- // the imperative positionCard honors it on subsequent same-index moves
278
- // (otherwise it would re-reveal an empty bordered box). Runs after commit, so
279
- // `render` (which may return null, or throw) never executes on the SSR path.
321
+ // Render props are ordinary React content. Resolve them once per commit so a
322
+ // custom renderer is not invoked separately by the visibility synchronizer.
323
+ // Keep it browser-only: the layer has no portal target during SSR.
324
+ const cardContent = typeof document === 'undefined' || hoveredIndex == null || datum == null ? null : render ? render(xValue, seriesValues) : /*#__PURE__*/_jsx(DefaultTooltipContent, {
325
+ xValue: xValue,
326
+ rows: seriesValues
327
+ });
328
+ const isCardHidden = hoveredIndex == null || datum == null || render != null && cardContent == null;
329
+ const hasNativePopover = typeof HTMLElement !== 'undefined' && typeof HTMLElement.prototype.showPopover === 'function';
330
+ const shouldHideLayerHost = isCardHidden || !hasNativePopover && !isLayerOpen;
331
+
332
+ // Synchronize the shared Layer's native Popover state after React commits the
333
+ // new card body. The second positioning pass measures that final body; later
334
+ // moves within the same data point usually resolve to the existing state.
280
335
  useEffect(() => {
281
- const hidden = hoveredIndex == null || datum == null || render != null && render(xValue, seriesValues) == null;
282
- cardHiddenRef.current = hidden;
283
- if (hidden) {
284
- const card = cardRef.current;
285
- if (card) {
286
- card.style.display = 'none';
287
- }
336
+ cardHiddenRef.current = isCardHidden;
337
+ if (isCardHidden) {
338
+ hideLayer();
339
+ return;
340
+ }
341
+ const pointerEvent = lastPointerEventRef.current;
342
+ if (pointerEvent) {
343
+ positionCard(pointerEvent);
288
344
  }
289
- }, [render, hoveredIndex, datum, xValue, seriesValues]);
345
+ }, [isCardHidden, hoveredIndex, xValue, seriesValues, render, hideLayer, positionCard]);
290
346
 
291
347
  // ─── Render ────────────────────────────────────────────────────────────
292
348
  if (typeof document === 'undefined') {
@@ -295,22 +351,21 @@ export function ChartTooltip({
295
351
  children: [hoverIndicatorElement, dots]
296
352
  });
297
353
  }
298
-
299
- // Mirrors the visibility effect's first two conditions: no card when there's
300
- // no hover or the hovered datum is stale/out-of-range (so `render` and the
301
- // default body are never called with an undefined x-value + empty rows).
302
- const cardContent = hoveredIndex == null || datum == null ? null : render ? render(xValue, seriesValues) : /*#__PURE__*/_jsx(DefaultTooltipContent, {
303
- xValue: xValue,
304
- rows: seriesValues
305
- });
306
354
  return /*#__PURE__*/_jsxs(_Fragment, {
307
- children: [hoverIndicatorElement, dots, /*#__PURE__*/createPortal(/*#__PURE__*/_jsx("div", {
355
+ children: [hoverIndicatorElement, dots, portalTarget ? /*#__PURE__*/createPortal(renderLayer(/*#__PURE__*/_jsx("div", {
308
356
  ref: cardRef,
309
357
  role: "tooltip",
310
- ...{
311
- className: "xixxii4 x1prclbq x1hviunn xce4md1 xrrkdod x14hfi27 x47corl xuxw1ft x1q2oy4v x1s85apg"
312
- },
313
358
  children: cardContent
314
- }), document.body)]
359
+ }), {
360
+ x: cardPosition.x,
361
+ y: cardPosition.y,
362
+ // Native Popover keeps a closed host out of layout itself, so
363
+ // remove our guard before showPopover() measures it. Layer's
364
+ // reduced fallback needs the explicit closed display state.
365
+ style: shouldHideLayerHost ? {
366
+ display: 'none'
367
+ } : undefined,
368
+ xstyle: styles.card
369
+ }), portalTarget) : null]
315
370
  });
316
371
  }
package/dist/charts.css CHANGED
@@ -14,15 +14,12 @@
14
14
  .x1prclbq:not(#\#):not(#\#){background-color:var(--color-background-popover)}
15
15
  .xl8spv7:not(#\#):not(#\#){background-color:var(--x-backgroundColor)}
16
16
  .x14hfi27:not(#\#):not(#\#){box-shadow:var(--shadow-med)}
17
- .x1s85apg:not(#\#):not(#\#){display:none}
18
17
  .x2lah0s:not(#\#):not(#\#){flex-shrink:0}
19
18
  .x47corl:not(#\#):not(#\#){pointer-events:none}
20
- .xixxii4:not(#\#):not(#\#){position:fixed}
21
19
  .xnpr05h:not(#\#):not(#\#){stroke-width:1}
22
20
  .x113i6qz:not(#\#):not(#\#){stroke:var(--color-border-emphasized)}
23
21
  .x1lo1t26:not(#\#):not(#\#){stroke:var(--color-text-primary)}
24
22
  .xuxw1ft:not(#\#):not(#\#){white-space:nowrap}
25
- .x1q2oy4v:not(#\#):not(#\#){z-index:9999}
26
23
  .xuoj239:not(#\#):not(#\#):not(#\#){height:3px}
27
24
  .xdk7pt:not(#\#):not(#\#):not(#\#){height:8px}
28
25
  .xwh1t77:not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-4)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/charts",
3
- "version": "0.6.2-canary.2b2113d",
3
+ "version": "0.6.2-canary.2fedb82",
4
4
  "description": "Astryx charts — a config-model data visualization library (d3-based). Published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@stylexjs/stylex": ">=0.10.0",
50
- "@astryxdesign/core": "0.6.2-canary.2b2113d",
50
+ "@astryxdesign/core": "0.6.2-canary.2fedb82",
51
51
  "react": ">=19.0.0",
52
52
  "react-dom": ">=19.0.0"
53
53
  },
@@ -57,7 +57,7 @@
57
57
  "d3-shape": "^3.2.0"
58
58
  },
59
59
  "devDependencies": {
60
- "@astryxdesign/cli": "0.6.2-canary.2b2113d",
60
+ "@astryxdesign/cli": "0.6.2-canary.2fedb82",
61
61
  "@babel/cli": "^7.29.7",
62
62
  "@babel/core": "^7.29.7",
63
63
  "@babel/preset-react": "^7.29.7",
@@ -0,0 +1,114 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'ChartGrid',
7
+ displayName: 'Chart Grid',
8
+ group: 'Charts',
9
+ category: 'Data Visualization',
10
+ isHiddenFromOverview: true,
11
+ keywords: ['chart', 'grid', 'guides', 'axis', 'ticks', 'data visualization'],
12
+
13
+ usage: {
14
+ description:
15
+ 'ChartGrid draws horizontal and vertical guide lines from the scales owned by a parent Chart. Use it in the Chart grid slot so guides share the plot dimensions and tick values used by the chart.',
16
+ bestPractices: [
17
+ {
18
+ guidance: true,
19
+ description:
20
+ 'Pair ChartGrid with ChartAxis when readers need labelled values beside the guide lines.',
21
+ },
22
+ {
23
+ guidance: true,
24
+ description:
25
+ 'Use horizontal guides for value comparison and add vertical guides only when they help readers align categories or continuous x values.',
26
+ },
27
+ {
28
+ guidance: false,
29
+ description:
30
+ 'Use grid lines as the only way to communicate a value or distinction. Keep meaningful chart information in labels, marks, and the parent Chart alternative.',
31
+ },
32
+ {
33
+ guidance: false,
34
+ description:
35
+ 'Expect tickCount to thin categorical band centers. It requests density only for continuous scales; categorical vertical guides render once per band.',
36
+ },
37
+ ],
38
+ accessibility: [
39
+ {
40
+ name: 'Chart alternative',
41
+ category: 'Content',
42
+ criterion: '1.1.1 Non-text Content',
43
+ requirement: 'Required on the parent Chart',
44
+ states: ['All'],
45
+ description:
46
+ 'Chart owns the accessible image name and supported small-data table. Grid lines supplement that chart-level alternative rather than replacing it.',
47
+ },
48
+ {
49
+ name: 'Visual-only guides',
50
+ category: 'Color contrast',
51
+ criterion: '1.4.11 Non-text Contrast',
52
+ requirement: 'Do not carry information alone',
53
+ states: ['Horizontal', 'Vertical', 'Combined'],
54
+ description:
55
+ 'Grid lines are supporting guides. Preserve visible labels, marks, or another qualifying cue so understanding does not depend on the grid alone.',
56
+ },
57
+ ],
58
+ anatomy: [
59
+ {
60
+ name: 'Grid lines',
61
+ required: false,
62
+ description:
63
+ 'Horizontal lines at continuous y ticks and vertical lines at x ticks or categorical band centers.',
64
+ },
65
+ ],
66
+ },
67
+
68
+ props: [
69
+ {
70
+ name: 'horizontal',
71
+ type: 'boolean',
72
+ description:
73
+ 'Whether to draw horizontal guides at continuous y ticks other than zero.',
74
+ default: 'true',
75
+ },
76
+ {
77
+ name: 'vertical',
78
+ type: 'boolean',
79
+ description:
80
+ 'Whether to draw vertical guides at continuous x ticks or categorical band centers.',
81
+ default: 'false',
82
+ },
83
+ {
84
+ name: 'tickCount',
85
+ type: 'number',
86
+ description:
87
+ 'Approximate guide count requested from continuous scales. It does not thin categorical band centers.',
88
+ default: '5',
89
+ },
90
+ ],
91
+
92
+ examples: [
93
+ {
94
+ label: 'Horizontal guides',
95
+ code: `import {Chart, ChartGrid, bar} from '@astryxdesign/charts';
96
+
97
+ <Chart
98
+ data={monthlyRevenue}
99
+ xKey="month"
100
+ series={[bar('revenue')]}
101
+ grid={<ChartGrid />}
102
+ />;`,
103
+ },
104
+ {
105
+ label: 'Horizontal and vertical guides',
106
+ code: `<Chart
107
+ data={monthlyRevenue}
108
+ xKey="month"
109
+ series={[bar('revenue')]}
110
+ grid={<ChartGrid horizontal vertical tickCount={6} />}
111
+ />;`,
112
+ },
113
+ ],
114
+ };
@@ -0,0 +1,184 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 5
4
+ kind: component
5
+ id: component:ChartGrid
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [public-api, behavior, layout, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/charts/src/ChartGrid.test.tsx,
16
+ apps/storybook/stories/charts/ChartGrid.stories.tsx,
17
+ ]
18
+ modules: []
19
+ families: []
20
+ design_specs: []
21
+ architecture:
22
+ [
23
+ architecture:component-test-sufficiency,
24
+ architecture:component-theming-surface,
25
+ ]
26
+ contributing: []
27
+ system_specs: [spec:AST-002, spec:AST-029]
28
+ ---
29
+
30
+ # ChartGrid component contract
31
+
32
+ ## Contract at a glance
33
+
34
+ | Area | Contract |
35
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | Public contract | No API shape or default changes. This draft records the current horizontal, vertical, and continuous-scale tick-density surface. |
37
+ | Behavior | Horizontal guides follow continuous y ticks except zero; vertical guides follow categorical x-band centers or continuous x ticks; both directions remain independently optional. |
38
+ | End-user impact | Chart readers receive guide lines aligned with the parent Chart's plot scales; this audit changes no rendered behavior. |
39
+ | Builder impact | Existing callsites remain valid and gain direct CLI documentation and focused evidence for current density fallbacks. |
40
+ | Compatibility/readiness | The package remains canary-only. This observational draft is non-authoritative, and exact rendered/accessibility evidence remains pending. |
41
+ | Review checks | Reject a changed public shape/default, a guide using a scale outside Chart context, a zero baseline drawn twice, or categorical guides no longer centered on their bands. |
42
+ | Governing rules | `spec:AST-029/FR3–FR7`, `architecture:component-test-sufficiency/INV1–INV11`, `architecture:component-theming-surface/INV1–INV6`, and `spec:AST-002`. |
43
+
44
+ This table is a review projection; the body below is authoritative.
45
+
46
+ ## Intent
47
+
48
+ ChartGrid renders passive Cartesian guide lines inside a Chart. It owns line
49
+ selection and placement from Chart-provided scales and plot dimensions while Chart
50
+ owns the coordinate system, rendering order, and chart-level alternative.
51
+
52
+ ## Compatibility and migration
53
+
54
+ - Released default preserved: yes at the canary surface; the package has no stable release.
55
+ - Compatibility class: no public API, default, or rendered-behavior change.
56
+ - Controlled/uncontrolled behavior: not applicable.
57
+ - Migration decision: none; this draft does not authorize a public API change.
58
+
59
+ Consumer migration instructions belong in consumer docs and release notes.
60
+
61
+ ## Ownership boundary
62
+
63
+ **Owns**
64
+
65
+ - Selecting horizontal and vertical guide values from Chart-provided scales.
66
+ - Positioning each guide across the current plot width or height.
67
+ - Omitting the horizontal zero guide that the paired axis may emphasize.
68
+ - Sanitizing continuous-scale tick requests before passing them to d3.
69
+
70
+ **Does not own / non-goals**
71
+
72
+ - Data, domains, scale construction, plot dimensions, or paint order — owned by `component:Chart`.
73
+ - Axis labels, tick marks, or plot-edge lines — owned by `component:ChartAxis`.
74
+ - The chart's accessible name, description, or data alternative — owned by `component:Chart` and the product callsite.
75
+ - Product-specific decisions about whether guides are needed.
76
+
77
+ ## Public concepts
78
+
79
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
80
+ | ------------------- | --------------------------------------- | ------------------------------------------------------------- | ----------------------------------------- | --------- | --------------------- | ------------ | ---------------------------------------------------------------------------------- |
81
+ | Horizontal guides | shown or hidden | Draws lines at continuous y ticks, excluding zero. | Every ChartGrid | shown | `component:ChartGrid` | experimental | Not applicable. |
82
+ | Vertical guides | shown or hidden | Draws lines at x ticks or categorical x-band centers. | Every ChartGrid | hidden | `component:ChartGrid` | experimental | Not applicable. |
83
+ | Continuous density | finite non-negative number | Requests an approximate line count from d3 continuous scales. | Horizontal guides and continuous x guides | `5` | `component:ChartGrid` | experimental | Fractions floor; values above 1000 cap; negative/non-finite values fall back to 5. |
84
+ | Categorical density | one guide per categorical x-band center | Keeps vertical guides aligned to every band. | Categorical x with vertical guides | all bands | `component:ChartGrid` | experimental | `tickCount` currently has no effect. |
85
+ | Chart context | inside a parent `Chart` | Supplies scales and current plot dimensions. | Always | none | `component:Chart` | experimental | Rendering outside Chart throws the shared context error. |
86
+
87
+ ## Behavioral and layout contract
88
+
89
+ Draft requirements identify their basis so observed code is not mistaken for an
90
+ intentional decision.
91
+
92
+ | ID | Candidate invariant | Basis | Draft review state |
93
+ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------ |
94
+ | FR1 | ChartGrid MUST consume the current width, height, x scale, and y scale from its parent Chart context. | implementation, package composition, and focused tests | verify |
95
+ | FR2 | `horizontal` and `vertical` MUST independently enable their guide sets; enabling both MUST render both sets. | implementation, JSDoc, tests, and Storybook | verify |
96
+ | FR3 | Horizontal guides MUST use continuous y ticks, span the plot width, and omit y=0 so a paired axis does not receive a duplicate line. | implementation, JSDoc, and focused tests | verify |
97
+ | FR4 | Vertical guides MUST use categorical band centers for band x scales and d3 tick positions for continuous x scales, spanning the plot height. | implementation and focused tests | verify |
98
+ | FR5 | Continuous tick requests MUST be finite, non-negative integers no greater than 1000; zero yields no continuous guides and invalid values use five. | implementation, crash-prevention rationale, and tests | verify |
99
+ | FR6 | Guide paint MUST consume a semantic theme token. Whether grid lines become a public theme target remains unsettled. | implementation and current theming architecture | verify |
100
+
101
+ ### Allowed variation
102
+
103
+ - **AV1 — Continuous tick count.** d3 may choose a nearby set of values rather than exactly the requested number.
104
+ - **AV2 — Categorical values.** Each current x-band domain value contributes one centered vertical guide.
105
+
106
+ ### Representative states
107
+
108
+ | State | Required invariant | Allowed variation |
109
+ | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- |
110
+ | Default | Horizontal guides only; continuous density defaults to five. | d3 chooses the exact y tick set. |
111
+ | Horizontal disabled | No horizontal guides. | Vertical guides may remain enabled. |
112
+ | Vertical categorical | One vertical guide at each x-band center. | Band width and center follow Chart's x scale. |
113
+ | Vertical continuous | One guide at each generated continuous x tick. | d3 chooses the exact x tick set. |
114
+ | Both directions | Horizontal and vertical sets render together. | Their exact counts may differ. |
115
+ | Zero continuous tick count | Continuous guide sets are empty. | Categorical vertical guides remain unaffected. |
116
+ | Invalid tick count | Continuous guide generation falls back to the default request. | d3 chooses the resulting exact ticks. |
117
+ | Narrow / RTL | Guides remain tied to physical data coordinates and plot bounds. | Plot width and data-domain order vary. |
118
+
119
+ ### Transformation and precedence order
120
+
121
+ - **ORD1 — Continuous density.** Validate finiteness and non-negativity → floor fractions → cap at 1000 → request d3 ticks.
122
+ - **ORD2 — Placement.** Select enabled directions → choose the scale path → derive tick or band-center positions → span the current opposite plot dimension.
123
+
124
+ ### Performance and resources
125
+
126
+ - **PR1 — Derived output.** Guide arrays remain render-derived and memoized from enabled directions, scales, dimensions, and sanitized density. ChartGrid owns no listener, observer, timer, or persistent resource.
127
+ - **PR2 — Bounded tick allocation.** A public density value cannot request more than 1000 continuous ticks from d3.
128
+
129
+ ## Accessibility contract
130
+
131
+ - **AR1 — Chart-level alternative.** ChartGrid renders passive supporting guides inside Chart's named image. Chart owns the accessible chart name and supported data-table alternative.
132
+ - **AR2 — No grid-only information.** Grid lines do not replace visible labels, meaningful marks, or the parent Chart alternative.
133
+
134
+ ## Design relationships
135
+
136
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
137
+ | ---------------- | ----------------------------------------------------------------- | --------------------------------------------- | -------------- | ------------------ |
138
+ | Grid lines | Support position comparison without carrying chart meaning alone. | unsettled; no current design record is linked | supporting | FR2–FR6, AR1–AR2 |
139
+
140
+ ### Theming anatomy
141
+
142
+ <!-- anatomy-theming:v1 -->
143
+
144
+ ```json
145
+ {
146
+ "Grid lines": {
147
+ "none": {
148
+ "reason": "unsettled: ChartGrid consumes a semantic color token, but no current component or family contract decides whether grid lines are a public theme target."
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ ## Family and system relationships
155
+
156
+ - No current family or design record governs ChartGrid.
157
+ - `architecture:component-test-sufficiency` owns rational public-state partitions, observable assertions, and audit evidence quality.
158
+ - `architecture:component-theming-surface` owns anatomy-to-target disposition; token consumption alone does not enroll ChartGrid in public component theming.
159
+ - `spec:AST-002` owns public API admission and prevents this audit from adding or reinterpreting a public choice.
160
+ - `spec:AST-029` owns this observational backfill, closed evidence receipt, safe remediation, and fail-closed eligibility report.
161
+
162
+ ## Verification map
163
+
164
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
165
+ | -------- | ------------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------- |
166
+ | FR1–FR2 | `ChartGrid.test.tsx`; `ChartGrid.stories.tsx` | default, horizontal off, vertical on, both, neither | A direction stops respecting its input or output escapes the parent Chart context. | `audit:ChartGrid/behavior` |
167
+ | FR3 | `ChartGrid.test.tsx` | positive and mixed-sign y domains | Guides stop spanning the plot or y=0 is drawn twice. | `audit:ChartGrid/behavior-layout` |
168
+ | FR4 | `ChartGrid.test.tsx`; `ChartGrid.stories.tsx` | categorical and continuous x | A band guide leaves its center or a continuous tick lands at the wrong coordinate. | `audit:ChartGrid/behavior-layout` |
169
+ | FR5, PR2 | focused zero/invalid-density tests and source review | zero and invalid requests; fractional and capped partitions | Invalid input crashes render or bypasses the bounded fallback. | `audit:ChartGrid/behavior-testing` |
170
+ | FR6 | strict lint; Storybook theme matrix remains pending | light, dark, and supported non-default themes | A raw color or component-side mode choice replaces semantic token paint. | `audit:ChartGrid/theming-design` |
171
+ | AR1–AR2 | Chart accessibility tests; package axe remains pending | named Chart, small-data table, grid enabled | Grid content replaces or becomes the only source of required chart information. | `audit:ChartGrid/accessibility` |
172
+
173
+ ## Decision log
174
+
175
+ None. This draft records observed behavior and evidence gaps only.
176
+
177
+ ## Open questions
178
+
179
+ - **OQ1 — Categorical density.** Should `tickCount` remain continuous-scale-only, or should vertical categorical guides gain a documented thinning contract aligned with ChartAxis? (`human-api`)
180
+
181
+ ## Content boundary
182
+
183
+ This file does not duplicate consumer prop tables/examples, current audit results,
184
+ implementation steps, or shared system rules. It links to their owners.