@bonnard/react 0.1.1
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/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/bonnard-chart.d.ts +21 -0
- package/dist/bonnard-chart.d.ts.map +1 -0
- package/dist/charts/area-chart.d.ts +3 -0
- package/dist/charts/area-chart.d.ts.map +1 -0
- package/dist/charts/bar-chart.d.ts +3 -0
- package/dist/charts/bar-chart.d.ts.map +1 -0
- package/dist/charts/big-value.d.ts +3 -0
- package/dist/charts/big-value.d.ts.map +1 -0
- package/dist/charts/data-table.d.ts +3 -0
- package/dist/charts/data-table.d.ts.map +1 -0
- package/dist/charts/index.d.ts +7 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/line-chart.d.ts +3 -0
- package/dist/charts/line-chart.d.ts.map +1 -0
- package/dist/charts/pie-chart.d.ts +3 -0
- package/dist/charts/pie-chart.d.ts.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/dashboard/dashboard-viewer.d.ts +11 -0
- package/dist/dashboard/dashboard-viewer.d.ts.map +1 -0
- package/dist/dashboard/dashboard.d.ts +8 -0
- package/dist/dashboard/dashboard.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +6 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/inputs/date-range-input.d.ts +10 -0
- package/dist/dashboard/inputs/date-range-input.d.ts.map +1 -0
- package/dist/dashboard/inputs/dropdown-input.d.ts +11 -0
- package/dist/dashboard/inputs/dropdown-input.d.ts.map +1 -0
- package/dist/dashboard/parser.d.ts +9 -0
- package/dist/dashboard/parser.d.ts.map +1 -0
- package/dist/dashboard/query-load.d.ts +13 -0
- package/dist/dashboard/query-load.d.ts.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +935 -0
- package/dist/data-table-DQKxzbS3.js +985 -0
- package/dist/hooks/use-dashboard.d.ts +8 -0
- package/dist/hooks/use-dashboard.d.ts.map +1 -0
- package/dist/hooks/use-query.d.ts +13 -0
- package/dist/hooks/use-query.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/lib/apply-inputs.d.ts +19 -0
- package/dist/lib/apply-inputs.d.ts.map +1 -0
- package/dist/lib/build-series.d.ts +33 -0
- package/dist/lib/build-series.d.ts.map +1 -0
- package/dist/lib/date-presets.d.ts +11 -0
- package/dist/lib/date-presets.d.ts.map +1 -0
- package/dist/lib/echarts-series.d.ts +22 -0
- package/dist/lib/echarts-series.d.ts.map +1 -0
- package/dist/lib/format-value.d.ts +16 -0
- package/dist/lib/format-value.d.ts.map +1 -0
- package/dist/lib/types.d.ts +104 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/styles/bonnard.css +68 -0
- package/dist/theme/chart-theme.d.ts +76 -0
- package/dist/theme/chart-theme.d.ts.map +1 -0
- package/dist/theme/use-chart-theme.d.ts +39 -0
- package/dist/theme/use-chart-theme.d.ts.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { format } from "numfmt";
|
|
4
|
+
import ReactECharts from "echarts-for-react";
|
|
5
|
+
|
|
6
|
+
//#region src/context.ts
|
|
7
|
+
const BonnardContext = createContext(null);
|
|
8
|
+
function useBonnard() {
|
|
9
|
+
const ctx = useContext(BonnardContext);
|
|
10
|
+
if (!ctx) throw new Error("useBonnard() must be used within a <BonnardProvider>");
|
|
11
|
+
return ctx;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/format-value.ts
|
|
16
|
+
/** Named presets mapping to Excel format codes */
|
|
17
|
+
const PRESETS = {
|
|
18
|
+
num0: "#,##0",
|
|
19
|
+
num1: "#,##0.0",
|
|
20
|
+
num2: "#,##0.00",
|
|
21
|
+
usd: "$#,##0",
|
|
22
|
+
usd2: "$#,##0.00",
|
|
23
|
+
eur: "#,##0 \"€\"",
|
|
24
|
+
eur2: "#,##0.00 \"€\"",
|
|
25
|
+
gbp: "£#,##0",
|
|
26
|
+
gbp2: "£#,##0.00",
|
|
27
|
+
chf: "\"CHF \"#,##0",
|
|
28
|
+
chf2: "\"CHF \"#,##0.00",
|
|
29
|
+
pct: "0%",
|
|
30
|
+
pct1: "0.0%",
|
|
31
|
+
pct2: "0.00%",
|
|
32
|
+
shortdate: "d mmm yyyy",
|
|
33
|
+
longdate: "d mmmm yyyy",
|
|
34
|
+
monthyear: "mmm yyyy"
|
|
35
|
+
};
|
|
36
|
+
/** Resolve a preset name to an Excel format code, or pass through raw codes */
|
|
37
|
+
function parsePreset(name) {
|
|
38
|
+
return PRESETS[name] ?? name;
|
|
39
|
+
}
|
|
40
|
+
/** Detect whether an Excel format code is a date pattern */
|
|
41
|
+
function isDatePattern(pattern) {
|
|
42
|
+
const stripped = pattern.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "");
|
|
43
|
+
return /[ymdhs]/i.test(stripped);
|
|
44
|
+
}
|
|
45
|
+
/** Format a value with a preset name or raw Excel format code */
|
|
46
|
+
function applyFormat(value, fmt) {
|
|
47
|
+
if (value == null) return "—";
|
|
48
|
+
const pattern = parsePreset(fmt);
|
|
49
|
+
if (isDatePattern(pattern) && typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
50
|
+
const d = new Date(value);
|
|
51
|
+
if (!isNaN(d.getTime())) return format(pattern, d);
|
|
52
|
+
}
|
|
53
|
+
const num = Number(value);
|
|
54
|
+
if (!isNaN(num)) return format(pattern, num);
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
/** Auto-detect value type and format with sensible defaults */
|
|
58
|
+
function autoFormat(value) {
|
|
59
|
+
if (value == null) return "—";
|
|
60
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
61
|
+
const d = new Date(value);
|
|
62
|
+
if (!isNaN(d.getTime())) return format("d mmm yyyy", d);
|
|
63
|
+
}
|
|
64
|
+
const num = Number(value);
|
|
65
|
+
if (typeof value === "number" || typeof value === "string" && value !== "" && !isNaN(num)) return Number.isInteger(num) ? format("#,##0", num) : format("#,##0.##", num);
|
|
66
|
+
return String(value);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a fmt prop string like "revenue:eur2,date:shortdate" into a column→format map.
|
|
70
|
+
* Splits on commas only when followed by a column name and colon (to avoid breaking
|
|
71
|
+
* Excel format codes that contain commas like `#,##0`).
|
|
72
|
+
* A single format without a colon (e.g. `fmt="eur2"`) is returned under the empty key.
|
|
73
|
+
*/
|
|
74
|
+
function parseFmtProp(fmt) {
|
|
75
|
+
const map = /* @__PURE__ */ new Map();
|
|
76
|
+
const entries = fmt.split(/,(?=\s*[a-zA-Z_]\w*\s*:)/);
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const trimmed = entry.trim();
|
|
79
|
+
if (!trimmed) continue;
|
|
80
|
+
const colonIdx = trimmed.indexOf(":");
|
|
81
|
+
if (colonIdx === -1) map.set("", trimmed);
|
|
82
|
+
else {
|
|
83
|
+
const col = trimmed.slice(0, colonIdx).trim();
|
|
84
|
+
const fmtVal = trimmed.slice(colonIdx + 1).trim();
|
|
85
|
+
map.set(col, fmtVal);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/theme/chart-theme.ts
|
|
93
|
+
/**
|
|
94
|
+
* Shared ECharts theme constants — adapted from metric-builder patterns.
|
|
95
|
+
*/
|
|
96
|
+
/** Named color palettes for charts */
|
|
97
|
+
const PALETTES = {
|
|
98
|
+
default: [
|
|
99
|
+
"#2563eb",
|
|
100
|
+
"#dc2626",
|
|
101
|
+
"#16a34a",
|
|
102
|
+
"#ca8a04",
|
|
103
|
+
"#9333ea",
|
|
104
|
+
"#ec4899",
|
|
105
|
+
"#0891b2",
|
|
106
|
+
"#ea580c"
|
|
107
|
+
],
|
|
108
|
+
tableau: [
|
|
109
|
+
"#4e79a7",
|
|
110
|
+
"#f28e2c",
|
|
111
|
+
"#e15759",
|
|
112
|
+
"#76b7b2",
|
|
113
|
+
"#59a14f",
|
|
114
|
+
"#edc949",
|
|
115
|
+
"#af7aa1",
|
|
116
|
+
"#ff9da7",
|
|
117
|
+
"#9c755f",
|
|
118
|
+
"#bab0ab"
|
|
119
|
+
],
|
|
120
|
+
observable: [
|
|
121
|
+
"#4269d0",
|
|
122
|
+
"#efb118",
|
|
123
|
+
"#ff725c",
|
|
124
|
+
"#6cc5b0",
|
|
125
|
+
"#3ca951",
|
|
126
|
+
"#ff8ab7",
|
|
127
|
+
"#a463f2",
|
|
128
|
+
"#97bbf5",
|
|
129
|
+
"#9c6b4e",
|
|
130
|
+
"#9498a0"
|
|
131
|
+
],
|
|
132
|
+
metabase: [
|
|
133
|
+
"#509EE3",
|
|
134
|
+
"#88BF4D",
|
|
135
|
+
"#A989C5",
|
|
136
|
+
"#EF8C8C",
|
|
137
|
+
"#F9D45C",
|
|
138
|
+
"#F2A86F",
|
|
139
|
+
"#98D9D9",
|
|
140
|
+
"#7172AD"
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
const CHART_COLORS = PALETTES.tableau;
|
|
144
|
+
const LIGHT_THEME = {
|
|
145
|
+
text: {
|
|
146
|
+
label: "#6b7280",
|
|
147
|
+
title: "#374151",
|
|
148
|
+
muted: "#9ca3af"
|
|
149
|
+
},
|
|
150
|
+
tooltip: {
|
|
151
|
+
backgroundColor: "#fff",
|
|
152
|
+
borderColor: "#e5e7eb",
|
|
153
|
+
textColor: "#374151",
|
|
154
|
+
shadow: "box-shadow: 0 2px 8px rgba(59,130,246,0.05);"
|
|
155
|
+
},
|
|
156
|
+
gridLine: "#f3f4f6",
|
|
157
|
+
legendText: "#6b7280"
|
|
158
|
+
};
|
|
159
|
+
const DARK_THEME = {
|
|
160
|
+
text: {
|
|
161
|
+
label: "#9ca3af",
|
|
162
|
+
title: "#e5e7eb",
|
|
163
|
+
muted: "#6b7280"
|
|
164
|
+
},
|
|
165
|
+
tooltip: {
|
|
166
|
+
backgroundColor: "#1f2937",
|
|
167
|
+
borderColor: "#374151",
|
|
168
|
+
textColor: "#e5e7eb",
|
|
169
|
+
shadow: "box-shadow: 0 2px 8px rgba(0,0,0,0.3);"
|
|
170
|
+
},
|
|
171
|
+
gridLine: "#374151",
|
|
172
|
+
legendText: "#9ca3af"
|
|
173
|
+
};
|
|
174
|
+
function getChartTheme(isDark) {
|
|
175
|
+
return isDark ? DARK_THEME : LIGHT_THEME;
|
|
176
|
+
}
|
|
177
|
+
/** Build tooltip config for the current theme */
|
|
178
|
+
function buildTooltip(theme) {
|
|
179
|
+
return {
|
|
180
|
+
backgroundColor: theme.tooltip.backgroundColor,
|
|
181
|
+
borderColor: theme.tooltip.borderColor,
|
|
182
|
+
borderWidth: 1,
|
|
183
|
+
textStyle: {
|
|
184
|
+
color: theme.tooltip.textColor,
|
|
185
|
+
fontSize: 13
|
|
186
|
+
},
|
|
187
|
+
extraCssText: theme.tooltip.shadow,
|
|
188
|
+
appendToBody: true,
|
|
189
|
+
position(point, _params, _dom, _rect, size) {
|
|
190
|
+
const [mouseX] = point;
|
|
191
|
+
const [tooltipW] = size.contentSize;
|
|
192
|
+
const [chartW] = size.viewSize;
|
|
193
|
+
const gap = 15;
|
|
194
|
+
return [mouseX + gap + tooltipW < chartW ? mouseX + gap : mouseX - tooltipW - gap, 10];
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Build legend config for the current theme */
|
|
199
|
+
function buildLegend(theme) {
|
|
200
|
+
return {
|
|
201
|
+
type: "scroll",
|
|
202
|
+
orient: "horizontal",
|
|
203
|
+
bottom: 0,
|
|
204
|
+
left: "center",
|
|
205
|
+
textStyle: {
|
|
206
|
+
color: theme.legendText,
|
|
207
|
+
fontSize: 12
|
|
208
|
+
},
|
|
209
|
+
pageTextStyle: { color: theme.legendText },
|
|
210
|
+
itemWidth: 16,
|
|
211
|
+
itemHeight: 4
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const DEFAULT_CHART_HEIGHT = 320;
|
|
215
|
+
/** Check if x-axis labels are ISO dates — if so, use ECharts time axis */
|
|
216
|
+
function isTimeAxis(rawLabels) {
|
|
217
|
+
if (rawLabels.length === 0) return false;
|
|
218
|
+
for (const label of rawLabels) {
|
|
219
|
+
if (!label) continue;
|
|
220
|
+
return /^\d{4}-\d{2}-\d{2}/.test(label);
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/** Determine axis label rotation based on longest label length */
|
|
225
|
+
function labelRotation(labels) {
|
|
226
|
+
return labels.reduce((max, l) => Math.max(max, String(l).length), 0) > 10 ? -45 : 0;
|
|
227
|
+
}
|
|
228
|
+
/** Grid bottom padding based on rotation */
|
|
229
|
+
function gridBottom(rotation) {
|
|
230
|
+
return rotation === 0 ? 5 : 30;
|
|
231
|
+
}
|
|
232
|
+
/** Format a number for display (compact notation for large values) */
|
|
233
|
+
function formatValue(val) {
|
|
234
|
+
if (val == null) return "—";
|
|
235
|
+
const num = Number(val);
|
|
236
|
+
if (isNaN(num)) return String(val);
|
|
237
|
+
if (Math.abs(num) >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
|
|
238
|
+
if (Math.abs(num) >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
|
|
239
|
+
if (Number.isInteger(num)) return num.toLocaleString();
|
|
240
|
+
return num.toFixed(2);
|
|
241
|
+
}
|
|
242
|
+
/** ECharts tooltip valueFormatter — uses explicit format or compact fallback */
|
|
243
|
+
function tooltipFormatter(yFmt) {
|
|
244
|
+
if (yFmt) return (val) => applyFormat(val, yFmt);
|
|
245
|
+
return (val) => formatValue(val);
|
|
246
|
+
}
|
|
247
|
+
/** Format axis label — detects ISO dates and formats them nicely */
|
|
248
|
+
function formatAxisLabel(val) {
|
|
249
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(val)) {
|
|
250
|
+
const d = new Date(val);
|
|
251
|
+
if (!isNaN(d.getTime())) return d.toLocaleDateString("en-GB", {
|
|
252
|
+
day: "numeric",
|
|
253
|
+
month: "short"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
|
257
|
+
const d = /* @__PURE__ */ new Date(val + "T00:00:00");
|
|
258
|
+
if (!isNaN(d.getTime())) return d.toLocaleDateString("en-GB", {
|
|
259
|
+
day: "numeric",
|
|
260
|
+
month: "short"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return val;
|
|
264
|
+
}
|
|
265
|
+
/** ECharts y-axis formatter — compact numbers */
|
|
266
|
+
function axisValueFormatter(val) {
|
|
267
|
+
if (Math.abs(val) >= 1e6) return `${(val / 1e6).toFixed(1)}M`;
|
|
268
|
+
if (Math.abs(val) >= 1e3) return `${(val / 1e3).toFixed(0)}K`;
|
|
269
|
+
return String(val);
|
|
270
|
+
}
|
|
271
|
+
/** Convert snake_case or camelCase field names to Title Case */
|
|
272
|
+
function formatColumnHeader(col) {
|
|
273
|
+
return col.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/charts/big-value.tsx
|
|
278
|
+
function BigValue({ data, value, title, fmt }) {
|
|
279
|
+
const row = data[0];
|
|
280
|
+
if (!row) return null;
|
|
281
|
+
const displayValue = fmt ? applyFormat(row[value], fmt) : formatValue(row[value]);
|
|
282
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
283
|
+
style: {
|
|
284
|
+
minWidth: 0,
|
|
285
|
+
borderRadius: "var(--bon-radius)",
|
|
286
|
+
border: "1px solid var(--bon-border)",
|
|
287
|
+
backgroundColor: "var(--bon-bg-card)",
|
|
288
|
+
padding: 20,
|
|
289
|
+
boxShadow: "var(--bon-shadow)"
|
|
290
|
+
},
|
|
291
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
292
|
+
style: {
|
|
293
|
+
overflow: "hidden",
|
|
294
|
+
textOverflow: "ellipsis",
|
|
295
|
+
whiteSpace: "nowrap",
|
|
296
|
+
fontSize: 14,
|
|
297
|
+
fontWeight: 500,
|
|
298
|
+
color: "var(--bon-text-muted)",
|
|
299
|
+
margin: 0
|
|
300
|
+
},
|
|
301
|
+
children: title ?? value
|
|
302
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
303
|
+
style: {
|
|
304
|
+
fontSize: 30,
|
|
305
|
+
fontWeight: 700,
|
|
306
|
+
letterSpacing: "-0.025em",
|
|
307
|
+
color: "var(--bon-text)",
|
|
308
|
+
margin: 0,
|
|
309
|
+
marginTop: 4
|
|
310
|
+
},
|
|
311
|
+
children: displayValue
|
|
312
|
+
})]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/lib/build-series.ts
|
|
318
|
+
/**
|
|
319
|
+
* Build multi-series datasets from flat query data.
|
|
320
|
+
*
|
|
321
|
+
* @param data - Flat array of row objects from query
|
|
322
|
+
* @param x - Column name for x-axis
|
|
323
|
+
* @param y - Column name(s) for y-axis, comma-separated for multiple
|
|
324
|
+
* @param series - Optional column name to split data into separate series
|
|
325
|
+
*/
|
|
326
|
+
function buildSeries(data, x, y, series) {
|
|
327
|
+
if (!data || data.length === 0) return {
|
|
328
|
+
labels: [],
|
|
329
|
+
datasets: []
|
|
330
|
+
};
|
|
331
|
+
const yColumns = y.split(",").map((col) => col.trim()).filter(Boolean);
|
|
332
|
+
if (!series) return {
|
|
333
|
+
labels: data.map((row) => String(row[x] ?? "")),
|
|
334
|
+
datasets: yColumns.map((col) => ({
|
|
335
|
+
name: col,
|
|
336
|
+
values: data.map((row) => {
|
|
337
|
+
const val = row[col];
|
|
338
|
+
return val == null ? null : Number(val);
|
|
339
|
+
})
|
|
340
|
+
}))
|
|
341
|
+
};
|
|
342
|
+
const xValues = [];
|
|
343
|
+
const xSet = /* @__PURE__ */ new Set();
|
|
344
|
+
const seriesKeys = [];
|
|
345
|
+
const seriesSet = /* @__PURE__ */ new Set();
|
|
346
|
+
for (const row of data) {
|
|
347
|
+
const xRaw = String(row[x] ?? "");
|
|
348
|
+
if (!xSet.has(xRaw)) {
|
|
349
|
+
xSet.add(xRaw);
|
|
350
|
+
xValues.push(xRaw);
|
|
351
|
+
}
|
|
352
|
+
const sk = String(row[series] ?? "");
|
|
353
|
+
if (!seriesSet.has(sk)) {
|
|
354
|
+
seriesSet.add(sk);
|
|
355
|
+
seriesKeys.push(sk);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const row of data) {
|
|
360
|
+
const key = `${String(row[x] ?? "")}\0${String(row[series] ?? "")}`;
|
|
361
|
+
lookup.set(key, row);
|
|
362
|
+
}
|
|
363
|
+
const datasets = [];
|
|
364
|
+
for (const sk of seriesKeys) for (const col of yColumns) {
|
|
365
|
+
const name = yColumns.length === 1 ? sk : `${sk} - ${col}`;
|
|
366
|
+
const values = xValues.map((xRaw) => {
|
|
367
|
+
const row = lookup.get(`${xRaw}\0${sk}`);
|
|
368
|
+
if (!row) return null;
|
|
369
|
+
const val = row[col];
|
|
370
|
+
return val == null ? null : Number(val);
|
|
371
|
+
});
|
|
372
|
+
datasets.push({
|
|
373
|
+
name,
|
|
374
|
+
values
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
labels: xValues,
|
|
379
|
+
datasets
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/lib/echarts-series.ts
|
|
385
|
+
/**
|
|
386
|
+
* Convert BuildSeriesResult to ECharts series and legend config.
|
|
387
|
+
*
|
|
388
|
+
* @param result - Output from buildSeries()
|
|
389
|
+
* @param chartKind - 'bar', 'line', or 'line-area'
|
|
390
|
+
* @param seriesType - 'stacked', 'grouped', or undefined
|
|
391
|
+
* @param horizontal - For bar charts: swap axes
|
|
392
|
+
* @param rawLabels - When provided, converts data to [label, value] pairs (for time axis)
|
|
393
|
+
*/
|
|
394
|
+
function toEChartsSeries(result, chartKind, seriesType, horizontal, rawLabels) {
|
|
395
|
+
const multiSeries = result.datasets.length > 1;
|
|
396
|
+
let stack;
|
|
397
|
+
if (chartKind === "bar") stack = multiSeries && seriesType !== "grouped" ? "stack1" : void 0;
|
|
398
|
+
else if (chartKind === "line-area") stack = seriesType === "stacked" ? "stack1" : void 0;
|
|
399
|
+
return {
|
|
400
|
+
series: result.datasets.map((ds, idx) => {
|
|
401
|
+
const data = rawLabels ? rawLabels.map((label, i) => [label, ds.values[i]]) : ds.values;
|
|
402
|
+
const base = {
|
|
403
|
+
name: ds.name,
|
|
404
|
+
data
|
|
405
|
+
};
|
|
406
|
+
if (chartKind === "bar") {
|
|
407
|
+
base.type = "bar";
|
|
408
|
+
base.barMaxWidth = 40;
|
|
409
|
+
if (stack) base.stack = stack;
|
|
410
|
+
base.itemStyle = { borderRadius: !stack || idx === result.datasets.length - 1 ? horizontal ? [
|
|
411
|
+
0,
|
|
412
|
+
4,
|
|
413
|
+
4,
|
|
414
|
+
0
|
|
415
|
+
] : [
|
|
416
|
+
4,
|
|
417
|
+
4,
|
|
418
|
+
0,
|
|
419
|
+
0
|
|
420
|
+
] : 0 };
|
|
421
|
+
} else {
|
|
422
|
+
base.type = "line";
|
|
423
|
+
base.smooth = true;
|
|
424
|
+
base.symbol = "circle";
|
|
425
|
+
base.symbolSize = 6;
|
|
426
|
+
base.lineStyle = { width: 2 };
|
|
427
|
+
if (stack) base.stack = stack;
|
|
428
|
+
if (chartKind === "line-area") base.areaStyle = { opacity: stack ? .6 : .15 };
|
|
429
|
+
}
|
|
430
|
+
return base;
|
|
431
|
+
}),
|
|
432
|
+
showLegend: multiSeries
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/theme/use-chart-theme.ts
|
|
438
|
+
/**
|
|
439
|
+
* Returns mode-aware chart theme values.
|
|
440
|
+
* Reads isDark from BonnardProvider context.
|
|
441
|
+
*/
|
|
442
|
+
function useChartTheme() {
|
|
443
|
+
const { isDark } = useBonnard();
|
|
444
|
+
const theme = getChartTheme(isDark);
|
|
445
|
+
return {
|
|
446
|
+
isDark,
|
|
447
|
+
theme,
|
|
448
|
+
tooltip: buildTooltip(theme),
|
|
449
|
+
legend: buildLegend(theme)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/charts/bar-chart.tsx
|
|
455
|
+
function BarChart({ data, x, y, title, horizontal, series, type, yFmt }) {
|
|
456
|
+
const { theme, tooltip, legend } = useChartTheme();
|
|
457
|
+
const result = buildSeries(data, x, y, series);
|
|
458
|
+
const useTimeAxis = !horizontal && isTimeAxis(result.labels);
|
|
459
|
+
const labels = useTimeAxis ? result.labels : result.labels.map(formatAxisLabel);
|
|
460
|
+
const rotation = horizontal ? 0 : useTimeAxis ? 0 : labelRotation(labels);
|
|
461
|
+
const { series: echartsSeriesList, showLegend } = toEChartsSeries(result, "bar", type, horizontal, useTimeAxis ? result.labels : void 0);
|
|
462
|
+
const categoryAxis = useTimeAxis ? {
|
|
463
|
+
type: "time",
|
|
464
|
+
axisLabel: {
|
|
465
|
+
color: theme.text.label,
|
|
466
|
+
fontSize: 12,
|
|
467
|
+
hideOverlap: true
|
|
468
|
+
}
|
|
469
|
+
} : {
|
|
470
|
+
type: "category",
|
|
471
|
+
data: labels,
|
|
472
|
+
axisLabel: {
|
|
473
|
+
color: theme.text.label,
|
|
474
|
+
fontSize: 12,
|
|
475
|
+
...horizontal ? {} : { rotate: rotation }
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const valueAxis = {
|
|
479
|
+
type: "value",
|
|
480
|
+
axisLabel: {
|
|
481
|
+
color: theme.text.label,
|
|
482
|
+
fontSize: 12,
|
|
483
|
+
formatter: axisValueFormatter
|
|
484
|
+
},
|
|
485
|
+
splitLine: { lineStyle: { color: theme.gridLine } }
|
|
486
|
+
};
|
|
487
|
+
const option = {
|
|
488
|
+
color: CHART_COLORS,
|
|
489
|
+
tooltip: {
|
|
490
|
+
...tooltip,
|
|
491
|
+
trigger: "axis",
|
|
492
|
+
valueFormatter: tooltipFormatter(yFmt)
|
|
493
|
+
},
|
|
494
|
+
...showLegend && { legend },
|
|
495
|
+
grid: {
|
|
496
|
+
left: 10,
|
|
497
|
+
right: 10,
|
|
498
|
+
top: 10,
|
|
499
|
+
bottom: (horizontal ? 40 : gridBottom(rotation)) + (showLegend ? 30 : 0),
|
|
500
|
+
containLabel: true
|
|
501
|
+
},
|
|
502
|
+
[horizontal ? "yAxis" : "xAxis"]: categoryAxis,
|
|
503
|
+
[horizontal ? "xAxis" : "yAxis"]: valueAxis,
|
|
504
|
+
series: echartsSeriesList
|
|
505
|
+
};
|
|
506
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
507
|
+
style: { width: "100%" },
|
|
508
|
+
children: [title && /* @__PURE__ */ jsx("h3", {
|
|
509
|
+
style: {
|
|
510
|
+
fontSize: 14,
|
|
511
|
+
fontWeight: 500,
|
|
512
|
+
color: "var(--bon-text-title)",
|
|
513
|
+
marginBottom: 4
|
|
514
|
+
},
|
|
515
|
+
children: title
|
|
516
|
+
}), /* @__PURE__ */ jsx(ReactECharts, {
|
|
517
|
+
option,
|
|
518
|
+
style: {
|
|
519
|
+
height: DEFAULT_CHART_HEIGHT,
|
|
520
|
+
width: "100%"
|
|
521
|
+
},
|
|
522
|
+
notMerge: true
|
|
523
|
+
})]
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/charts/line-chart.tsx
|
|
529
|
+
function LineChart({ data, x, y, title, series, type, yFmt }) {
|
|
530
|
+
const { theme, tooltip, legend } = useChartTheme();
|
|
531
|
+
const result = buildSeries(data, x, y, series);
|
|
532
|
+
const useTimeAxis = isTimeAxis(result.labels);
|
|
533
|
+
const labels = useTimeAxis ? result.labels : result.labels.map(formatAxisLabel);
|
|
534
|
+
const rotation = useTimeAxis ? 0 : labelRotation(labels);
|
|
535
|
+
const { series: echartsSeriesList, showLegend } = toEChartsSeries(result, "line", type, void 0, useTimeAxis ? result.labels : void 0);
|
|
536
|
+
const xAxisConfig = useTimeAxis ? {
|
|
537
|
+
type: "time",
|
|
538
|
+
axisLabel: {
|
|
539
|
+
color: theme.text.label,
|
|
540
|
+
fontSize: 12,
|
|
541
|
+
hideOverlap: true
|
|
542
|
+
}
|
|
543
|
+
} : {
|
|
544
|
+
type: "category",
|
|
545
|
+
data: labels,
|
|
546
|
+
axisLabel: {
|
|
547
|
+
color: theme.text.label,
|
|
548
|
+
fontSize: 12,
|
|
549
|
+
rotate: rotation
|
|
550
|
+
},
|
|
551
|
+
boundaryGap: false
|
|
552
|
+
};
|
|
553
|
+
const option = {
|
|
554
|
+
color: CHART_COLORS,
|
|
555
|
+
tooltip: {
|
|
556
|
+
...tooltip,
|
|
557
|
+
trigger: "axis",
|
|
558
|
+
valueFormatter: tooltipFormatter(yFmt)
|
|
559
|
+
},
|
|
560
|
+
...showLegend && { legend },
|
|
561
|
+
grid: {
|
|
562
|
+
left: 10,
|
|
563
|
+
right: 10,
|
|
564
|
+
top: 10,
|
|
565
|
+
bottom: gridBottom(rotation) + (showLegend ? 30 : 0),
|
|
566
|
+
containLabel: true
|
|
567
|
+
},
|
|
568
|
+
xAxis: xAxisConfig,
|
|
569
|
+
yAxis: {
|
|
570
|
+
type: "value",
|
|
571
|
+
axisLabel: {
|
|
572
|
+
color: theme.text.label,
|
|
573
|
+
fontSize: 12,
|
|
574
|
+
formatter: axisValueFormatter
|
|
575
|
+
},
|
|
576
|
+
splitLine: { lineStyle: { color: theme.gridLine } }
|
|
577
|
+
},
|
|
578
|
+
series: echartsSeriesList
|
|
579
|
+
};
|
|
580
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
581
|
+
style: { width: "100%" },
|
|
582
|
+
children: [title && /* @__PURE__ */ jsx("h3", {
|
|
583
|
+
style: {
|
|
584
|
+
fontSize: 14,
|
|
585
|
+
fontWeight: 500,
|
|
586
|
+
color: "var(--bon-text-title)",
|
|
587
|
+
marginBottom: 4
|
|
588
|
+
},
|
|
589
|
+
children: title
|
|
590
|
+
}), /* @__PURE__ */ jsx(ReactECharts, {
|
|
591
|
+
option,
|
|
592
|
+
style: {
|
|
593
|
+
height: DEFAULT_CHART_HEIGHT,
|
|
594
|
+
width: "100%"
|
|
595
|
+
},
|
|
596
|
+
notMerge: true
|
|
597
|
+
})]
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/charts/area-chart.tsx
|
|
603
|
+
function AreaChart({ data, x, y, title, series, type, yFmt }) {
|
|
604
|
+
const { theme, tooltip, legend } = useChartTheme();
|
|
605
|
+
const result = buildSeries(data, x, y, series);
|
|
606
|
+
const useTimeAxis = isTimeAxis(result.labels);
|
|
607
|
+
const labels = useTimeAxis ? result.labels : result.labels.map(formatAxisLabel);
|
|
608
|
+
const rotation = useTimeAxis ? 0 : labelRotation(labels);
|
|
609
|
+
const { series: echartsSeriesList, showLegend } = toEChartsSeries(result, "line-area", type, void 0, useTimeAxis ? result.labels : void 0);
|
|
610
|
+
const xAxisConfig = useTimeAxis ? {
|
|
611
|
+
type: "time",
|
|
612
|
+
axisLabel: {
|
|
613
|
+
color: theme.text.label,
|
|
614
|
+
fontSize: 12,
|
|
615
|
+
hideOverlap: true
|
|
616
|
+
}
|
|
617
|
+
} : {
|
|
618
|
+
type: "category",
|
|
619
|
+
data: labels,
|
|
620
|
+
axisLabel: {
|
|
621
|
+
color: theme.text.label,
|
|
622
|
+
fontSize: 12,
|
|
623
|
+
rotate: rotation
|
|
624
|
+
},
|
|
625
|
+
boundaryGap: false
|
|
626
|
+
};
|
|
627
|
+
const option = {
|
|
628
|
+
color: CHART_COLORS,
|
|
629
|
+
tooltip: {
|
|
630
|
+
...tooltip,
|
|
631
|
+
trigger: "axis",
|
|
632
|
+
valueFormatter: tooltipFormatter(yFmt)
|
|
633
|
+
},
|
|
634
|
+
...showLegend && { legend },
|
|
635
|
+
grid: {
|
|
636
|
+
left: 10,
|
|
637
|
+
right: 10,
|
|
638
|
+
top: 10,
|
|
639
|
+
bottom: gridBottom(rotation) + (showLegend ? 30 : 0),
|
|
640
|
+
containLabel: true
|
|
641
|
+
},
|
|
642
|
+
xAxis: xAxisConfig,
|
|
643
|
+
yAxis: {
|
|
644
|
+
type: "value",
|
|
645
|
+
axisLabel: {
|
|
646
|
+
color: theme.text.label,
|
|
647
|
+
fontSize: 12,
|
|
648
|
+
formatter: axisValueFormatter
|
|
649
|
+
},
|
|
650
|
+
splitLine: { lineStyle: { color: theme.gridLine } }
|
|
651
|
+
},
|
|
652
|
+
series: echartsSeriesList
|
|
653
|
+
};
|
|
654
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
655
|
+
style: { width: "100%" },
|
|
656
|
+
children: [title && /* @__PURE__ */ jsx("h3", {
|
|
657
|
+
style: {
|
|
658
|
+
fontSize: 14,
|
|
659
|
+
fontWeight: 500,
|
|
660
|
+
color: "var(--bon-text-title)",
|
|
661
|
+
marginBottom: 4
|
|
662
|
+
},
|
|
663
|
+
children: title
|
|
664
|
+
}), /* @__PURE__ */ jsx(ReactECharts, {
|
|
665
|
+
option,
|
|
666
|
+
style: {
|
|
667
|
+
height: DEFAULT_CHART_HEIGHT,
|
|
668
|
+
width: "100%"
|
|
669
|
+
},
|
|
670
|
+
notMerge: true
|
|
671
|
+
})]
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region src/charts/pie-chart.tsx
|
|
677
|
+
function PieChart({ data, name: nameField, value: valueField, title }) {
|
|
678
|
+
const { theme, tooltip } = useChartTheme();
|
|
679
|
+
const pieData = data.map((row) => ({
|
|
680
|
+
name: String(row[nameField] ?? ""),
|
|
681
|
+
value: Number(row[valueField]) || 0
|
|
682
|
+
}));
|
|
683
|
+
const option = {
|
|
684
|
+
color: CHART_COLORS,
|
|
685
|
+
tooltip: {
|
|
686
|
+
...tooltip,
|
|
687
|
+
trigger: "item",
|
|
688
|
+
formatter: "{b}: {c} ({d}%)"
|
|
689
|
+
},
|
|
690
|
+
legend: {
|
|
691
|
+
orient: "horizontal",
|
|
692
|
+
bottom: 0,
|
|
693
|
+
left: "center",
|
|
694
|
+
type: "scroll",
|
|
695
|
+
textStyle: {
|
|
696
|
+
color: theme.legendText,
|
|
697
|
+
fontSize: 12
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
grid: { bottom: 0 },
|
|
701
|
+
series: [{
|
|
702
|
+
type: "pie",
|
|
703
|
+
radius: ["35%", "65%"],
|
|
704
|
+
center: ["50%", "40%"],
|
|
705
|
+
data: pieData,
|
|
706
|
+
label: { show: false },
|
|
707
|
+
emphasis: {
|
|
708
|
+
label: {
|
|
709
|
+
show: true,
|
|
710
|
+
fontWeight: "bold"
|
|
711
|
+
},
|
|
712
|
+
itemStyle: {
|
|
713
|
+
shadowBlur: 10,
|
|
714
|
+
shadowColor: "rgba(0,0,0,0.1)"
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}]
|
|
718
|
+
};
|
|
719
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
720
|
+
style: { width: "100%" },
|
|
721
|
+
children: [title && /* @__PURE__ */ jsx("h3", {
|
|
722
|
+
style: {
|
|
723
|
+
fontSize: 14,
|
|
724
|
+
fontWeight: 500,
|
|
725
|
+
color: "var(--bon-text-title)",
|
|
726
|
+
marginBottom: 4
|
|
727
|
+
},
|
|
728
|
+
children: title
|
|
729
|
+
}), /* @__PURE__ */ jsx(ReactECharts, {
|
|
730
|
+
option,
|
|
731
|
+
style: {
|
|
732
|
+
height: DEFAULT_CHART_HEIGHT,
|
|
733
|
+
width: "100%"
|
|
734
|
+
},
|
|
735
|
+
notMerge: true
|
|
736
|
+
})]
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/charts/data-table.tsx
|
|
742
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
743
|
+
const ChevronUpIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
744
|
+
width: "14",
|
|
745
|
+
height: "14",
|
|
746
|
+
viewBox: "0 0 24 24",
|
|
747
|
+
fill: "none",
|
|
748
|
+
stroke: "currentColor",
|
|
749
|
+
strokeWidth: "2",
|
|
750
|
+
strokeLinecap: "round",
|
|
751
|
+
strokeLinejoin: "round",
|
|
752
|
+
children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" })
|
|
753
|
+
});
|
|
754
|
+
const ChevronDownIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
755
|
+
width: "14",
|
|
756
|
+
height: "14",
|
|
757
|
+
viewBox: "0 0 24 24",
|
|
758
|
+
fill: "none",
|
|
759
|
+
stroke: "currentColor",
|
|
760
|
+
strokeWidth: "2",
|
|
761
|
+
strokeLinecap: "round",
|
|
762
|
+
strokeLinejoin: "round",
|
|
763
|
+
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
764
|
+
});
|
|
765
|
+
const ChevronsUpDownIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
766
|
+
width: "14",
|
|
767
|
+
height: "14",
|
|
768
|
+
viewBox: "0 0 24 24",
|
|
769
|
+
fill: "none",
|
|
770
|
+
stroke: "currentColor",
|
|
771
|
+
strokeWidth: "2",
|
|
772
|
+
strokeLinecap: "round",
|
|
773
|
+
strokeLinejoin: "round",
|
|
774
|
+
children: [/* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }), /* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })]
|
|
775
|
+
});
|
|
776
|
+
const ChevronLeftIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
777
|
+
width: "14",
|
|
778
|
+
height: "14",
|
|
779
|
+
viewBox: "0 0 24 24",
|
|
780
|
+
fill: "none",
|
|
781
|
+
stroke: "currentColor",
|
|
782
|
+
strokeWidth: "2",
|
|
783
|
+
strokeLinecap: "round",
|
|
784
|
+
strokeLinejoin: "round",
|
|
785
|
+
children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" })
|
|
786
|
+
});
|
|
787
|
+
const ChevronRightIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
788
|
+
width: "14",
|
|
789
|
+
height: "14",
|
|
790
|
+
viewBox: "0 0 24 24",
|
|
791
|
+
fill: "none",
|
|
792
|
+
stroke: "currentColor",
|
|
793
|
+
strokeWidth: "2",
|
|
794
|
+
strokeLinecap: "round",
|
|
795
|
+
strokeLinejoin: "round",
|
|
796
|
+
children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
|
|
797
|
+
});
|
|
798
|
+
/** Check if the first non-null value in a column is numeric */
|
|
799
|
+
function isNumericColumn(data, col) {
|
|
800
|
+
for (const row of data) {
|
|
801
|
+
const v = row[col];
|
|
802
|
+
if (v == null) continue;
|
|
803
|
+
return typeof v === "number" || typeof v === "string" && v !== "" && !isNaN(Number(v));
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
/** Check if the first non-null value in a column looks like an ISO date */
|
|
808
|
+
function isDateColumn(data, col) {
|
|
809
|
+
for (const row of data) {
|
|
810
|
+
const v = row[col];
|
|
811
|
+
if (v == null) continue;
|
|
812
|
+
return typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v);
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
/** Compare two values for sorting — nulls always last */
|
|
817
|
+
function compareValues(a, b, asc) {
|
|
818
|
+
if (a == null && b == null) return 0;
|
|
819
|
+
if (a == null) return 1;
|
|
820
|
+
if (b == null) return -1;
|
|
821
|
+
const numA = Number(a);
|
|
822
|
+
const numB = Number(b);
|
|
823
|
+
const bothNumeric = !isNaN(numA) && !isNaN(numB) && a !== "" && b !== "";
|
|
824
|
+
let cmp;
|
|
825
|
+
if (bothNumeric) cmp = numA - numB;
|
|
826
|
+
else cmp = String(a).localeCompare(String(b), void 0, { sensitivity: "base" });
|
|
827
|
+
return asc ? cmp : -cmp;
|
|
828
|
+
}
|
|
829
|
+
/** Cube's default row limit when no explicit limit is set */
|
|
830
|
+
const CUBE_DEFAULT_LIMIT = 1e4;
|
|
831
|
+
function DataTable({ data, columns: explicitColumns, fmt, rows: rowsProp, queryLimit }) {
|
|
832
|
+
const [sort, setSort] = useState(null);
|
|
833
|
+
const [page, setPage] = useState(0);
|
|
834
|
+
if (data.length === 0) return null;
|
|
835
|
+
const columns = explicitColumns ?? Object.keys(data[0]);
|
|
836
|
+
const fmtMap = fmt ? parseFmtProp(fmt) : null;
|
|
837
|
+
const numericCols = new Set(columns.filter((col) => isNumericColumn(data, col)));
|
|
838
|
+
const dateCols = new Set(columns.filter((col) => isDateColumn(data, col)));
|
|
839
|
+
const pageSize = rowsProp === "all" ? data.length : rowsProp ?? DEFAULT_PAGE_SIZE;
|
|
840
|
+
const paginated = pageSize < data.length;
|
|
841
|
+
const effectiveLimit = queryLimit ?? CUBE_DEFAULT_LIMIT;
|
|
842
|
+
const isTruncated = data.length >= effectiveLimit;
|
|
843
|
+
const formatCell = (col, val) => {
|
|
844
|
+
const colFmt = fmtMap?.get(col);
|
|
845
|
+
if (colFmt) return applyFormat(val, colFmt);
|
|
846
|
+
return autoFormat(val);
|
|
847
|
+
};
|
|
848
|
+
const sortedData = useMemo(() => {
|
|
849
|
+
if (!sort) return data;
|
|
850
|
+
return [...data].sort((a, b) => compareValues(a[sort.col], b[sort.col], sort.asc));
|
|
851
|
+
}, [data, sort]);
|
|
852
|
+
const pageData = paginated ? sortedData.slice(page * pageSize, (page + 1) * pageSize) : sortedData;
|
|
853
|
+
const totalPages = Math.ceil(sortedData.length / pageSize);
|
|
854
|
+
const handleSort = (col) => {
|
|
855
|
+
setSort((prev) => {
|
|
856
|
+
if (prev?.col === col) return {
|
|
857
|
+
col,
|
|
858
|
+
asc: !prev.asc
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
col,
|
|
862
|
+
asc: true
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
setPage(0);
|
|
866
|
+
};
|
|
867
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
868
|
+
style: { width: "100%" },
|
|
869
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
870
|
+
style: {
|
|
871
|
+
overflowX: "auto",
|
|
872
|
+
borderRadius: "var(--bon-radius)",
|
|
873
|
+
border: "1px solid var(--bon-border)",
|
|
874
|
+
scrollbarWidth: "thin"
|
|
875
|
+
},
|
|
876
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
877
|
+
style: {
|
|
878
|
+
width: "100%",
|
|
879
|
+
fontSize: 14,
|
|
880
|
+
borderCollapse: "collapse",
|
|
881
|
+
fontVariantNumeric: "tabular-nums"
|
|
882
|
+
},
|
|
883
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
884
|
+
style: {
|
|
885
|
+
borderBottom: "1px solid var(--bon-border)",
|
|
886
|
+
backgroundColor: "var(--bon-table-header-bg)"
|
|
887
|
+
},
|
|
888
|
+
children: columns.map((col) => {
|
|
889
|
+
const isNumeric = numericCols.has(col);
|
|
890
|
+
const isSorted = sort?.col === col;
|
|
891
|
+
return /* @__PURE__ */ jsx("th", {
|
|
892
|
+
onClick: () => handleSort(col),
|
|
893
|
+
style: {
|
|
894
|
+
padding: "8px 12px",
|
|
895
|
+
fontWeight: 500,
|
|
896
|
+
color: "var(--bon-text-muted)",
|
|
897
|
+
whiteSpace: "nowrap",
|
|
898
|
+
userSelect: "none",
|
|
899
|
+
cursor: "pointer",
|
|
900
|
+
textAlign: isNumeric ? "right" : "left"
|
|
901
|
+
},
|
|
902
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
903
|
+
style: {
|
|
904
|
+
display: "inline-flex",
|
|
905
|
+
alignItems: "center",
|
|
906
|
+
gap: 4,
|
|
907
|
+
flexDirection: isNumeric ? "row-reverse" : "row"
|
|
908
|
+
},
|
|
909
|
+
children: [formatColumnHeader(col), /* @__PURE__ */ jsx("span", {
|
|
910
|
+
style: { opacity: .5 },
|
|
911
|
+
children: isSorted ? sort.asc ? /* @__PURE__ */ jsx(ChevronUpIcon, {}) : /* @__PURE__ */ jsx(ChevronDownIcon, {}) : /* @__PURE__ */ jsx(ChevronsUpDownIcon, {})
|
|
912
|
+
})]
|
|
913
|
+
})
|
|
914
|
+
}, col);
|
|
915
|
+
})
|
|
916
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: pageData.map((row, i) => /* @__PURE__ */ jsx("tr", {
|
|
917
|
+
style: { borderBottom: i < pageData.length - 1 ? "1px solid var(--bon-border)" : void 0 },
|
|
918
|
+
children: columns.map((col) => /* @__PURE__ */ jsx("td", {
|
|
919
|
+
style: {
|
|
920
|
+
padding: "6px 12px",
|
|
921
|
+
textAlign: numericCols.has(col) ? "right" : "left",
|
|
922
|
+
whiteSpace: dateCols.has(col) ? "nowrap" : void 0,
|
|
923
|
+
color: "var(--bon-text)"
|
|
924
|
+
},
|
|
925
|
+
children: formatCell(col, row[col])
|
|
926
|
+
}, col))
|
|
927
|
+
}, i)) })]
|
|
928
|
+
})
|
|
929
|
+
}), (paginated || isTruncated) && /* @__PURE__ */ jsxs("div", {
|
|
930
|
+
style: {
|
|
931
|
+
display: "flex",
|
|
932
|
+
alignItems: "center",
|
|
933
|
+
justifyContent: "space-between",
|
|
934
|
+
padding: "8px 4px 0",
|
|
935
|
+
fontSize: 12,
|
|
936
|
+
color: "var(--bon-text-muted)"
|
|
937
|
+
},
|
|
938
|
+
children: [/* @__PURE__ */ jsx("span", { children: isTruncated ? `Showing first ${data.length.toLocaleString()} rows` : paginated ? `${(page * pageSize + 1).toLocaleString()}\u2013${Math.min((page + 1) * pageSize, sortedData.length).toLocaleString()} of ${sortedData.length.toLocaleString()}` : `${data.length.toLocaleString()} rows` }), paginated && totalPages > 1 && /* @__PURE__ */ jsxs("div", {
|
|
939
|
+
style: {
|
|
940
|
+
display: "flex",
|
|
941
|
+
alignItems: "center",
|
|
942
|
+
gap: 4
|
|
943
|
+
},
|
|
944
|
+
children: [
|
|
945
|
+
/* @__PURE__ */ jsx("button", {
|
|
946
|
+
onClick: () => setPage((p) => Math.max(0, p - 1)),
|
|
947
|
+
disabled: page === 0,
|
|
948
|
+
style: {
|
|
949
|
+
padding: 4,
|
|
950
|
+
borderRadius: 4,
|
|
951
|
+
border: "none",
|
|
952
|
+
background: "none",
|
|
953
|
+
cursor: page === 0 ? "not-allowed" : "pointer",
|
|
954
|
+
opacity: page === 0 ? .3 : 1,
|
|
955
|
+
color: "var(--bon-text-muted)"
|
|
956
|
+
},
|
|
957
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
958
|
+
}),
|
|
959
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
960
|
+
page + 1,
|
|
961
|
+
" / ",
|
|
962
|
+
totalPages
|
|
963
|
+
] }),
|
|
964
|
+
/* @__PURE__ */ jsx("button", {
|
|
965
|
+
onClick: () => setPage((p) => Math.min(totalPages - 1, p + 1)),
|
|
966
|
+
disabled: page >= totalPages - 1,
|
|
967
|
+
style: {
|
|
968
|
+
padding: 4,
|
|
969
|
+
borderRadius: 4,
|
|
970
|
+
border: "none",
|
|
971
|
+
background: "none",
|
|
972
|
+
cursor: page >= totalPages - 1 ? "not-allowed" : "pointer",
|
|
973
|
+
opacity: page >= totalPages - 1 ? .3 : 1,
|
|
974
|
+
color: "var(--bon-text-muted)"
|
|
975
|
+
},
|
|
976
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
977
|
+
})
|
|
978
|
+
]
|
|
979
|
+
})]
|
|
980
|
+
})]
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
//#endregion
|
|
985
|
+
export { BarChart as a, PALETTES as c, LineChart as i, BonnardContext as l, PieChart as n, BigValue as o, AreaChart as r, CHART_COLORS as s, DataTable as t, useBonnard as u };
|