@astryxdesign/lab 0.1.4-canary.37c19d3 → 0.1.4-canary.39ffd21

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.
Files changed (142) hide show
  1. package/dist/ChartV2/Chart.d.ts +32 -0
  2. package/dist/ChartV2/Chart.d.ts.map +1 -0
  3. package/dist/ChartV2/Chart.js +281 -0
  4. package/dist/ChartV2/ChartAxis.d.ts +36 -0
  5. package/dist/ChartV2/ChartAxis.d.ts.map +1 -0
  6. package/dist/ChartV2/ChartAxis.js +161 -0
  7. package/dist/ChartV2/ChartGrid.d.ts +20 -0
  8. package/dist/ChartV2/ChartGrid.d.ts.map +1 -0
  9. package/dist/ChartV2/ChartGrid.js +76 -0
  10. package/dist/ChartV2/ChartLegend.d.ts +14 -0
  11. package/dist/ChartV2/ChartLegend.d.ts.map +1 -0
  12. package/dist/ChartV2/ChartLegend.js +54 -0
  13. package/dist/ChartV2/ChartSwatch.d.ts +36 -0
  14. package/dist/ChartV2/ChartSwatch.d.ts.map +1 -0
  15. package/dist/ChartV2/ChartSwatch.js +82 -0
  16. package/dist/ChartV2/ChartTooltip.d.ts +46 -0
  17. package/dist/ChartV2/ChartTooltip.d.ts.map +1 -0
  18. package/dist/ChartV2/ChartTooltip.js +267 -0
  19. package/dist/ChartV2/ChartV2Context.d.ts +4 -0
  20. package/dist/ChartV2/ChartV2Context.d.ts.map +1 -0
  21. package/dist/ChartV2/ChartV2Context.js +12 -0
  22. package/dist/ChartV2/index.d.ts +13 -0
  23. package/dist/ChartV2/index.d.ts.map +1 -0
  24. package/dist/ChartV2/index.js +10 -0
  25. package/dist/ChartV2/layout.d.ts +25 -0
  26. package/dist/ChartV2/layout.d.ts.map +1 -0
  27. package/dist/ChartV2/layout.js +195 -0
  28. package/dist/ChartV2/legend.d.ts +18 -0
  29. package/dist/ChartV2/legend.d.ts.map +1 -0
  30. package/dist/ChartV2/legend.js +24 -0
  31. package/dist/ChartV2/marks/area.d.ts +19 -0
  32. package/dist/ChartV2/marks/area.d.ts.map +1 -0
  33. package/dist/ChartV2/marks/area.js +109 -0
  34. package/dist/ChartV2/marks/band.d.ts +9 -0
  35. package/dist/ChartV2/marks/band.d.ts.map +1 -0
  36. package/dist/ChartV2/marks/band.js +59 -0
  37. package/dist/ChartV2/marks/bar.d.ts +17 -0
  38. package/dist/ChartV2/marks/bar.d.ts.map +1 -0
  39. package/dist/ChartV2/marks/bar.js +120 -0
  40. package/dist/ChartV2/marks/candlestick.d.ts +15 -0
  41. package/dist/ChartV2/marks/candlestick.d.ts.map +1 -0
  42. package/dist/ChartV2/marks/candlestick.js +83 -0
  43. package/dist/ChartV2/marks/dot.d.ts +15 -0
  44. package/dist/ChartV2/marks/dot.d.ts.map +1 -0
  45. package/dist/ChartV2/marks/dot.js +65 -0
  46. package/dist/ChartV2/marks/dotGL.d.ts +16 -0
  47. package/dist/ChartV2/marks/dotGL.d.ts.map +1 -0
  48. package/dist/ChartV2/marks/dotGL.js +178 -0
  49. package/dist/ChartV2/marks/dotGLInteractive.d.ts +30 -0
  50. package/dist/ChartV2/marks/dotGLInteractive.d.ts.map +1 -0
  51. package/dist/ChartV2/marks/dotGLInteractive.js +464 -0
  52. package/dist/ChartV2/marks/errorBar.d.ts +27 -0
  53. package/dist/ChartV2/marks/errorBar.d.ts.map +1 -0
  54. package/dist/ChartV2/marks/errorBar.js +87 -0
  55. package/dist/ChartV2/marks/heatmapGL.d.ts +23 -0
  56. package/dist/ChartV2/marks/heatmapGL.d.ts.map +1 -0
  57. package/dist/ChartV2/marks/heatmapGL.js +242 -0
  58. package/dist/ChartV2/marks/index.d.ts +13 -0
  59. package/dist/ChartV2/marks/index.d.ts.map +1 -0
  60. package/dist/ChartV2/marks/index.js +14 -0
  61. package/dist/ChartV2/marks/line.d.ts +18 -0
  62. package/dist/ChartV2/marks/line.d.ts.map +1 -0
  63. package/dist/ChartV2/marks/line.js +76 -0
  64. package/dist/ChartV2/marks/referenceLine.d.ts +35 -0
  65. package/dist/ChartV2/marks/referenceLine.d.ts.map +1 -0
  66. package/dist/ChartV2/marks/referenceLine.js +186 -0
  67. package/dist/ChartV2/marks/streamGL.d.ts +36 -0
  68. package/dist/ChartV2/marks/streamGL.d.ts.map +1 -0
  69. package/dist/ChartV2/marks/streamGL.js +204 -0
  70. package/dist/ChartV2/tooltip.d.ts +38 -0
  71. package/dist/ChartV2/tooltip.d.ts.map +1 -0
  72. package/dist/ChartV2/tooltip.js +54 -0
  73. package/dist/ChartV2/types.d.ts +115 -0
  74. package/dist/ChartV2/types.d.ts.map +1 -0
  75. package/dist/ChartV2/types.js +35 -0
  76. package/dist/CodeEditor/CodeEditor.d.ts +1 -4
  77. package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
  78. package/dist/CodeEditor/CodeEditor.js +0 -3
  79. package/dist/Sankey/SankeyChart.d.ts.map +1 -1
  80. package/dist/Sankey/SankeyChart.js +0 -3
  81. package/dist/Schedule/ListView.d.ts.map +1 -1
  82. package/dist/Schedule/ListView.js +4 -7
  83. package/dist/Schedule/MonthlyView.js +2 -3
  84. package/dist/Schedule/Schedule.d.ts +1 -3
  85. package/dist/Schedule/Schedule.d.ts.map +1 -1
  86. package/dist/Schedule/Schedule.js +4 -9
  87. package/dist/Schedule/TimeGridView.js +2 -3
  88. package/dist/Schedule/context.d.ts +0 -2
  89. package/dist/Schedule/context.d.ts.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +2 -5
  93. package/dist/lab.css +15 -6
  94. package/package.json +2 -2
  95. package/src/ChartV2/Chart.tsx +348 -0
  96. package/src/ChartV2/ChartAxis.tsx +209 -0
  97. package/src/ChartV2/ChartGrid.tsx +99 -0
  98. package/src/ChartV2/ChartLegend.tsx +69 -0
  99. package/src/ChartV2/ChartSwatch.doc.mjs +17 -0
  100. package/src/ChartV2/ChartSwatch.tsx +86 -0
  101. package/src/ChartV2/ChartTooltip.tsx +382 -0
  102. package/src/ChartV2/ChartV2.doc.mjs +16 -0
  103. package/src/ChartV2/ChartV2Context.ts +15 -0
  104. package/src/ChartV2/index.ts +62 -0
  105. package/src/ChartV2/layout.ts +223 -0
  106. package/src/ChartV2/legend.ts +32 -0
  107. package/src/ChartV2/marks/area.tsx +122 -0
  108. package/src/ChartV2/marks/band.tsx +70 -0
  109. package/src/ChartV2/marks/bar.tsx +159 -0
  110. package/src/ChartV2/marks/candlestick.tsx +106 -0
  111. package/src/ChartV2/marks/dot.tsx +64 -0
  112. package/src/ChartV2/marks/dotGL.tsx +202 -0
  113. package/src/ChartV2/marks/dotGLInteractive.tsx +546 -0
  114. package/src/ChartV2/marks/errorBar.tsx +89 -0
  115. package/src/ChartV2/marks/heatmapGL.tsx +299 -0
  116. package/src/ChartV2/marks/index.ts +14 -0
  117. package/src/ChartV2/marks/line.tsx +94 -0
  118. package/src/ChartV2/marks/referenceLine.tsx +224 -0
  119. package/src/ChartV2/marks/streamGL.tsx +251 -0
  120. package/src/ChartV2/tooltip.ts +70 -0
  121. package/src/ChartV2/types.ts +129 -0
  122. package/src/CodeEditor/CodeEditor.tsx +0 -5
  123. package/src/Sankey/SankeyChart.tsx +0 -3
  124. package/src/Schedule/ListView.tsx +2 -5
  125. package/src/Schedule/MonthlyView.tsx +2 -2
  126. package/src/Schedule/Schedule.test.tsx +0 -39
  127. package/src/Schedule/Schedule.tsx +0 -8
  128. package/src/Schedule/TimeGridView.tsx +2 -2
  129. package/src/Schedule/context.tsx +0 -2
  130. package/src/index.ts +46 -5
  131. package/dist/InfoTip/InfoTip.d.ts +0 -58
  132. package/dist/InfoTip/InfoTip.d.ts.map +0 -1
  133. package/dist/InfoTip/InfoTip.js +0 -91
  134. package/dist/InfoTip/index.d.ts +0 -6
  135. package/dist/InfoTip/index.d.ts.map +0 -1
  136. package/dist/InfoTip/index.js +0 -8
  137. package/src/CodeEditor/CodeEditor.test.tsx +0 -21
  138. package/src/InfoTip/InfoTip.doc.mjs +0 -97
  139. package/src/InfoTip/InfoTip.test.tsx +0 -169
  140. package/src/InfoTip/InfoTip.tsx +0 -155
  141. package/src/InfoTip/index.ts +0 -10
  142. package/src/Sankey/SankeyChart.test.tsx +0 -77
@@ -0,0 +1,382 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ChartTooltip.tsx
5
+ * @output Grouped chart tooltip — shows all series values at the hovered x-position.
6
+ * Composable: drop into `<Chart tooltip>` or render directly inside a chart
7
+ * to take full control of props.
8
+ * @position Reads from ChartV2Context — must render inside an <Chart>.
9
+ *
10
+ * @example
11
+ * ```
12
+ * // Default — declarative on Chart
13
+ * <Chart tooltip />
14
+ *
15
+ * // Custom render function via Chart prop
16
+ * <Chart tooltip={{render: (x, rows) => <MyCard x={x} rows={rows} />}} />
17
+ *
18
+ * // Composable — drop into the chart's children for full control
19
+ * <Chart>
20
+ * <ChartTooltip hoverIndicator={false} placement="top" />
21
+ * </Chart>
22
+ * ```
23
+ */
24
+
25
+ 'use client';
26
+
27
+ import {
28
+ memo,
29
+ useState,
30
+ useEffect,
31
+ useMemo,
32
+ useRef,
33
+ useCallback,
34
+ type ReactNode,
35
+ } from 'react';
36
+ import {createPortal} from 'react-dom';
37
+ import * as stylex from '@stylexjs/stylex';
38
+ import {
39
+ colorVars,
40
+ radiusVars,
41
+ shadowVars,
42
+ spacingVars,
43
+ } from '@astryxdesign/core/theme/tokens.stylex';
44
+ import {Text} from '@astryxdesign/core';
45
+ import {VStack, HStack} from '@astryxdesign/core';
46
+ import {useChartV2} from './ChartV2Context';
47
+ import {ChartSwatch, swatchVariantForType} from './ChartSwatch';
48
+ import {
49
+ deriveTooltipSeriesValues,
50
+ shouldRenderHoverDot,
51
+ type TooltipSeriesValue,
52
+ } from './tooltip';
53
+ import type {SeriesDef, ChartPointerEvent} from './types';
54
+ import type {ScaleBand} from 'd3-scale';
55
+
56
+ export type ChartTooltipPlacement = 'auto' | 'right' | 'left' | 'top';
57
+
58
+ export interface ChartTooltipProps {
59
+ /**
60
+ * Series definitions — used to map the hovered datum to per-series rows.
61
+ * When rendered via `<Chart tooltip />`, Chart injects this automatically.
62
+ * When composing directly, pass the same `series` array you gave Chart.
63
+ */
64
+ series?: readonly SeriesDef[];
65
+ /**
66
+ * Custom render function. Receives the x-value and one row per visible series.
67
+ * Returning `null` hides the card while still rendering the band highlight + dots.
68
+ */
69
+ render?: (xValue: unknown, seriesValues: TooltipSeriesValue[]) => ReactNode;
70
+ /**
71
+ * Whether to draw a hover indicator at the focused x-position.
72
+ * Picks the right shape automatically:
73
+ * - **band-highlight rect** when the chart includes bar series on a band scale
74
+ * - **vertical crosshair line** for line / area / dot charts
75
+ *
76
+ * Default: `true`.
77
+ */
78
+ hoverIndicator?: boolean;
79
+ /**
80
+ * Whether to draw a hover dot at each series' point (skipped for bars).
81
+ * Default: `true`.
82
+ */
83
+ showHoverDots?: boolean;
84
+ /**
85
+ * Card placement relative to the hovered point:
86
+ * - `'auto'` (default): right of the point, flips to left if it would overflow
87
+ * - `'right'` / `'left'` / `'top'`: pinned position
88
+ */
89
+ placement?: ChartTooltipPlacement;
90
+ }
91
+
92
+ const styles = stylex.create({
93
+ // Card chrome — static visual styles. Position (left/top/display) is updated
94
+ // imperatively via ref to keep pointer-move cost free of React renders.
95
+ card: {
96
+ position: 'fixed',
97
+ backgroundColor: colorVars['--color-background-popover'],
98
+ border: `1px solid ${colorVars['--color-border']}`,
99
+ borderRadius: radiusVars['--radius-container'],
100
+ paddingBlock: spacingVars['--spacing-2'],
101
+ paddingInline: spacingVars['--spacing-3'],
102
+ boxShadow: shadowVars['--shadow-med'],
103
+ pointerEvents: 'none',
104
+ whiteSpace: 'nowrap',
105
+ zIndex: 9999,
106
+ display: 'none',
107
+ },
108
+ crosshair: {
109
+ stroke: colorVars['--color-text-primary'],
110
+ strokeWidth: 1,
111
+ },
112
+ });
113
+
114
+ /** Default tooltip body — used when `render` is not provided. */
115
+ const DefaultTooltipContent = memo(function DefaultTooltipContent({
116
+ xValue,
117
+ rows,
118
+ }: {
119
+ xValue: unknown;
120
+ rows: TooltipSeriesValue[];
121
+ }) {
122
+ return (
123
+ <VStack gap={1}>
124
+ <Text type="body" weight="semibold">
125
+ {String(xValue)}
126
+ </Text>
127
+ {rows.length === 1 ? (
128
+ <Text type="supporting">{String(rows[0].value)}</Text>
129
+ ) : (
130
+ <VStack gap={1}>
131
+ {rows.map(row => (
132
+ <HStack key={row.key} gap={2} vAlign="center">
133
+ <ChartSwatch
134
+ color={row.color}
135
+ variant={swatchVariantForType(row.type)}
136
+ />
137
+ <Text type="supporting">{row.label}</Text>
138
+ <Text type="supporting" weight="semibold">
139
+ {String(row.value)}
140
+ </Text>
141
+ </HStack>
142
+ ))}
143
+ </VStack>
144
+ )}
145
+ </VStack>
146
+ );
147
+ });
148
+
149
+ /**
150
+ * Composable grouped tooltip for Chart.
151
+ *
152
+ * Subscribes to the chart's pointer event stream and only re-renders when the
153
+ * hovered data index changes (not on every pointer move). Card position is
154
+ * updated imperatively via a ref to keep pointer-move cost ~free.
155
+ */
156
+ export function ChartTooltip({
157
+ series = [],
158
+ render,
159
+ hoverIndicator = true,
160
+ showHoverDots = true,
161
+ placement = 'auto',
162
+ }: ChartTooltipProps) {
163
+ const {
164
+ data,
165
+ xKey,
166
+ resolved,
167
+ onPointer,
168
+ svgRef,
169
+ margin,
170
+ xScale,
171
+ width,
172
+ height,
173
+ } = useChartV2();
174
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
175
+ const cardRef = useRef<HTMLDivElement>(null);
176
+
177
+ // Stable Set of resolved series keys — used by deriveTooltipSeriesValues.
178
+ // Recomputed only when the resolved map identity changes (per layout pass).
179
+ const resolvedKeys = useMemo(() => new Set(resolved.keys()), [resolved]);
180
+
181
+ // ─── Card positioning ──────────────────────────────────────────────────
182
+ // Imperative — the card is portaled, and we don't want pointer-move to
183
+ // trigger React renders.
184
+ const positionCard = useCallback(
185
+ (e: ChartPointerEvent) => {
186
+ const card = cardRef.current;
187
+ const svg = svgRef.current;
188
+ if (!card) {
189
+ return;
190
+ }
191
+ if (!svg || !e.nearest) {
192
+ card.style.display = 'none';
193
+ return;
194
+ }
195
+ const svgRect = svg.getBoundingClientRect();
196
+ const screenX = svgRect.left + margin.left + e.nearest.px;
197
+ const screenY = svgRect.top + margin.top;
198
+ const cardWidth = card.offsetWidth;
199
+ const cardHeight = card.offsetHeight;
200
+ const viewportWidth = window.innerWidth;
201
+ const gap = 8;
202
+
203
+ let x: number;
204
+ let y = screenY;
205
+ switch (placement) {
206
+ case 'left':
207
+ x = screenX - cardWidth - gap;
208
+ break;
209
+ case 'right':
210
+ x = screenX + gap;
211
+ break;
212
+ case 'top':
213
+ x = screenX - cardWidth / 2;
214
+ y = screenY - cardHeight - gap;
215
+ break;
216
+ case 'auto':
217
+ default: {
218
+ const rightX = screenX + gap;
219
+ x =
220
+ rightX + cardWidth > viewportWidth
221
+ ? screenX - cardWidth - gap
222
+ : rightX;
223
+ }
224
+ }
225
+
226
+ card.style.left = `${x}px`;
227
+ card.style.top = `${y}px`;
228
+ card.style.display = 'block';
229
+ },
230
+ [svgRef, margin, placement],
231
+ );
232
+
233
+ // ─── Subscribe to pointer events ───────────────────────────────────────
234
+ useEffect(() => {
235
+ let currentIndex: number | null = null;
236
+ const unsub = onPointer((e: ChartPointerEvent) => {
237
+ const newIndex = e.nearest?.dataIndex ?? null;
238
+ if (newIndex !== currentIndex) {
239
+ currentIndex = newIndex;
240
+ setHoveredIndex(newIndex);
241
+ }
242
+ positionCard(e);
243
+ });
244
+ return unsub;
245
+ }, [onPointer, positionCard]);
246
+
247
+ // ─── Derived data ──────────────────────────────────────────────────────
248
+ const datum = hoveredIndex != null ? data[hoveredIndex] : undefined;
249
+ const xValue = datum?.[xKey];
250
+
251
+ const seriesValues = useMemo(
252
+ () => deriveTooltipSeriesValues(series, datum, resolvedKeys),
253
+ [series, datum, resolvedKeys],
254
+ );
255
+
256
+ // Hover dots — read from the resolved map at the hovered index.
257
+ const dots = useMemo(() => {
258
+ if (!showHoverDots || hoveredIndex == null) {
259
+ return null;
260
+ }
261
+ const elements: ReactNode[] = [];
262
+ for (const s of series) {
263
+ if (!shouldRenderHoverDot(s.type)) {
264
+ continue;
265
+ }
266
+ const points = resolved.get(s.key);
267
+ const point = points?.find(p => p.dataIndex === hoveredIndex);
268
+ if (!point) {
269
+ continue;
270
+ }
271
+ elements.push(
272
+ <circle
273
+ key={s.key}
274
+ cx={point.px}
275
+ cy={point.py}
276
+ r={4}
277
+ fill="var(--color-background-surface)"
278
+ stroke={s.color ?? 'var(--color-border-emphasized)'}
279
+ strokeWidth={2}
280
+ pointerEvents="none"
281
+ />,
282
+ );
283
+ }
284
+ return elements;
285
+ }, [showHoverDots, hoveredIndex, series, resolved]);
286
+
287
+ // Hover indicator — band-highlight rect for bar charts, vertical crosshair
288
+ // line for line/area charts. Same source-of-truth (hoveredIndex), distinct
289
+ // visual idioms picked automatically based on the series mix.
290
+ const hoverIndicatorElement = useMemo(() => {
291
+ if (!hoverIndicator || hoveredIndex == null) {
292
+ return null;
293
+ }
294
+ const xv = data[hoveredIndex]?.[xKey];
295
+ if (xv == null) {
296
+ return null;
297
+ }
298
+ const isBandScale = 'bandwidth' in xScale;
299
+ const hasBars = series.some(s => s.type === 'bar');
300
+
301
+ // Bar charts on a band scale → soft column highlight.
302
+ if (hasBars && isBandScale) {
303
+ const bandScale = xScale as ScaleBand<string>;
304
+ const x = bandScale(String(xv)) ?? 0;
305
+ const bw = bandScale.bandwidth();
306
+ const pad = 8;
307
+ const rectX = Math.max(0, x - pad);
308
+ const rectRight = Math.min(width, x + bw + pad);
309
+ const rectWidth = rectRight - rectX;
310
+ const topPad = Math.min(pad, margin.top);
311
+ return (
312
+ <rect
313
+ x={rectX}
314
+ y={-topPad}
315
+ width={rectWidth}
316
+ height={height + topPad}
317
+ fill="currentColor"
318
+ opacity={0.06}
319
+ rx={4}
320
+ pointerEvents="none"
321
+ />
322
+ );
323
+ }
324
+
325
+ // Line / area / dot charts → vertical crosshair through the point.
326
+ const px = isBandScale
327
+ ? ((xScale as ScaleBand<string>)(String(xv)) ?? 0) +
328
+ (xScale as ScaleBand<string>).bandwidth() / 2
329
+ : (xScale as (v: number) => number)(xv as number);
330
+ return (
331
+ <line
332
+ x1={px}
333
+ x2={px}
334
+ y1={0}
335
+ y2={height}
336
+ {...stylex.props(styles.crosshair)}
337
+ pointerEvents="none"
338
+ />
339
+ );
340
+ }, [
341
+ hoverIndicator,
342
+ hoveredIndex,
343
+ series,
344
+ xScale,
345
+ data,
346
+ xKey,
347
+ width,
348
+ height,
349
+ margin.top,
350
+ ]);
351
+
352
+ // ─── Render ────────────────────────────────────────────────────────────
353
+ if (typeof document === 'undefined') {
354
+ // SSR: no DOM portal target; band highlight + dots still render in SVG.
355
+ return (
356
+ <>
357
+ {hoverIndicatorElement}
358
+ {dots}
359
+ </>
360
+ );
361
+ }
362
+
363
+ const cardContent =
364
+ hoveredIndex == null ? null : render ? (
365
+ render(xValue, seriesValues)
366
+ ) : (
367
+ <DefaultTooltipContent xValue={xValue} rows={seriesValues} />
368
+ );
369
+
370
+ return (
371
+ <>
372
+ {hoverIndicatorElement}
373
+ {dots}
374
+ {createPortal(
375
+ <div ref={cardRef} {...stylex.props(styles.card)}>
376
+ {cardContent}
377
+ </div>,
378
+ document.body,
379
+ )}
380
+ </>
381
+ );
382
+ }
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../core/src/docs-types').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'ChartV2',
7
+ displayName: 'Chart V2',
8
+ group: 'ChartV2',
9
+ category: 'Data Visualization',
10
+
11
+ usage: {
12
+ description: '',
13
+ },
14
+
15
+ props: [],
16
+ };
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {createContext, useContext} from 'react';
4
+ import type {ChartV2Context} from './types';
5
+
6
+ const Ctx = createContext<ChartV2Context | null>(null);
7
+ export const ChartV2Provider = Ctx.Provider;
8
+
9
+ export function useChartV2(): ChartV2Context {
10
+ const ctx = useContext(Ctx);
11
+ if (!ctx) {
12
+ throw new Error('Must be used inside <Chart>');
13
+ }
14
+ return ctx;
15
+ }
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {Chart, type ChartProps} from './Chart';
4
+ export {ChartAxis, type ChartAxisProps} from './ChartAxis';
5
+ export {ChartGrid, type ChartGridProps} from './ChartGrid';
6
+ export {
7
+ ChartLegend,
8
+ type ChartLegendProps,
9
+ type LegendPosition,
10
+ type LegendAlignment,
11
+ } from './ChartLegend';
12
+ export {type LegendItem} from './legend';
13
+ export {
14
+ ChartSwatch,
15
+ swatchVariantForType,
16
+ type ChartSwatchProps,
17
+ type ChartSwatchVariant,
18
+ } from './ChartSwatch';
19
+ export {
20
+ ChartTooltip,
21
+ type ChartTooltipProps,
22
+ type ChartTooltipPlacement,
23
+ } from './ChartTooltip';
24
+ export {type TooltipSeriesValue} from './tooltip';
25
+ export {
26
+ bar,
27
+ line,
28
+ dot,
29
+ area,
30
+ band,
31
+ candlestick,
32
+ errorBar,
33
+ referenceLine,
34
+ dotGL,
35
+ dotGLInteractive,
36
+ heatmapGL,
37
+ streamGL,
38
+ } from './marks';
39
+ export type {
40
+ BarOptions,
41
+ LineOptions,
42
+ DotOptions,
43
+ AreaOptions,
44
+ BandOptions,
45
+ CandlestickOptions,
46
+ ErrorBarOptions,
47
+ ReferenceLineOptions,
48
+ DotGLOptions,
49
+ DotGLInteractiveOptions,
50
+ HeatmapGLOptions,
51
+ StreamGLOptions,
52
+ StreamGLHandle,
53
+ } from './marks';
54
+ export {useChartV2} from './ChartV2Context';
55
+ export type {
56
+ SeriesDef,
57
+ ChartV2Context,
58
+ ChartPointerEvent,
59
+ ResolvedPoint,
60
+ ResolvedPositions,
61
+ SeriesContext,
62
+ } from './types';
@@ -0,0 +1,223 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file layout.ts
5
+ * @output Computes scales, stacking, grouping, then delegates to each series.resolve()
6
+ * @position Called by Chart root — the single layout pass
7
+ *
8
+ * The layout engine does NOT know about specific mark types.
9
+ * It handles cross-series coordination (scales, stacking, grouping)
10
+ * then calls each series' own resolve() method.
11
+ */
12
+
13
+ import {scaleLinear, scaleBand} from 'd3-scale';
14
+ import {stack as d3Stack, stackOrderNone, stackOffsetNone} from 'd3-shape';
15
+ import type {
16
+ SeriesDef,
17
+ ChartScale,
18
+ ResolvedPoint,
19
+ SeriesContext,
20
+ } from './types';
21
+
22
+ export interface LayoutInput {
23
+ data: Record<string, unknown>[];
24
+ xKey: string;
25
+ series: SeriesDef[];
26
+ width: number;
27
+ height: number;
28
+ }
29
+
30
+ export interface LayoutResult {
31
+ xScale: ChartScale;
32
+ yScale: ReturnType<typeof scaleLinear<number, number>>;
33
+ resolved: Map<string, ResolvedPoint[]>;
34
+ }
35
+
36
+ export function computeLayout({
37
+ data,
38
+ xKey,
39
+ series,
40
+ width,
41
+ height,
42
+ }: LayoutInput): LayoutResult {
43
+ // ─── 1. X scale ──────────────────────────────────────────────────────
44
+ const xValues = data.map(d => d[xKey]);
45
+ const isNumericX =
46
+ xValues.length > 0 && xValues.every(v => typeof v === 'number');
47
+
48
+ let xScale: ChartScale;
49
+ if (isNumericX) {
50
+ const nums = xValues as number[];
51
+ xScale = scaleLinear()
52
+ .domain([Math.min(...nums), Math.max(...nums)])
53
+ .range([0, width])
54
+ .nice();
55
+ } else {
56
+ xScale = scaleBand<string>()
57
+ .domain(xValues.map(String))
58
+ .range([0, width])
59
+ .padding(0.2);
60
+ }
61
+
62
+ // ─── 2. Y domain from all series dataKeys ────────────────────────────
63
+ const allKeys = new Set<string>();
64
+ let includeZero = false;
65
+ for (const s of series) {
66
+ for (const k of s.dataKeys) {
67
+ allKeys.add(k);
68
+ }
69
+ if (s.layout.includeZero) {
70
+ includeZero = true;
71
+ }
72
+ }
73
+
74
+ // Collect which keys are stacked together so we can compute stacked extents
75
+ const stackGroupKeys = new Map<string, string[]>();
76
+ for (const s of series) {
77
+ if (s.layout.stack) {
78
+ const group = stackGroupKeys.get(s.layout.stack) ?? [];
79
+ group.push(s.dataKeys[0]);
80
+ stackGroupKeys.set(s.layout.stack, group);
81
+ }
82
+ }
83
+
84
+ let yMin = Infinity,
85
+ yMax = -Infinity;
86
+
87
+ // For stacked series, compute the sum at each data point
88
+ const stackedKeys = new Set<string>();
89
+ for (const [, keys] of stackGroupKeys) {
90
+ for (const k of keys) {
91
+ stackedKeys.add(k);
92
+ }
93
+ for (const d of data) {
94
+ let sum = 0;
95
+ for (const k of keys) {
96
+ const v = d[k];
97
+ if (typeof v === 'number') {
98
+ sum += v;
99
+ }
100
+ }
101
+ if (sum > yMax) {
102
+ yMax = sum;
103
+ }
104
+ if (sum < yMin) {
105
+ yMin = sum;
106
+ }
107
+ }
108
+ }
109
+
110
+ // For non-stacked series, use individual values
111
+ for (const d of data) {
112
+ for (const k of allKeys) {
113
+ if (stackedKeys.has(k)) {
114
+ continue;
115
+ } // already handled above
116
+ const v = d[k];
117
+ if (typeof v === 'number') {
118
+ if (v < yMin) {
119
+ yMin = v;
120
+ }
121
+ if (v > yMax) {
122
+ yMax = v;
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ if (includeZero) {
129
+ if (yMin > 0) {
130
+ yMin = 0;
131
+ }
132
+ if (yMax < 0) {
133
+ yMax = 0;
134
+ }
135
+ }
136
+ const yScale = scaleLinear().domain([yMin, yMax]).range([height, 0]).nice();
137
+
138
+ // ─── 3. Stacking ─────────────────────────────────────────────────────
139
+ const stackGroups = new Map<string, string[]>();
140
+ for (const s of series) {
141
+ if (s.layout.stack) {
142
+ const group = stackGroups.get(s.layout.stack) ?? [];
143
+ group.push(s.dataKeys[0]);
144
+ stackGroups.set(s.layout.stack, group);
145
+ }
146
+ }
147
+
148
+ const stackedData = new Map<string, {y0: number; y1: number}[]>();
149
+ for (const [, keys] of stackGroups) {
150
+ const stackGen = d3Stack<Record<string, unknown>>()
151
+ .keys(keys)
152
+ .order(stackOrderNone)
153
+ .offset(stackOffsetNone);
154
+ const stacked = stackGen(data);
155
+ for (const layer of stacked) {
156
+ stackedData.set(
157
+ layer.key,
158
+ layer.map(d => ({y0: d[0], y1: d[1]})),
159
+ );
160
+ }
161
+ }
162
+
163
+ // ─── 4. Bar grouping ─────────────────────────────────────────────────
164
+ // Bars can be grouped (side-by-side) whether or not they are stacked.
165
+ // For grouped stacks, each unique stack group name within the same bar
166
+ // group gets its own sub-band. For ungrouped non-stacked bars with a
167
+ // group key, each series gets its own sub-band.
168
+ const barGroups = new Map<string, string[]>();
169
+ for (const s of series) {
170
+ if (s.type === 'bar' && s.layout.group) {
171
+ const group = barGroups.get(s.layout.group) ?? [];
172
+ // For grouped stacks: use the stack name as the group slot identifier
173
+ // so all series in the same stack share one slot.
174
+ const slotKey = s.layout.stack ?? s.key;
175
+ if (!group.includes(slotKey)) {
176
+ group.push(slotKey);
177
+ }
178
+ barGroups.set(s.layout.group, group);
179
+ }
180
+ }
181
+
182
+ // ─── 5. Resolve each series ──────────────────────────────────────────
183
+ const ctx: SeriesContext = {data, xKey, xScale, yScale, width, height};
184
+ const resolved = new Map<string, ResolvedPoint[]>();
185
+
186
+ // Determine which series is the topmost in each stack group
187
+ const stackTopKeys = new Set<string>();
188
+ for (const [, keys] of stackGroups) {
189
+ // Last key in the stack group is rendered on top
190
+ if (keys.length > 0) {
191
+ stackTopKeys.add(keys[keys.length - 1]);
192
+ }
193
+ }
194
+
195
+ for (const s of series) {
196
+ const stackOffsets = stackedData.get(s.dataKeys[0]);
197
+
198
+ // Mark whether this series is the topmost in its stack
199
+ if (s.layout.stack) {
200
+ s._isTopOfStack = stackTopKeys.has(s.dataKeys[0]);
201
+ } else {
202
+ s._isTopOfStack = true;
203
+ }
204
+
205
+ let groupInfo: {index: number; count: number} | undefined;
206
+ if (s.layout.group) {
207
+ const groupKeys = barGroups.get(s.layout.group);
208
+ if (groupKeys) {
209
+ // For grouped stacks, the slot key is the stack name
210
+ const slotKey = s.layout.stack ?? s.key;
211
+ groupInfo = {
212
+ index: groupKeys.indexOf(slotKey),
213
+ count: groupKeys.length,
214
+ };
215
+ }
216
+ }
217
+
218
+ const points = s.resolve(ctx, stackOffsets, groupInfo);
219
+ resolved.set(s.key, points);
220
+ }
221
+
222
+ return {xScale, yScale, resolved};
223
+ }