@pharos-ds/charts 0.0.0-stage → 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.
Files changed (139) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +24 -0
  3. package/README.md +117 -2
  4. package/dist/Chart.d.ts +62 -0
  5. package/dist/Chart.d.ts.map +1 -0
  6. package/dist/Chart.js +401 -0
  7. package/dist/ChartAxis.d.ts +37 -0
  8. package/dist/ChartAxis.d.ts.map +1 -0
  9. package/dist/ChartAxis.js +233 -0
  10. package/dist/ChartContext.d.ts +4 -0
  11. package/dist/ChartContext.d.ts.map +1 -0
  12. package/dist/ChartContext.js +15 -0
  13. package/dist/ChartGrid.d.ts +26 -0
  14. package/dist/ChartGrid.d.ts.map +1 -0
  15. package/dist/ChartGrid.js +118 -0
  16. package/dist/ChartLegend.d.ts +14 -0
  17. package/dist/ChartLegend.d.ts.map +1 -0
  18. package/dist/ChartLegend.js +62 -0
  19. package/dist/ChartSwatch.d.ts +36 -0
  20. package/dist/ChartSwatch.d.ts.map +1 -0
  21. package/dist/ChartSwatch.js +85 -0
  22. package/dist/ChartTooltip.d.ts +46 -0
  23. package/dist/ChartTooltip.d.ts.map +1 -0
  24. package/dist/ChartTooltip.js +330 -0
  25. package/dist/charts.css +33 -0
  26. package/dist/dataTable.d.ts +56 -0
  27. package/dist/dataTable.d.ts.map +1 -0
  28. package/dist/dataTable.js +88 -0
  29. package/dist/formatters.d.ts +37 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +156 -0
  32. package/dist/getChartColors.d.ts +67 -0
  33. package/dist/getChartColors.d.ts.map +1 -0
  34. package/dist/getChartColors.js +191 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +19 -0
  38. package/dist/layout.d.ts +40 -0
  39. package/dist/layout.d.ts.map +1 -0
  40. package/dist/layout.js +322 -0
  41. package/dist/legend.d.ts +17 -0
  42. package/dist/legend.d.ts.map +1 -0
  43. package/dist/legend.js +40 -0
  44. package/dist/markColor.d.ts +20 -0
  45. package/dist/markColor.d.ts.map +1 -0
  46. package/dist/markColor.js +56 -0
  47. package/dist/marks/area.d.ts +19 -0
  48. package/dist/marks/area.d.ts.map +1 -0
  49. package/dist/marks/area.js +121 -0
  50. package/dist/marks/band.d.ts +9 -0
  51. package/dist/marks/band.d.ts.map +1 -0
  52. package/dist/marks/band.js +64 -0
  53. package/dist/marks/bar.d.ts +21 -0
  54. package/dist/marks/bar.d.ts.map +1 -0
  55. package/dist/marks/bar.js +161 -0
  56. package/dist/marks/candlestick.d.ts +16 -0
  57. package/dist/marks/candlestick.d.ts.map +1 -0
  58. package/dist/marks/candlestick.js +110 -0
  59. package/dist/marks/dot.d.ts +19 -0
  60. package/dist/marks/dot.d.ts.map +1 -0
  61. package/dist/marks/dot.js +98 -0
  62. package/dist/marks/dotGL.d.ts +16 -0
  63. package/dist/marks/dotGL.d.ts.map +1 -0
  64. package/dist/marks/dotGL.js +231 -0
  65. package/dist/marks/dotGLInteractive.d.ts +35 -0
  66. package/dist/marks/dotGLInteractive.d.ts.map +1 -0
  67. package/dist/marks/dotGLInteractive.js +584 -0
  68. package/dist/marks/errorBar.d.ts +29 -0
  69. package/dist/marks/errorBar.d.ts.map +1 -0
  70. package/dist/marks/errorBar.js +95 -0
  71. package/dist/marks/heatmapGL.d.ts +27 -0
  72. package/dist/marks/heatmapGL.d.ts.map +1 -0
  73. package/dist/marks/heatmapGL.js +335 -0
  74. package/dist/marks/index.d.ts +13 -0
  75. package/dist/marks/index.d.ts.map +1 -0
  76. package/dist/marks/index.js +14 -0
  77. package/dist/marks/line.d.ts +18 -0
  78. package/dist/marks/line.d.ts.map +1 -0
  79. package/dist/marks/line.js +83 -0
  80. package/dist/marks/referenceLine.d.ts +37 -0
  81. package/dist/marks/referenceLine.d.ts.map +1 -0
  82. package/dist/marks/referenceLine.js +204 -0
  83. package/dist/marks/streamGL.d.ts +41 -0
  84. package/dist/marks/streamGL.d.ts.map +1 -0
  85. package/dist/marks/streamGL.js +317 -0
  86. package/dist/tooltip.d.ts +38 -0
  87. package/dist/tooltip.d.ts.map +1 -0
  88. package/dist/tooltip.js +57 -0
  89. package/dist/types.d.ts +193 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +70 -0
  92. package/dist/useChartColors.d.ts +16 -0
  93. package/dist/useChartColors.d.ts.map +1 -0
  94. package/dist/useChartColors.js +37 -0
  95. package/dist/utils.d.ts +15 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +31 -0
  98. package/dist/webgl.d.ts +77 -0
  99. package/dist/webgl.d.ts.map +1 -0
  100. package/dist/webgl.js +235 -0
  101. package/package.json +71 -3
  102. package/src/Chart.doc.mjs +174 -0
  103. package/src/Chart.test.tsx +273 -0
  104. package/src/Chart.tsx +535 -0
  105. package/src/ChartAxis.tsx +289 -0
  106. package/src/ChartContext.ts +18 -0
  107. package/src/ChartGrid.tsx +136 -0
  108. package/src/ChartLegend.tsx +77 -0
  109. package/src/ChartSwatch.doc.mjs +17 -0
  110. package/src/ChartSwatch.tsx +89 -0
  111. package/src/ChartTooltip.tsx +452 -0
  112. package/src/charts.css.d.ts +3 -0
  113. package/src/dataTable.test.ts +86 -0
  114. package/src/dataTable.ts +124 -0
  115. package/src/formatters.ts +167 -0
  116. package/src/getChartColors.test.ts +114 -0
  117. package/src/getChartColors.ts +282 -0
  118. package/src/index.ts +88 -0
  119. package/src/layout.ts +372 -0
  120. package/src/legend.ts +44 -0
  121. package/src/markColor.ts +70 -0
  122. package/src/marks/area.tsx +142 -0
  123. package/src/marks/band.tsx +86 -0
  124. package/src/marks/bar.tsx +210 -0
  125. package/src/marks/candlestick.tsx +128 -0
  126. package/src/marks/dot.tsx +119 -0
  127. package/src/marks/dotGL.tsx +274 -0
  128. package/src/marks/dotGLInteractive.tsx +660 -0
  129. package/src/marks/errorBar.tsx +119 -0
  130. package/src/marks/heatmapGL.tsx +392 -0
  131. package/src/marks/index.ts +17 -0
  132. package/src/marks/line.tsx +118 -0
  133. package/src/marks/referenceLine.tsx +252 -0
  134. package/src/marks/streamGL.tsx +381 -0
  135. package/src/tooltip.ts +73 -0
  136. package/src/types.ts +212 -0
  137. package/src/useChartColors.ts +41 -0
  138. package/src/utils.ts +38 -0
  139. package/src/webgl.ts +262 -0
@@ -0,0 +1,86 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/band.tsx
5
+ * @output Band (confidence interval) series — area between upper/lower bounds.
6
+ * Non-numeric/NaN bounds break the area into gaps instead of collapsing to a
7
+ * false zero baseline.
8
+ */
9
+
10
+ import {area as d3Area, curveMonotoneX} from 'd3-shape';
11
+ import {
12
+ DEFAULT_SERIES_COLOR,
13
+ type SeriesDef,
14
+ type ResolvedPoint,
15
+ } from '../types';
16
+ import {xPixel} from '../utils';
17
+
18
+ export interface BandOptions {
19
+ upper: string;
20
+ lower: string;
21
+ color?: string;
22
+ opacity?: number;
23
+ }
24
+
25
+ export function band(options: BandOptions): SeriesDef {
26
+ const color = options.color ?? DEFAULT_SERIES_COLOR;
27
+ const opacity = options.opacity ?? 0.15;
28
+
29
+ return {
30
+ type: 'band',
31
+ key: `band-${options.upper}-${options.lower}`,
32
+ dataKeys: [options.upper, options.lower],
33
+ layout: {},
34
+
35
+ resolve(ctx) {
36
+ const {data, xKey, xScale, yScale} = ctx;
37
+ const points: ResolvedPoint[] = [];
38
+ for (let i = 0; i < data.length; i++) {
39
+ const d = data[i];
40
+ // NaN for missing/non-numeric bounds so render() can gap the area
41
+ // rather than draw a spike down to the axis.
42
+ const upperVal = d[options.upper];
43
+ const lowerVal = d[options.lower];
44
+ const upper =
45
+ typeof upperVal === 'number' && Number.isFinite(upperVal)
46
+ ? upperVal
47
+ : NaN;
48
+ const lower =
49
+ typeof lowerVal === 'number' && Number.isFinite(lowerVal)
50
+ ? lowerVal
51
+ : NaN;
52
+ points.push({
53
+ px: xPixel(d, xKey, xScale),
54
+ py: yScale(upper),
55
+ py0: yScale(lower),
56
+ dataIndex: i,
57
+ });
58
+ }
59
+ return points;
60
+ },
61
+
62
+ render(resolved) {
63
+ if (resolved.length === 0) {
64
+ return null;
65
+ }
66
+ const areaGen = d3Area<ResolvedPoint>()
67
+ .defined(
68
+ d =>
69
+ Number.isFinite(d.px) &&
70
+ Number.isFinite(d.py) &&
71
+ Number.isFinite(d.py0),
72
+ )
73
+ .x(d => d.px)
74
+ .y0(d => d.py0)
75
+ .y1(d => d.py)
76
+ .curve(curveMonotoneX);
77
+ const pathD = areaGen(resolved) ?? '';
78
+ if (!pathD) {
79
+ return null;
80
+ }
81
+ return (
82
+ <path d={pathD} fill={color} fillOpacity={opacity} stroke="none" />
83
+ );
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,210 @@
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
+ * Robust to missing/NaN values (bars with no finite height are skipped rather
9
+ * than drawn at the baseline) and to degenerate bandwidths (grouped bar widths
10
+ * are clamped to a non-negative value). Negative values grow downward from zero.
11
+ */
12
+
13
+ import type {SeriesDef, ResolvedPoint, ColorAccessor} from '../types';
14
+ import type {ScaleBand} from 'd3-scale';
15
+ import {pointFill} from '../markColor';
16
+
17
+ export type {ColorAccessor};
18
+
19
+ export interface BarOptions {
20
+ color?: ColorAccessor;
21
+ opacity?: number;
22
+ radius?: number;
23
+ stack?: string;
24
+ group?: string;
25
+ label?: string;
26
+ }
27
+
28
+ /** Clamp a user opacity into [0,1], falling back when it isn't a finite number. */
29
+ function clampOpacity(value: number | undefined, fallback: number): number {
30
+ const n =
31
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
32
+ return Math.min(1, Math.max(0, n));
33
+ }
34
+
35
+ /** Coerce a user length (radius/width) to a finite, non-negative number. */
36
+ function nonNegative(value: number | undefined, fallback: number): number {
37
+ return typeof value === 'number' && Number.isFinite(value)
38
+ ? Math.max(0, value)
39
+ : fallback;
40
+ }
41
+
42
+ /** True when every coordinate is finite — a NaN/Infinity would corrupt the path. */
43
+ function rectIsFinite(x: number, y: number, w: number, h: number): boolean {
44
+ return (
45
+ Number.isFinite(x) &&
46
+ Number.isFinite(y) &&
47
+ Number.isFinite(w) &&
48
+ Number.isFinite(h)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Build an SVG path for a rectangle with only the top corners rounded.
54
+ * When `r` is 0, produces a simple rect path with no curves. Returns an empty
55
+ * path (draws nothing) if any coordinate is non-finite.
56
+ */
57
+ function roundedTopRect(
58
+ x: number,
59
+ y: number,
60
+ w: number,
61
+ h: number,
62
+ r: number,
63
+ ): string {
64
+ if (!rectIsFinite(x, y, w, h)) {
65
+ return '';
66
+ }
67
+ // Clamp radius to half the smaller dimension
68
+ const cr = Math.min(r, w / 2, h / 2);
69
+ if (!(cr > 0)) {
70
+ return `M${x},${y + h}V${y}H${x + w}V${y + h}Z`;
71
+ }
72
+ return (
73
+ `M${x},${y + h}` +
74
+ `V${y + cr}Q${x},${y} ${x + cr},${y}` +
75
+ `H${x + w - cr}Q${x + w},${y} ${x + w},${y + cr}` +
76
+ `V${y + h}Z`
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Build an SVG path for a rectangle with only the bottom corners rounded.
82
+ * Returns an empty path if any coordinate is non-finite.
83
+ */
84
+ function roundedBottomRect(
85
+ x: number,
86
+ y: number,
87
+ w: number,
88
+ h: number,
89
+ r: number,
90
+ ): string {
91
+ if (!rectIsFinite(x, y, w, h)) {
92
+ return '';
93
+ }
94
+ const cr = Math.min(r, w / 2, h / 2);
95
+ if (!(cr > 0)) {
96
+ return `M${x},${y}V${y + h}H${x + w}V${y}Z`;
97
+ }
98
+ return (
99
+ `M${x},${y}` +
100
+ `V${y + h - cr}Q${x},${y + h} ${x + cr},${y + h}` +
101
+ `H${x + w - cr}Q${x + w},${y + h} ${x + w},${y + h - cr}` +
102
+ `V${y}Z`
103
+ );
104
+ }
105
+
106
+ export function bar(dataKey: string, options: BarOptions = {}): SeriesDef {
107
+ const color = options.color;
108
+ const opacity = clampOpacity(options.opacity, 1);
109
+ const radius = nonNegative(options.radius, 4);
110
+
111
+ let barWidth = 0;
112
+ let barOffset = 0;
113
+
114
+ const seriesDef: SeriesDef = {
115
+ type: 'bar',
116
+ key: dataKey,
117
+ dataKeys: [dataKey],
118
+ color: typeof color === 'string' ? color : undefined,
119
+ label: options.label ?? dataKey,
120
+ layout: {
121
+ stack: options.stack,
122
+ group: options.group,
123
+ includeZero: true,
124
+ },
125
+
126
+ resolve(ctx, stackOffsets, groupInfo) {
127
+ const {data, xKey, xScale, yScale} = ctx;
128
+ if (!('bandwidth' in xScale)) {
129
+ return [];
130
+ }
131
+
132
+ const bandScale = xScale as ScaleBand<string>;
133
+ const bw = bandScale.bandwidth();
134
+
135
+ if (groupInfo && groupInfo.count > 0) {
136
+ const gutter = bw * 0.05;
137
+ const totalGutters = gutter * (groupInfo.count - 1);
138
+ // Clamp: with many groups in a narrow band, gutters can exceed the band
139
+ // and drive the raw width negative — a negative width mirrors the path.
140
+ barWidth = Math.max(0, (bw - totalGutters) / groupInfo.count);
141
+ barOffset = groupInfo.index * (barWidth + gutter);
142
+ } else {
143
+ barWidth = bw;
144
+ barOffset = 0;
145
+ }
146
+
147
+ const points: ResolvedPoint[] = [];
148
+ for (let i = 0; i < data.length; i++) {
149
+ const d = data[i];
150
+ // Unknown categories aren't in the band domain → NaN px so render skips
151
+ // them rather than stacking phantom bars at the origin.
152
+ const xBase = bandScale(String(d[xKey]));
153
+ const px = xBase == null ? NaN : xBase + barOffset + barWidth / 2;
154
+
155
+ let py: number;
156
+ let py0: number;
157
+ if (stackOffsets) {
158
+ const offset = stackOffsets[i];
159
+ py = offset ? yScale(offset.y1) : NaN;
160
+ py0 = offset ? yScale(offset.y0) : NaN;
161
+ } else {
162
+ const raw = d[dataKey];
163
+ const v = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN;
164
+ py = yScale(v);
165
+ py0 = yScale(0);
166
+ }
167
+ points.push({px, py, py0, dataIndex: i});
168
+ }
169
+ return points;
170
+ },
171
+
172
+ render(resolved, ctx) {
173
+ const {data} = ctx;
174
+ const isTop = seriesDef._isTopOfStack !== false;
175
+ const r = isTop ? radius : 0;
176
+ if (!(barWidth > 0)) {
177
+ return null;
178
+ }
179
+
180
+ return (
181
+ <g opacity={opacity}>
182
+ {resolved.map(p => {
183
+ const d = data[p.dataIndex];
184
+ if (
185
+ !d ||
186
+ !Number.isFinite(p.px) ||
187
+ !Number.isFinite(p.py) ||
188
+ !Number.isFinite(p.py0)
189
+ ) {
190
+ return null;
191
+ }
192
+ const fill = pointFill(seriesDef, color, d, p.dataIndex);
193
+ const x = p.px - barWidth / 2;
194
+ const y = Math.min(p.py, p.py0);
195
+ const h = Math.max(0, Math.abs(p.py0 - p.py));
196
+ const growsUp = p.py <= p.py0;
197
+
198
+ const d_attr = growsUp
199
+ ? roundedTopRect(x, y, barWidth, h, r)
200
+ : roundedBottomRect(x, y, barWidth, h, r);
201
+
202
+ return <path key={p.dataIndex} d={d_attr} fill={fill} />;
203
+ })}
204
+ </g>
205
+ );
206
+ },
207
+ };
208
+
209
+ return seriesDef;
210
+ }
@@ -0,0 +1,128 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/candlestick.tsx
5
+ * @output Candlestick (OHLC) series. Renders on both band and linear/time x
6
+ * scales; candles missing any OHLC field are skipped rather than drawn to 0.
7
+ */
8
+
9
+ import type {SeriesDef, ResolvedPoint, SeriesContext} from '../types';
10
+ import {xPixel, isBandScale} from '../utils';
11
+
12
+ export interface CandlestickOptions {
13
+ open: string;
14
+ high: string;
15
+ low: string;
16
+ close: string;
17
+ upColor?: string;
18
+ downColor?: string;
19
+ }
20
+
21
+ /** Read a datum field as a finite number, or NaN when missing/non-numeric. */
22
+ function readNumber(d: Record<string, unknown>, key: string): number {
23
+ const v = d[key];
24
+ return typeof v === 'number' && Number.isFinite(v) ? v : NaN;
25
+ }
26
+
27
+ /**
28
+ * Pixel width of one candle body. On a band scale this is the bandwidth; on a
29
+ * linear/time scale it's derived from the smallest gap between adjacent points
30
+ * (falling back to a modest width when there's only one point).
31
+ */
32
+ function candleSlot(resolved: ResolvedPoint[], ctx: SeriesContext): number {
33
+ const {xScale, width} = ctx;
34
+ if (isBandScale(xScale)) {
35
+ return xScale.bandwidth();
36
+ }
37
+ const xs = resolved
38
+ .map(p => p.px)
39
+ .filter(x => Number.isFinite(x))
40
+ .sort((a, b) => a - b);
41
+ let minGap = Infinity;
42
+ for (let i = 1; i < xs.length; i++) {
43
+ const gap = xs[i] - xs[i - 1];
44
+ if (gap > 0 && gap < minGap) {
45
+ minGap = gap;
46
+ }
47
+ }
48
+ return Number.isFinite(minGap) ? minGap : Math.min(width, 40);
49
+ }
50
+
51
+ export function candlestick(options: CandlestickOptions): SeriesDef {
52
+ // Emitted core semantic tokens (data-viz --color-data-* tokens aren't CSS vars).
53
+ const upColor = options.upColor ?? 'var(--color-success)';
54
+ const downColor = options.downColor ?? 'var(--color-error)';
55
+
56
+ return {
57
+ type: 'candlestick',
58
+ key: `ohlc-${options.close}`,
59
+ dataKeys: [options.open, options.high, options.low, options.close],
60
+ layout: {},
61
+
62
+ resolve(ctx) {
63
+ const {data, xKey, xScale, yScale} = ctx;
64
+ const points: ResolvedPoint[] = [];
65
+ for (let i = 0; i < data.length; i++) {
66
+ const d = data[i];
67
+ const close = readNumber(d, options.close);
68
+ points.push({
69
+ px: xPixel(d, xKey, xScale),
70
+ py: yScale(close),
71
+ py0: yScale(0),
72
+ dataIndex: i,
73
+ });
74
+ }
75
+ return points;
76
+ },
77
+
78
+ render(resolved, ctx) {
79
+ const {data, yScale} = ctx;
80
+ const bodyWidth = Math.max(1, candleSlot(resolved, ctx) * 0.6);
81
+
82
+ return (
83
+ <g>
84
+ {resolved.map(p => {
85
+ const d = data[p.dataIndex];
86
+ const o = readNumber(d, options.open);
87
+ const c = readNumber(d, options.close);
88
+ const h = readNumber(d, options.high);
89
+ const l = readNumber(d, options.low);
90
+ // Drop candles missing any OHLC field (or an unplaceable x) — a gap
91
+ // reads more honestly than a full-height candle anchored at 0.
92
+ if (
93
+ !Number.isFinite(p.px) ||
94
+ !Number.isFinite(o) ||
95
+ !Number.isFinite(c) ||
96
+ !Number.isFinite(h) ||
97
+ !Number.isFinite(l)
98
+ ) {
99
+ return null;
100
+ }
101
+ const col = c >= o ? upColor : downColor;
102
+ const bodyTop = yScale(Math.max(o, c));
103
+ const bodyHeight = Math.max(1, Math.abs(yScale(o) - yScale(c)));
104
+ return (
105
+ <g key={p.dataIndex}>
106
+ <line
107
+ x1={p.px}
108
+ x2={p.px}
109
+ y1={yScale(h)}
110
+ y2={yScale(l)}
111
+ stroke={col}
112
+ strokeWidth={1}
113
+ />
114
+ <rect
115
+ x={p.px - bodyWidth / 2}
116
+ y={bodyTop}
117
+ width={bodyWidth}
118
+ height={bodyHeight}
119
+ fill={col}
120
+ />
121
+ </g>
122
+ );
123
+ })}
124
+ </g>
125
+ );
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,119 @@
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
+ * Points with missing / non-finite values are skipped rather than plotted at the
8
+ * baseline. Optional `dodge` spreads coincident points horizontally, ignoring
9
+ * non-finite ones so a single missing value can't collapse a whole dodge group.
10
+ */
11
+
12
+ import type {SeriesDef, ResolvedPoint, ColorAccessor} from '../types';
13
+ import {pointFill} from '../markColor';
14
+ import {xPixel} from '../utils';
15
+
16
+ export type {ColorAccessor};
17
+
18
+ export interface DotOptions {
19
+ color?: ColorAccessor;
20
+ radius?: number;
21
+ opacity?: number;
22
+ dodge?: boolean;
23
+ label?: string;
24
+ }
25
+
26
+ export function dot(dataKey: string, options: DotOptions = {}): SeriesDef {
27
+ const color = options.color;
28
+ const radius =
29
+ typeof options.radius === 'number' && Number.isFinite(options.radius)
30
+ ? Math.max(0, options.radius)
31
+ : 4;
32
+ const opacity =
33
+ typeof options.opacity === 'number' && Number.isFinite(options.opacity)
34
+ ? Math.min(1, Math.max(0, options.opacity))
35
+ : 0.8;
36
+ const dodge = options.dodge ?? false;
37
+
38
+ const seriesDef: SeriesDef = {
39
+ type: 'dot',
40
+ key: dataKey,
41
+ dataKeys: [dataKey],
42
+ color: typeof color === 'string' ? color : undefined,
43
+ label: options.label ?? dataKey,
44
+ layout: {},
45
+
46
+ resolve(ctx) {
47
+ const {data, xKey, xScale, yScale} = ctx;
48
+ const points: ResolvedPoint[] = [];
49
+ for (let i = 0; i < data.length; i++) {
50
+ const d = data[i];
51
+ const raw = d[dataKey];
52
+ const v = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN;
53
+ points.push({
54
+ px: xPixel(d, xKey, xScale),
55
+ py: yScale(v),
56
+ py0: yScale(0),
57
+ dataIndex: i,
58
+ });
59
+ }
60
+
61
+ if (dodge) {
62
+ // Spread points that land on the same x (e.g. dots over band categories)
63
+ // horizontally so they don't overlap, centered on the original x.
64
+ const groups = new Map<number, ResolvedPoint[]>();
65
+ for (const p of points) {
66
+ // Only points render() will actually draw (finite px AND py) join a
67
+ // dodge group; a missing y-value must not reserve a slot and knock the
68
+ // visible points off-center.
69
+ if (!Number.isFinite(p.px) || !Number.isFinite(p.py)) {
70
+ continue;
71
+ }
72
+ const key = Math.round(p.px);
73
+ const group = groups.get(key) ?? [];
74
+ group.push(p);
75
+ groups.set(key, group);
76
+ }
77
+ const spacing = radius * 2 + 1;
78
+ for (const group of groups.values()) {
79
+ if (group.length < 2) {
80
+ continue;
81
+ }
82
+ const mid = (group.length - 1) / 2;
83
+ group.forEach((p, idx) => {
84
+ p.px += (idx - mid) * spacing;
85
+ });
86
+ }
87
+ }
88
+
89
+ return points;
90
+ },
91
+
92
+ render(resolved, ctx) {
93
+ const {data} = ctx;
94
+ return (
95
+ <g>
96
+ {resolved.map(p => {
97
+ const d = data[p.dataIndex];
98
+ if (!d || !Number.isFinite(p.px) || !Number.isFinite(p.py)) {
99
+ return null;
100
+ }
101
+ const fill = pointFill(seriesDef, color, d, p.dataIndex);
102
+ return (
103
+ <circle
104
+ key={p.dataIndex}
105
+ cx={p.px}
106
+ cy={p.py}
107
+ r={radius}
108
+ fill={fill}
109
+ opacity={opacity}
110
+ />
111
+ );
112
+ })}
113
+ </g>
114
+ );
115
+ },
116
+ };
117
+
118
+ return seriesDef;
119
+ }