@pharos-ds/charts 0.0.0-bootstrap.0 → 1.0.10
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/CHANGELOG.md +73 -0
- package/LICENSE +24 -0
- package/README.md +118 -0
- package/dist/Chart.d.ts +62 -0
- package/dist/Chart.d.ts.map +1 -0
- package/dist/Chart.js +401 -0
- package/dist/ChartAxis.d.ts +37 -0
- package/dist/ChartAxis.d.ts.map +1 -0
- package/dist/ChartAxis.js +233 -0
- package/dist/ChartContext.d.ts +4 -0
- package/dist/ChartContext.d.ts.map +1 -0
- package/dist/ChartContext.js +15 -0
- package/dist/ChartGrid.d.ts +26 -0
- package/dist/ChartGrid.d.ts.map +1 -0
- package/dist/ChartGrid.js +118 -0
- package/dist/ChartLegend.d.ts +14 -0
- package/dist/ChartLegend.d.ts.map +1 -0
- package/dist/ChartLegend.js +62 -0
- package/dist/ChartSwatch.d.ts +36 -0
- package/dist/ChartSwatch.d.ts.map +1 -0
- package/dist/ChartSwatch.js +85 -0
- package/dist/ChartTooltip.d.ts +46 -0
- package/dist/ChartTooltip.d.ts.map +1 -0
- package/dist/ChartTooltip.js +330 -0
- package/dist/charts.css +33 -0
- package/dist/dataTable.d.ts +56 -0
- package/dist/dataTable.d.ts.map +1 -0
- package/dist/dataTable.js +88 -0
- package/dist/formatters.d.ts +37 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +156 -0
- package/dist/getChartColors.d.ts +67 -0
- package/dist/getChartColors.d.ts.map +1 -0
- package/dist/getChartColors.js +191 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +322 -0
- package/dist/legend.d.ts +17 -0
- package/dist/legend.d.ts.map +1 -0
- package/dist/legend.js +40 -0
- package/dist/markColor.d.ts +20 -0
- package/dist/markColor.d.ts.map +1 -0
- package/dist/markColor.js +56 -0
- package/dist/marks/area.d.ts +19 -0
- package/dist/marks/area.d.ts.map +1 -0
- package/dist/marks/area.js +121 -0
- package/dist/marks/band.d.ts +9 -0
- package/dist/marks/band.d.ts.map +1 -0
- package/dist/marks/band.js +64 -0
- package/dist/marks/bar.d.ts +21 -0
- package/dist/marks/bar.d.ts.map +1 -0
- package/dist/marks/bar.js +161 -0
- package/dist/marks/candlestick.d.ts +16 -0
- package/dist/marks/candlestick.d.ts.map +1 -0
- package/dist/marks/candlestick.js +110 -0
- package/dist/marks/dot.d.ts +19 -0
- package/dist/marks/dot.d.ts.map +1 -0
- package/dist/marks/dot.js +98 -0
- package/dist/marks/dotGL.d.ts +16 -0
- package/dist/marks/dotGL.d.ts.map +1 -0
- package/dist/marks/dotGL.js +231 -0
- package/dist/marks/dotGLInteractive.d.ts +35 -0
- package/dist/marks/dotGLInteractive.d.ts.map +1 -0
- package/dist/marks/dotGLInteractive.js +584 -0
- package/dist/marks/errorBar.d.ts +29 -0
- package/dist/marks/errorBar.d.ts.map +1 -0
- package/dist/marks/errorBar.js +95 -0
- package/dist/marks/heatmapGL.d.ts +27 -0
- package/dist/marks/heatmapGL.d.ts.map +1 -0
- package/dist/marks/heatmapGL.js +335 -0
- package/dist/marks/index.d.ts +13 -0
- package/dist/marks/index.d.ts.map +1 -0
- package/dist/marks/index.js +14 -0
- package/dist/marks/line.d.ts +18 -0
- package/dist/marks/line.d.ts.map +1 -0
- package/dist/marks/line.js +83 -0
- package/dist/marks/referenceLine.d.ts +37 -0
- package/dist/marks/referenceLine.d.ts.map +1 -0
- package/dist/marks/referenceLine.js +204 -0
- package/dist/marks/streamGL.d.ts +41 -0
- package/dist/marks/streamGL.d.ts.map +1 -0
- package/dist/marks/streamGL.js +317 -0
- package/dist/tooltip.d.ts +38 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +57 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +70 -0
- package/dist/useChartColors.d.ts +16 -0
- package/dist/useChartColors.d.ts.map +1 -0
- package/dist/useChartColors.js +37 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/webgl.d.ts +77 -0
- package/dist/webgl.d.ts.map +1 -0
- package/dist/webgl.js +235 -0
- package/package.json +72 -4
- package/src/Chart.doc.mjs +174 -0
- package/src/Chart.test.tsx +273 -0
- package/src/Chart.tsx +535 -0
- package/src/ChartAxis.tsx +289 -0
- package/src/ChartContext.ts +18 -0
- package/src/ChartGrid.tsx +136 -0
- package/src/ChartLegend.tsx +77 -0
- package/src/ChartSwatch.doc.mjs +17 -0
- package/src/ChartSwatch.tsx +89 -0
- package/src/ChartTooltip.tsx +452 -0
- package/src/charts.css.d.ts +3 -0
- package/src/dataTable.test.ts +86 -0
- package/src/dataTable.ts +124 -0
- package/src/formatters.ts +167 -0
- package/src/getChartColors.test.ts +114 -0
- package/src/getChartColors.ts +282 -0
- package/src/index.ts +88 -0
- package/src/layout.ts +372 -0
- package/src/legend.ts +44 -0
- package/src/markColor.ts +70 -0
- package/src/marks/area.tsx +142 -0
- package/src/marks/band.tsx +86 -0
- package/src/marks/bar.tsx +210 -0
- package/src/marks/candlestick.tsx +128 -0
- package/src/marks/dot.tsx +119 -0
- package/src/marks/dotGL.tsx +274 -0
- package/src/marks/dotGLInteractive.tsx +660 -0
- package/src/marks/errorBar.tsx +119 -0
- package/src/marks/heatmapGL.tsx +392 -0
- package/src/marks/index.ts +17 -0
- package/src/marks/line.tsx +118 -0
- package/src/marks/referenceLine.tsx +252 -0
- package/src/marks/streamGL.tsx +381 -0
- package/src/tooltip.ts +73 -0
- package/src/types.ts +212 -0
- package/src/useChartColors.ts +41 -0
- package/src/utils.ts +38 -0
- package/src/webgl.ts +262 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/referenceLine.tsx
|
|
5
|
+
* @output Horizontal or vertical reference line/band annotation. Labels are
|
|
6
|
+
* clamped to stay fully inside the plot (which is clipped) for both
|
|
7
|
+
* orientations, and non-finite y/x values render nothing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {SeriesDef} from '../types';
|
|
11
|
+
import {isBandScale} from '../utils';
|
|
12
|
+
|
|
13
|
+
export interface ReferenceLineOptions {
|
|
14
|
+
/** Horizontal reference at this y value */
|
|
15
|
+
y?: number;
|
|
16
|
+
/** Second y value — draws a shaded band between y and y2 */
|
|
17
|
+
y2?: number;
|
|
18
|
+
/** Vertical reference at this x value */
|
|
19
|
+
x?: number;
|
|
20
|
+
/** Line/band color */
|
|
21
|
+
color?: string;
|
|
22
|
+
/** Stroke width (for lines) */
|
|
23
|
+
strokeWidth?: number;
|
|
24
|
+
/** Dash pattern */
|
|
25
|
+
strokeDasharray?: string;
|
|
26
|
+
/** Text label */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Label position */
|
|
29
|
+
labelPosition?: 'start' | 'end';
|
|
30
|
+
/** Band fill opacity (when y2 is set) */
|
|
31
|
+
bandOpacity?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Badge dimensions — matches crosshair label style.
|
|
35
|
+
const BADGE_H = 14;
|
|
36
|
+
const BADGE_RX = 3;
|
|
37
|
+
const LABEL_FONT_SIZE = 10;
|
|
38
|
+
// Rough per-glyph advance + horizontal padding for width estimation.
|
|
39
|
+
const LABEL_CHAR_W = 6.5;
|
|
40
|
+
const LABEL_PAD = 12;
|
|
41
|
+
|
|
42
|
+
const clamp = (v: number, lo: number, hi: number): number =>
|
|
43
|
+
Math.max(lo, Math.min(v, hi));
|
|
44
|
+
|
|
45
|
+
/** Estimated badge width for a label (0 when there's no label). */
|
|
46
|
+
function labelWidth(label: string | undefined): number {
|
|
47
|
+
return label ? label.length * LABEL_CHAR_W + LABEL_PAD : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The label badge, positioned by its top-left corner so callers can clamp it
|
|
52
|
+
* into the plot uniformly regardless of orientation.
|
|
53
|
+
*/
|
|
54
|
+
function ReferenceLabel({
|
|
55
|
+
x,
|
|
56
|
+
topY,
|
|
57
|
+
width,
|
|
58
|
+
color,
|
|
59
|
+
text,
|
|
60
|
+
}: {
|
|
61
|
+
x: number;
|
|
62
|
+
topY: number;
|
|
63
|
+
width: number;
|
|
64
|
+
color: string;
|
|
65
|
+
text: string;
|
|
66
|
+
}) {
|
|
67
|
+
return (
|
|
68
|
+
<g transform={`translate(${x},${topY})`} pointerEvents="none">
|
|
69
|
+
<rect
|
|
70
|
+
x={0}
|
|
71
|
+
y={0}
|
|
72
|
+
width={width}
|
|
73
|
+
height={BADGE_H}
|
|
74
|
+
rx={BADGE_RX}
|
|
75
|
+
fill="var(--color-background-popover)"
|
|
76
|
+
fillOpacity={0.85}
|
|
77
|
+
stroke={color}
|
|
78
|
+
strokeWidth={0.5}
|
|
79
|
+
/>
|
|
80
|
+
<text
|
|
81
|
+
x={width / 2}
|
|
82
|
+
y={BADGE_H / 2}
|
|
83
|
+
dy="0.35em"
|
|
84
|
+
textAnchor="middle"
|
|
85
|
+
fontSize={LABEL_FONT_SIZE}
|
|
86
|
+
fontWeight={500}
|
|
87
|
+
fill={color}>
|
|
88
|
+
{text}
|
|
89
|
+
</text>
|
|
90
|
+
</g>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reference line or band annotation. Not a data series — renders at fixed values.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```
|
|
99
|
+
* series={[line('revenue'), referenceLine({y: 100, label: 'Target'})]}
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function referenceLine(options: ReferenceLineOptions): SeriesDef {
|
|
103
|
+
const color = options.color ?? 'var(--color-border-emphasized)';
|
|
104
|
+
const strokeWidth = options.strokeWidth ?? 1;
|
|
105
|
+
const strokeDasharray = options.strokeDasharray ?? '6 3';
|
|
106
|
+
const label = options.label;
|
|
107
|
+
const labelPosition = options.labelPosition ?? 'end';
|
|
108
|
+
const bandOpacity = options.bandOpacity ?? 0.1;
|
|
109
|
+
|
|
110
|
+
const textW = labelWidth(label);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
type: 'referenceLine',
|
|
114
|
+
key: `ref-${options.y ?? options.x ?? 'none'}`,
|
|
115
|
+
dataKeys: [],
|
|
116
|
+
layout: {},
|
|
117
|
+
|
|
118
|
+
// No data points to resolve — this is a fixed annotation
|
|
119
|
+
resolve() {
|
|
120
|
+
return [];
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
render(_resolved, ctx) {
|
|
124
|
+
const {width, height, xScale, yScale} = ctx;
|
|
125
|
+
// Keep the badge fully inside the (clipped) plot on both axes.
|
|
126
|
+
const bxMax = width - textW - 2;
|
|
127
|
+
|
|
128
|
+
// Horizontal reference line or band
|
|
129
|
+
if (options.y != null) {
|
|
130
|
+
if (!Number.isFinite(options.y)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const py = yScale(options.y);
|
|
134
|
+
if (!Number.isFinite(py)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const desiredBx = labelPosition === 'end' ? bxMax : 2;
|
|
138
|
+
const bx = clamp(desiredBx, 2, bxMax);
|
|
139
|
+
const labelTopY = clamp(py - BADGE_H / 2, 0, height - BADGE_H);
|
|
140
|
+
|
|
141
|
+
// Band mode: shaded region between y and y2
|
|
142
|
+
if (options.y2 != null && Number.isFinite(options.y2)) {
|
|
143
|
+
const py2 = yScale(options.y2);
|
|
144
|
+
const top = Math.min(py, py2);
|
|
145
|
+
const bandHeight = Math.abs(py2 - py);
|
|
146
|
+
return (
|
|
147
|
+
<g>
|
|
148
|
+
<rect
|
|
149
|
+
x={0}
|
|
150
|
+
y={top}
|
|
151
|
+
width={width}
|
|
152
|
+
height={bandHeight}
|
|
153
|
+
fill={color}
|
|
154
|
+
opacity={bandOpacity}
|
|
155
|
+
/>
|
|
156
|
+
<line
|
|
157
|
+
x1={0}
|
|
158
|
+
x2={width}
|
|
159
|
+
y1={py}
|
|
160
|
+
y2={py}
|
|
161
|
+
stroke={color}
|
|
162
|
+
strokeWidth={strokeWidth}
|
|
163
|
+
strokeDasharray={strokeDasharray}
|
|
164
|
+
/>
|
|
165
|
+
<line
|
|
166
|
+
x1={0}
|
|
167
|
+
x2={width}
|
|
168
|
+
y1={py2}
|
|
169
|
+
y2={py2}
|
|
170
|
+
stroke={color}
|
|
171
|
+
strokeWidth={strokeWidth}
|
|
172
|
+
strokeDasharray={strokeDasharray}
|
|
173
|
+
/>
|
|
174
|
+
{label && (
|
|
175
|
+
<ReferenceLabel
|
|
176
|
+
x={bx}
|
|
177
|
+
topY={labelTopY}
|
|
178
|
+
width={textW}
|
|
179
|
+
color={color}
|
|
180
|
+
text={label}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</g>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Single horizontal line
|
|
188
|
+
return (
|
|
189
|
+
<g>
|
|
190
|
+
<line
|
|
191
|
+
x1={0}
|
|
192
|
+
x2={width}
|
|
193
|
+
y1={py}
|
|
194
|
+
y2={py}
|
|
195
|
+
stroke={color}
|
|
196
|
+
strokeWidth={strokeWidth}
|
|
197
|
+
strokeDasharray={strokeDasharray}
|
|
198
|
+
/>
|
|
199
|
+
{label && (
|
|
200
|
+
<ReferenceLabel
|
|
201
|
+
x={bx}
|
|
202
|
+
topY={labelTopY}
|
|
203
|
+
width={textW}
|
|
204
|
+
color={color}
|
|
205
|
+
text={label}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</g>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Vertical reference line (linear x only — a band scale has no pixel
|
|
213
|
+
// position for an arbitrary numeric x).
|
|
214
|
+
if (options.x != null && !isBandScale(xScale)) {
|
|
215
|
+
if (!Number.isFinite(options.x)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const px = xScale(options.x);
|
|
219
|
+
if (!Number.isFinite(px)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const desiredTopY = labelPosition === 'end' ? 4 : height - BADGE_H - 4;
|
|
223
|
+
const labelTopY = clamp(desiredTopY, 0, height - BADGE_H);
|
|
224
|
+
const bx = clamp(px - textW / 2, 2, bxMax);
|
|
225
|
+
return (
|
|
226
|
+
<g>
|
|
227
|
+
<line
|
|
228
|
+
x1={px}
|
|
229
|
+
x2={px}
|
|
230
|
+
y1={0}
|
|
231
|
+
y2={height}
|
|
232
|
+
stroke={color}
|
|
233
|
+
strokeWidth={strokeWidth}
|
|
234
|
+
strokeDasharray={strokeDasharray}
|
|
235
|
+
/>
|
|
236
|
+
{label && (
|
|
237
|
+
<ReferenceLabel
|
|
238
|
+
x={bx}
|
|
239
|
+
topY={labelTopY}
|
|
240
|
+
width={textW}
|
|
241
|
+
color={color}
|
|
242
|
+
text={label}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
</g>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/streamGL.tsx
|
|
5
|
+
* @output WebGL streaming time-series line — ring buffer, canvas outside SVG
|
|
6
|
+
*
|
|
7
|
+
* Data is pushed imperatively via the returned handle, not from the static dataset.
|
|
8
|
+
* The chart's xScale/yScale map pushed values to pixel coordinates.
|
|
9
|
+
*
|
|
10
|
+
* Draws are coalesced onto a single requestAnimationFrame so a burst of pushes
|
|
11
|
+
* repaints once per frame. The GL context, program, and vertex buffer are made
|
|
12
|
+
* once and reused; all are released (rAF cancelled, context eagerly lost) on
|
|
13
|
+
* unmount. Redraws on resize / scale change and recovers from context loss.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
useRef,
|
|
18
|
+
useEffect,
|
|
19
|
+
useImperativeHandle,
|
|
20
|
+
useCallback,
|
|
21
|
+
useState,
|
|
22
|
+
type MutableRefObject,
|
|
23
|
+
} from 'react';
|
|
24
|
+
import type {SeriesDef} from '../types';
|
|
25
|
+
import {useChart} from '../ChartContext';
|
|
26
|
+
import {
|
|
27
|
+
hexToGL,
|
|
28
|
+
getWebGLContext,
|
|
29
|
+
setupGLState,
|
|
30
|
+
sizeCanvas,
|
|
31
|
+
mountCanvasOverSVG,
|
|
32
|
+
createProgram,
|
|
33
|
+
registerContextLossHandlers,
|
|
34
|
+
loseContext,
|
|
35
|
+
} from '../webgl';
|
|
36
|
+
|
|
37
|
+
export interface StreamGLHandle {
|
|
38
|
+
push(x: number, y: number): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StreamGLOptions {
|
|
43
|
+
color: string;
|
|
44
|
+
bufferSize?: number;
|
|
45
|
+
lineWidth?: number;
|
|
46
|
+
opacity?: number;
|
|
47
|
+
/** Ref to receive the push/clear handle */
|
|
48
|
+
handleRef?: MutableRefObject<StreamGLHandle | null>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const VERT = `
|
|
52
|
+
attribute vec2 a_position;
|
|
53
|
+
uniform vec2 u_resolution;
|
|
54
|
+
void main() {
|
|
55
|
+
vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
|
|
56
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const FRAG = `
|
|
61
|
+
precision mediump float;
|
|
62
|
+
uniform vec3 u_color;
|
|
63
|
+
uniform float u_opacity;
|
|
64
|
+
void main() {
|
|
65
|
+
float a = u_opacity;
|
|
66
|
+
gl_FragColor = vec4(u_color * a, a);
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
type StreamGLLocations = {
|
|
71
|
+
aPosition: number;
|
|
72
|
+
uResolution: WebGLUniformLocation | null;
|
|
73
|
+
uColor: WebGLUniformLocation | null;
|
|
74
|
+
uOpacity: WebGLUniformLocation | null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Inline component — manages the ring buffer, canvas, and rAF draw loop.
|
|
79
|
+
*/
|
|
80
|
+
function StreamGLCanvas({
|
|
81
|
+
color,
|
|
82
|
+
bufferSize,
|
|
83
|
+
lineWidth,
|
|
84
|
+
opacity,
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
handleRef,
|
|
88
|
+
}: {
|
|
89
|
+
color: string;
|
|
90
|
+
bufferSize: number;
|
|
91
|
+
lineWidth: number;
|
|
92
|
+
opacity: number;
|
|
93
|
+
width: number;
|
|
94
|
+
height: number;
|
|
95
|
+
handleRef?: MutableRefObject<StreamGLHandle | null>;
|
|
96
|
+
}) {
|
|
97
|
+
const {xScale, yScale} = useChart();
|
|
98
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
99
|
+
const glRef = useRef<WebGLRenderingContext | null>(null);
|
|
100
|
+
const programRef = useRef<WebGLProgram | null>(null);
|
|
101
|
+
const bufRef = useRef<WebGLBuffer | null>(null);
|
|
102
|
+
const locsRef = useRef<StreamGLLocations | null>(null);
|
|
103
|
+
const markerRef = useRef<SVGGElement>(null);
|
|
104
|
+
const rafRef = useRef<number | null>(null);
|
|
105
|
+
// Bumped on context restore to force a rebuild + repaint.
|
|
106
|
+
const [contextEpoch, setContextEpoch] = useState(0);
|
|
107
|
+
|
|
108
|
+
// Ring buffer + a reusable scratch array for the pixel-space draw data. The
|
|
109
|
+
// capacity is captured on the ring itself so head/count math stays consistent
|
|
110
|
+
// even if the `bufferSize` prop changes (handled by the resize effect below).
|
|
111
|
+
const ringRef = useRef({
|
|
112
|
+
data: new Float32Array(bufferSize * 2),
|
|
113
|
+
head: 0,
|
|
114
|
+
count: 0,
|
|
115
|
+
capacity: bufferSize,
|
|
116
|
+
});
|
|
117
|
+
const scratchRef = useRef<Float32Array>(new Float32Array(bufferSize * 2));
|
|
118
|
+
|
|
119
|
+
// Re-allocate the ring if bufferSize changes (rare — resets the stream).
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const ring = ringRef.current;
|
|
122
|
+
if (ring.capacity !== bufferSize) {
|
|
123
|
+
ring.data = new Float32Array(bufferSize * 2);
|
|
124
|
+
ring.head = 0;
|
|
125
|
+
ring.count = 0;
|
|
126
|
+
ring.capacity = bufferSize;
|
|
127
|
+
scratchRef.current = new Float32Array(bufferSize * 2);
|
|
128
|
+
}
|
|
129
|
+
}, [bufferSize]);
|
|
130
|
+
|
|
131
|
+
const draw = useCallback(() => {
|
|
132
|
+
const canvas = canvasRef.current;
|
|
133
|
+
if (!canvas || width <= 0 || height <= 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!glRef.current) {
|
|
137
|
+
glRef.current = getWebGLContext(canvas);
|
|
138
|
+
}
|
|
139
|
+
const gl = glRef.current;
|
|
140
|
+
if (!gl || gl.isContextLost()) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!programRef.current) {
|
|
145
|
+
programRef.current = createProgram(gl, VERT, FRAG);
|
|
146
|
+
locsRef.current = null;
|
|
147
|
+
}
|
|
148
|
+
const program = programRef.current;
|
|
149
|
+
if (!program) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!bufRef.current) {
|
|
154
|
+
bufRef.current = gl.createBuffer();
|
|
155
|
+
}
|
|
156
|
+
const buffer = bufRef.current;
|
|
157
|
+
if (!buffer) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!locsRef.current) {
|
|
162
|
+
locsRef.current = {
|
|
163
|
+
aPosition: gl.getAttribLocation(program, 'a_position'),
|
|
164
|
+
uResolution: gl.getUniformLocation(program, 'u_resolution'),
|
|
165
|
+
uColor: gl.getUniformLocation(program, 'u_color'),
|
|
166
|
+
uOpacity: gl.getUniformLocation(program, 'u_opacity'),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const locs = locsRef.current;
|
|
170
|
+
|
|
171
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
172
|
+
setupGLState(gl);
|
|
173
|
+
|
|
174
|
+
const {data: ringData, head, count, capacity} = ringRef.current;
|
|
175
|
+
if (count < 2) {
|
|
176
|
+
// Buffer already cleared above — nothing to stroke yet.
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const linearX = xScale as (v: number) => number;
|
|
181
|
+
const drawBuf = scratchRef.current;
|
|
182
|
+
for (let i = 0; i < count; i++) {
|
|
183
|
+
const idx = ((head - count + i + capacity) % capacity) * 2;
|
|
184
|
+
drawBuf[i * 2] = linearX(ringData[idx]);
|
|
185
|
+
drawBuf[i * 2 + 1] = yScale(ringData[idx + 1]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
gl.useProgram(program);
|
|
189
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
190
|
+
gl.bufferData(
|
|
191
|
+
gl.ARRAY_BUFFER,
|
|
192
|
+
drawBuf.subarray(0, count * 2),
|
|
193
|
+
gl.DYNAMIC_DRAW,
|
|
194
|
+
);
|
|
195
|
+
gl.enableVertexAttribArray(locs.aPosition);
|
|
196
|
+
gl.vertexAttribPointer(locs.aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
197
|
+
|
|
198
|
+
const [r, g, b] = hexToGL(color);
|
|
199
|
+
gl.uniform2f(locs.uResolution, width, height);
|
|
200
|
+
gl.uniform3f(locs.uColor, r, g, b);
|
|
201
|
+
gl.uniform1f(locs.uOpacity, opacity);
|
|
202
|
+
|
|
203
|
+
gl.lineWidth(lineWidth);
|
|
204
|
+
gl.drawArrays(gl.LINE_STRIP, 0, count);
|
|
205
|
+
}, [width, height, color, lineWidth, opacity, xScale, yScale]);
|
|
206
|
+
|
|
207
|
+
// Keep the latest draw reachable from the single coalesced rAF callback, so a
|
|
208
|
+
// frame scheduled before a scale/size change still paints with current state.
|
|
209
|
+
const drawRef = useRef(draw);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
drawRef.current = draw;
|
|
212
|
+
}, [draw]);
|
|
213
|
+
|
|
214
|
+
// Coalesce redraws onto one rAF so a burst of pushes paints once per frame.
|
|
215
|
+
// Stable identity keeps the imperative push/clear handle from churning.
|
|
216
|
+
const scheduleDraw = useCallback(() => {
|
|
217
|
+
if (rafRef.current != null) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
221
|
+
rafRef.current = null;
|
|
222
|
+
drawRef.current();
|
|
223
|
+
});
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
// Mount the canvas outside the SVG and wire context-loss recovery.
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const marker = markerRef.current;
|
|
229
|
+
if (!marker) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!canvasRef.current) {
|
|
233
|
+
canvasRef.current = document.createElement('canvas');
|
|
234
|
+
}
|
|
235
|
+
const canvas = canvasRef.current;
|
|
236
|
+
const detach = mountCanvasOverSVG(marker, canvas, width, height);
|
|
237
|
+
const unregister = registerContextLossHandlers(
|
|
238
|
+
canvas,
|
|
239
|
+
() => {
|
|
240
|
+
if (rafRef.current != null) {
|
|
241
|
+
cancelAnimationFrame(rafRef.current);
|
|
242
|
+
rafRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
glRef.current = null;
|
|
245
|
+
programRef.current = null;
|
|
246
|
+
bufRef.current = null;
|
|
247
|
+
locsRef.current = null;
|
|
248
|
+
},
|
|
249
|
+
() => setContextEpoch(e => e + 1),
|
|
250
|
+
);
|
|
251
|
+
return () => {
|
|
252
|
+
unregister();
|
|
253
|
+
detach?.();
|
|
254
|
+
};
|
|
255
|
+
}, [width, height]);
|
|
256
|
+
|
|
257
|
+
// Size the canvas when the plot area changes.
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const canvas = canvasRef.current;
|
|
260
|
+
if (!canvas || width <= 0 || height <= 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
sizeCanvas(canvas, width, height);
|
|
264
|
+
}, [width, height]);
|
|
265
|
+
|
|
266
|
+
// Redraw the retained buffer when scales, size, or the GL context change so a
|
|
267
|
+
// paused stream doesn't leave a stale frame after a resize or window slide.
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
scheduleDraw();
|
|
270
|
+
}, [draw, scheduleDraw, contextEpoch]);
|
|
271
|
+
|
|
272
|
+
// Expose push/clear handle via ref.
|
|
273
|
+
useImperativeHandle(
|
|
274
|
+
handleRef as MutableRefObject<StreamGLHandle | null>,
|
|
275
|
+
() => ({
|
|
276
|
+
push(x: number, y: number) {
|
|
277
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const ring = ringRef.current;
|
|
281
|
+
const idx = ring.head * 2;
|
|
282
|
+
ring.data[idx] = x;
|
|
283
|
+
ring.data[idx + 1] = y;
|
|
284
|
+
ring.head = (ring.head + 1) % ring.capacity;
|
|
285
|
+
ring.count = Math.min(ring.count + 1, ring.capacity);
|
|
286
|
+
scheduleDraw();
|
|
287
|
+
},
|
|
288
|
+
clear() {
|
|
289
|
+
const ring = ringRef.current;
|
|
290
|
+
ring.head = 0;
|
|
291
|
+
ring.count = 0;
|
|
292
|
+
if (rafRef.current != null) {
|
|
293
|
+
cancelAnimationFrame(rafRef.current);
|
|
294
|
+
rafRef.current = null;
|
|
295
|
+
}
|
|
296
|
+
const gl = glRef.current;
|
|
297
|
+
if (gl && !gl.isContextLost()) {
|
|
298
|
+
gl.clearColor(0, 0, 0, 0);
|
|
299
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
[scheduleDraw],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// Cancel any pending frame and release all GL resources on unmount.
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
return () => {
|
|
309
|
+
if (rafRef.current != null) {
|
|
310
|
+
cancelAnimationFrame(rafRef.current);
|
|
311
|
+
rafRef.current = null;
|
|
312
|
+
}
|
|
313
|
+
const gl = glRef.current;
|
|
314
|
+
if (gl) {
|
|
315
|
+
if (bufRef.current) {
|
|
316
|
+
gl.deleteBuffer(bufRef.current);
|
|
317
|
+
}
|
|
318
|
+
if (programRef.current) {
|
|
319
|
+
gl.deleteProgram(programRef.current);
|
|
320
|
+
}
|
|
321
|
+
loseContext(gl);
|
|
322
|
+
}
|
|
323
|
+
glRef.current = null;
|
|
324
|
+
programRef.current = null;
|
|
325
|
+
bufRef.current = null;
|
|
326
|
+
locsRef.current = null;
|
|
327
|
+
};
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
if (width <= 0 || height <= 0) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return <g ref={markerRef} />;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* WebGL streaming line. Data is pushed imperatively via handleRef,
|
|
338
|
+
* not from the chart's static dataset. Internally uses a ring buffer.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```
|
|
342
|
+
* const streamRef = useRef<StreamGLHandle>(null);
|
|
343
|
+
* // Push data points in a rAF loop or event handler:
|
|
344
|
+
* streamRef.current?.push(Date.now(), value);
|
|
345
|
+
*
|
|
346
|
+
* <Chart series={[streamGL({color: '#10b981', handleRef: streamRef})]} />
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
export function streamGL(options: StreamGLOptions): SeriesDef {
|
|
350
|
+
const {color} = options;
|
|
351
|
+
const bufferSize = options.bufferSize ?? 500;
|
|
352
|
+
const lineWidth = options.lineWidth ?? 2;
|
|
353
|
+
const opacity = options.opacity ?? 1;
|
|
354
|
+
const handleRef = options.handleRef;
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
type: 'streamGL',
|
|
358
|
+
key: 'stream',
|
|
359
|
+
dataKeys: [],
|
|
360
|
+
layout: {},
|
|
361
|
+
|
|
362
|
+
// Streaming data is pushed, not in the static dataset
|
|
363
|
+
resolve() {
|
|
364
|
+
return [];
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
render(_resolved, ctx) {
|
|
368
|
+
return (
|
|
369
|
+
<StreamGLCanvas
|
|
370
|
+
color={color}
|
|
371
|
+
bufferSize={bufferSize}
|
|
372
|
+
lineWidth={lineWidth}
|
|
373
|
+
opacity={opacity}
|
|
374
|
+
width={ctx.width}
|
|
375
|
+
height={ctx.height}
|
|
376
|
+
handleRef={handleRef}
|
|
377
|
+
/>
|
|
378
|
+
);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
package/src/tooltip.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file tooltip.ts
|
|
5
|
+
* @output Tooltip types + series-value derivation
|
|
6
|
+
* @position Used by Chart to bridge series + hovered datum → tooltip rows.
|
|
7
|
+
* Mirrors the legend.ts split: data shaping lives here, presentation
|
|
8
|
+
* lives in ChartTooltip.tsx.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {DEFAULT_SERIES_COLOR, isUtilityMarkType, type SeriesDef} from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One row of tooltip data — represents a single series' value at the
|
|
15
|
+
* hovered x-position. Stable shape so custom `render` functions can rely on it.
|
|
16
|
+
*/
|
|
17
|
+
export interface TooltipSeriesValue {
|
|
18
|
+
/** Series key (matches SeriesDef.key) */
|
|
19
|
+
key: string;
|
|
20
|
+
/** Display label — falls back to key when not set on the series */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Series color token */
|
|
23
|
+
color: string;
|
|
24
|
+
/** Mark type — drives swatch variant in the default render */
|
|
25
|
+
type: string;
|
|
26
|
+
/** Raw value at the hovered x for this series */
|
|
27
|
+
value: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the ordered list of tooltip rows for a given hovered datum.
|
|
32
|
+
* Skips utility marks and any series the layout couldn't resolve.
|
|
33
|
+
*
|
|
34
|
+
* Pure / synchronous — safe to memoize on the caller.
|
|
35
|
+
*/
|
|
36
|
+
export function deriveTooltipSeriesValues(
|
|
37
|
+
series: readonly SeriesDef[],
|
|
38
|
+
datum: Record<string, unknown> | undefined,
|
|
39
|
+
resolvedKeys: ReadonlySet<string>,
|
|
40
|
+
): TooltipSeriesValue[] {
|
|
41
|
+
if (!datum) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const out: TooltipSeriesValue[] = [];
|
|
45
|
+
for (const s of series) {
|
|
46
|
+
if (isUtilityMarkType(s.type)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// resolvedKeys holds layout-assigned _uids (collision-free), not `key`.
|
|
50
|
+
if (!s._uid || !resolvedKeys.has(s._uid)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
out.push({
|
|
54
|
+
key: s.key,
|
|
55
|
+
label: s.label ?? s.key,
|
|
56
|
+
// Same representative-color precedence as the legend, so a series' tooltip
|
|
57
|
+
// swatch and legend swatch always match.
|
|
58
|
+
color: s.color ?? s._resolvedColor ?? DEFAULT_SERIES_COLOR,
|
|
59
|
+
type: s.type,
|
|
60
|
+
value: datum[s.dataKeys[0]],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a series should render a hover dot at the focused index.
|
|
68
|
+
* Bars get a band-highlight rect instead, and utility marks have no
|
|
69
|
+
* data point to highlight.
|
|
70
|
+
*/
|
|
71
|
+
export function shouldRenderHoverDot(seriesType: string): boolean {
|
|
72
|
+
return seriesType !== 'bar' && !isUtilityMarkType(seriesType);
|
|
73
|
+
}
|