@gridui/chart 0.0.0-stage → 1.0.0
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/README.md +65 -2
- package/dist/bar-chart/bar-chart.d.ts +12 -0
- package/dist/bar-series/bar-series.d.ts +17 -0
- package/dist/chart-cursor/chart-cursor.d.ts +13 -0
- package/dist/chart-grid/chart-grid.d.ts +18 -0
- package/dist/chart-plot/chart-plot.d.ts +13 -0
- package/dist/chart-tooltip/chart-tooltip.d.ts +14 -0
- package/dist/chart-tooltip/layout.d.ts +31 -0
- package/dist/chart-value-labels/chart-value-labels.d.ts +17 -0
- package/dist/chart-value-labels/layout.d.ts +28 -0
- package/dist/chart-x-axis/chart-x-axis.d.ts +12 -0
- package/dist/chart-y-axis/chart-y-axis.d.ts +12 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1276 -0
- package/dist/index.js.map +7 -0
- package/dist/line-chart/line-chart.d.ts +12 -0
- package/dist/line-series/line-series.d.ts +16 -0
- package/dist/shared/axis.d.ts +13 -0
- package/dist/shared/chart-root.d.ts +14 -0
- package/dist/shared/context.d.ts +38 -0
- package/dist/shared/datum-mark.d.ts +13 -0
- package/dist/shared/model.d.ts +13 -0
- package/dist/shared/ticks.d.ts +14 -0
- package/dist/shared/types.d.ts +46 -0
- package/dist/shared/use-measured-layout.d.ts +12 -0
- package/dist/styles.css +37 -0
- package/docs/catalog.json +168 -0
- package/docs/reference/README.md +14 -0
- package/docs/reference/bar-chart.md +97 -0
- package/docs/reference/bar-series.md +61 -0
- package/docs/reference/chart-cursor.md +39 -0
- package/docs/reference/chart-grid.md +61 -0
- package/docs/reference/chart-plot.md +37 -0
- package/docs/reference/chart-tooltip.md +37 -0
- package/docs/reference/chart-value-labels.md +57 -0
- package/docs/reference/chart-xaxis.md +71 -0
- package/docs/reference/chart-yaxis.md +71 -0
- package/docs/reference/line-chart.md +97 -0
- package/docs/reference/line-series.md +47 -0
- package/package.json +50 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1276 @@
|
|
|
1
|
+
// src/shared/context.ts
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
var ChartContext = createContext(null);
|
|
4
|
+
var useChart = () => {
|
|
5
|
+
const state = useContext(ChartContext);
|
|
6
|
+
if (!state) {
|
|
7
|
+
throw new Error("Chart parts require a LineChart or BarChart.");
|
|
8
|
+
}
|
|
9
|
+
return state;
|
|
10
|
+
};
|
|
11
|
+
var PlotContext = createContext(null);
|
|
12
|
+
var usePlot = () => {
|
|
13
|
+
const state = useContext(PlotContext);
|
|
14
|
+
if (!state) {
|
|
15
|
+
throw new Error("Chart drawing parts require a ChartPlot.");
|
|
16
|
+
}
|
|
17
|
+
return state;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/shared/model.ts
|
|
21
|
+
var numericX = (value, scale) => {
|
|
22
|
+
const parsed = scale === "time" && typeof value === "string" ? Date.parse(value) : value;
|
|
23
|
+
const number = value instanceof Date ? value.getTime() : parsed;
|
|
24
|
+
if (typeof number !== "number" || !Number.isFinite(number)) {
|
|
25
|
+
throw new RangeError("Continuous chart coordinates must be finite numbers or valid dates.");
|
|
26
|
+
}
|
|
27
|
+
return number;
|
|
28
|
+
};
|
|
29
|
+
var prepareData = (data, scale) => {
|
|
30
|
+
const ids = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const datum of data) {
|
|
32
|
+
if (!datum.id || ids.has(datum.id)) {
|
|
33
|
+
throw new Error("Chart datum IDs must be nonempty and unique.");
|
|
34
|
+
}
|
|
35
|
+
ids.add(datum.id);
|
|
36
|
+
if (datum.y !== null && !Number.isFinite(datum.y)) {
|
|
37
|
+
throw new RangeError("Chart values must be finite numbers or null.");
|
|
38
|
+
}
|
|
39
|
+
if (scale !== "category") {
|
|
40
|
+
numericX(datum.x, scale);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return scale === "category" ? data : [...data].sort((a, b) => {
|
|
44
|
+
return numericX(a.x, scale) - numericX(b.x, scale);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
var valueDomain = (data, explicit) => {
|
|
48
|
+
let min = 0;
|
|
49
|
+
let max = 0;
|
|
50
|
+
for (const datum of data) {
|
|
51
|
+
if (datum.y !== null) {
|
|
52
|
+
min = Math.min(min, datum.y);
|
|
53
|
+
max = Math.max(max, datum.y);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (explicit) {
|
|
57
|
+
if (!explicit.every(Number.isFinite) || explicit[0] >= explicit[1] || explicit[0] > min || explicit[1] < max) {
|
|
58
|
+
throw new RangeError("Chart domains must be increasing, finite, and include zero and all values.");
|
|
59
|
+
}
|
|
60
|
+
return explicit;
|
|
61
|
+
}
|
|
62
|
+
return min === max ? [
|
|
63
|
+
0,
|
|
64
|
+
1
|
|
65
|
+
] : [
|
|
66
|
+
min,
|
|
67
|
+
max
|
|
68
|
+
];
|
|
69
|
+
};
|
|
70
|
+
var valueTicks = (domain) => {
|
|
71
|
+
return [
|
|
72
|
+
.../* @__PURE__ */ new Set([
|
|
73
|
+
0,
|
|
74
|
+
...Array.from({ length: 5 }, (_, index) => {
|
|
75
|
+
return domain[0] + (domain[1] - domain[0]) * index / 4;
|
|
76
|
+
})
|
|
77
|
+
])
|
|
78
|
+
].sort((a, b) => {
|
|
79
|
+
return a - b;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
var linePath = (data, x, y) => {
|
|
83
|
+
let connected = false;
|
|
84
|
+
return data.map((datum) => {
|
|
85
|
+
if (datum.y === null) {
|
|
86
|
+
connected = false;
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
const command = connected ? "L" : "M";
|
|
90
|
+
connected = true;
|
|
91
|
+
return `${command}${x(datum)},${y(datum.y)}`;
|
|
92
|
+
}).join(" ");
|
|
93
|
+
};
|
|
94
|
+
var datumLabel = (datum) => {
|
|
95
|
+
const x = datum.x instanceof Date ? datum.x.toISOString() : String(datum.x);
|
|
96
|
+
return `${x}: ${datum.y === null ? "\u2014" : String(datum.y)}`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/shared/chart-root.tsx
|
|
100
|
+
import { cn } from "@gridui/style-utils";
|
|
101
|
+
import { useEffect, useMemo, useState } from "react";
|
|
102
|
+
import { jsx } from "react/jsx-runtime";
|
|
103
|
+
var ChartRoot = (props) => {
|
|
104
|
+
const {
|
|
105
|
+
label,
|
|
106
|
+
data,
|
|
107
|
+
children,
|
|
108
|
+
height = 300,
|
|
109
|
+
yDomain,
|
|
110
|
+
selectedId,
|
|
111
|
+
defaultSelectedId,
|
|
112
|
+
onSelectionChange,
|
|
113
|
+
formatDatum = datumLabel,
|
|
114
|
+
kind,
|
|
115
|
+
horizontal,
|
|
116
|
+
scale,
|
|
117
|
+
className,
|
|
118
|
+
...rest
|
|
119
|
+
} = props;
|
|
120
|
+
const [
|
|
121
|
+
localSelection,
|
|
122
|
+
setLocalSelection
|
|
123
|
+
] = useState(defaultSelectedId ?? null);
|
|
124
|
+
const [
|
|
125
|
+
hoveredId,
|
|
126
|
+
setHoveredId
|
|
127
|
+
] = useState(null);
|
|
128
|
+
const [
|
|
129
|
+
focusedId,
|
|
130
|
+
setFocusedId
|
|
131
|
+
] = useState(null);
|
|
132
|
+
const { ordered, domain, availableIds } = useMemo(() => {
|
|
133
|
+
const prepared = prepareData(data, scale);
|
|
134
|
+
return {
|
|
135
|
+
ordered: prepared,
|
|
136
|
+
domain: valueDomain(prepared, yDomain),
|
|
137
|
+
availableIds: new Set(prepared.filter((datum) => {
|
|
138
|
+
return datum.y !== null;
|
|
139
|
+
}).map((datum) => {
|
|
140
|
+
return datum.id;
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
}, [
|
|
144
|
+
data,
|
|
145
|
+
scale,
|
|
146
|
+
yDomain
|
|
147
|
+
]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
setHoveredId((current) => {
|
|
150
|
+
return current !== null && availableIds.has(current) ? current : null;
|
|
151
|
+
});
|
|
152
|
+
setFocusedId((current) => {
|
|
153
|
+
return current !== null && availableIds.has(current) ? current : null;
|
|
154
|
+
});
|
|
155
|
+
}, [availableIds]);
|
|
156
|
+
const activeId = [
|
|
157
|
+
hoveredId,
|
|
158
|
+
focusedId
|
|
159
|
+
].find((id) => {
|
|
160
|
+
return id !== null && availableIds.has(id);
|
|
161
|
+
}) ?? null;
|
|
162
|
+
const activate = (id, source) => {
|
|
163
|
+
if (source === "pointer") {
|
|
164
|
+
setHoveredId(id);
|
|
165
|
+
} else {
|
|
166
|
+
setFocusedId(id);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const dismiss = () => {
|
|
170
|
+
setHoveredId(null);
|
|
171
|
+
setFocusedId(null);
|
|
172
|
+
};
|
|
173
|
+
if (!Number.isFinite(height) || height < 160) {
|
|
174
|
+
throw new RangeError("Chart height must be finite and at least 160 pixels.");
|
|
175
|
+
}
|
|
176
|
+
const candidate = selectedId === void 0 ? localSelection : selectedId;
|
|
177
|
+
const selection = candidate !== null && availableIds.has(candidate) ? candidate : null;
|
|
178
|
+
const select = (id) => {
|
|
179
|
+
if (selectedId === void 0) {
|
|
180
|
+
setLocalSelection(id);
|
|
181
|
+
}
|
|
182
|
+
onSelectionChange?.(id);
|
|
183
|
+
};
|
|
184
|
+
return /* @__PURE__ */ jsx(
|
|
185
|
+
"div",
|
|
186
|
+
{
|
|
187
|
+
...rest,
|
|
188
|
+
role: "group",
|
|
189
|
+
"aria-label": label,
|
|
190
|
+
className: cn("gridui-chart", className),
|
|
191
|
+
children: /* @__PURE__ */ jsx(
|
|
192
|
+
ChartContext,
|
|
193
|
+
{
|
|
194
|
+
value: {
|
|
195
|
+
data: ordered,
|
|
196
|
+
kind,
|
|
197
|
+
horizontal,
|
|
198
|
+
scale,
|
|
199
|
+
domain,
|
|
200
|
+
label,
|
|
201
|
+
height,
|
|
202
|
+
selectedId: selection,
|
|
203
|
+
activeId,
|
|
204
|
+
interactive: onSelectionChange !== void 0 || defaultSelectedId !== void 0,
|
|
205
|
+
select,
|
|
206
|
+
activate,
|
|
207
|
+
dismiss,
|
|
208
|
+
formatDatum
|
|
209
|
+
},
|
|
210
|
+
children
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/bar-chart/bar-chart.tsx
|
|
218
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
219
|
+
var BarChart = (props) => {
|
|
220
|
+
const { orientation = "vertical", ...restProps } = props;
|
|
221
|
+
return /* @__PURE__ */ jsx2(
|
|
222
|
+
ChartRoot,
|
|
223
|
+
{
|
|
224
|
+
...restProps,
|
|
225
|
+
kind: "bar",
|
|
226
|
+
horizontal: orientation === "horizontal",
|
|
227
|
+
scale: "category"
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/shared/datum-mark.tsx
|
|
233
|
+
import { isComposingKeyEvent } from "@gridui/interaction";
|
|
234
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
235
|
+
var DatumMark = (props) => {
|
|
236
|
+
const { datum, children } = props;
|
|
237
|
+
const chart = useChart();
|
|
238
|
+
const selected = chart.selectedId === datum.id;
|
|
239
|
+
return /* @__PURE__ */ jsx3(
|
|
240
|
+
"g",
|
|
241
|
+
{
|
|
242
|
+
className: "gridui-chart-mark",
|
|
243
|
+
"data-selected": selected || void 0,
|
|
244
|
+
role: chart.interactive ? "button" : void 0,
|
|
245
|
+
tabIndex: chart.interactive ? 0 : void 0,
|
|
246
|
+
"aria-label": chart.formatDatum(datum),
|
|
247
|
+
"aria-pressed": chart.interactive ? selected : void 0,
|
|
248
|
+
onPointerEnter: () => {
|
|
249
|
+
return void chart.activate(datum.id, "pointer");
|
|
250
|
+
},
|
|
251
|
+
onPointerLeave: () => {
|
|
252
|
+
return void chart.activate(null, "pointer");
|
|
253
|
+
},
|
|
254
|
+
onFocus: () => {
|
|
255
|
+
return void chart.activate(datum.id, "focus");
|
|
256
|
+
},
|
|
257
|
+
onBlur: () => {
|
|
258
|
+
return void chart.activate(null, "focus");
|
|
259
|
+
},
|
|
260
|
+
onClick: chart.interactive ? () => {
|
|
261
|
+
return void chart.select(selected ? null : datum.id);
|
|
262
|
+
} : void 0,
|
|
263
|
+
onKeyDown: chart.interactive ? (event) => {
|
|
264
|
+
if (isComposingKeyEvent(event.nativeEvent)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
if (!event.repeat) {
|
|
270
|
+
chart.select(selected ? null : datum.id);
|
|
271
|
+
}
|
|
272
|
+
} else if (event.key === "Escape") {
|
|
273
|
+
chart.dismiss();
|
|
274
|
+
chart.select(null);
|
|
275
|
+
}
|
|
276
|
+
} : void 0,
|
|
277
|
+
children
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/bar-series/bar-series.tsx
|
|
283
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
284
|
+
var BarSeries = (props) => {
|
|
285
|
+
const { color = "var(--gridui-chart-series)", selectedColor = "var(--gridui-chart-selected)", width, widthRatio = 0.7, gap = 4, opacity = 1, inactiveOpacity = 0.45 } = props;
|
|
286
|
+
const chart = useChart();
|
|
287
|
+
const plot = usePlot();
|
|
288
|
+
if (chart.kind !== "bar") {
|
|
289
|
+
throw new Error("BarSeries requires BarChart.");
|
|
290
|
+
}
|
|
291
|
+
const invalidWidth = width !== void 0 && (!Number.isFinite(width) || width <= 0);
|
|
292
|
+
if (!Number.isFinite(widthRatio) || widthRatio <= 0 || widthRatio > 1 || !Number.isFinite(gap) || gap < 0 || invalidWidth) {
|
|
293
|
+
throw new RangeError("Bar width must be positive, its ratio at most one, and gap nonnegative.");
|
|
294
|
+
}
|
|
295
|
+
const thickness = Math.min(width ?? plot.band * widthRatio, Math.max(Math.min(1, plot.band), plot.band - gap));
|
|
296
|
+
const baseline = plot.value(0);
|
|
297
|
+
return /* @__PURE__ */ jsx4("g", { children: chart.data.map((datum) => {
|
|
298
|
+
if (datum.y === null) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const at = plot.category(datum);
|
|
302
|
+
const end = plot.value(datum.y);
|
|
303
|
+
const selected = chart.selectedId === datum.id;
|
|
304
|
+
return /* @__PURE__ */ jsxs(
|
|
305
|
+
DatumMark,
|
|
306
|
+
{
|
|
307
|
+
datum,
|
|
308
|
+
children: [
|
|
309
|
+
/* @__PURE__ */ jsx4(
|
|
310
|
+
"rect",
|
|
311
|
+
{
|
|
312
|
+
className: "gridui-chart-hit",
|
|
313
|
+
x: chart.horizontal ? plot.left : at - plot.band / 2,
|
|
314
|
+
y: chart.horizontal ? at - plot.band / 2 : plot.top,
|
|
315
|
+
width: chart.horizontal ? plot.right - plot.left : plot.band,
|
|
316
|
+
height: chart.horizontal ? plot.band : plot.bottom - plot.top,
|
|
317
|
+
fill: "transparent"
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
/* @__PURE__ */ jsx4(
|
|
321
|
+
"rect",
|
|
322
|
+
{
|
|
323
|
+
x: chart.horizontal ? Math.min(end, baseline) : at - thickness / 2,
|
|
324
|
+
y: chart.horizontal ? at - thickness / 2 : Math.min(end, baseline),
|
|
325
|
+
width: chart.horizontal ? Math.abs(end - baseline) : thickness,
|
|
326
|
+
height: chart.horizontal ? thickness : Math.abs(end - baseline),
|
|
327
|
+
fill: selected ? selectedColor : color,
|
|
328
|
+
opacity: chart.selectedId && !selected ? inactiveOpacity : opacity,
|
|
329
|
+
pointerEvents: "none"
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
datum.id
|
|
335
|
+
);
|
|
336
|
+
}) });
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/chart-cursor/chart-cursor.tsx
|
|
340
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
341
|
+
var ChartCursor = (props) => {
|
|
342
|
+
const { color = "var(--gridui-chart-axis)", strokeWidth = 1, dashArray = "3 3" } = props;
|
|
343
|
+
const chart = useChart();
|
|
344
|
+
const plot = usePlot();
|
|
345
|
+
const datum = chart.data.find((item) => {
|
|
346
|
+
return item.id === (chart.activeId ?? chart.selectedId);
|
|
347
|
+
});
|
|
348
|
+
if (!datum || datum.y === null) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const at = plot.category(datum);
|
|
352
|
+
return /* @__PURE__ */ jsx5(
|
|
353
|
+
"line",
|
|
354
|
+
{
|
|
355
|
+
"aria-hidden": "true",
|
|
356
|
+
pointerEvents: "none",
|
|
357
|
+
x1: chart.horizontal ? plot.left : at,
|
|
358
|
+
x2: chart.horizontal ? plot.right : at,
|
|
359
|
+
y1: chart.horizontal ? at : plot.top,
|
|
360
|
+
y2: chart.horizontal ? at : plot.bottom,
|
|
361
|
+
stroke: color,
|
|
362
|
+
strokeWidth,
|
|
363
|
+
strokeDasharray: dashArray
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/shared/ticks.ts
|
|
369
|
+
var axisTicks = (chart, plot, axis, options) => {
|
|
370
|
+
const { tickCount, tickValues } = options;
|
|
371
|
+
if (tickCount !== void 0 && (!Number.isSafeInteger(tickCount) || tickCount < 2)) {
|
|
372
|
+
throw new RangeError("Chart tickCount must be an integer of at least two.");
|
|
373
|
+
}
|
|
374
|
+
const dataAxis = axis === "x" !== chart.horizontal;
|
|
375
|
+
const first = chart.data[0];
|
|
376
|
+
const last = chart.data.at(-1);
|
|
377
|
+
if (dataAxis && (!first || !last)) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
if (dataAxis && chart.scale === "category") {
|
|
381
|
+
const requested = tickValues && new Set(tickValues.map((value) => {
|
|
382
|
+
return value instanceof Date ? value.getTime() : value;
|
|
383
|
+
}));
|
|
384
|
+
const candidates = chart.data.filter((datum) => {
|
|
385
|
+
return !requested || requested.has(datum.x instanceof Date ? datum.x.getTime() : datum.x);
|
|
386
|
+
});
|
|
387
|
+
const indices = tickCount !== void 0 && !requested && candidates.length > tickCount ? new Set(Array.from({ length: tickCount }, (_, index) => {
|
|
388
|
+
return Math.round(index * (candidates.length - 1) / (tickCount - 1));
|
|
389
|
+
})) : null;
|
|
390
|
+
return candidates.flatMap((datum, index) => {
|
|
391
|
+
return indices && !indices.has(index) ? [] : [
|
|
392
|
+
{
|
|
393
|
+
key: datum.id,
|
|
394
|
+
value: datum.x,
|
|
395
|
+
at: plot.category(datum)
|
|
396
|
+
}
|
|
397
|
+
];
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const low = dataAxis && first ? numericX(first.x, chart.scale) : chart.domain[0];
|
|
401
|
+
const high = dataAxis && last ? numericX(last.x, chart.scale) : chart.domain[1];
|
|
402
|
+
const count = tickCount ?? 5;
|
|
403
|
+
const generated = low === high ? [low] : Array.from({ length: count }, (_, index) => {
|
|
404
|
+
return low + (high - low) * index / (count - 1);
|
|
405
|
+
});
|
|
406
|
+
const defaults = dataAxis ? chart.data.map((datum) => {
|
|
407
|
+
return datum.x;
|
|
408
|
+
}) : valueTicks(chart.domain);
|
|
409
|
+
const values = tickValues ?? (tickCount !== void 0 ? generated : defaults);
|
|
410
|
+
const seen = /* @__PURE__ */ new Set();
|
|
411
|
+
return values.flatMap((value) => {
|
|
412
|
+
const number = numericX(value, dataAxis ? chart.scale : "linear");
|
|
413
|
+
if (number < low || number > high || seen.has(number)) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
seen.add(number);
|
|
417
|
+
const formattedValue = dataAxis && chart.scale === "time" ? new Date(number) : number;
|
|
418
|
+
return [
|
|
419
|
+
{
|
|
420
|
+
key: String(number),
|
|
421
|
+
value: formattedValue,
|
|
422
|
+
at: dataAxis ? plot.category({
|
|
423
|
+
id: "",
|
|
424
|
+
x: formattedValue,
|
|
425
|
+
y: null
|
|
426
|
+
}) : plot.value(number)
|
|
427
|
+
}
|
|
428
|
+
];
|
|
429
|
+
}).sort((a, b) => {
|
|
430
|
+
return a.at - b.at;
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/chart-grid/chart-grid.tsx
|
|
435
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
436
|
+
var ChartGrid = (props) => {
|
|
437
|
+
const { xTicks = {}, yTicks = {}, horizontal = true, vertical = false, color = "var(--gridui-chart-grid)", strokeWidth = 1, dashArray } = props;
|
|
438
|
+
const chart = useChart();
|
|
439
|
+
const plot = usePlot();
|
|
440
|
+
const x = axisTicks(chart, plot, "x", xTicks);
|
|
441
|
+
const y = axisTicks(chart, plot, "y", yTicks);
|
|
442
|
+
return /* @__PURE__ */ jsxs2(
|
|
443
|
+
"g",
|
|
444
|
+
{
|
|
445
|
+
className: "gridui-chart-grid",
|
|
446
|
+
"aria-hidden": "true",
|
|
447
|
+
pointerEvents: "none",
|
|
448
|
+
stroke: color,
|
|
449
|
+
strokeWidth,
|
|
450
|
+
strokeDasharray: dashArray,
|
|
451
|
+
children: [
|
|
452
|
+
horizontal && y.map(({ at }, index) => {
|
|
453
|
+
return /* @__PURE__ */ jsx6(
|
|
454
|
+
"line",
|
|
455
|
+
{
|
|
456
|
+
x1: plot.left,
|
|
457
|
+
x2: plot.right,
|
|
458
|
+
y1: at,
|
|
459
|
+
y2: at
|
|
460
|
+
},
|
|
461
|
+
`h-${index}`
|
|
462
|
+
);
|
|
463
|
+
}),
|
|
464
|
+
vertical && x.map(({ at }, index) => {
|
|
465
|
+
return /* @__PURE__ */ jsx6(
|
|
466
|
+
"line",
|
|
467
|
+
{
|
|
468
|
+
x1: at,
|
|
469
|
+
x2: at,
|
|
470
|
+
y1: plot.top,
|
|
471
|
+
y2: plot.bottom
|
|
472
|
+
},
|
|
473
|
+
`v-${index}`
|
|
474
|
+
);
|
|
475
|
+
})
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/chart-plot/chart-plot.tsx
|
|
482
|
+
import { useEffect as useEffect2, useId, useMemo as useMemo2, useRef, useState as useState2 } from "react";
|
|
483
|
+
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
484
|
+
var ChartPlot = (props) => {
|
|
485
|
+
const { children, background = "var(--gridui-chart-background)", padding } = props;
|
|
486
|
+
const chart = useChart();
|
|
487
|
+
const ref = useRef(null);
|
|
488
|
+
const [
|
|
489
|
+
width,
|
|
490
|
+
setWidth
|
|
491
|
+
] = useState2(640);
|
|
492
|
+
const descriptionId = useId();
|
|
493
|
+
useEffect2(() => {
|
|
494
|
+
const element = ref.current;
|
|
495
|
+
if (!element) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const observer = new ResizeObserver((entries) => {
|
|
499
|
+
const measured = entries[0]?.contentRect.width;
|
|
500
|
+
if (measured && measured > 0) {
|
|
501
|
+
setWidth(measured);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
observer.observe(element);
|
|
505
|
+
return () => {
|
|
506
|
+
return void observer.disconnect();
|
|
507
|
+
};
|
|
508
|
+
}, []);
|
|
509
|
+
const inset = {
|
|
510
|
+
left: padding?.left ?? (chart.horizontal ? 88 : 56),
|
|
511
|
+
right: padding?.right ?? 24,
|
|
512
|
+
top: padding?.top ?? 28,
|
|
513
|
+
bottom: padding?.bottom ?? 48
|
|
514
|
+
};
|
|
515
|
+
if (Object.values(inset).some((value) => {
|
|
516
|
+
return !Number.isFinite(value) || value < 0;
|
|
517
|
+
})) {
|
|
518
|
+
throw new RangeError("Chart padding must be finite and nonnegative.");
|
|
519
|
+
}
|
|
520
|
+
const left = Math.min(width, inset.left);
|
|
521
|
+
const right = Math.max(left, width - inset.right);
|
|
522
|
+
const top = Math.min(chart.height, inset.top);
|
|
523
|
+
const bottom = Math.max(top, chart.height - inset.bottom);
|
|
524
|
+
const drawable = right > left && bottom > top;
|
|
525
|
+
const plot = useMemo2(() => {
|
|
526
|
+
const extent = Math.max(1, chart.horizontal ? bottom - top : right - left);
|
|
527
|
+
const start = chart.horizontal ? top : left;
|
|
528
|
+
const count = chart.data.length;
|
|
529
|
+
const band = extent / Math.max(1, count);
|
|
530
|
+
const first = chart.data[0];
|
|
531
|
+
const last = chart.data[count - 1];
|
|
532
|
+
const low = first && chart.scale !== "category" ? numericX(first.x, chart.scale) : 0;
|
|
533
|
+
const high = last && chart.scale !== "category" ? numericX(last.x, chart.scale) : 0;
|
|
534
|
+
const indices = new Map(chart.data.map((datum, index) => {
|
|
535
|
+
return [
|
|
536
|
+
datum.id,
|
|
537
|
+
index
|
|
538
|
+
];
|
|
539
|
+
}));
|
|
540
|
+
const category = (datum) => {
|
|
541
|
+
const index = indices.get(datum.id) ?? 0;
|
|
542
|
+
if (chart.scale !== "category") {
|
|
543
|
+
return start + (high === low ? extent / 2 : (numericX(datum.x, chart.scale) - low) / (high - low) * extent);
|
|
544
|
+
}
|
|
545
|
+
const linePosition = count < 2 ? extent / 2 : index * extent / (count - 1);
|
|
546
|
+
return start + (chart.kind === "bar" ? (index + 0.5) * band : linePosition);
|
|
547
|
+
};
|
|
548
|
+
const value = (number) => {
|
|
549
|
+
const ratio = (number - chart.domain[0]) / (chart.domain[1] - chart.domain[0]);
|
|
550
|
+
return chart.horizontal ? left + ratio * Math.max(1, right - left) : bottom - ratio * Math.max(1, bottom - top);
|
|
551
|
+
};
|
|
552
|
+
return {
|
|
553
|
+
width,
|
|
554
|
+
height: chart.height,
|
|
555
|
+
left,
|
|
556
|
+
right,
|
|
557
|
+
top,
|
|
558
|
+
bottom,
|
|
559
|
+
band,
|
|
560
|
+
category,
|
|
561
|
+
value
|
|
562
|
+
};
|
|
563
|
+
}, [
|
|
564
|
+
chart.data,
|
|
565
|
+
chart.domain,
|
|
566
|
+
chart.height,
|
|
567
|
+
chart.horizontal,
|
|
568
|
+
chart.kind,
|
|
569
|
+
chart.scale,
|
|
570
|
+
bottom,
|
|
571
|
+
left,
|
|
572
|
+
right,
|
|
573
|
+
top,
|
|
574
|
+
width
|
|
575
|
+
]);
|
|
576
|
+
return /* @__PURE__ */ jsx7(
|
|
577
|
+
"div",
|
|
578
|
+
{
|
|
579
|
+
ref,
|
|
580
|
+
className: "gridui-chart-plot",
|
|
581
|
+
children: /* @__PURE__ */ jsxs3(
|
|
582
|
+
"svg",
|
|
583
|
+
{
|
|
584
|
+
width: "100%",
|
|
585
|
+
height: chart.height,
|
|
586
|
+
viewBox: `0 0 ${width} ${chart.height}`,
|
|
587
|
+
role: chart.interactive ? "group" : "img",
|
|
588
|
+
"aria-label": chart.label,
|
|
589
|
+
"aria-describedby": descriptionId,
|
|
590
|
+
children: [
|
|
591
|
+
/* @__PURE__ */ jsx7(
|
|
592
|
+
"desc",
|
|
593
|
+
{
|
|
594
|
+
id: descriptionId,
|
|
595
|
+
children: chart.data.map(chart.formatDatum).join("; ")
|
|
596
|
+
}
|
|
597
|
+
),
|
|
598
|
+
/* @__PURE__ */ jsx7(
|
|
599
|
+
"rect",
|
|
600
|
+
{
|
|
601
|
+
x: left,
|
|
602
|
+
y: top,
|
|
603
|
+
width: Math.max(0, right - left),
|
|
604
|
+
height: Math.max(0, bottom - top),
|
|
605
|
+
fill: background,
|
|
606
|
+
pointerEvents: "none"
|
|
607
|
+
}
|
|
608
|
+
),
|
|
609
|
+
/* @__PURE__ */ jsx7(
|
|
610
|
+
PlotContext,
|
|
611
|
+
{
|
|
612
|
+
value: plot,
|
|
613
|
+
children: drawable ? children : null
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/shared/use-measured-layout.ts
|
|
624
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
|
|
625
|
+
var useMeasuredLayout = (calculate) => {
|
|
626
|
+
const ref = useRef2(null);
|
|
627
|
+
const [
|
|
628
|
+
result,
|
|
629
|
+
setResult
|
|
630
|
+
] = useState3(null);
|
|
631
|
+
useLayoutEffect(() => {
|
|
632
|
+
const node = ref.current;
|
|
633
|
+
if (!node) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
let disposed = false;
|
|
637
|
+
const update = () => {
|
|
638
|
+
if (disposed) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const cache = /* @__PURE__ */ new Map();
|
|
642
|
+
const value = calculate((text) => {
|
|
643
|
+
const cached = cache.get(text);
|
|
644
|
+
if (cached) {
|
|
645
|
+
return cached;
|
|
646
|
+
}
|
|
647
|
+
node.textContent = text;
|
|
648
|
+
const box = node.getBBox();
|
|
649
|
+
const bounds = {
|
|
650
|
+
x: box.x,
|
|
651
|
+
y: box.y,
|
|
652
|
+
width: box.width,
|
|
653
|
+
height: box.height
|
|
654
|
+
};
|
|
655
|
+
cache.set(text, bounds);
|
|
656
|
+
return bounds;
|
|
657
|
+
});
|
|
658
|
+
node.textContent = "M";
|
|
659
|
+
setResult({
|
|
660
|
+
calculate,
|
|
661
|
+
value
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
update();
|
|
665
|
+
const observer = new ResizeObserver(update);
|
|
666
|
+
observer.observe(node);
|
|
667
|
+
const fonts = node.ownerDocument.fonts;
|
|
668
|
+
void fonts.ready.then(update);
|
|
669
|
+
fonts.addEventListener("loadingdone", update);
|
|
670
|
+
return () => {
|
|
671
|
+
disposed = true;
|
|
672
|
+
observer.disconnect();
|
|
673
|
+
fonts.removeEventListener("loadingdone", update);
|
|
674
|
+
};
|
|
675
|
+
}, [calculate]);
|
|
676
|
+
return {
|
|
677
|
+
ref,
|
|
678
|
+
layout: result?.calculate === calculate ? result.value : void 0
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/chart-tooltip/layout.ts
|
|
683
|
+
var PLOT_INSET = 4;
|
|
684
|
+
var POINT_GAP = 12;
|
|
685
|
+
var PADDING_INLINE = 10;
|
|
686
|
+
var PADDING_TOP = 8;
|
|
687
|
+
var PADDING_BOTTOM = 4;
|
|
688
|
+
var LINE_GAP = 4;
|
|
689
|
+
var MIN_LINE_HEIGHT = 18;
|
|
690
|
+
var fittingGraphemeCount = (segments, start, availableWidth, measure) => {
|
|
691
|
+
let low = 1;
|
|
692
|
+
let high = segments.length - start;
|
|
693
|
+
while (low < high) {
|
|
694
|
+
const count = Math.ceil((low + high) / 2);
|
|
695
|
+
if (measure(segments.slice(start, start + count).join("")).width <= availableWidth) {
|
|
696
|
+
low = count;
|
|
697
|
+
} else {
|
|
698
|
+
high = count - 1;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return low;
|
|
702
|
+
};
|
|
703
|
+
var wrapTooltipText = (text, availableWidth, measure) => {
|
|
704
|
+
const lines = [];
|
|
705
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
706
|
+
for (const paragraph of text.split("\n")) {
|
|
707
|
+
const segments = Array.from(segmenter.segment(paragraph), (part) => {
|
|
708
|
+
return part.segment;
|
|
709
|
+
});
|
|
710
|
+
if (!segments.length) {
|
|
711
|
+
lines.push("");
|
|
712
|
+
}
|
|
713
|
+
let start = 0;
|
|
714
|
+
while (start < segments.length) {
|
|
715
|
+
let count = fittingGraphemeCount(segments, start, availableWidth, measure);
|
|
716
|
+
if (start + count < segments.length) {
|
|
717
|
+
const wordBreak = segments.slice(start, start + count).reduce((last, segment, index) => {
|
|
718
|
+
return /\s/u.test(segment) ? index : last;
|
|
719
|
+
}, -1);
|
|
720
|
+
if (wordBreak > 0) {
|
|
721
|
+
count = wordBreak + 1;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
lines.push(segments.slice(start, start + count).join(""));
|
|
725
|
+
start += count;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return lines;
|
|
729
|
+
};
|
|
730
|
+
var calculateTooltipLayout = (text, plotWidth, measure) => {
|
|
731
|
+
if (text === null) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const availableWidth = Math.max(1, plotWidth - 2 * (PLOT_INSET + PADDING_INLINE));
|
|
735
|
+
const lines = wrapTooltipText(text, availableWidth, measure);
|
|
736
|
+
const bounds = lines.map(measure);
|
|
737
|
+
const lineHeight = Math.max(MIN_LINE_HEIGHT, ...bounds.map((box) => {
|
|
738
|
+
return box.height + LINE_GAP;
|
|
739
|
+
}));
|
|
740
|
+
return {
|
|
741
|
+
lines: lines.map((line, index) => {
|
|
742
|
+
const box = bounds[index];
|
|
743
|
+
return {
|
|
744
|
+
text: line,
|
|
745
|
+
x: PADDING_INLINE - (box?.x ?? 0),
|
|
746
|
+
y: PADDING_TOP + index * lineHeight - (box?.y ?? 0)
|
|
747
|
+
};
|
|
748
|
+
}),
|
|
749
|
+
width: Math.max(PADDING_INLINE * 2, ...bounds.map((box) => {
|
|
750
|
+
return box.width + PADDING_INLINE * 2;
|
|
751
|
+
})),
|
|
752
|
+
height: lines.length * lineHeight + PADDING_TOP + PADDING_BOTTOM
|
|
753
|
+
};
|
|
754
|
+
};
|
|
755
|
+
var calculateTooltipPosition = (options) => {
|
|
756
|
+
const { datum, horizontal, plot, width, height } = options;
|
|
757
|
+
let pointX = 0;
|
|
758
|
+
let pointY = 0;
|
|
759
|
+
if (datum && datum.y !== null) {
|
|
760
|
+
if (horizontal) {
|
|
761
|
+
pointX = plot.value(datum.y);
|
|
762
|
+
pointY = plot.category(datum);
|
|
763
|
+
} else {
|
|
764
|
+
pointX = plot.category(datum);
|
|
765
|
+
pointY = plot.value(datum.y);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
x: Math.max(PLOT_INSET, Math.min(plot.width - width - PLOT_INSET, pointX - width / 2)),
|
|
770
|
+
y: Math.max(PLOT_INSET, Math.min(plot.height - height - PLOT_INSET, pointY - height - POINT_GAP))
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/chart-tooltip/chart-tooltip.tsx
|
|
775
|
+
import { useCallback } from "react";
|
|
776
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
777
|
+
var ChartTooltip = (props) => {
|
|
778
|
+
const { format, background = "var(--gridui-background)", color = "var(--gridui-foreground)" } = props;
|
|
779
|
+
const chart = useChart();
|
|
780
|
+
const plot = usePlot();
|
|
781
|
+
const datum = chart.data.find((item) => {
|
|
782
|
+
return item.id === chart.activeId;
|
|
783
|
+
});
|
|
784
|
+
const text = datum && datum.y !== null ? (format ?? chart.formatDatum)(datum) : null;
|
|
785
|
+
const calculate = useCallback((measure) => {
|
|
786
|
+
return calculateTooltipLayout(text, plot.width, measure);
|
|
787
|
+
}, [
|
|
788
|
+
text,
|
|
789
|
+
plot.width
|
|
790
|
+
]);
|
|
791
|
+
const { ref, layout } = useMeasuredLayout(calculate);
|
|
792
|
+
const { x, y } = calculateTooltipPosition({
|
|
793
|
+
datum,
|
|
794
|
+
horizontal: chart.horizontal,
|
|
795
|
+
plot,
|
|
796
|
+
width: layout?.width ?? 0,
|
|
797
|
+
height: layout?.height ?? 0
|
|
798
|
+
});
|
|
799
|
+
return /* @__PURE__ */ jsxs4(
|
|
800
|
+
"g",
|
|
801
|
+
{
|
|
802
|
+
className: text === null ? void 0 : "gridui-chart-tooltip",
|
|
803
|
+
"aria-hidden": "true",
|
|
804
|
+
pointerEvents: "none",
|
|
805
|
+
fontSize: 12,
|
|
806
|
+
children: [
|
|
807
|
+
/* @__PURE__ */ jsx8(
|
|
808
|
+
"text",
|
|
809
|
+
{
|
|
810
|
+
ref,
|
|
811
|
+
visibility: "hidden"
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
layout && /* @__PURE__ */ jsxs4(
|
|
815
|
+
"g",
|
|
816
|
+
{
|
|
817
|
+
transform: `translate(${x} ${y})`,
|
|
818
|
+
children: [
|
|
819
|
+
/* @__PURE__ */ jsx8(
|
|
820
|
+
"rect",
|
|
821
|
+
{
|
|
822
|
+
width: layout.width,
|
|
823
|
+
height: layout.height,
|
|
824
|
+
fill: background,
|
|
825
|
+
stroke: "var(--gridui-border)"
|
|
826
|
+
}
|
|
827
|
+
),
|
|
828
|
+
/* @__PURE__ */ jsx8(
|
|
829
|
+
"text",
|
|
830
|
+
{
|
|
831
|
+
fill: color,
|
|
832
|
+
children: layout.lines.map((line, index) => {
|
|
833
|
+
return /* @__PURE__ */ jsx8(
|
|
834
|
+
"tspan",
|
|
835
|
+
{
|
|
836
|
+
x: line.x,
|
|
837
|
+
y: line.y,
|
|
838
|
+
children: line.text
|
|
839
|
+
},
|
|
840
|
+
index
|
|
841
|
+
);
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
]
|
|
846
|
+
}
|
|
847
|
+
)
|
|
848
|
+
]
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/chart-value-labels/layout.ts
|
|
854
|
+
var PLOT_INSET2 = 2;
|
|
855
|
+
var LABEL_GAP = 4;
|
|
856
|
+
var labelsOverlap = (left, right) => {
|
|
857
|
+
return left.x < right.x + right.width + LABEL_GAP && left.x + left.width + LABEL_GAP > right.x && left.y < right.y + right.height + LABEL_GAP && left.y + left.height + LABEL_GAP > right.y;
|
|
858
|
+
};
|
|
859
|
+
var calculateValueLabelLayout = (options) => {
|
|
860
|
+
const { data, selectedId, horizontal, plot, visibility, position, format, offset, measure } = options;
|
|
861
|
+
const occupied = [];
|
|
862
|
+
const labels = [];
|
|
863
|
+
for (const datum of data) {
|
|
864
|
+
if (visibility === "none" || datum.y === null || visibility === "selected" && datum.id !== selectedId) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const text = format(datum.y, datum);
|
|
868
|
+
const box = measure(text);
|
|
869
|
+
if (box.width > plot.width - PLOT_INSET2 * 2 || box.height > plot.height - PLOT_INSET2 * 2) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const categoryPosition = plot.category(datum);
|
|
873
|
+
let valuePosition = plot.value(datum.y);
|
|
874
|
+
if (position === "center") {
|
|
875
|
+
valuePosition = (valuePosition + plot.value(0)) / 2;
|
|
876
|
+
}
|
|
877
|
+
let centerX = categoryPosition;
|
|
878
|
+
let centerY = valuePosition;
|
|
879
|
+
if (horizontal) {
|
|
880
|
+
centerX = valuePosition;
|
|
881
|
+
centerY = categoryPosition;
|
|
882
|
+
}
|
|
883
|
+
if (position === "end") {
|
|
884
|
+
if (horizontal) {
|
|
885
|
+
const direction = datum.y < 0 ? -1 : 1;
|
|
886
|
+
centerX += direction * (offset + box.width / 2);
|
|
887
|
+
} else {
|
|
888
|
+
centerY -= Math.sign(datum.y || 1) * offset;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const bounds = {
|
|
892
|
+
x: Math.max(PLOT_INSET2, Math.min(plot.width - box.width - PLOT_INSET2, centerX - box.width / 2)),
|
|
893
|
+
y: Math.max(PLOT_INSET2, Math.min(plot.height - box.height - PLOT_INSET2, centerY - box.height / 2)),
|
|
894
|
+
width: box.width,
|
|
895
|
+
height: box.height
|
|
896
|
+
};
|
|
897
|
+
if (occupied.some((other) => {
|
|
898
|
+
return labelsOverlap(bounds, other);
|
|
899
|
+
})) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
occupied.push(bounds);
|
|
903
|
+
labels.push({
|
|
904
|
+
id: datum.id,
|
|
905
|
+
text,
|
|
906
|
+
x: bounds.x - box.x,
|
|
907
|
+
y: bounds.y - box.y
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return labels;
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// src/chart-value-labels/chart-value-labels.tsx
|
|
914
|
+
import { useCallback as useCallback2 } from "react";
|
|
915
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
916
|
+
var ChartValueLabels = (props) => {
|
|
917
|
+
const { visibility = "selected", position = "end", format = String, color = "var(--gridui-chart-value)", fontSize = 12, offset = 12 } = props;
|
|
918
|
+
const chart = useChart();
|
|
919
|
+
const plot = usePlot();
|
|
920
|
+
const calculate = useCallback2((measure) => {
|
|
921
|
+
return calculateValueLabelLayout({
|
|
922
|
+
data: chart.data,
|
|
923
|
+
selectedId: chart.selectedId,
|
|
924
|
+
horizontal: chart.horizontal,
|
|
925
|
+
plot,
|
|
926
|
+
visibility,
|
|
927
|
+
position,
|
|
928
|
+
format,
|
|
929
|
+
offset,
|
|
930
|
+
measure
|
|
931
|
+
});
|
|
932
|
+
}, [
|
|
933
|
+
chart,
|
|
934
|
+
plot,
|
|
935
|
+
visibility,
|
|
936
|
+
position,
|
|
937
|
+
format,
|
|
938
|
+
offset
|
|
939
|
+
]);
|
|
940
|
+
const { ref, layout } = useMeasuredLayout(calculate);
|
|
941
|
+
return /* @__PURE__ */ jsxs5(
|
|
942
|
+
"g",
|
|
943
|
+
{
|
|
944
|
+
className: "gridui-chart-values",
|
|
945
|
+
"aria-hidden": "true",
|
|
946
|
+
pointerEvents: "none",
|
|
947
|
+
fill: color,
|
|
948
|
+
fontSize,
|
|
949
|
+
children: [
|
|
950
|
+
/* @__PURE__ */ jsx9(
|
|
951
|
+
"text",
|
|
952
|
+
{
|
|
953
|
+
ref,
|
|
954
|
+
visibility: "hidden"
|
|
955
|
+
}
|
|
956
|
+
),
|
|
957
|
+
layout?.map((label) => {
|
|
958
|
+
return /* @__PURE__ */ jsx9(
|
|
959
|
+
"text",
|
|
960
|
+
{
|
|
961
|
+
x: label.x,
|
|
962
|
+
y: label.y,
|
|
963
|
+
children: label.text
|
|
964
|
+
},
|
|
965
|
+
label.id
|
|
966
|
+
);
|
|
967
|
+
})
|
|
968
|
+
]
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// src/shared/axis.tsx
|
|
974
|
+
import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
|
|
975
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
976
|
+
var Axis = (props) => {
|
|
977
|
+
const { axis, position, line = true, ticks = true, labels = true, title, tickFormat, tickCount, tickValues, color = "var(--gridui-chart-axis)", strokeWidth = 1, labelColor = "var(--gridui-chart-label)", fontSize = 12 } = props;
|
|
978
|
+
const chart = useChart();
|
|
979
|
+
const plot = usePlot();
|
|
980
|
+
const { data, horizontal, scale, domain } = chart;
|
|
981
|
+
const isX = axis === "x";
|
|
982
|
+
const xEdge = position === "start" ? plot.top : plot.bottom;
|
|
983
|
+
const yEdge = position === "start" ? plot.left : plot.right;
|
|
984
|
+
const edge = isX ? xEdge : yEdge;
|
|
985
|
+
const titleX = position === "start" ? 12 : plot.width - 12;
|
|
986
|
+
const titleY = position === "start" ? 12 : plot.height - 2;
|
|
987
|
+
const sign = position === "start" ? -1 : 1;
|
|
988
|
+
const all = useMemo3(() => {
|
|
989
|
+
return axisTicks({
|
|
990
|
+
data,
|
|
991
|
+
horizontal,
|
|
992
|
+
scale,
|
|
993
|
+
domain
|
|
994
|
+
}, plot, axis, {
|
|
995
|
+
...tickCount === void 0 ? {} : { tickCount },
|
|
996
|
+
...tickValues === void 0 ? {} : { tickValues }
|
|
997
|
+
});
|
|
998
|
+
}, [
|
|
999
|
+
data,
|
|
1000
|
+
horizontal,
|
|
1001
|
+
scale,
|
|
1002
|
+
domain,
|
|
1003
|
+
plot,
|
|
1004
|
+
axis,
|
|
1005
|
+
tickCount,
|
|
1006
|
+
tickValues
|
|
1007
|
+
]);
|
|
1008
|
+
const calculate = useCallback3((measure) => {
|
|
1009
|
+
const occupied = [];
|
|
1010
|
+
const shown = /* @__PURE__ */ new Map();
|
|
1011
|
+
const ordered = all.length > 1 ? [
|
|
1012
|
+
all[0],
|
|
1013
|
+
all.at(-1),
|
|
1014
|
+
...all.slice(1, -1)
|
|
1015
|
+
] : all;
|
|
1016
|
+
for (const tick of ordered) {
|
|
1017
|
+
const defaultText = tick.value instanceof Date ? tick.value.toISOString().slice(0, 10) : String(tick.value);
|
|
1018
|
+
const text = tickFormat ? tickFormat(tick.value) : defaultText;
|
|
1019
|
+
const box = measure(text);
|
|
1020
|
+
if (box.width > plot.width - 4 || box.height > plot.height - 4) {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const outsideX = position === "start" ? edge - 9 - box.width : edge + 9;
|
|
1024
|
+
const outsideY = position === "start" ? edge - 9 - box.height : edge + 9;
|
|
1025
|
+
const originX = isX ? tick.at - box.width / 2 : outsideX;
|
|
1026
|
+
const originY = isX ? outsideY : tick.at - box.height / 2;
|
|
1027
|
+
const x = Math.max(2, Math.min(plot.width - box.width - 2, originX));
|
|
1028
|
+
const y = Math.max(2, Math.min(plot.height - box.height - 2, originY));
|
|
1029
|
+
if (occupied.some((other) => {
|
|
1030
|
+
return x < other.x + other.width + 4 && x + box.width + 4 > other.x && y < other.y + other.height + 4 && y + box.height + 4 > other.y;
|
|
1031
|
+
})) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
occupied.push({
|
|
1035
|
+
x,
|
|
1036
|
+
y,
|
|
1037
|
+
width: box.width,
|
|
1038
|
+
height: box.height
|
|
1039
|
+
});
|
|
1040
|
+
shown.set(tick.key, {
|
|
1041
|
+
text,
|
|
1042
|
+
x: x - box.x,
|
|
1043
|
+
y: y - box.y
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return shown;
|
|
1047
|
+
}, [
|
|
1048
|
+
all,
|
|
1049
|
+
tickFormat,
|
|
1050
|
+
plot.width,
|
|
1051
|
+
plot.height,
|
|
1052
|
+
isX,
|
|
1053
|
+
position,
|
|
1054
|
+
edge
|
|
1055
|
+
]);
|
|
1056
|
+
const { ref, layout } = useMeasuredLayout(calculate);
|
|
1057
|
+
return /* @__PURE__ */ jsxs6(
|
|
1058
|
+
"g",
|
|
1059
|
+
{
|
|
1060
|
+
"aria-hidden": "true",
|
|
1061
|
+
className: "gridui-chart-axis",
|
|
1062
|
+
"data-axis": axis,
|
|
1063
|
+
children: [
|
|
1064
|
+
line && /* @__PURE__ */ jsx10(
|
|
1065
|
+
"line",
|
|
1066
|
+
{
|
|
1067
|
+
x1: isX ? plot.left : edge,
|
|
1068
|
+
y1: isX ? edge : plot.top,
|
|
1069
|
+
x2: isX ? plot.right : edge,
|
|
1070
|
+
y2: isX ? edge : plot.bottom,
|
|
1071
|
+
stroke: color,
|
|
1072
|
+
strokeWidth
|
|
1073
|
+
}
|
|
1074
|
+
),
|
|
1075
|
+
/* @__PURE__ */ jsx10(
|
|
1076
|
+
"text",
|
|
1077
|
+
{
|
|
1078
|
+
ref,
|
|
1079
|
+
visibility: "hidden",
|
|
1080
|
+
pointerEvents: "none",
|
|
1081
|
+
fontSize
|
|
1082
|
+
}
|
|
1083
|
+
),
|
|
1084
|
+
all.map((tick) => {
|
|
1085
|
+
const label = layout?.get(tick.key);
|
|
1086
|
+
return /* @__PURE__ */ jsxs6(
|
|
1087
|
+
"g",
|
|
1088
|
+
{
|
|
1089
|
+
children: [
|
|
1090
|
+
ticks && /* @__PURE__ */ jsx10(
|
|
1091
|
+
"line",
|
|
1092
|
+
{
|
|
1093
|
+
x1: isX ? tick.at : edge,
|
|
1094
|
+
y1: isX ? edge : tick.at,
|
|
1095
|
+
x2: isX ? tick.at : edge + sign * 4,
|
|
1096
|
+
y2: isX ? edge + sign * 4 : tick.at,
|
|
1097
|
+
stroke: color,
|
|
1098
|
+
strokeWidth
|
|
1099
|
+
}
|
|
1100
|
+
),
|
|
1101
|
+
labels && label && /* @__PURE__ */ jsx10(
|
|
1102
|
+
"text",
|
|
1103
|
+
{
|
|
1104
|
+
x: label.x,
|
|
1105
|
+
y: label.y,
|
|
1106
|
+
fill: labelColor,
|
|
1107
|
+
fontSize,
|
|
1108
|
+
children: label.text
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
]
|
|
1112
|
+
},
|
|
1113
|
+
tick.key
|
|
1114
|
+
);
|
|
1115
|
+
}),
|
|
1116
|
+
title && /* @__PURE__ */ jsx10(
|
|
1117
|
+
"text",
|
|
1118
|
+
{
|
|
1119
|
+
x: isX ? (plot.left + plot.right) / 2 : titleX,
|
|
1120
|
+
y: isX ? titleY : (plot.top + plot.bottom) / 2,
|
|
1121
|
+
transform: isX ? void 0 : `rotate(-90 ${position === "start" ? 12 : plot.width - 12} ${(plot.top + plot.bottom) / 2})`,
|
|
1122
|
+
textAnchor: "middle",
|
|
1123
|
+
fill: labelColor,
|
|
1124
|
+
fontSize,
|
|
1125
|
+
children: title
|
|
1126
|
+
}
|
|
1127
|
+
)
|
|
1128
|
+
]
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// src/chart-x-axis/chart-x-axis.tsx
|
|
1134
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1135
|
+
var ChartXAxis = (props) => {
|
|
1136
|
+
const { position = "bottom", ...restProps } = props;
|
|
1137
|
+
return /* @__PURE__ */ jsx11(
|
|
1138
|
+
Axis,
|
|
1139
|
+
{
|
|
1140
|
+
...restProps,
|
|
1141
|
+
axis: "x",
|
|
1142
|
+
position: position === "top" ? "start" : "end"
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
// src/chart-y-axis/chart-y-axis.tsx
|
|
1148
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1149
|
+
var ChartYAxis = (props) => {
|
|
1150
|
+
const { position = "left", ...restProps } = props;
|
|
1151
|
+
return /* @__PURE__ */ jsx12(
|
|
1152
|
+
Axis,
|
|
1153
|
+
{
|
|
1154
|
+
...restProps,
|
|
1155
|
+
axis: "y",
|
|
1156
|
+
position: position === "left" ? "start" : "end"
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// src/line-chart/line-chart.tsx
|
|
1162
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1163
|
+
var LineChart = (props) => {
|
|
1164
|
+
const { xScale = "category", ...restProps } = props;
|
|
1165
|
+
return /* @__PURE__ */ jsx13(
|
|
1166
|
+
ChartRoot,
|
|
1167
|
+
{
|
|
1168
|
+
...restProps,
|
|
1169
|
+
kind: "line",
|
|
1170
|
+
horizontal: false,
|
|
1171
|
+
scale: xScale
|
|
1172
|
+
}
|
|
1173
|
+
);
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
// src/line-series/line-series.tsx
|
|
1177
|
+
import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1178
|
+
var LineSeries = (props) => {
|
|
1179
|
+
const { color = "var(--gridui-chart-series)", strokeWidth = 2, dashArray, points, selectedColor = "var(--gridui-chart-selected)" } = props;
|
|
1180
|
+
const chart = useChart();
|
|
1181
|
+
const plot = usePlot();
|
|
1182
|
+
if (chart.kind !== "line") {
|
|
1183
|
+
throw new Error("LineSeries requires LineChart.");
|
|
1184
|
+
}
|
|
1185
|
+
const size = points?.size ?? 5;
|
|
1186
|
+
const shape = points?.shape ?? "none";
|
|
1187
|
+
return /* @__PURE__ */ jsxs7("g", { children: [
|
|
1188
|
+
/* @__PURE__ */ jsx14(
|
|
1189
|
+
"path",
|
|
1190
|
+
{
|
|
1191
|
+
d: linePath(chart.data, plot.category, plot.value),
|
|
1192
|
+
fill: "none",
|
|
1193
|
+
stroke: color,
|
|
1194
|
+
strokeWidth,
|
|
1195
|
+
strokeDasharray: dashArray,
|
|
1196
|
+
strokeLinejoin: "miter",
|
|
1197
|
+
pointerEvents: "none"
|
|
1198
|
+
}
|
|
1199
|
+
),
|
|
1200
|
+
chart.data.map((datum, index) => {
|
|
1201
|
+
if (datum.y === null) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
const selected = chart.selectedId === datum.id;
|
|
1205
|
+
const isolated = (chart.data[index - 1]?.y ?? null) === null && (chart.data[index + 1]?.y ?? null) === null;
|
|
1206
|
+
const visible = shape !== "none" || selected || isolated;
|
|
1207
|
+
const markSize = selected ? Math.max(8, size) : size;
|
|
1208
|
+
const pointColor = selected ? selectedColor : points?.color ?? color;
|
|
1209
|
+
const fill = points?.fill === "hollow" && !selected ? "var(--gridui-chart-point-background)" : pointColor;
|
|
1210
|
+
const x = plot.category(datum);
|
|
1211
|
+
const y = plot.value(datum.y);
|
|
1212
|
+
const previous = chart.data[index - 1];
|
|
1213
|
+
const next = chart.data[index + 1];
|
|
1214
|
+
const start = previous ? (plot.category(previous) + x) / 2 : plot.left;
|
|
1215
|
+
const end = next ? (plot.category(next) + x) / 2 : plot.right;
|
|
1216
|
+
return /* @__PURE__ */ jsxs7(
|
|
1217
|
+
DatumMark,
|
|
1218
|
+
{
|
|
1219
|
+
datum,
|
|
1220
|
+
children: [
|
|
1221
|
+
/* @__PURE__ */ jsx14(
|
|
1222
|
+
"rect",
|
|
1223
|
+
{
|
|
1224
|
+
className: "gridui-chart-hit",
|
|
1225
|
+
x: start,
|
|
1226
|
+
y: plot.top,
|
|
1227
|
+
width: Math.max(1, end - start),
|
|
1228
|
+
height: plot.bottom - plot.top,
|
|
1229
|
+
fill: "transparent"
|
|
1230
|
+
}
|
|
1231
|
+
),
|
|
1232
|
+
visible && (shape === "square" ? /* @__PURE__ */ jsx14(
|
|
1233
|
+
"rect",
|
|
1234
|
+
{
|
|
1235
|
+
x: x - markSize / 2,
|
|
1236
|
+
y: y - markSize / 2,
|
|
1237
|
+
width: markSize,
|
|
1238
|
+
height: markSize,
|
|
1239
|
+
fill,
|
|
1240
|
+
stroke: selected ? selectedColor : points?.color ?? color,
|
|
1241
|
+
strokeWidth: points?.strokeWidth ?? 1.5,
|
|
1242
|
+
pointerEvents: "none"
|
|
1243
|
+
}
|
|
1244
|
+
) : /* @__PURE__ */ jsx14(
|
|
1245
|
+
"circle",
|
|
1246
|
+
{
|
|
1247
|
+
cx: x,
|
|
1248
|
+
cy: y,
|
|
1249
|
+
r: markSize / 2,
|
|
1250
|
+
fill,
|
|
1251
|
+
stroke: selected ? selectedColor : points?.color ?? color,
|
|
1252
|
+
strokeWidth: points?.strokeWidth ?? 1.5,
|
|
1253
|
+
pointerEvents: "none"
|
|
1254
|
+
}
|
|
1255
|
+
))
|
|
1256
|
+
]
|
|
1257
|
+
},
|
|
1258
|
+
datum.id
|
|
1259
|
+
);
|
|
1260
|
+
})
|
|
1261
|
+
] });
|
|
1262
|
+
};
|
|
1263
|
+
export {
|
|
1264
|
+
BarChart,
|
|
1265
|
+
BarSeries,
|
|
1266
|
+
ChartCursor,
|
|
1267
|
+
ChartGrid,
|
|
1268
|
+
ChartPlot,
|
|
1269
|
+
ChartTooltip,
|
|
1270
|
+
ChartValueLabels,
|
|
1271
|
+
ChartXAxis,
|
|
1272
|
+
ChartYAxis,
|
|
1273
|
+
LineChart,
|
|
1274
|
+
LineSeries
|
|
1275
|
+
};
|
|
1276
|
+
//# sourceMappingURL=index.js.map
|