@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.
- package/dist/ChartGrid.d.ts.map +1 -1
- package/dist/ChartGrid.js +1 -0
- package/dist/ChartSwatch.js +3 -3
- package/dist/ChartTooltip.d.ts +2 -2
- package/dist/ChartTooltip.d.ts.map +1 -1
- package/dist/ChartTooltip.js +109 -54
- package/dist/charts.css +0 -3
- package/package.json +3 -3
- package/src/ChartGrid.doc.mjs +114 -0
- package/src/ChartGrid.spec.md +184 -0
- package/src/ChartGrid.test.tsx +20 -1
- package/src/ChartGrid.tsx +1 -0
- package/src/ChartLegend.doc.mjs +146 -0
- package/src/ChartLegend.spec.md +203 -0
- package/src/ChartSwatch.doc.mjs +104 -2
- package/src/ChartSwatch.spec.md +189 -0
- package/src/ChartSwatch.tsx +3 -3
- package/src/ChartTooltip.doc.mjs +167 -0
- package/src/ChartTooltip.spec.md +240 -0
- package/src/ChartTooltip.test.tsx +324 -31
- package/src/ChartTooltip.tsx +122 -65
package/dist/ChartGrid.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChartGrid.d.ts","sourceRoot":"","sources":["../src/ChartGrid.tsx"],"names":[],"mappings":"
|
|
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
package/dist/ChartSwatch.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
* ```
|
package/dist/ChartTooltip.d.ts
CHANGED
|
@@ -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
|
|
43
|
-
*
|
|
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":"
|
|
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"}
|
package/dist/ChartTooltip.js
CHANGED
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
*
|
|
18
18
|
* // Composable — drop into the chart's children for full control
|
|
19
19
|
* <Chart>
|
|
20
|
-
* <ChartTooltip
|
|
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
|
|
79
|
-
*
|
|
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).
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
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
|
-
//
|
|
113
|
-
//
|
|
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
|
-
|
|
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 =
|
|
128
|
-
const cardHeight =
|
|
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
|
|
154
|
-
//
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
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
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
}, [
|
|
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
|
-
}),
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|