@astryxdesign/charts 0.6.2-canary.bfee8a4 → 0.6.2-canary.d4cc232
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/dist/ChartSwatch.js +3 -3
- 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/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/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/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/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.d4cc232",
|
|
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.d4cc232",
|
|
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.d4cc232",
|
|
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
|
+
});
|