@astryxdesign/charts 0.6.2-canary.bfee8a4 → 0.6.2-canary.da998a2
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/ChartAxis.d.ts +1 -1
- package/dist/ChartAxis.d.ts.map +1 -1
- package/dist/ChartAxis.js +16 -6
- package/dist/ChartGrid.d.ts.map +1 -1
- package/dist/ChartGrid.js +1 -0
- package/package.json +3 -3
- package/src/ChartAxis.doc.mjs +162 -0
- package/src/ChartAxis.spec.md +211 -0
- package/src/ChartAxis.test.tsx +80 -2
- package/src/ChartAxis.tsx +15 -8
- 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/dist/ChartAxis.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface ChartAxisProps {
|
|
|
7
7
|
maxTicks?: number;
|
|
8
8
|
/** Custom tick formatter */
|
|
9
9
|
tickFormat?: (value: unknown) => string;
|
|
10
|
-
/** Truncate labels to this many characters (appends "\u2026"). */
|
|
10
|
+
/** Truncate labels to this many user-perceived characters (appends "\u2026"). */
|
|
11
11
|
truncate?: number;
|
|
12
12
|
/** Enable smooth transitions for streaming (default: true) */
|
|
13
13
|
animated?: boolean;
|
package/dist/ChartAxis.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChartAxis.d.ts","sourceRoot":"","sources":["../src/ChartAxis.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ChartAxis.d.ts","sourceRoot":"","sources":["../src/ChartAxis.tsx"],"names":[],"mappings":"AA4BA,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,SAAa,EACb,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,QAAe,EACf,YAAoC,EACpC,SAAiB,GAClB,EAAE,cAAc,+BA4LhB"}
|
package/dist/ChartAxis.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file ChartAxis.tsx (v2)
|
|
5
|
+
* @input Chart context scales, axis presentation props, and grapheme-safe text utilities
|
|
5
6
|
* @output Renders an axis (top, right, bottom, left) using the chart's scales
|
|
6
7
|
* @position Child of Chart v2; reads scales from chart context
|
|
7
8
|
*
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
import { useCallback, useMemo } from 'react';
|
|
21
22
|
import * as stylex from '@stylexjs/stylex';
|
|
23
|
+
import { characterCount, truncateCharacters } from '@astryxdesign/core/utils';
|
|
22
24
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
23
25
|
import { colorVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
24
26
|
import { useChart } from "./ChartContext.js";
|
|
@@ -85,20 +87,28 @@ export function ChartAxis({
|
|
|
85
87
|
}, [tickFormat, scale, safeTickCount]);
|
|
86
88
|
const format = useCallback(value => {
|
|
87
89
|
const str = (tickFormat ?? autoFormat ?? String)(value);
|
|
88
|
-
|
|
90
|
+
if (!truncate) {
|
|
91
|
+
return str;
|
|
92
|
+
}
|
|
93
|
+
const shortened = truncateCharacters(str, truncate, '');
|
|
94
|
+
return shortened === str ? str : shortened + '\u2026';
|
|
89
95
|
}, [tickFormat, autoFormat, truncate]);
|
|
90
96
|
const ticks = useMemo(() => {
|
|
97
|
+
// Format each generated tick once so density and rendering share the same
|
|
98
|
+
// grapheme-safe label instead of repeating consumer formatting/segmentation.
|
|
91
99
|
let allTicks;
|
|
92
100
|
if (isBandScale(scale)) {
|
|
93
101
|
allTicks = scale.domain().map(d => ({
|
|
94
102
|
value: d,
|
|
95
|
-
offset: (scale(d) ?? 0) + scale.bandwidth() / 2
|
|
103
|
+
offset: (scale(d) ?? 0) + scale.bandwidth() / 2,
|
|
104
|
+
label: format(d)
|
|
96
105
|
}));
|
|
97
106
|
} else {
|
|
98
107
|
const linearScale = scale;
|
|
99
108
|
allTicks = linearScale.ticks(safeTickCount).map(d => ({
|
|
100
109
|
value: d,
|
|
101
|
-
offset: linearScale(d)
|
|
110
|
+
offset: linearScale(d),
|
|
111
|
+
label: format(d)
|
|
102
112
|
}));
|
|
103
113
|
}
|
|
104
114
|
|
|
@@ -111,7 +121,7 @@ export function ChartAxis({
|
|
|
111
121
|
let cap = maxTicks;
|
|
112
122
|
if (cap == null && allTicks.length > 1) {
|
|
113
123
|
if (isHorizontal && width > 0) {
|
|
114
|
-
const widestChars = allTicks.reduce((m,
|
|
124
|
+
const widestChars = allTicks.reduce((m, tick) => Math.max(m, characterCount(tick.label)), 1);
|
|
115
125
|
const approxLabelPx = widestChars * 7 + 16;
|
|
116
126
|
cap = Math.max(1, Math.floor(width / approxLabelPx));
|
|
117
127
|
} else if (!isHorizontal && height > 0) {
|
|
@@ -157,9 +167,9 @@ export function ChartAxis({
|
|
|
157
167
|
}
|
|
158
168
|
}), ticks.map(({
|
|
159
169
|
value,
|
|
160
|
-
offset
|
|
170
|
+
offset,
|
|
171
|
+
label
|
|
161
172
|
}) => {
|
|
162
|
-
const label = format(value);
|
|
163
173
|
// Key off the raw tick value, not the formatted label: distinct ticks
|
|
164
174
|
// can format (or truncate) to the same string and would collide as keys.
|
|
165
175
|
const key = String(value);
|
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/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.da998a2",
|
|
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.da998a2",
|
|
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.da998a2",
|
|
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,162 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
|
|
4
|
+
|
|
5
|
+
export const docs = {
|
|
6
|
+
name: 'ChartAxis',
|
|
7
|
+
displayName: 'Chart Axis',
|
|
8
|
+
group: 'Charts',
|
|
9
|
+
category: 'Data Visualization',
|
|
10
|
+
isHiddenFromOverview: true,
|
|
11
|
+
keywords: ['chart', 'axis', 'ticks', 'labels', 'scale', 'data visualization'],
|
|
12
|
+
|
|
13
|
+
usage: {
|
|
14
|
+
description:
|
|
15
|
+
'ChartAxis renders tick labels and optional edge and tick lines from the scales owned by a parent Chart. Use it in the Chart axes slot for physical top, right, bottom, or left plot edges.',
|
|
16
|
+
bestPractices: [
|
|
17
|
+
{
|
|
18
|
+
guidance: true,
|
|
19
|
+
description:
|
|
20
|
+
'Use ChartAxis inside Chart so its labels, ticks, and edge line share the same scales and plot dimensions as the marks.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
guidance: true,
|
|
24
|
+
description:
|
|
25
|
+
'Use tickFormat for product-specific units or locale formatting, and maxTicks or truncate when dense category labels need a bounded presentation.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
guidance: false,
|
|
29
|
+
description:
|
|
30
|
+
'Use axis labels or a pointer tooltip as the only way to obtain important values. Give Chart an accessible name and preserve an equivalent data view or summary.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
guidance: false,
|
|
34
|
+
description:
|
|
35
|
+
'Hide the edge line while showing tick marks. showTicks intentionally keeps the line visible so each mark has a grounded edge.',
|
|
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. Tick labels supplement that chart-level alternative rather than replacing it.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Label text integrity',
|
|
50
|
+
category: 'Content',
|
|
51
|
+
criterion: 'Unicode grapheme clusters',
|
|
52
|
+
requirement: 'Required when truncate is set',
|
|
53
|
+
states: ['Truncated labels'],
|
|
54
|
+
description:
|
|
55
|
+
'Truncation keeps emoji, joined emoji sequences, flags, and combining marks intact before appending the ellipsis.',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
anatomy: [
|
|
59
|
+
{
|
|
60
|
+
name: 'Axis edge line',
|
|
61
|
+
required: false,
|
|
62
|
+
description:
|
|
63
|
+
'Line along the selected plot edge. It is shown by default only on the bottom edge and whenever tick marks are shown.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Tick marks',
|
|
67
|
+
required: false,
|
|
68
|
+
description:
|
|
69
|
+
'Short lines extending outward from each retained tick position.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Tick labels',
|
|
73
|
+
required: true,
|
|
74
|
+
description:
|
|
75
|
+
'Formatted scale values or categories positioned outside the selected plot edge.',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
props: [
|
|
81
|
+
{
|
|
82
|
+
name: 'position',
|
|
83
|
+
type: "'top' | 'right' | 'bottom' | 'left'",
|
|
84
|
+
description: 'Physical plot edge on which to render the axis.',
|
|
85
|
+
required: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'tickCount',
|
|
89
|
+
type: 'number',
|
|
90
|
+
description:
|
|
91
|
+
'Approximate continuous-scale tick count. d3 chooses the final values.',
|
|
92
|
+
default: '5',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'maxTicks',
|
|
96
|
+
type: 'number',
|
|
97
|
+
description:
|
|
98
|
+
'Maximum displayed label count. Labels are evenly skipped when the generated set is larger.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'tickFormat',
|
|
102
|
+
type: '(value: unknown) => string',
|
|
103
|
+
description:
|
|
104
|
+
'Formats each retained tick. When omitted, continuous scales use d3 formatting and band scales use their category strings.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'truncate',
|
|
108
|
+
type: 'number',
|
|
109
|
+
description:
|
|
110
|
+
'Maximum content characters retained before an ellipsis is appended. User-perceived characters remain intact.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'animated',
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
description:
|
|
116
|
+
'Whether tick position and visibility changes use the built-in transition.',
|
|
117
|
+
default: 'true',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'showAxisLine',
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
description:
|
|
123
|
+
'Whether to draw the edge line. Tick marks force the line on so they remain grounded.',
|
|
124
|
+
default: 'true for bottom; false for top, right, and left',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'showTicks',
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Whether to draw an outward mark at each retained tick.',
|
|
130
|
+
default: 'false',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
|
|
134
|
+
examples: [
|
|
135
|
+
{
|
|
136
|
+
label: 'Bottom and left axes',
|
|
137
|
+
code: `import {Chart, ChartAxis, ChartGrid, bar} from '@astryxdesign/charts';
|
|
138
|
+
|
|
139
|
+
<Chart
|
|
140
|
+
data={monthlyRevenue}
|
|
141
|
+
xKey="month"
|
|
142
|
+
series={[bar('revenue')]}
|
|
143
|
+
grid={<ChartGrid horizontal />}
|
|
144
|
+
axes={
|
|
145
|
+
<>
|
|
146
|
+
<ChartAxis position="bottom" />
|
|
147
|
+
<ChartAxis position="left" tickFormat={value => String(value)} />
|
|
148
|
+
</>
|
|
149
|
+
}
|
|
150
|
+
/>;`,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: 'Dense category labels',
|
|
154
|
+
code: `<ChartAxis
|
|
155
|
+
position="bottom"
|
|
156
|
+
maxTicks={8}
|
|
157
|
+
truncate={12}
|
|
158
|
+
showTicks
|
|
159
|
+
/>;`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 5
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ChartAxis
|
|
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/ChartAxis.test.tsx,
|
|
16
|
+
apps/storybook/stories/charts/ChartAxis.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
|
+
# ChartAxis component contract
|
|
31
|
+
|
|
32
|
+
## Contract at a glance
|
|
33
|
+
|
|
34
|
+
| Area | Contract |
|
|
35
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| Public contract | No API shape or default changes. Truncation and automatic horizontal density now count complete grapheme clusters under the objective Unicode text-segmentation standard. |
|
|
37
|
+
| Behavior | The selected physical plot edge determines scale, placement, label direction, and the default edge line; explicit formatting and density inputs refine labels. |
|
|
38
|
+
| End-user impact | Chart readers keep complete tick-label characters, and equally short Unicode labels are no longer thinned more aggressively than ASCII labels. |
|
|
39
|
+
| Builder impact | None. Existing callsites remain valid and receive direct CLI documentation for the current surface. |
|
|
40
|
+
| Compatibility/readiness | The canary API and defaults are preserved. This record is draft observational evidence; rendered and accessibility-tree evidence remains incomplete. |
|
|
41
|
+
| Review checks | Reject a changed public shape/default, an axis using the wrong Chart scale, broken physical-edge placement, split grapheme labels, or hidden line/tick precedence. |
|
|
42
|
+
| Governing rules | `spec:AST-029/FR3–FR7`, `architecture:component-test-sufficiency/INV1–INV9`, and [Unicode Text Segmentation](https://www.unicode.org/reports/tr29/). |
|
|
43
|
+
|
|
44
|
+
This table is a review projection; the body below is authoritative.
|
|
45
|
+
|
|
46
|
+
## Intent
|
|
47
|
+
|
|
48
|
+
ChartAxis renders labelled Cartesian x or y scale output inside a Chart. It owns
|
|
49
|
+
physical-edge placement, tick selection and formatting, optional tick marks, and the
|
|
50
|
+
optional axis edge line while Chart owns the scale domains and plot geometry.
|
|
51
|
+
|
|
52
|
+
## Compatibility and migration
|
|
53
|
+
|
|
54
|
+
- Released default preserved: yes; the package is canary-only and has no stable release.
|
|
55
|
+
- Compatibility class: no public API or default changes; Unicode truncation and automatic thinning intentionally change only labels containing multi-code-unit graphemes, while representative ASCII behavior is preserved.
|
|
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 the Chart-provided x or y scale for one physical plot edge.
|
|
66
|
+
- Generating, formatting, thinning, truncating, positioning, and hiding tick labels.
|
|
67
|
+
- Rendering its optional edge line and optional outward tick marks.
|
|
68
|
+
- Applying optional transition styling to tick position and visibility changes.
|
|
69
|
+
|
|
70
|
+
**Does not own / non-goals**
|
|
71
|
+
|
|
72
|
+
- Data, domains, scale construction, or plot dimensions — owned by `component:Chart`.
|
|
73
|
+
- Grid lines — owned by `component:ChartGrid`.
|
|
74
|
+
- Product-specific units, locale choices, or label wording supplied through `tickFormat` — owned by the product callsite.
|
|
75
|
+
- A standalone chart data alternative — owned by `component:Chart` and its surrounding product content.
|
|
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
|
+
| Physical edge | `top`, `right`, `bottom`, `left` | Chooses the plot edge, scale axis, transform, and outward label side. | Required for every axis. | none | `component:ChartAxis` | experimental | Unknown values are rejected by TypeScript. |
|
|
82
|
+
| Tick target | finite number | Requests an approximate continuous-scale tick count from d3. | Continuous scales; also configures the automatic formatter. | `5` | `component:ChartAxis` | experimental | Negative/non-finite values fall back to 5; fractions floor; values cap at 1000. |
|
|
83
|
+
| Label cap | number or absent | Evenly skips labels when the generated set exceeds the requested cap. | Every scale. | automatic from available space | `component:ChartAxis` | experimental | Values outside the intended positive-count domain are not specified. |
|
|
84
|
+
| Label formatter | `(value: unknown) => string` or absent | Replaces the band string or d3 continuous-scale formatter. | Every tick. | scale-appropriate built-in formatter | `component:ChartAxis` | experimental | Exceptions from consumer formatters propagate. |
|
|
85
|
+
| Label truncation | positive character count or absent | Keeps that many user-perceived characters, then appends an ellipsis. | Every formatted label. | absent | `component:ChartAxis` | experimental | Zero/omitted disables truncation; other invalid values are not specified. |
|
|
86
|
+
| Edge line | shown or hidden | Renders a line along the selected axis edge. | Every edge; tick marks force it on. | shown only for `bottom` | `component:ChartAxis` | experimental | Not applicable. |
|
|
87
|
+
| Tick marks | shown or hidden | Renders an outward perpendicular mark beside each visible tick label. | Every edge. | hidden | `component:ChartAxis` | experimental | Not applicable. |
|
|
88
|
+
| Tick transition | animated or immediate | Animates tick transform and opacity during scale changes. | Generated ticks. | animated | `component:ChartAxis` | experimental | Not applicable. |
|
|
89
|
+
|
|
90
|
+
## Behavioral and layout contract
|
|
91
|
+
|
|
92
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
93
|
+
intentional decision.
|
|
94
|
+
|
|
95
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
96
|
+
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------ |
|
|
97
|
+
| FR1 | A top or bottom axis MUST consume Chart's x scale; a left or right axis MUST consume the categorical y scale when present and the linear y scale otherwise. | implementation, types, and focused tests | verify |
|
|
98
|
+
| FR2 | Continuous tick generation MUST use one sanitized count for both d3 tick generation and its default formatter; band scales MUST use their complete domain before thinning. | implementation | verify |
|
|
99
|
+
| FR3 | A supplied `tickFormat` MUST win; otherwise continuous scales use d3 formatting and band values use `String`. | implementation and focused tests | verify |
|
|
100
|
+
| FR4 | Explicit `maxTicks` MUST win over the size-derived cap. | implementation and focused tests | verify |
|
|
101
|
+
| FR5 | Truncation MUST keep complete Unicode grapheme clusters while preserving the documented content-count-plus-ellipsis output. | focused red/green test and Unicode Standard Annex #29 | settled |
|
|
102
|
+
| FR6 | Automatic horizontal density MUST estimate formatted label length in user-perceived characters rather than UTF-16 storage units. | focused red/green equivalence test and Unicode segmentation | settled |
|
|
103
|
+
| FR7 | Bottom MUST show an edge line by default and the other edges MUST hide it by default. `showTicks` MUST force the edge line on and render one mark per surviving tick. | implementation, JSDoc, focused tests, and Storybook | verify |
|
|
104
|
+
| FR8 | Bottom and right axes MUST translate to the corresponding plot edge. Top and left remain at the plot origin. Labels and tick marks extend outwards from that physical edge. | implementation and focused tests | verify |
|
|
105
|
+
| FR9 | On a mixed-sign linear y domain, the bottom edge line MUST align with y=0; categorical y leaves it on the plot edge. | implementation | verify |
|
|
106
|
+
| FR10 | Tick movement and visibility MAY transition when `animated` is true and MUST be immediate when false. Out-of-range ticks remain mounted with zero opacity during updates. | implementation and streaming examples; motion intent unsettled | human decision |
|
|
107
|
+
|
|
108
|
+
### Allowed variation
|
|
109
|
+
|
|
110
|
+
- **AV1 — Tick count.** d3 may choose a nearby continuous tick count rather than the requested number.
|
|
111
|
+
- **AV2 — Label set.** Automatic and explicit density limits may omit evenly spaced labels while preserving their scale order.
|
|
112
|
+
- **AV3 — Supplied text.** Product callsites may format values in their own units and locale through `tickFormat`.
|
|
113
|
+
|
|
114
|
+
### Representative states
|
|
115
|
+
|
|
116
|
+
| State | Required invariant | Allowed variation |
|
|
117
|
+
| --------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
118
|
+
| Bottom categorical | Band labels are centered; the edge line is shown by default. | Automatic density may omit labels. |
|
|
119
|
+
| Top categorical | Band labels render outside the top edge. | Edge line remains hidden unless requested. |
|
|
120
|
+
| Left continuous | d3 labels render outside the left edge. | d3 chooses exact tick values. |
|
|
121
|
+
| Right continuous | Axis translates to the right edge and labels render outside it. | d3 chooses exact tick values. |
|
|
122
|
+
| Vertical categorical | Row labels use `yBandScale` and align with band centers. | Automatic density may omit labels. |
|
|
123
|
+
| Explicit formatting | Every retained label uses the supplied formatter. | Consumer-owned wording and units. |
|
|
124
|
+
| Truncated label | Complete grapheme clusters precede the appended ellipsis. | The caller chooses the content-character limit. |
|
|
125
|
+
| Tick marks shown | Every retained label has an outward mark and an edge line. | Edge-line visibility input is subsumed in this state. |
|
|
126
|
+
| Mixed-sign linear y data | Bottom line represents y=0 rather than the plot boundary. | Exact pixel position follows the supplied domain. |
|
|
127
|
+
| Streaming / changing domain | Tick positions and visibility update; animation follows `animated`. | Tick identity follows the raw value. |
|
|
128
|
+
|
|
129
|
+
### Transformation and precedence order
|
|
130
|
+
|
|
131
|
+
- **ORD1 — Scale.** Resolve physical orientation → choose Chart scale → generate and format each tick once → derive the grapheme-aware density cap → evenly filter labels.
|
|
132
|
+
- **ORD2 — Label.** Apply the supplied formatter or the scale default → preserve complete grapheme clusters when truncating → render the result.
|
|
133
|
+
- **ORD3 — Edge.** Resolve the position default → apply `showAxisLine` → force the line on when `showTicks` is true.
|
|
134
|
+
|
|
135
|
+
### Performance and resources
|
|
136
|
+
|
|
137
|
+
- **PR1 — Bounded continuous ticks.** Sanitization caps the d3 request at 1000 so a consumer value cannot allocate an unbounded tick array.
|
|
138
|
+
- **PR2 — Derived tick state.** Tick generation and formatting remain render-derived and memoized; each generated label is formatted once and shared by density and rendering, and ChartAxis owns no listener, observer, timer, or persistent resource.
|
|
139
|
+
|
|
140
|
+
## Accessibility contract
|
|
141
|
+
|
|
142
|
+
- **AR1 — Visible text integrity.** A truncated tick label MUST preserve complete user-perceived characters under Unicode grapheme-cluster boundaries.
|
|
143
|
+
- **AR2 — Chart-level alternative.** ChartAxis renders inside Chart's named image. Chart owns the accessible chart name and supported small-data table; axis labels and pointer content do not replace that alternative.
|
|
144
|
+
- **AR3 — Nested axis semantics gap.** The implementation currently emits a position-labelled `role="group"`. Whether that group should be independently exposed and localized beneath Chart's `role="img"` remains unverified and requires owner review.
|
|
145
|
+
|
|
146
|
+
## Design relationships
|
|
147
|
+
|
|
148
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
149
|
+
| ---------------- | ------------------------------------------------------ | ------------------------------------------------- | -------------- | ------------------ |
|
|
150
|
+
| Axis edge line | Grounds the physical plot edge when present. | unsettled; no current design record is linked | supporting | FR7, FR9 |
|
|
151
|
+
| Tick marks | Associate retained labels with positions on the scale. | unsettled; no current design record is linked | supporting | FR7 |
|
|
152
|
+
| Tick labels | Preserve readable value/category text around the plot. | Unicode grapheme boundaries settle text integrity | supporting | FR3, FR5, FR6, AR1 |
|
|
153
|
+
| Tick transition | Shows continuity during scale changes. | unsettled; `design:motion` remains draft | supporting | FR10 |
|
|
154
|
+
|
|
155
|
+
### Theming anatomy
|
|
156
|
+
|
|
157
|
+
<!-- anatomy-theming:v1 -->
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"Axis edge line": {
|
|
162
|
+
"none": {
|
|
163
|
+
"reason": "unsettled: ChartAxis consumes semantic color tokens but no current component or family contract decides whether the line is a public theme target."
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"Tick marks": {
|
|
167
|
+
"none": {
|
|
168
|
+
"reason": "unsettled: ChartAxis consumes semantic color tokens but no current component or family contract decides whether tick marks are a public theme target."
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"Tick labels": {
|
|
172
|
+
"none": {
|
|
173
|
+
"reason": "unsettled: ChartAxis consumes semantic color tokens but no current component or family contract decides whether labels are a public theme target."
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Family and system relationships
|
|
180
|
+
|
|
181
|
+
- No current family or design record governs ChartAxis.
|
|
182
|
+
- `architecture:component-test-sufficiency` owns rational public-state partitions, observable assertions, and red-before-green audit remediation.
|
|
183
|
+
- `architecture:component-theming-surface` owns anatomy-to-target disposition; token consumption alone does not enroll ChartAxis in public component theming.
|
|
184
|
+
- `spec:AST-002` owns public API admission and forbids an audit from smuggling a new public choice into a repair.
|
|
185
|
+
- `spec:AST-029` owns this observational backfill, closed evidence receipt, safe remediation, and fail-closed eligibility report.
|
|
186
|
+
|
|
187
|
+
## Verification map
|
|
188
|
+
|
|
189
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
190
|
+
| ------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------- |
|
|
191
|
+
| FR1, FR8 | `ChartAxis.test.tsx`; `ChartAxis.stories.tsx` | top, right, bottom, left; band and linear | An edge uses the wrong scale, transform, or outward label side. | `audit:ChartAxis/behavior-layout` |
|
|
192
|
+
| FR2, FR3 | `ChartAxis.test.tsx` | default count; band, linear, and custom formatting | Generation and formatting disagree or a supplied formatter is ignored. | `audit:ChartAxis/behavior` |
|
|
193
|
+
| FR4 | `ChartAxis.test.tsx` | automatic cap and explicit `maxTicks` | Explicit capping stops winning over size-derived thinning. | `audit:ChartAxis/behavior` |
|
|
194
|
+
| FR5, FR6, AR1 | red-before-green grapheme regressions; density tests; browser story | ASCII, emoji, flag, and ZWJ labels; truncated wide/narrow plots | A visible label splits a grapheme or equal perceived-width labels retain different ticks. | `audit:ChartAxis/i18n-testing` |
|
|
195
|
+
| FR7 | `ChartAxis.test.tsx`; Axes & Grids Storybook fixture | defaults; line requested; ticks requested | Tick marks lose their edge line or line defaults change. | `audit:ChartAxis/behavior` |
|
|
196
|
+
| FR9 | source review; browser evidence remains pending | positive-only, negative-only, mixed-sign, y-band | The mixed-sign zero line moves to the edge or a categorical axis crosses the plot. | `audit:ChartAxis/behavior-design` |
|
|
197
|
+
| FR10 | streaming stories; exact-head browser evidence remains pending | animated, immediate, entering/leaving tick | Disabling animation retains a transition or a moving label jumps to stale geometry. | `audit:ChartAxis/design-testing` |
|
|
198
|
+
| AR2, AR3 | Chart accessibility tests; package axe; accessibility-tree review | title/subtitle, small-data table, nested axis groups | Chart loses its alternative or an unreviewed nested semantic claim is treated as fact. | `audit:ChartAxis/accessibility` |
|
|
199
|
+
|
|
200
|
+
## Decision log
|
|
201
|
+
|
|
202
|
+
None. This draft records observed behavior and one objective Unicode correction; it does not approve new ChartAxis policy.
|
|
203
|
+
|
|
204
|
+
## Open questions
|
|
205
|
+
|
|
206
|
+
- **OQ1 — Axis semantics.** Should ChartAxis remain purely visual content within Chart's single named image, or expose each axis as an independently localized accessibility group? (`human-api`)
|
|
207
|
+
|
|
208
|
+
## Content boundary
|
|
209
|
+
|
|
210
|
+
This file does not duplicate consumer prop tables/examples, current audit results,
|
|
211
|
+
implementation steps, or shared system rules. It links to their owners.
|
package/src/ChartAxis.test.tsx
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @file ChartAxis.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, Chart, ChartAxis, bar mark
|
|
6
6
|
* @output Functional tests for axis rendering — group semantics, band/linear
|
|
7
|
-
* tick labels, default edge-line rules,
|
|
8
|
-
* and label capping
|
|
7
|
+
* tick labels, physical edge placement, default edge-line rules,
|
|
8
|
+
* custom formatting, grapheme-safe truncation and density, and label capping
|
|
9
9
|
* @position Colocated test for ChartAxis.tsx (issue #4295 viz coverage)
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -88,6 +88,52 @@ describe('ChartAxis bottom (band scale)', () => {
|
|
|
88
88
|
expect(within(axis).queryByText('Feb')).not.toBeInTheDocument();
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it('automatically reduces categorical labels as the plot narrows', () => {
|
|
92
|
+
const denseData = Array.from({length: 20}, (_, index) => ({
|
|
93
|
+
month: `Period ${index + 1}`,
|
|
94
|
+
sales: index,
|
|
95
|
+
}));
|
|
96
|
+
renderChart(<ChartAxis position="bottom" />, denseData);
|
|
97
|
+
const axis = screen.getByRole('group', {name: 'bottom axis'});
|
|
98
|
+
const wideLabelCount = axis.querySelectorAll('text').length;
|
|
99
|
+
|
|
100
|
+
reportWidth(120);
|
|
101
|
+
|
|
102
|
+
const narrowLabelCount = axis.querySelectorAll('text').length;
|
|
103
|
+
expect(narrowLabelCount).toBeGreaterThan(0);
|
|
104
|
+
expect(narrowLabelCount).toBeLessThan(wideLabelCount);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('keeps the same auto-thinned labels after ASCII and Unicode truncation', () => {
|
|
108
|
+
const asciiData = Array.from({length: 9}, (_, index) => ({
|
|
109
|
+
month: `${index + 1}AB`,
|
|
110
|
+
sales: index,
|
|
111
|
+
}));
|
|
112
|
+
const clusters = ['👨👩👧👦', '🇯🇵', '👩🏽🚀'];
|
|
113
|
+
const unicodeData = Array.from({length: 9}, (_, index) => ({
|
|
114
|
+
month: `${index + 1}${clusters[index % clusters.length]}X`,
|
|
115
|
+
sales: index,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const retainedIndexes = (data: typeof asciiData) => {
|
|
119
|
+
const {unmount} = renderChart(
|
|
120
|
+
<ChartAxis position="bottom" truncate={2} />,
|
|
121
|
+
data,
|
|
122
|
+
);
|
|
123
|
+
reportWidth(300);
|
|
124
|
+
const indexes = Array.from(
|
|
125
|
+
screen
|
|
126
|
+
.getByRole('group', {name: 'bottom axis'})
|
|
127
|
+
.querySelectorAll('text'),
|
|
128
|
+
label => Number(label.textContent?.match(/^(\d+)/)?.[1]),
|
|
129
|
+
);
|
|
130
|
+
unmount();
|
|
131
|
+
return indexes;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
expect(retainedIndexes(unicodeData)).toEqual(retainedIndexes(asciiData));
|
|
135
|
+
});
|
|
136
|
+
|
|
91
137
|
it('truncates long category labels with an ellipsis', () => {
|
|
92
138
|
renderChart(<ChartAxis position="bottom" truncate={4} />, [
|
|
93
139
|
{month: 'January', sales: 4},
|
|
@@ -98,6 +144,15 @@ describe('ChartAxis bottom (band scale)', () => {
|
|
|
98
144
|
// Short labels are left alone.
|
|
99
145
|
expect(within(axis).getByText('Feb')).toBeInTheDocument();
|
|
100
146
|
});
|
|
147
|
+
|
|
148
|
+
it('preserves whole grapheme clusters when truncating labels', () => {
|
|
149
|
+
const family = '👨👩👧👦';
|
|
150
|
+
renderChart(<ChartAxis position="bottom" truncate={1} />, [
|
|
151
|
+
{month: `${family}Q`, sales: 4},
|
|
152
|
+
]);
|
|
153
|
+
const axis = screen.getByRole('group', {name: 'bottom axis'});
|
|
154
|
+
expect(within(axis).getByText(`${family}…`)).toBeInTheDocument();
|
|
155
|
+
});
|
|
101
156
|
});
|
|
102
157
|
|
|
103
158
|
describe('ChartAxis left (linear scale)', () => {
|
|
@@ -129,3 +184,26 @@ describe('ChartAxis left (linear scale)', () => {
|
|
|
129
184
|
expect(lines).toHaveLength(7);
|
|
130
185
|
});
|
|
131
186
|
});
|
|
187
|
+
|
|
188
|
+
describe('ChartAxis top and right positions', () => {
|
|
189
|
+
it('places both axes on their physical plot edges with outward labels', () => {
|
|
190
|
+
renderChart(
|
|
191
|
+
<>
|
|
192
|
+
<ChartAxis position="top" showTicks />
|
|
193
|
+
<ChartAxis position="right" showTicks />
|
|
194
|
+
</>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const top = screen.getByRole('group', {name: 'top axis'});
|
|
198
|
+
expect(top).not.toHaveAttribute('transform');
|
|
199
|
+
expect(within(top).getByText('Jan')).toHaveAttribute('y', '-10');
|
|
200
|
+
|
|
201
|
+
const right = screen.getByRole('group', {name: 'right axis'});
|
|
202
|
+
expect(right).toHaveAttribute('transform', 'translate(528,0)');
|
|
203
|
+
expect(within(right).getByText('0')).toHaveAttribute('x', '10');
|
|
204
|
+
expect(within(right).getByText('0')).toHaveAttribute(
|
|
205
|
+
'text-anchor',
|
|
206
|
+
'start',
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
package/src/ChartAxis.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file ChartAxis.tsx (v2)
|
|
5
|
+
* @input Chart context scales, axis presentation props, and grapheme-safe text utilities
|
|
5
6
|
* @output Renders an axis (top, right, bottom, left) using the chart's scales
|
|
6
7
|
* @position Child of Chart v2; reads scales from chart context
|
|
7
8
|
*
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
import {useCallback, useMemo} from 'react';
|
|
21
22
|
import * as stylex from '@stylexjs/stylex';
|
|
23
|
+
import {characterCount, truncateCharacters} from '@astryxdesign/core/utils';
|
|
22
24
|
import {colorVars} from '@astryxdesign/core/theme/tokens.stylex';
|
|
23
25
|
import {useChart} from './ChartContext';
|
|
24
26
|
import {isBandScale} from './utils';
|
|
@@ -33,7 +35,7 @@ export interface ChartAxisProps {
|
|
|
33
35
|
maxTicks?: number;
|
|
34
36
|
/** Custom tick formatter */
|
|
35
37
|
tickFormat?: (value: unknown) => string;
|
|
36
|
-
/** Truncate labels to this many characters (appends "\u2026"). */
|
|
38
|
+
/** Truncate labels to this many user-perceived characters (appends "\u2026"). */
|
|
37
39
|
truncate?: number;
|
|
38
40
|
/** Enable smooth transitions for streaming (default: true) */
|
|
39
41
|
animated?: boolean;
|
|
@@ -121,19 +123,24 @@ export function ChartAxis({
|
|
|
121
123
|
const format = useCallback(
|
|
122
124
|
(value: unknown): string => {
|
|
123
125
|
const str = (tickFormat ?? autoFormat ?? String)(value);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
if (!truncate) {
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
const shortened = truncateCharacters(str, truncate, '');
|
|
130
|
+
return shortened === str ? str : shortened + '\u2026';
|
|
127
131
|
},
|
|
128
132
|
[tickFormat, autoFormat, truncate],
|
|
129
133
|
);
|
|
130
134
|
|
|
131
135
|
const ticks = useMemo(() => {
|
|
132
|
-
|
|
136
|
+
// Format each generated tick once so density and rendering share the same
|
|
137
|
+
// grapheme-safe label instead of repeating consumer formatting/segmentation.
|
|
138
|
+
let allTicks: {value: unknown; offset: number; label: string}[];
|
|
133
139
|
if (isBandScale(scale)) {
|
|
134
140
|
allTicks = scale.domain().map(d => ({
|
|
135
141
|
value: d,
|
|
136
142
|
offset: (scale(d) ?? 0) + scale.bandwidth() / 2,
|
|
143
|
+
label: format(d),
|
|
137
144
|
}));
|
|
138
145
|
} else {
|
|
139
146
|
const linearScale = scale as
|
|
@@ -141,6 +148,7 @@ export function ChartAxis({
|
|
|
141
148
|
allTicks = linearScale.ticks(safeTickCount).map(d => ({
|
|
142
149
|
value: d,
|
|
143
150
|
offset: linearScale(d as number & Date),
|
|
151
|
+
label: format(d),
|
|
144
152
|
}));
|
|
145
153
|
}
|
|
146
154
|
|
|
@@ -154,7 +162,7 @@ export function ChartAxis({
|
|
|
154
162
|
if (cap == null && allTicks.length > 1) {
|
|
155
163
|
if (isHorizontal && width > 0) {
|
|
156
164
|
const widestChars = allTicks.reduce(
|
|
157
|
-
(m,
|
|
165
|
+
(m, tick) => Math.max(m, characterCount(tick.label)),
|
|
158
166
|
1,
|
|
159
167
|
);
|
|
160
168
|
const approxLabelPx = widestChars * 7 + 16;
|
|
@@ -210,8 +218,7 @@ export function ChartAxis({
|
|
|
210
218
|
{...stylex.props(styles.axisLine)}
|
|
211
219
|
/>
|
|
212
220
|
)}
|
|
213
|
-
{ticks.map(({value, offset}) => {
|
|
214
|
-
const label = format(value);
|
|
221
|
+
{ticks.map(({value, offset, label}) => {
|
|
215
222
|
// Key off the raw tick value, not the formatted label: distinct ticks
|
|
216
223
|
// can format (or truncate) to the same string and would collide as keys.
|
|
217
224
|
const key = String(value);
|
|
@@ -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.
|
package/src/ChartGrid.test.tsx
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @file ChartGrid.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, Chart, ChartGrid, bar mark
|
|
6
6
|
* @output Functional tests for grid lines — horizontal default, the skipped
|
|
7
|
-
* y=0 line,
|
|
7
|
+
* y=0 line, vertical lines at band/continuous positions, combined
|
|
8
|
+
* directions, and safe continuous tick-count handling
|
|
8
9
|
* @position Colocated test for ChartGrid.tsx (issue #4295 viz coverage)
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -95,6 +96,24 @@ describe('ChartGrid', () => {
|
|
|
95
96
|
expect(lines).toHaveLength(8); // 5 horizontal + 3 vertical
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
it('uses zero to request no continuous grid lines', () => {
|
|
100
|
+
const {lines} = renderGrid(<ChartGrid tickCount={0} />);
|
|
101
|
+
expect(lines).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('falls back to the default density for a non-finite tick count', () => {
|
|
105
|
+
const {lines} = renderGrid(
|
|
106
|
+
<ChartGrid tickCount={Number.POSITIVE_INFINITY} />,
|
|
107
|
+
);
|
|
108
|
+
expect(lines).toHaveLength(5);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('caps a huge continuous tick request without throwing', () => {
|
|
112
|
+
const {lines} = renderGrid(<ChartGrid tickCount={1e9} />);
|
|
113
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
114
|
+
expect(lines.length).toBeLessThanOrEqual(1001);
|
|
115
|
+
});
|
|
116
|
+
|
|
98
117
|
it('places vertical lines at d3 tick values on a linear x scale', () => {
|
|
99
118
|
const numeric = [
|
|
100
119
|
{month: 0, sales: 1},
|