@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,299 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/heatmapGL.tsx
5
+ * @output WebGL heatmap — 2D grid of colored cells on canvas
6
+ */
7
+
8
+ import {useRef, useEffect, useMemo} from 'react';
9
+ import {scaleBand} from 'd3-scale';
10
+ import type {SeriesDef, ResolvedPoint} from '../types';
11
+ import type {ScaleBand} from 'd3-scale';
12
+ import {
13
+ hexToGL,
14
+ getWebGLContext,
15
+ setupGLState,
16
+ sizeCanvas,
17
+ mountCanvasOverSVG,
18
+ createProgram,
19
+ } from '../../Chart/webgl';
20
+
21
+ export interface HeatmapGLOptions {
22
+ xKey: string;
23
+ yKey: string;
24
+ valueKey: string;
25
+ colorRange: string[];
26
+ domain?: [number, number];
27
+ cellGap?: number;
28
+ }
29
+
30
+ const VERT = `
31
+ attribute vec2 a_position;
32
+ attribute vec3 a_color;
33
+ uniform vec2 u_resolution;
34
+ varying vec3 v_color;
35
+ void main() {
36
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
37
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
38
+ v_color = a_color;
39
+ }
40
+ `;
41
+
42
+ const FRAG = `
43
+ precision mediump float;
44
+ varying vec3 v_color;
45
+ void main() {
46
+ gl_FragColor = vec4(v_color, 1.0);
47
+ }
48
+ `;
49
+
50
+ function sampleRamp(
51
+ ramp: [number, number, number][],
52
+ t: number,
53
+ ): [number, number, number] {
54
+ const c = Math.max(0, Math.min(1, t));
55
+ if (ramp.length === 1) {
56
+ return ramp[0];
57
+ }
58
+ const s = c * (ramp.length - 1);
59
+ const lo = Math.floor(s);
60
+ const hi = Math.min(lo + 1, ramp.length - 1);
61
+ const f = s - lo;
62
+ return [
63
+ ramp[lo][0] + f * (ramp[hi][0] - ramp[lo][0]),
64
+ ramp[lo][1] + f * (ramp[hi][1] - ramp[lo][1]),
65
+ ramp[lo][2] + f * (ramp[hi][2] - ramp[lo][2]),
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * Inline component — manages the heatmap WebGL canvas and its own y band scale.
71
+ */
72
+ function HeatmapGLCanvas({
73
+ data,
74
+ xKey,
75
+ yKey,
76
+ valueKey,
77
+ xScale,
78
+ colorRange,
79
+ domain: domainProp,
80
+ cellGap,
81
+ width,
82
+ height,
83
+ }: {
84
+ data: Record<string, unknown>[];
85
+ xKey: string;
86
+ yKey: string;
87
+ valueKey: string;
88
+ xScale: ScaleBand<string>;
89
+ colorRange: string[];
90
+ domain?: [number, number];
91
+ cellGap: number;
92
+ width: number;
93
+ height: number;
94
+ }) {
95
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
96
+ const glRef = useRef<WebGLRenderingContext | null>(null);
97
+ const programRef = useRef<WebGLProgram | null>(null);
98
+ const markerRef = useRef<SVGGElement>(null);
99
+
100
+ const ramp = useMemo(() => colorRange.map(hexToGL), [colorRange]);
101
+
102
+ const yBandScale = useMemo(() => {
103
+ const cats = [...new Set(data.map(d => String(d[yKey])))];
104
+ return scaleBand<string>().domain(cats).range([0, height]).padding(0.05);
105
+ }, [data, yKey, height]);
106
+
107
+ const domain = useMemo((): [number, number] => {
108
+ if (domainProp) {
109
+ return domainProp;
110
+ }
111
+ let min = Infinity,
112
+ max = -Infinity;
113
+ for (const d of data) {
114
+ const v = d[valueKey];
115
+ if (typeof v === 'number') {
116
+ if (v < min) {
117
+ min = v;
118
+ }
119
+ if (v > max) {
120
+ max = v;
121
+ }
122
+ }
123
+ }
124
+ return [min, max];
125
+ }, [data, valueKey, domainProp]);
126
+
127
+ // Mount canvas outside SVG
128
+ useEffect(() => {
129
+ const marker = markerRef.current;
130
+ if (!marker) {
131
+ return;
132
+ }
133
+ if (!canvasRef.current) {
134
+ canvasRef.current = document.createElement('canvas');
135
+ }
136
+ return mountCanvasOverSVG(marker, canvasRef.current, width, height);
137
+ }, [width, height]);
138
+
139
+ // Draw
140
+ useEffect(() => {
141
+ const canvas = canvasRef.current;
142
+ if (!canvas || width <= 0 || height <= 0) {
143
+ return;
144
+ }
145
+
146
+ sizeCanvas(canvas, width, height);
147
+
148
+ if (!glRef.current) {
149
+ glRef.current = getWebGLContext(canvas);
150
+ }
151
+ const gl = glRef.current;
152
+ if (!gl) {
153
+ return;
154
+ }
155
+
156
+ if (!programRef.current) {
157
+ programRef.current = createProgram(gl, VERT, FRAG);
158
+ }
159
+ const program = programRef.current;
160
+ if (!program) {
161
+ return;
162
+ }
163
+
164
+ gl.viewport(0, 0, canvas.width, canvas.height);
165
+ setupGLState(gl);
166
+ gl.useProgram(program);
167
+
168
+ const positions: number[] = [];
169
+ const colors: number[] = [];
170
+ const [dMin, dMax] = domain;
171
+ const range = dMax - dMin || 1;
172
+ const gap = cellGap;
173
+ const xBW = xScale.bandwidth();
174
+ const yBW = yBandScale.bandwidth();
175
+
176
+ for (const d of data) {
177
+ const xVal = xScale(String(d[xKey]));
178
+ const yVal = yBandScale(String(d[yKey]));
179
+ if (xVal == null || yVal == null) {
180
+ continue;
181
+ }
182
+
183
+ const v = typeof d[valueKey] === 'number' ? (d[valueKey] as number) : 0;
184
+ const t = (v - dMin) / range;
185
+ const [r, g, b] = sampleRamp(ramp, t);
186
+
187
+ const x0 = xVal + gap / 2;
188
+ const x1 = xVal + xBW - gap / 2;
189
+ const y0 = yVal + gap / 2;
190
+ const y1 = yVal + yBW - gap / 2;
191
+
192
+ // Two triangles per cell
193
+ positions.push(x0, y0, x1, y0, x0, y1, x1, y0, x1, y1, x0, y1);
194
+ for (let i = 0; i < 6; i++) {
195
+ colors.push(r, g, b);
196
+ }
197
+ }
198
+
199
+ const posBuf = gl.createBuffer();
200
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
201
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
202
+ const aPos = gl.getAttribLocation(program, 'a_position');
203
+ gl.enableVertexAttribArray(aPos);
204
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
205
+
206
+ const colBuf = gl.createBuffer();
207
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
208
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
209
+ const aCol = gl.getAttribLocation(program, 'a_color');
210
+ gl.enableVertexAttribArray(aCol);
211
+ gl.vertexAttribPointer(aCol, 3, gl.FLOAT, false, 0, 0);
212
+
213
+ gl.uniform2f(gl.getUniformLocation(program, 'u_resolution'), width, height);
214
+ gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
215
+
216
+ gl.deleteBuffer(posBuf);
217
+ gl.deleteBuffer(colBuf);
218
+ }, [
219
+ data,
220
+ xKey,
221
+ yKey,
222
+ valueKey,
223
+ xScale,
224
+ yBandScale,
225
+ width,
226
+ height,
227
+ domain,
228
+ ramp,
229
+ cellGap,
230
+ ]);
231
+
232
+ if (width <= 0 || height <= 0) {
233
+ return null;
234
+ }
235
+ return <g ref={markerRef} />;
236
+ }
237
+
238
+ /**
239
+ * WebGL heatmap. Renders a 2D grid of colored cells using triangles.
240
+ * Manages its own y band scale for rows; uses the chart's x band scale for columns.
241
+ *
242
+ * @example
243
+ * ```
244
+ * series={[heatmapGL({
245
+ * xKey: 'hour', yKey: 'day', valueKey: 'count',
246
+ * colorRange: ['#f0f9ff', '#1e40af'],
247
+ * })]}
248
+ * ```
249
+ */
250
+ export function heatmapGL(options: HeatmapGLOptions): SeriesDef {
251
+ const {xKey, yKey, valueKey, colorRange} = options;
252
+ const cellGap = options.cellGap ?? 1;
253
+
254
+ return {
255
+ type: 'heatmapGL',
256
+ key: `heatmap-${valueKey}`,
257
+ dataKeys: [valueKey],
258
+ layout: {},
259
+
260
+ resolve(ctx) {
261
+ // Heatmap doesn't resolve per-point positions the standard way —
262
+ // the canvas component handles its own cell layout.
263
+ const {data, xScale, yScale} = ctx;
264
+ const points: ResolvedPoint[] = [];
265
+ if (!('bandwidth' in xScale)) {
266
+ return points;
267
+ }
268
+ for (let i = 0; i < data.length; i++) {
269
+ const d = data[i];
270
+ const px =
271
+ ((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
272
+ (xScale as ScaleBand<string>).bandwidth() / 2;
273
+ const v = typeof d[valueKey] === 'number' ? (d[valueKey] as number) : 0;
274
+ points.push({px, py: yScale(v), py0: yScale(0), dataIndex: i});
275
+ }
276
+ return points;
277
+ },
278
+
279
+ render(_resolved, ctx) {
280
+ if (!('bandwidth' in ctx.xScale)) {
281
+ return null;
282
+ }
283
+ return (
284
+ <HeatmapGLCanvas
285
+ data={ctx.data}
286
+ xKey={xKey}
287
+ yKey={yKey}
288
+ valueKey={valueKey}
289
+ xScale={ctx.xScale as ScaleBand<string>}
290
+ colorRange={colorRange}
291
+ domain={options.domain}
292
+ cellGap={cellGap}
293
+ width={ctx.width}
294
+ height={ctx.height}
295
+ />
296
+ );
297
+ },
298
+ };
299
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {bar, type BarOptions} from './bar';
4
+ export {line, type LineOptions} from './line';
5
+ export {dot, type DotOptions} from './dot';
6
+ export {area, type AreaOptions} from './area';
7
+ export {band, type BandOptions} from './band';
8
+ export {candlestick, type CandlestickOptions} from './candlestick';
9
+ export {errorBar, type ErrorBarOptions} from './errorBar';
10
+ export {referenceLine, type ReferenceLineOptions} from './referenceLine';
11
+ export {dotGL, type DotGLOptions} from './dotGL';
12
+ export {dotGLInteractive, type DotGLInteractiveOptions} from './dotGLInteractive';
13
+ export {heatmapGL, type HeatmapGLOptions} from './heatmapGL';
14
+ export {streamGL, type StreamGLOptions, type StreamGLHandle} from './streamGL';
@@ -0,0 +1,94 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/line.tsx
5
+ * @output Line series — self-contained resolve + render
6
+ */
7
+
8
+ import {
9
+ line as d3Line,
10
+ curveLinear,
11
+ curveMonotoneX,
12
+ curveNatural,
13
+ curveStep,
14
+ } from 'd3-shape';
15
+ import type {SeriesDef, ResolvedPoint} from '../types';
16
+ import type {ScaleBand} from 'd3-scale';
17
+
18
+ const CURVES = {
19
+ linear: curveLinear,
20
+ monotone: curveMonotoneX,
21
+ natural: curveNatural,
22
+ step: curveStep,
23
+ } as const;
24
+ export type CurveType = keyof typeof CURVES;
25
+
26
+ export interface LineOptions {
27
+ color?: string;
28
+ curve?: CurveType;
29
+ strokeWidth?: number;
30
+ dots?: boolean;
31
+ label?: string;
32
+ }
33
+
34
+ export function line(dataKey: string, options: LineOptions = {}): SeriesDef {
35
+ const color = options.color ?? 'var(--color-chart-1)';
36
+ const curve = options.curve ?? 'monotone';
37
+ const strokeWidth = options.strokeWidth ?? 2;
38
+ const dots = options.dots ?? false;
39
+
40
+ return {
41
+ type: 'line',
42
+ key: dataKey,
43
+ dataKeys: [dataKey],
44
+ color,
45
+ label: options.label ?? dataKey,
46
+ layout: {},
47
+
48
+ resolve(ctx) {
49
+ const {data, xKey, xScale, yScale} = ctx;
50
+ const points: ResolvedPoint[] = [];
51
+ for (let i = 0; i < data.length; i++) {
52
+ const d = data[i];
53
+ let px: number;
54
+ if ('bandwidth' in xScale) {
55
+ px =
56
+ ((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
57
+ (xScale as ScaleBand<string>).bandwidth() / 2;
58
+ } else {
59
+ px = xScale(d[xKey] as number);
60
+ }
61
+ const v = typeof d[dataKey] === 'number' ? (d[dataKey] as number) : 0;
62
+ points.push({px, py: yScale(v), py0: yScale(0), dataIndex: i});
63
+ }
64
+ return points;
65
+ },
66
+
67
+ render(resolved) {
68
+ if (resolved.length === 0) {
69
+ return null;
70
+ }
71
+ const curveFactory = CURVES[curve];
72
+ const pathGen = d3Line<ResolvedPoint>()
73
+ .x(d => d.px)
74
+ .y(d => d.py)
75
+ .curve(curveFactory);
76
+
77
+ const pathD = pathGen(resolved) ?? '';
78
+ return (
79
+ <g>
80
+ <path
81
+ d={pathD}
82
+ fill="none"
83
+ stroke={color}
84
+ strokeWidth={strokeWidth}
85
+ />
86
+ {dots &&
87
+ resolved.map((p, i) => (
88
+ <circle key={i} cx={p.px} cy={p.py} r={3} fill={color} />
89
+ ))}
90
+ </g>
91
+ );
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,224 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/referenceLine.tsx
5
+ * @output Horizontal or vertical reference line/band annotation
6
+ */
7
+
8
+ import type {SeriesDef} from '../types';
9
+ import type {ScaleLinear} from 'd3-scale';
10
+
11
+ export interface ReferenceLineOptions {
12
+ /** Horizontal reference at this y value */
13
+ y?: number;
14
+ /** Second y value — draws a shaded band between y and y2 */
15
+ y2?: number;
16
+ /** Vertical reference at this x value */
17
+ x?: number;
18
+ /** Line/band color */
19
+ color?: string;
20
+ /** Stroke width (for lines) */
21
+ strokeWidth?: number;
22
+ /** Dash pattern */
23
+ strokeDasharray?: string;
24
+ /** Text label */
25
+ label?: string;
26
+ /** Label position */
27
+ labelPosition?: 'start' | 'end';
28
+ /** Band fill opacity (when y2 is set) */
29
+ bandOpacity?: number;
30
+ }
31
+
32
+ /**
33
+ * Reference line or band annotation. Not a data series — renders at fixed values.
34
+ *
35
+ * @example
36
+ * ```
37
+ * series={[line('revenue'), referenceLine({y: 100, label: 'Target'})]}
38
+ * ```
39
+ */
40
+ export function referenceLine(options: ReferenceLineOptions): SeriesDef {
41
+ const color = options.color ?? 'var(--color-border-emphasized)';
42
+ const strokeWidth = options.strokeWidth ?? 1;
43
+ const strokeDasharray = options.strokeDasharray ?? '6 3';
44
+ const label = options.label;
45
+ const labelPosition = options.labelPosition ?? 'end';
46
+ const bandOpacity = options.bandOpacity ?? 0.1;
47
+
48
+ // Badge dimensions — matches crosshair label style
49
+ const badgeH = 14;
50
+ const badgeRx = 3;
51
+ const fontSize = 10;
52
+
53
+ return {
54
+ type: 'referenceLine',
55
+ key: `ref-${options.y ?? options.x ?? 'none'}`,
56
+ dataKeys: [],
57
+ layout: {},
58
+
59
+ // No data points to resolve — this is a fixed annotation
60
+ resolve() {
61
+ return [];
62
+ },
63
+
64
+ render(_resolved, ctx) {
65
+ const {width, height, xScale, yScale} = ctx;
66
+
67
+ // Horizontal reference line or band
68
+ if (options.y != null) {
69
+ const py = yScale(options.y);
70
+ const textW = label ? label.length * 5.5 + 8 : 0;
71
+ const bx = labelPosition === 'end' ? width - textW - 2 : 2;
72
+
73
+ // Band mode: shaded region between y and y2
74
+ if (options.y2 != null) {
75
+ const py2 = yScale(options.y2);
76
+ const top = Math.min(py, py2);
77
+ const bandHeight = Math.abs(py2 - py);
78
+ return (
79
+ <g>
80
+ <rect
81
+ x={0}
82
+ y={top}
83
+ width={width}
84
+ height={bandHeight}
85
+ fill={color}
86
+ opacity={bandOpacity}
87
+ />
88
+ <line
89
+ x1={0}
90
+ x2={width}
91
+ y1={py}
92
+ y2={py}
93
+ stroke={color}
94
+ strokeWidth={strokeWidth}
95
+ strokeDasharray={strokeDasharray}
96
+ />
97
+ <line
98
+ x1={0}
99
+ x2={width}
100
+ y1={py2}
101
+ y2={py2}
102
+ stroke={color}
103
+ strokeWidth={strokeWidth}
104
+ strokeDasharray={strokeDasharray}
105
+ />
106
+ {label && (
107
+ <g transform={`translate(${bx},${py})`} pointerEvents="none">
108
+ <rect
109
+ x={0}
110
+ y={-badgeH / 2}
111
+ width={textW}
112
+ height={badgeH}
113
+ rx={badgeRx}
114
+ fill="var(--color-background-popover)"
115
+ fillOpacity={0.85}
116
+ stroke={color}
117
+ strokeWidth={0.5}
118
+ />
119
+ <text
120
+ x={textW / 2}
121
+ dy="0.35em"
122
+ textAnchor="middle"
123
+ fontSize={fontSize}
124
+ fontWeight={500}
125
+ fill={color}>
126
+ {label}
127
+ </text>
128
+ </g>
129
+ )}
130
+ </g>
131
+ );
132
+ }
133
+
134
+ // Single horizontal line
135
+ return (
136
+ <g>
137
+ <line
138
+ x1={0}
139
+ x2={width}
140
+ y1={py}
141
+ y2={py}
142
+ stroke={color}
143
+ strokeWidth={strokeWidth}
144
+ strokeDasharray={strokeDasharray}
145
+ />
146
+ {label && (
147
+ <g transform={`translate(${bx},${py})`} pointerEvents="none">
148
+ <rect
149
+ x={0}
150
+ y={-badgeH / 2}
151
+ width={textW}
152
+ height={badgeH}
153
+ rx={badgeRx}
154
+ fill="var(--color-background-popover)"
155
+ fillOpacity={0.85}
156
+ stroke={color}
157
+ strokeWidth={0.5}
158
+ />
159
+ <text
160
+ x={textW / 2}
161
+ dy="0.35em"
162
+ textAnchor="middle"
163
+ fontSize={fontSize}
164
+ fontWeight={500}
165
+ fill={color}>
166
+ {label}
167
+ </text>
168
+ </g>
169
+ )}
170
+ </g>
171
+ );
172
+ }
173
+
174
+ // Vertical reference line
175
+ if (options.x != null && !('bandwidth' in xScale)) {
176
+ const px = (xScale as ScaleLinear<number, number>)(options.x);
177
+ const textW = label ? label.length * 5.5 + 8 : 0;
178
+ const by = labelPosition === 'end' ? 4 : height - badgeH - 4;
179
+ return (
180
+ <g>
181
+ <line
182
+ x1={px}
183
+ x2={px}
184
+ y1={0}
185
+ y2={height}
186
+ stroke={color}
187
+ strokeWidth={strokeWidth}
188
+ strokeDasharray={strokeDasharray}
189
+ />
190
+ {label && (
191
+ <g
192
+ transform={`translate(${px - textW / 2},${by})`}
193
+ pointerEvents="none">
194
+ <rect
195
+ x={0}
196
+ y={0}
197
+ width={textW}
198
+ height={badgeH}
199
+ rx={badgeRx}
200
+ fill="var(--color-background-popover)"
201
+ fillOpacity={0.85}
202
+ stroke={color}
203
+ strokeWidth={0.5}
204
+ />
205
+ <text
206
+ x={textW / 2}
207
+ y={badgeH / 2}
208
+ dy="0.35em"
209
+ textAnchor="middle"
210
+ fontSize={fontSize}
211
+ fontWeight={500}
212
+ fill={color}>
213
+ {label}
214
+ </text>
215
+ </g>
216
+ )}
217
+ </g>
218
+ );
219
+ }
220
+
221
+ return null;
222
+ },
223
+ };
224
+ }