@astryxdesign/charts 0.6.2-canary.bfee8a4 → 0.6.2-canary.cb7a565

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.
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartAxis.d.ts","sourceRoot":"","sources":["../src/ChartAxis.tsx"],"names":[],"mappings":"AA0BA,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,kEAAkE;IAClE,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,+BAuLhB"}
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
- return truncate && str.length > truncate ? str.slice(0, truncate) + '\u2026' : str;
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, t) => Math.max(m, format(t.value).length), 1);
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);
@@ -1 +1 @@
1
- {"version":3,"file":"ChartGrid.d.ts","sourceRoot":"","sources":["../src/ChartGrid.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAiB,EACjB,QAAgB,EAChB,SAAa,GACd,EAAE,cAAc,+BAmEhB"}
1
+ {"version":3,"file":"ChartGrid.d.ts","sourceRoot":"","sources":["../src/ChartGrid.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAiB,EACjB,QAAgB,EAChB,SAAa,GACd,EAAE,cAAc,+BAmEhB"}
package/dist/ChartGrid.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @file ChartGrid.tsx (v2)
5
+ * @input Chart context scales and dimensions, guide visibility, and tick density
5
6
  * @output Grid lines behind chart marks
6
7
  * @position Child of Chart v2; reads scales from chart context
7
8
  */
@@ -2,10 +2,10 @@
2
2
 
3
3
  /**
4
4
  * @file ChartSwatch.tsx
5
+ * @input Caller-owned CSS color and optional square or line shape
5
6
  * @output Color swatch primitive for legends, tooltips, and standalone use
6
- * @position Composable primitive — used by ChartLegend (and future
7
- * ChartTooltip), or independently anywhere a series indicator
8
- * is needed.
7
+ * @position Composable primitive — used by ChartLegend and ChartTooltip, or
8
+ * independently anywhere a series indicator is needed.
9
9
  *
10
10
  * @example
11
11
  * ```
@@ -39,8 +39,8 @@ export interface ChartTooltipProps {
39
39
  * Composable grouped tooltip for Chart.
40
40
  *
41
41
  * Subscribes to the chart's pointer event stream and only re-renders when the
42
- * hovered data index changes (not on every pointer move). Card position is
43
- * updated imperatively via a ref to keep pointer-move cost ~free.
42
+ * hovered data index changes (not on every pointer move). Card coordinates retain
43
+ * their previous state when the resolved point and browser geometry are unchanged.
44
44
  */
45
45
  export declare function ChartTooltip({ series, render, hoverIndicator, showHoverDots, placement, }: ChartTooltipProps): import("react").JSX.Element;
46
46
  //# sourceMappingURL=ChartTooltip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../src/ChartTooltip.tsx"],"names":[],"mappings":"AA0BA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAaf,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAC,SAAS,EAAoB,MAAM,SAAS,CAAC;AAG1D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,SAAS,CAAC;IAC5E;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AA2DD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAW,EACX,MAAM,EACN,cAAqB,EACrB,aAAoB,EACpB,SAAkB,GACnB,EAAE,iBAAiB,+BA4QnB"}
1
+ {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../src/ChartTooltip.tsx"],"names":[],"mappings":"AA8BA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAYf,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAC,SAAS,EAAoB,MAAM,SAAS,CAAC;AAG1D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,SAAS,CAAC;IAC5E;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAwDD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAW,EACX,MAAM,EACN,cAAqB,EACrB,aAAoB,EACpB,SAAkB,GACnB,EAAE,iBAAiB,+BAqUnB"}
@@ -17,7 +17,11 @@
17
17
  *
18
18
  * // Composable — drop into the chart's children for full control
19
19
  * <Chart>
20
- * <ChartTooltip hoverIndicator={false} placement="top" />
20
+ * <ChartTooltip
21
+ * series={series}
22
+ * hoverIndicator={false}
23
+ * placement="top"
24
+ * />
21
25
  * </Chart>
22
26
  * ```
23
27
  */
@@ -29,13 +33,25 @@ import { createPortal } from 'react-dom';
29
33
  import * as stylex from '@stylexjs/stylex';
30
34
  import '@astryxdesign/core/theme/tokens.stylex';
31
35
  import { colorVars, radiusVars, shadowVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
32
- import { Text } from '@astryxdesign/core';
33
- import { VStack, HStack } from '@astryxdesign/core';
36
+ import { HStack, Text, useLayer, VStack } from '@astryxdesign/core';
34
37
  import { useChart } from "./ChartContext.js";
35
38
  import { ChartSwatch, swatchVariantForType } from "./ChartSwatch.js";
36
39
  import { deriveTooltipSeriesValues, shouldRenderHoverDot } from "./tooltip.js";
37
40
  import { isBandScale, xPixel } from "./utils.js";
38
41
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
42
+ const styles = {
43
+ card: {
44
+ kWkggS: "x1prclbq",
45
+ kaIpWk: "x1hviunn",
46
+ k8WAf4: "xce4md1",
47
+ kg3NbH: "xrrkdod",
48
+ kGVxlE: "x14hfi27",
49
+ kfzvcC: "x47corl",
50
+ khDVqt: "xuxw1ft",
51
+ $$css: true
52
+ }
53
+ };
54
+
39
55
  /** Default tooltip body — used when `render` is not provided. */
40
56
  const DefaultTooltipContent = /*#__PURE__*/memo(function DefaultTooltipContent({
41
57
  xValue,
@@ -75,8 +91,8 @@ const DefaultTooltipContent = /*#__PURE__*/memo(function DefaultTooltipContent({
75
91
  * Composable grouped tooltip for Chart.
76
92
  *
77
93
  * Subscribes to the chart's pointer event stream and only re-renders when the
78
- * hovered data index changes (not on every pointer move). Card position is
79
- * updated imperatively via a ref to keep pointer-move cost ~free.
94
+ * hovered data index changes (not on every pointer move). Card coordinates retain
95
+ * their previous state when the resolved point and browser geometry are unchanged.
80
96
  */
81
97
  export function ChartTooltip({
82
98
  series = [],
@@ -97,20 +113,44 @@ export function ChartTooltip({
97
113
  height
98
114
  } = useChart();
99
115
  const [hoveredIndex, setHoveredIndex] = useState(null);
116
+ const [cardPosition, setCardPosition] = useState({
117
+ x: 0,
118
+ y: 0
119
+ });
120
+ const [portalTarget, setPortalTarget] = useState(null);
100
121
  const cardRef = useRef(null);
122
+ const lastPointerEventRef = useRef(null);
123
+ const {
124
+ hide: hideLayer,
125
+ isOpen: isLayerOpen,
126
+ render: renderLayer,
127
+ show: showLayer
128
+ } = useLayer({
129
+ mode: 'fixed'
130
+ });
101
131
  // Whether the card is currently suppressed (no content, or a custom `render`
102
- // opted out by returning null). positionCard reads this so a same-index
103
- // pointer move — which repositions the card without a React commit — can't
104
- // re-reveal a card the visibility effect below has hidden.
105
- const cardHiddenRef = useRef(false);
132
+ // opted out by returning null). Start closed; once present content opens the
133
+ // Layer, a content-bearing index change keeps it open while React swaps the
134
+ // content. positionCard also reads this so a same-index pointer move cannot
135
+ // re-reveal a suppressed card before the visibility effect confirms content.
136
+ const cardHiddenRef = useRef(true);
137
+
138
+ // The Layer host must remain in the chart's nearest HTML subtree so nested
139
+ // Theme and MediaTheme scopes keep applying. The portal crosses only the SVG
140
+ // boundary; browser top-layer promotion remains Layer's responsibility.
141
+ useEffect(() => {
142
+ const nextTarget = svgRef.current?.parentElement ?? null;
143
+ setPortalTarget(current => current === nextTarget ? current : nextTarget);
144
+ }, [svgRef, width]);
106
145
 
107
146
  // Stable Set of resolved series keys — used by deriveTooltipSeriesValues.
108
147
  // Recomputed only when the resolved map identity changes (per layout pass).
109
148
  const resolvedKeys = useMemo(() => new Set(resolved.keys()), [resolved]);
110
149
 
111
150
  // ─── Card positioning ──────────────────────────────────────────────────
112
- // Imperative — the card is portaled, and we don't want pointer-move to
113
- // trigger React renders.
151
+ // Layer owns top-layer promotion. Pointer moves update coordinates only when
152
+ // the hovered point or browser geometry changes, so moves within one point do
153
+ // not force a React commit.
114
154
  const positionCard = useCallback(e => {
115
155
  const card = cardRef.current;
116
156
  const svg = svgRef.current;
@@ -118,14 +158,19 @@ export function ChartTooltip({
118
158
  return;
119
159
  }
120
160
  if (!svg || !e.nearest || cardHiddenRef.current) {
121
- card.style.display = 'none';
161
+ hideLayer();
162
+ return;
163
+ }
164
+ showLayer();
165
+ const layerHost = card.parentElement;
166
+ if (!layerHost) {
122
167
  return;
123
168
  }
124
169
  const svgRect = svg.getBoundingClientRect();
125
170
  const screenX = svgRect.left + margin.left + e.nearest.px;
126
171
  const screenY = svgRect.top + margin.top;
127
- const cardWidth = card.offsetWidth;
128
- const cardHeight = card.offsetHeight;
172
+ const cardWidth = layerHost.offsetWidth;
173
+ const cardHeight = layerHost.offsetHeight;
129
174
  const viewportWidth = window.innerWidth;
130
175
  const viewportHeight = window.innerHeight;
131
176
  const gap = 8;
@@ -150,27 +195,31 @@ export function ChartTooltip({
150
195
  }
151
196
  }
152
197
 
153
- // Clamp to the viewport so the card never clips off-screen — it's
154
- // portaled and position:fixed, so this keeps it fully visible for every
155
- // placement (pinned placements and the near-edge `auto` flip alike).
198
+ // Clamp the card coordinates to the viewport. Oversized custom content
199
+ // keeps its caller-owned dimensions and may extend past the far edge.
156
200
  x = Math.max(gap, Math.min(x, viewportWidth - cardWidth - gap));
157
201
  y = Math.max(gap, Math.min(y, viewportHeight - cardHeight - gap));
158
- card.style.left = `${x}px`;
159
- card.style.top = `${y}px`;
160
- card.style.display = 'block';
161
- }, [svgRef, margin, placement]);
202
+ setCardPosition(current => current.x === x && current.y === y ? current : {
203
+ x,
204
+ y
205
+ });
206
+ }, [svgRef, margin, placement, hideLayer, showLayer]);
162
207
 
163
208
  // ─── Subscribe to pointer events ───────────────────────────────────────
164
209
  useEffect(() => {
165
210
  let currentIndex = null;
166
211
  const unsub = onPointer(e => {
212
+ lastPointerEventRef.current = e;
167
213
  const newIndex = e.nearest?.dataIndex ?? null;
168
214
  if (newIndex !== currentIndex) {
169
215
  currentIndex = newIndex;
170
- // Optimistically treat the new index as showable; the visibility effect
171
- // re-hides it after commit if there's no content (e.g. custom `render`
172
- // returned null). This keeps the card appearing on the first move.
173
- cardHiddenRef.current = false;
216
+ // Keep a closed/suppressed card closed until React resolves content.
217
+ // Do not preemptively close an already-visible card for another index:
218
+ // the visibility effect closes absent content after commit, while
219
+ // content-bearing transitions keep the same Layer continuously open.
220
+ if (newIndex == null) {
221
+ cardHiddenRef.current = true;
222
+ }
174
223
  setHoveredIndex(newIndex);
175
224
  }
176
225
  positionCard(e);
@@ -269,24 +318,31 @@ export function ChartTooltip({
269
318
  });
270
319
  }, [hoverIndicator, hoveredIndex, series, xScale, data, xKey, width, height, margin.top]);
271
320
 
272
- // Single source of truth for whether the card has anything to show. It's
273
- // hidden when there's no hover, when the hovered index is stale/out-of-range
274
- // (e.g. `data` shrank while hovering, so `datum` is undefined), or when a
275
- // custom `render` opts out by returning null — the documented contract, which
276
- // still shows the band highlight + dots. The decision is stored in a ref so
277
- // the imperative positionCard honors it on subsequent same-index moves
278
- // (otherwise it would re-reveal an empty bordered box). Runs after commit, so
279
- // `render` (which may return null, or throw) never executes on the SSR path.
321
+ // Render props are ordinary React content. Resolve them once per commit so a
322
+ // custom renderer is not invoked separately by the visibility synchronizer.
323
+ // Keep it browser-only: the layer has no portal target during SSR.
324
+ const cardContent = typeof document === 'undefined' || hoveredIndex == null || datum == null ? null : render ? render(xValue, seriesValues) : /*#__PURE__*/_jsx(DefaultTooltipContent, {
325
+ xValue: xValue,
326
+ rows: seriesValues
327
+ });
328
+ const isCardHidden = hoveredIndex == null || datum == null || render != null && cardContent == null;
329
+ const hasNativePopover = typeof HTMLElement !== 'undefined' && typeof HTMLElement.prototype.showPopover === 'function';
330
+ const shouldHideLayerHost = isCardHidden || !hasNativePopover && !isLayerOpen;
331
+
332
+ // Synchronize the shared Layer's native Popover state after React commits the
333
+ // new card body. The second positioning pass measures that final body; later
334
+ // moves within the same data point usually resolve to the existing state.
280
335
  useEffect(() => {
281
- const hidden = hoveredIndex == null || datum == null || render != null && render(xValue, seriesValues) == null;
282
- cardHiddenRef.current = hidden;
283
- if (hidden) {
284
- const card = cardRef.current;
285
- if (card) {
286
- card.style.display = 'none';
287
- }
336
+ cardHiddenRef.current = isCardHidden;
337
+ if (isCardHidden) {
338
+ hideLayer();
339
+ return;
340
+ }
341
+ const pointerEvent = lastPointerEventRef.current;
342
+ if (pointerEvent) {
343
+ positionCard(pointerEvent);
288
344
  }
289
- }, [render, hoveredIndex, datum, xValue, seriesValues]);
345
+ }, [isCardHidden, hoveredIndex, xValue, seriesValues, render, hideLayer, positionCard]);
290
346
 
291
347
  // ─── Render ────────────────────────────────────────────────────────────
292
348
  if (typeof document === 'undefined') {
@@ -295,22 +351,21 @@ export function ChartTooltip({
295
351
  children: [hoverIndicatorElement, dots]
296
352
  });
297
353
  }
298
-
299
- // Mirrors the visibility effect's first two conditions: no card when there's
300
- // no hover or the hovered datum is stale/out-of-range (so `render` and the
301
- // default body are never called with an undefined x-value + empty rows).
302
- const cardContent = hoveredIndex == null || datum == null ? null : render ? render(xValue, seriesValues) : /*#__PURE__*/_jsx(DefaultTooltipContent, {
303
- xValue: xValue,
304
- rows: seriesValues
305
- });
306
354
  return /*#__PURE__*/_jsxs(_Fragment, {
307
- children: [hoverIndicatorElement, dots, /*#__PURE__*/createPortal(/*#__PURE__*/_jsx("div", {
355
+ children: [hoverIndicatorElement, dots, portalTarget ? /*#__PURE__*/createPortal(renderLayer(/*#__PURE__*/_jsx("div", {
308
356
  ref: cardRef,
309
357
  role: "tooltip",
310
- ...{
311
- className: "xixxii4 x1prclbq x1hviunn xce4md1 xrrkdod x14hfi27 x47corl xuxw1ft x1q2oy4v x1s85apg"
312
- },
313
358
  children: cardContent
314
- }), document.body)]
359
+ }), {
360
+ x: cardPosition.x,
361
+ y: cardPosition.y,
362
+ // Native Popover keeps a closed host out of layout itself, so
363
+ // remove our guard before showPopover() measures it. Layer's
364
+ // reduced fallback needs the explicit closed display state.
365
+ style: shouldHideLayerHost ? {
366
+ display: 'none'
367
+ } : undefined,
368
+ xstyle: styles.card
369
+ }), portalTarget) : null]
315
370
  });
316
371
  }
package/dist/charts.css CHANGED
@@ -14,15 +14,12 @@
14
14
  .x1prclbq:not(#\#):not(#\#){background-color:var(--color-background-popover)}
15
15
  .xl8spv7:not(#\#):not(#\#){background-color:var(--x-backgroundColor)}
16
16
  .x14hfi27:not(#\#):not(#\#){box-shadow:var(--shadow-med)}
17
- .x1s85apg:not(#\#):not(#\#){display:none}
18
17
  .x2lah0s:not(#\#):not(#\#){flex-shrink:0}
19
18
  .x47corl:not(#\#):not(#\#){pointer-events:none}
20
- .xixxii4:not(#\#):not(#\#){position:fixed}
21
19
  .xnpr05h:not(#\#):not(#\#){stroke-width:1}
22
20
  .x113i6qz:not(#\#):not(#\#){stroke:var(--color-border-emphasized)}
23
21
  .x1lo1t26:not(#\#):not(#\#){stroke:var(--color-text-primary)}
24
22
  .xuxw1ft:not(#\#):not(#\#){white-space:nowrap}
25
- .x1q2oy4v:not(#\#):not(#\#){z-index:9999}
26
23
  .xuoj239:not(#\#):not(#\#):not(#\#){height:3px}
27
24
  .xdk7pt:not(#\#):not(#\#):not(#\#){height:8px}
28
25
  .xwh1t77:not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-4)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/charts",
3
- "version": "0.6.2-canary.bfee8a4",
3
+ "version": "0.6.2-canary.cb7a565",
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.bfee8a4",
50
+ "@astryxdesign/core": "0.6.2-canary.cb7a565",
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.bfee8a4",
60
+ "@astryxdesign/cli": "0.6.2-canary.cb7a565",
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
+ };