@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.
- package/dist/ChartV2/Chart.d.ts +32 -0
- package/dist/ChartV2/Chart.d.ts.map +1 -0
- package/dist/ChartV2/Chart.js +281 -0
- package/dist/ChartV2/ChartAxis.d.ts +36 -0
- package/dist/ChartV2/ChartAxis.d.ts.map +1 -0
- package/dist/ChartV2/ChartAxis.js +161 -0
- package/dist/ChartV2/ChartGrid.d.ts +20 -0
- package/dist/ChartV2/ChartGrid.d.ts.map +1 -0
- package/dist/ChartV2/ChartGrid.js +76 -0
- package/dist/ChartV2/ChartLegend.d.ts +14 -0
- package/dist/ChartV2/ChartLegend.d.ts.map +1 -0
- package/dist/ChartV2/ChartLegend.js +54 -0
- package/dist/ChartV2/ChartSwatch.d.ts +36 -0
- package/dist/ChartV2/ChartSwatch.d.ts.map +1 -0
- package/dist/ChartV2/ChartSwatch.js +82 -0
- package/dist/ChartV2/ChartTooltip.d.ts +46 -0
- package/dist/ChartV2/ChartTooltip.d.ts.map +1 -0
- package/dist/ChartV2/ChartTooltip.js +267 -0
- package/dist/ChartV2/ChartV2Context.d.ts +4 -0
- package/dist/ChartV2/ChartV2Context.d.ts.map +1 -0
- package/dist/ChartV2/ChartV2Context.js +12 -0
- package/dist/ChartV2/index.d.ts +13 -0
- package/dist/ChartV2/index.d.ts.map +1 -0
- package/dist/ChartV2/index.js +10 -0
- package/dist/ChartV2/layout.d.ts +25 -0
- package/dist/ChartV2/layout.d.ts.map +1 -0
- package/dist/ChartV2/layout.js +195 -0
- package/dist/ChartV2/legend.d.ts +18 -0
- package/dist/ChartV2/legend.d.ts.map +1 -0
- package/dist/ChartV2/legend.js +24 -0
- package/dist/ChartV2/marks/area.d.ts +19 -0
- package/dist/ChartV2/marks/area.d.ts.map +1 -0
- package/dist/ChartV2/marks/area.js +109 -0
- package/dist/ChartV2/marks/band.d.ts +9 -0
- package/dist/ChartV2/marks/band.d.ts.map +1 -0
- package/dist/ChartV2/marks/band.js +59 -0
- package/dist/ChartV2/marks/bar.d.ts +17 -0
- package/dist/ChartV2/marks/bar.d.ts.map +1 -0
- package/dist/ChartV2/marks/bar.js +120 -0
- package/dist/ChartV2/marks/candlestick.d.ts +15 -0
- package/dist/ChartV2/marks/candlestick.d.ts.map +1 -0
- package/dist/ChartV2/marks/candlestick.js +83 -0
- package/dist/ChartV2/marks/dot.d.ts +15 -0
- package/dist/ChartV2/marks/dot.d.ts.map +1 -0
- package/dist/ChartV2/marks/dot.js +65 -0
- package/dist/ChartV2/marks/dotGL.d.ts +16 -0
- package/dist/ChartV2/marks/dotGL.d.ts.map +1 -0
- package/dist/ChartV2/marks/dotGL.js +178 -0
- package/dist/ChartV2/marks/dotGLInteractive.d.ts +30 -0
- package/dist/ChartV2/marks/dotGLInteractive.d.ts.map +1 -0
- package/dist/ChartV2/marks/dotGLInteractive.js +464 -0
- package/dist/ChartV2/marks/errorBar.d.ts +27 -0
- package/dist/ChartV2/marks/errorBar.d.ts.map +1 -0
- package/dist/ChartV2/marks/errorBar.js +87 -0
- package/dist/ChartV2/marks/heatmapGL.d.ts +23 -0
- package/dist/ChartV2/marks/heatmapGL.d.ts.map +1 -0
- package/dist/ChartV2/marks/heatmapGL.js +242 -0
- package/dist/ChartV2/marks/index.d.ts +13 -0
- package/dist/ChartV2/marks/index.d.ts.map +1 -0
- package/dist/ChartV2/marks/index.js +14 -0
- package/dist/ChartV2/marks/line.d.ts +18 -0
- package/dist/ChartV2/marks/line.d.ts.map +1 -0
- package/dist/ChartV2/marks/line.js +76 -0
- package/dist/ChartV2/marks/referenceLine.d.ts +35 -0
- package/dist/ChartV2/marks/referenceLine.d.ts.map +1 -0
- package/dist/ChartV2/marks/referenceLine.js +186 -0
- package/dist/ChartV2/marks/streamGL.d.ts +36 -0
- package/dist/ChartV2/marks/streamGL.d.ts.map +1 -0
- package/dist/ChartV2/marks/streamGL.js +204 -0
- package/dist/ChartV2/tooltip.d.ts +38 -0
- package/dist/ChartV2/tooltip.d.ts.map +1 -0
- package/dist/ChartV2/tooltip.js +54 -0
- package/dist/ChartV2/types.d.ts +115 -0
- package/dist/ChartV2/types.d.ts.map +1 -0
- package/dist/ChartV2/types.js +35 -0
- package/dist/CodeEditor/CodeEditor.d.ts +1 -4
- package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
- package/dist/CodeEditor/CodeEditor.js +0 -3
- package/dist/Sankey/SankeyChart.d.ts.map +1 -1
- package/dist/Sankey/SankeyChart.js +0 -3
- package/dist/Schedule/ListView.d.ts.map +1 -1
- package/dist/Schedule/ListView.js +4 -7
- package/dist/Schedule/MonthlyView.js +2 -3
- package/dist/Schedule/Schedule.d.ts +1 -3
- package/dist/Schedule/Schedule.d.ts.map +1 -1
- package/dist/Schedule/Schedule.js +4 -9
- package/dist/Schedule/TimeGridView.js +2 -3
- package/dist/Schedule/context.d.ts +0 -2
- package/dist/Schedule/context.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/lab.css +15 -6
- package/package.json +2 -2
- package/src/ChartV2/Chart.tsx +348 -0
- package/src/ChartV2/ChartAxis.tsx +209 -0
- package/src/ChartV2/ChartGrid.tsx +99 -0
- package/src/ChartV2/ChartLegend.tsx +69 -0
- package/src/ChartV2/ChartSwatch.doc.mjs +17 -0
- package/src/ChartV2/ChartSwatch.tsx +86 -0
- package/src/ChartV2/ChartTooltip.tsx +382 -0
- package/src/ChartV2/ChartV2.doc.mjs +16 -0
- package/src/ChartV2/ChartV2Context.ts +15 -0
- package/src/ChartV2/index.ts +62 -0
- package/src/ChartV2/layout.ts +223 -0
- package/src/ChartV2/legend.ts +32 -0
- package/src/ChartV2/marks/area.tsx +122 -0
- package/src/ChartV2/marks/band.tsx +70 -0
- package/src/ChartV2/marks/bar.tsx +159 -0
- package/src/ChartV2/marks/candlestick.tsx +106 -0
- package/src/ChartV2/marks/dot.tsx +64 -0
- package/src/ChartV2/marks/dotGL.tsx +202 -0
- package/src/ChartV2/marks/dotGLInteractive.tsx +546 -0
- package/src/ChartV2/marks/errorBar.tsx +89 -0
- package/src/ChartV2/marks/heatmapGL.tsx +299 -0
- package/src/ChartV2/marks/index.ts +14 -0
- package/src/ChartV2/marks/line.tsx +94 -0
- package/src/ChartV2/marks/referenceLine.tsx +224 -0
- package/src/ChartV2/marks/streamGL.tsx +251 -0
- package/src/ChartV2/tooltip.ts +70 -0
- package/src/ChartV2/types.ts +129 -0
- package/src/CodeEditor/CodeEditor.tsx +0 -5
- package/src/Sankey/SankeyChart.tsx +0 -3
- package/src/Schedule/ListView.tsx +2 -5
- package/src/Schedule/MonthlyView.tsx +2 -2
- package/src/Schedule/Schedule.test.tsx +0 -39
- package/src/Schedule/Schedule.tsx +0 -8
- package/src/Schedule/TimeGridView.tsx +2 -2
- package/src/Schedule/context.tsx +0 -2
- package/src/index.ts +46 -5
- package/dist/InfoTip/InfoTip.d.ts +0 -58
- package/dist/InfoTip/InfoTip.d.ts.map +0 -1
- package/dist/InfoTip/InfoTip.js +0 -91
- package/dist/InfoTip/index.d.ts +0 -6
- package/dist/InfoTip/index.d.ts.map +0 -1
- package/dist/InfoTip/index.js +0 -8
- package/src/CodeEditor/CodeEditor.test.tsx +0 -21
- package/src/InfoTip/InfoTip.doc.mjs +0 -97
- package/src/InfoTip/InfoTip.test.tsx +0 -169
- package/src/InfoTip/InfoTip.tsx +0 -155
- package/src/InfoTip/index.ts +0 -10
- package/src/Sankey/SankeyChart.test.tsx +0 -77
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file legend.ts
|
|
5
|
+
* @output Legend utility — derives legend items from series definitions
|
|
6
|
+
* @position Used by Chart to bridge series → legend items
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {isUtilityMarkType, type SeriesDef} from './types';
|
|
10
|
+
|
|
11
|
+
export interface LegendItem {
|
|
12
|
+
label: string;
|
|
13
|
+
color: string;
|
|
14
|
+
/** Mark type — determines swatch shape (square for bar, line for everything else) */
|
|
15
|
+
type?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasLegendColor(
|
|
19
|
+
series: SeriesDef,
|
|
20
|
+
): series is SeriesDef & {readonly color: string} {
|
|
21
|
+
return !isUtilityMarkType(series.type) && series.color != null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive legend items from series definitions.
|
|
26
|
+
* Skips utility marks and series without a color.
|
|
27
|
+
*/
|
|
28
|
+
export function deriveLegendItems(series: readonly SeriesDef[]): LegendItem[] {
|
|
29
|
+
return series
|
|
30
|
+
.filter(hasLegendColor)
|
|
31
|
+
.map(s => ({label: s.label ?? s.key, color: s.color, type: s.type}));
|
|
32
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/area.tsx
|
|
5
|
+
* @output Area series — fill under line with stacking + gradient support
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
area as d3Area,
|
|
10
|
+
curveLinear,
|
|
11
|
+
curveMonotoneX,
|
|
12
|
+
curveNatural,
|
|
13
|
+
curveStep,
|
|
14
|
+
} from 'd3-shape';
|
|
15
|
+
import {line as d3Line} from 'd3-shape';
|
|
16
|
+
import type {SeriesDef, ResolvedPoint} from '../types';
|
|
17
|
+
import type {ScaleBand} from 'd3-scale';
|
|
18
|
+
|
|
19
|
+
const CURVES = {
|
|
20
|
+
linear: curveLinear,
|
|
21
|
+
monotone: curveMonotoneX,
|
|
22
|
+
natural: curveNatural,
|
|
23
|
+
step: curveStep,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export interface AreaOptions {
|
|
27
|
+
color?: string;
|
|
28
|
+
opacity?: number;
|
|
29
|
+
curve?: keyof typeof CURVES;
|
|
30
|
+
stack?: string;
|
|
31
|
+
gradient?: boolean;
|
|
32
|
+
stroke?: boolean;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function area(dataKey: string, options: AreaOptions = {}): SeriesDef {
|
|
37
|
+
const color = options.color ?? 'var(--color-chart-1)';
|
|
38
|
+
const opacity = options.opacity ?? 0.3;
|
|
39
|
+
const curve = options.curve ?? 'monotone';
|
|
40
|
+
const gradient = options.gradient ?? false;
|
|
41
|
+
const stroke = options.stroke ?? true;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
type: 'area',
|
|
45
|
+
key: dataKey,
|
|
46
|
+
dataKeys: [dataKey],
|
|
47
|
+
color,
|
|
48
|
+
label: options.label ?? dataKey,
|
|
49
|
+
layout: {stack: options.stack, includeZero: true},
|
|
50
|
+
|
|
51
|
+
resolve(ctx, stackOffsets) {
|
|
52
|
+
const {data, xKey, xScale, yScale} = ctx;
|
|
53
|
+
const points: ResolvedPoint[] = [];
|
|
54
|
+
for (let i = 0; i < data.length; i++) {
|
|
55
|
+
const d = data[i];
|
|
56
|
+
let px: number;
|
|
57
|
+
if ('bandwidth' in xScale) {
|
|
58
|
+
px =
|
|
59
|
+
((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
|
|
60
|
+
(xScale as ScaleBand<string>).bandwidth() / 2;
|
|
61
|
+
} else {
|
|
62
|
+
px = xScale(d[xKey] as number);
|
|
63
|
+
}
|
|
64
|
+
let py: number, py0: number;
|
|
65
|
+
if (stackOffsets) {
|
|
66
|
+
py = yScale(stackOffsets[i].y1);
|
|
67
|
+
py0 = yScale(stackOffsets[i].y0);
|
|
68
|
+
} else {
|
|
69
|
+
const v = typeof d[dataKey] === 'number' ? (d[dataKey] as number) : 0;
|
|
70
|
+
py = yScale(v);
|
|
71
|
+
py0 = yScale(0);
|
|
72
|
+
}
|
|
73
|
+
points.push({px, py, py0, dataIndex: i});
|
|
74
|
+
}
|
|
75
|
+
return points;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
render(resolved) {
|
|
79
|
+
if (resolved.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const curveFactory = CURVES[curve];
|
|
83
|
+
|
|
84
|
+
const areaGen = d3Area<ResolvedPoint>()
|
|
85
|
+
.x(d => d.px)
|
|
86
|
+
.y0(d => d.py0)
|
|
87
|
+
.y1(d => d.py)
|
|
88
|
+
.curve(curveFactory);
|
|
89
|
+
const pathD = areaGen(resolved) ?? '';
|
|
90
|
+
|
|
91
|
+
const lineGen = d3Line<ResolvedPoint>()
|
|
92
|
+
.x(d => d.px)
|
|
93
|
+
.y(d => d.py)
|
|
94
|
+
.curve(curveFactory);
|
|
95
|
+
const strokeD = lineGen(resolved) ?? '';
|
|
96
|
+
|
|
97
|
+
const gradientId = `area-grad-${dataKey}`;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<g>
|
|
101
|
+
{gradient && (
|
|
102
|
+
<defs>
|
|
103
|
+
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
104
|
+
<stop offset="0%" stopColor={color} stopOpacity={opacity} />
|
|
105
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
106
|
+
</linearGradient>
|
|
107
|
+
</defs>
|
|
108
|
+
)}
|
|
109
|
+
<path
|
|
110
|
+
d={pathD}
|
|
111
|
+
fill={gradient ? `url(#${gradientId})` : color}
|
|
112
|
+
fillOpacity={gradient ? 1 : opacity}
|
|
113
|
+
stroke="none"
|
|
114
|
+
/>
|
|
115
|
+
{stroke && (
|
|
116
|
+
<path d={strokeD} fill="none" stroke={color} strokeWidth={2} />
|
|
117
|
+
)}
|
|
118
|
+
</g>
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/band.tsx
|
|
5
|
+
* @output Band (confidence interval) series
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {area as d3Area, curveMonotoneX} from 'd3-shape';
|
|
9
|
+
import type {SeriesDef, ResolvedPoint} from '../types';
|
|
10
|
+
import type {ScaleBand} from 'd3-scale';
|
|
11
|
+
|
|
12
|
+
export interface BandOptions {
|
|
13
|
+
upper: string;
|
|
14
|
+
lower: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
opacity?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function band(options: BandOptions): SeriesDef {
|
|
20
|
+
const color = options.color ?? 'var(--color-chart-1)';
|
|
21
|
+
const opacity = options.opacity ?? 0.15;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
type: 'band',
|
|
25
|
+
key: `band-${options.upper}-${options.lower}`,
|
|
26
|
+
dataKeys: [options.upper, options.lower],
|
|
27
|
+
layout: {},
|
|
28
|
+
|
|
29
|
+
resolve(ctx) {
|
|
30
|
+
const {data, xKey, xScale, yScale} = ctx;
|
|
31
|
+
const points: ResolvedPoint[] = [];
|
|
32
|
+
for (let i = 0; i < data.length; i++) {
|
|
33
|
+
const d = data[i];
|
|
34
|
+
let px: number;
|
|
35
|
+
if ('bandwidth' in xScale) {
|
|
36
|
+
px =
|
|
37
|
+
((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
|
|
38
|
+
(xScale as ScaleBand<string>).bandwidth() / 2;
|
|
39
|
+
} else {
|
|
40
|
+
px = xScale(d[xKey] as number);
|
|
41
|
+
}
|
|
42
|
+
const upper =
|
|
43
|
+
typeof d[options.upper] === 'number'
|
|
44
|
+
? (d[options.upper] as number)
|
|
45
|
+
: 0;
|
|
46
|
+
const lower =
|
|
47
|
+
typeof d[options.lower] === 'number'
|
|
48
|
+
? (d[options.lower] as number)
|
|
49
|
+
: 0;
|
|
50
|
+
points.push({px, py: yScale(upper), py0: yScale(lower), dataIndex: i});
|
|
51
|
+
}
|
|
52
|
+
return points;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
render(resolved) {
|
|
56
|
+
if (resolved.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const areaGen = d3Area<ResolvedPoint>()
|
|
60
|
+
.x(d => d.px)
|
|
61
|
+
.y0(d => d.py0)
|
|
62
|
+
.y1(d => d.py)
|
|
63
|
+
.curve(curveMonotoneX);
|
|
64
|
+
const pathD = areaGen(resolved) ?? '';
|
|
65
|
+
return (
|
|
66
|
+
<path d={pathD} fill={color} fillOpacity={opacity} stroke="none" />
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/bar.tsx
|
|
5
|
+
* @output Bar series — self-contained resolve + render
|
|
6
|
+
* @position Standalone mark; chart root calls resolve() then render()
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {SeriesDef, ResolvedPoint} from '../types';
|
|
10
|
+
import type {ScaleBand} from 'd3-scale';
|
|
11
|
+
|
|
12
|
+
export type ColorAccessor =
|
|
13
|
+
| string
|
|
14
|
+
| ((datum: Record<string, unknown>, index: number) => string);
|
|
15
|
+
|
|
16
|
+
export interface BarOptions {
|
|
17
|
+
color?: ColorAccessor;
|
|
18
|
+
opacity?: number;
|
|
19
|
+
radius?: number;
|
|
20
|
+
stack?: string;
|
|
21
|
+
group?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build an SVG path for a rectangle with only the top corners rounded.
|
|
27
|
+
* When `r` is 0, produces a simple rect path with no curves.
|
|
28
|
+
*/
|
|
29
|
+
function roundedTopRect(
|
|
30
|
+
x: number,
|
|
31
|
+
y: number,
|
|
32
|
+
w: number,
|
|
33
|
+
h: number,
|
|
34
|
+
r: number,
|
|
35
|
+
): string {
|
|
36
|
+
// Clamp radius to half the smaller dimension
|
|
37
|
+
const cr = Math.min(r, w / 2, h / 2);
|
|
38
|
+
if (cr <= 0) {
|
|
39
|
+
return `M${x},${y + h}V${y}H${x + w}V${y + h}Z`;
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
`M${x},${y + h}` +
|
|
43
|
+
`V${y + cr}Q${x},${y} ${x + cr},${y}` +
|
|
44
|
+
`H${x + w - cr}Q${x + w},${y} ${x + w},${y + cr}` +
|
|
45
|
+
`V${y + h}Z`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build an SVG path for a rectangle with only the bottom corners rounded.
|
|
51
|
+
*/
|
|
52
|
+
function roundedBottomRect(
|
|
53
|
+
x: number,
|
|
54
|
+
y: number,
|
|
55
|
+
w: number,
|
|
56
|
+
h: number,
|
|
57
|
+
r: number,
|
|
58
|
+
): string {
|
|
59
|
+
const cr = Math.min(r, w / 2, h / 2);
|
|
60
|
+
if (cr <= 0) {
|
|
61
|
+
return `M${x},${y}V${y + h}H${x + w}V${y}Z`;
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
`M${x},${y}` +
|
|
65
|
+
`V${y + h - cr}Q${x},${y + h} ${x + cr},${y + h}` +
|
|
66
|
+
`H${x + w - cr}Q${x + w},${y + h} ${x + w},${y + h - cr}` +
|
|
67
|
+
`V${y}Z`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function bar(dataKey: string, options: BarOptions = {}): SeriesDef {
|
|
72
|
+
const color = options.color ?? 'var(--color-chart-1)';
|
|
73
|
+
const opacity = options.opacity ?? 1;
|
|
74
|
+
const radius = options.radius ?? 4;
|
|
75
|
+
|
|
76
|
+
let barWidth = 0;
|
|
77
|
+
let barOffset = 0;
|
|
78
|
+
|
|
79
|
+
const seriesDef: SeriesDef = {
|
|
80
|
+
type: 'bar',
|
|
81
|
+
key: dataKey,
|
|
82
|
+
dataKeys: [dataKey],
|
|
83
|
+
color: typeof color === 'string' ? color : undefined,
|
|
84
|
+
label: options.label ?? dataKey,
|
|
85
|
+
layout: {
|
|
86
|
+
stack: options.stack,
|
|
87
|
+
group: options.group,
|
|
88
|
+
includeZero: true,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
resolve(ctx, stackOffsets, groupInfo) {
|
|
92
|
+
const {data, xKey, xScale, yScale} = ctx;
|
|
93
|
+
if (!('bandwidth' in xScale)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const bandScale = xScale as ScaleBand<string>;
|
|
98
|
+
const bw = bandScale.bandwidth();
|
|
99
|
+
|
|
100
|
+
if (groupInfo) {
|
|
101
|
+
const gutter = bw * 0.05;
|
|
102
|
+
const totalGutters = gutter * (groupInfo.count - 1);
|
|
103
|
+
barWidth = (bw - totalGutters) / groupInfo.count;
|
|
104
|
+
barOffset = groupInfo.index * (barWidth + gutter);
|
|
105
|
+
} else {
|
|
106
|
+
barWidth = bw;
|
|
107
|
+
barOffset = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const points: ResolvedPoint[] = [];
|
|
111
|
+
for (let i = 0; i < data.length; i++) {
|
|
112
|
+
const d = data[i];
|
|
113
|
+
const xBase = bandScale(String(d[xKey])) ?? 0;
|
|
114
|
+
const px = xBase + barOffset + barWidth / 2;
|
|
115
|
+
|
|
116
|
+
let py: number;
|
|
117
|
+
let py0: number;
|
|
118
|
+
if (stackOffsets) {
|
|
119
|
+
py = yScale(stackOffsets[i].y1);
|
|
120
|
+
py0 = yScale(stackOffsets[i].y0);
|
|
121
|
+
} else {
|
|
122
|
+
const v = typeof d[dataKey] === 'number' ? (d[dataKey] as number) : 0;
|
|
123
|
+
py = yScale(v);
|
|
124
|
+
py0 = yScale(0);
|
|
125
|
+
}
|
|
126
|
+
points.push({px, py, py0, dataIndex: i});
|
|
127
|
+
}
|
|
128
|
+
return points;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
render(resolved, ctx) {
|
|
132
|
+
const {data} = ctx;
|
|
133
|
+
const isTop = seriesDef._isTopOfStack !== false;
|
|
134
|
+
const r = isTop ? radius : 0;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<g opacity={opacity}>
|
|
138
|
+
{resolved.map((p, i) => {
|
|
139
|
+
const d = data[p.dataIndex];
|
|
140
|
+
const fill =
|
|
141
|
+
typeof color === 'function' ? color(d, p.dataIndex) : color;
|
|
142
|
+
const x = p.px - barWidth / 2;
|
|
143
|
+
const y = Math.min(p.py, p.py0);
|
|
144
|
+
const h = Math.max(0, Math.abs(p.py0 - p.py));
|
|
145
|
+
const growsUp = p.py <= p.py0;
|
|
146
|
+
|
|
147
|
+
const d_attr = growsUp
|
|
148
|
+
? roundedTopRect(x, y, barWidth, h, r)
|
|
149
|
+
: roundedBottomRect(x, y, barWidth, h, r);
|
|
150
|
+
|
|
151
|
+
return <path key={i} d={d_attr} fill={fill} />;
|
|
152
|
+
})}
|
|
153
|
+
</g>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return seriesDef;
|
|
159
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/candlestick.tsx
|
|
5
|
+
* @output Candlestick (OHLC) series
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {SeriesDef, ResolvedPoint} from '../types';
|
|
9
|
+
import type {ScaleBand} from 'd3-scale';
|
|
10
|
+
|
|
11
|
+
export interface CandlestickOptions {
|
|
12
|
+
open: string;
|
|
13
|
+
high: string;
|
|
14
|
+
low: string;
|
|
15
|
+
close: string;
|
|
16
|
+
upColor?: string;
|
|
17
|
+
downColor?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function candlestick(options: CandlestickOptions): SeriesDef {
|
|
21
|
+
const upColor = options.upColor ?? 'var(--color-positive)';
|
|
22
|
+
const downColor = options.downColor ?? 'var(--color-negative)';
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
type: 'candlestick',
|
|
26
|
+
key: `ohlc-${options.close}`,
|
|
27
|
+
dataKeys: [options.open, options.high, options.low, options.close],
|
|
28
|
+
layout: {},
|
|
29
|
+
|
|
30
|
+
resolve(ctx) {
|
|
31
|
+
const {data, xKey, xScale, yScale} = ctx;
|
|
32
|
+
const points: ResolvedPoint[] = [];
|
|
33
|
+
for (let i = 0; i < data.length; i++) {
|
|
34
|
+
const d = data[i];
|
|
35
|
+
let px: number;
|
|
36
|
+
if ('bandwidth' in xScale) {
|
|
37
|
+
px =
|
|
38
|
+
((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
|
|
39
|
+
(xScale as ScaleBand<string>).bandwidth() / 2;
|
|
40
|
+
} else {
|
|
41
|
+
px = xScale(d[xKey] as number);
|
|
42
|
+
}
|
|
43
|
+
const close =
|
|
44
|
+
typeof d[options.close] === 'number'
|
|
45
|
+
? (d[options.close] as number)
|
|
46
|
+
: 0;
|
|
47
|
+
points.push({px, py: yScale(close), py0: yScale(0), dataIndex: i});
|
|
48
|
+
}
|
|
49
|
+
return points;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
render(resolved, ctx) {
|
|
53
|
+
const {data, xScale, yScale} = ctx;
|
|
54
|
+
if (!('bandwidth' in xScale)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const bw = (xScale as ScaleBand<string>).bandwidth();
|
|
58
|
+
const bodyWidth = bw * 0.6;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<g>
|
|
62
|
+
{resolved.map((p, i) => {
|
|
63
|
+
const d = data[p.dataIndex];
|
|
64
|
+
const o =
|
|
65
|
+
typeof d[options.open] === 'number'
|
|
66
|
+
? (d[options.open] as number)
|
|
67
|
+
: 0;
|
|
68
|
+
const c =
|
|
69
|
+
typeof d[options.close] === 'number'
|
|
70
|
+
? (d[options.close] as number)
|
|
71
|
+
: 0;
|
|
72
|
+
const h =
|
|
73
|
+
typeof d[options.high] === 'number'
|
|
74
|
+
? (d[options.high] as number)
|
|
75
|
+
: 0;
|
|
76
|
+
const l =
|
|
77
|
+
typeof d[options.low] === 'number'
|
|
78
|
+
? (d[options.low] as number)
|
|
79
|
+
: 0;
|
|
80
|
+
const isUp = c >= o;
|
|
81
|
+
const col = isUp ? upColor : downColor;
|
|
82
|
+
return (
|
|
83
|
+
<g key={i}>
|
|
84
|
+
<line
|
|
85
|
+
x1={p.px}
|
|
86
|
+
x2={p.px}
|
|
87
|
+
y1={yScale(h)}
|
|
88
|
+
y2={yScale(l)}
|
|
89
|
+
stroke={col}
|
|
90
|
+
strokeWidth={1}
|
|
91
|
+
/>
|
|
92
|
+
<rect
|
|
93
|
+
x={p.px - bodyWidth / 2}
|
|
94
|
+
y={yScale(Math.max(o, c))}
|
|
95
|
+
width={bodyWidth}
|
|
96
|
+
height={Math.max(1, Math.abs(yScale(o) - yScale(c)))}
|
|
97
|
+
fill={col}
|
|
98
|
+
/>
|
|
99
|
+
</g>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</g>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file marks/dot.tsx
|
|
5
|
+
* @output Dot/scatter series — self-contained resolve + render
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {SeriesDef, ResolvedPoint} from '../types';
|
|
9
|
+
import type {ScaleBand} from 'd3-scale';
|
|
10
|
+
|
|
11
|
+
export type ColorAccessor = string | ((datum: Record<string, unknown>, index: number) => string);
|
|
12
|
+
|
|
13
|
+
export interface DotOptions {
|
|
14
|
+
color?: ColorAccessor;
|
|
15
|
+
radius?: number;
|
|
16
|
+
opacity?: number;
|
|
17
|
+
dodge?: boolean;
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function dot(dataKey: string, options: DotOptions = {}): SeriesDef {
|
|
22
|
+
const color = options.color ?? 'var(--color-chart-1)';
|
|
23
|
+
const radius = options.radius ?? 4;
|
|
24
|
+
const opacity = options.opacity ?? 0.8;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
type: 'dot',
|
|
28
|
+
key: dataKey,
|
|
29
|
+
dataKeys: [dataKey],
|
|
30
|
+
color: typeof color === 'string' ? color : undefined,
|
|
31
|
+
label: options.label ?? dataKey,
|
|
32
|
+
layout: {},
|
|
33
|
+
|
|
34
|
+
resolve(ctx) {
|
|
35
|
+
const {data, xKey, xScale, yScale} = ctx;
|
|
36
|
+
const points: ResolvedPoint[] = [];
|
|
37
|
+
for (let i = 0; i < data.length; i++) {
|
|
38
|
+
const d = data[i];
|
|
39
|
+
let px: number;
|
|
40
|
+
if ('bandwidth' in xScale) {
|
|
41
|
+
px = ((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) + (xScale as ScaleBand<string>).bandwidth() / 2;
|
|
42
|
+
} else {
|
|
43
|
+
px = xScale(d[xKey] as number);
|
|
44
|
+
}
|
|
45
|
+
const v = typeof d[dataKey] === 'number' ? (d[dataKey] as number) : 0;
|
|
46
|
+
points.push({px, py: yScale(v), py0: yScale(0), dataIndex: i});
|
|
47
|
+
}
|
|
48
|
+
// TODO: dodge layout runs here — has access to radius + all points
|
|
49
|
+
return points;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
render(resolved, ctx) {
|
|
53
|
+
const {data} = ctx;
|
|
54
|
+
return (
|
|
55
|
+
<g>
|
|
56
|
+
{resolved.map((p, i) => {
|
|
57
|
+
const fill = typeof color === 'function' ? color(data[p.dataIndex], p.dataIndex) : color;
|
|
58
|
+
return <circle key={i} cx={p.px} cy={p.py} r={radius} fill={fill} opacity={opacity} />;
|
|
59
|
+
})}
|
|
60
|
+
</g>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|