@umituz/web-dashboard 4.3.0 → 4.5.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Installable app skeleton for the fleet
|
|
3
|
+
"version": "4.5.0",
|
|
4
|
+
"description": "Installable app skeleton for the fleet — layouts, auth, onboarding, analytics (dependency-free SVG charts), calendar, i18n injection, permissions/session/module registry; ships its own udb-* stylesheet and --u-* tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { BarChart, Donut, LineChart, Sparkline } from './components';
|
|
4
|
+
|
|
5
|
+
describe('charts markup smoke', () => {
|
|
6
|
+
it('Sparkline renders an svg path for enough data and null for less', () => {
|
|
7
|
+
const html = renderToString(<Sparkline data={[1, 3, 2, 5]} />);
|
|
8
|
+
expect(html).toContain('<svg');
|
|
9
|
+
expect(html).toContain('<path');
|
|
10
|
+
expect(renderToString(<Sparkline data={[1]} />)).toBe('');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('BarChart renders one grouped column per label with one bar per series', () => {
|
|
14
|
+
const html = renderToString(
|
|
15
|
+
<BarChart
|
|
16
|
+
labels={['mon', 'tue', 'wed']}
|
|
17
|
+
series={[
|
|
18
|
+
{ key: 'a', label: 'A', values: [4, 8, 0] },
|
|
19
|
+
{ key: 'b', label: 'B', values: [2, 4, 6], tone: '#111111' },
|
|
20
|
+
]}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
expect(html.match(/udb-barchart__col(?![a-z])/g)?.length).toBe(3);
|
|
24
|
+
expect(html.match(/udb-barchart__bar(?![a-z_])/g)?.length).toBe(6);
|
|
25
|
+
expect(html).toContain('height:100%');
|
|
26
|
+
expect(html).toContain('height:50%');
|
|
27
|
+
expect(html).toContain('#111111');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('Donut renders a segment circle per label with dash math and center fallback', () => {
|
|
31
|
+
const html = renderToString(
|
|
32
|
+
<Donut
|
|
33
|
+
segments={[
|
|
34
|
+
{ label: 'a', value: 3, tone: '#111111' },
|
|
35
|
+
{ label: 'b', value: 1, tone: '#222222' },
|
|
36
|
+
]}
|
|
37
|
+
centerValue="4"
|
|
38
|
+
centerLabel="total"
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
expect(html.match(/udb-donut__item(?![a-z])/g)?.length).toBe(2);
|
|
42
|
+
expect(html).toContain('75<!-- -->%');
|
|
43
|
+
expect(html).toContain('#111111');
|
|
44
|
+
expect(html).toContain('>total<');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('LineChart renders series paths, grid lines and a legend entry per series', () => {
|
|
48
|
+
const html = renderToString(
|
|
49
|
+
<LineChart
|
|
50
|
+
points={[{ label: 'w1' }, { label: 'w2' }, { label: 'w3' }]}
|
|
51
|
+
series={[
|
|
52
|
+
{ key: 's1', label: 'revenue', values: [2, 4, 8] },
|
|
53
|
+
{ key: 's2', label: 'cost', values: [5, 3, 1], kind: 'bars', axis: 'right' },
|
|
54
|
+
]}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
expect(html).toContain('udb-linechart__grid');
|
|
58
|
+
expect(html).toContain('revenue');
|
|
59
|
+
expect(html).toContain('cost');
|
|
60
|
+
expect(html.match(/<rect /g)?.length).toBe(3);
|
|
61
|
+
expect(html.match(/udb-chartlegend__dot/g)?.length).toBeGreaterThanOrEqual(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('LineChart renders nothing without points', () => {
|
|
65
|
+
expect(renderToString(<LineChart points={[]} series={[]} />)).toBe('');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useId } from 'react';
|
|
1
|
+
import { useId, useMemo, useState } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import type
|
|
3
|
+
import { PALETTE, gridTicks, niceMax, type TipState } from './utils';
|
|
4
4
|
|
|
5
5
|
export function ChartTip({ tip }: { tip: TipState }) {
|
|
6
6
|
return (
|
|
@@ -137,3 +137,358 @@ export function HBars({ rows, formatValue = (n: number) => String(n) }: { rows:
|
|
|
137
137
|
</div>
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
export interface BarSeries {
|
|
142
|
+
key: string;
|
|
143
|
+
label: string;
|
|
144
|
+
values: number[];
|
|
145
|
+
tone?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function BarChart({
|
|
149
|
+
labels,
|
|
150
|
+
series,
|
|
151
|
+
height = 130,
|
|
152
|
+
formatValue = (v: number) => String(v),
|
|
153
|
+
formatLabel = (l: string) => l,
|
|
154
|
+
}: {
|
|
155
|
+
labels: string[];
|
|
156
|
+
series: BarSeries[];
|
|
157
|
+
height?: number;
|
|
158
|
+
formatValue?: (v: number) => string;
|
|
159
|
+
formatLabel?: (l: string) => string;
|
|
160
|
+
}) {
|
|
161
|
+
const [hover, setHover] = useState<number | null>(null);
|
|
162
|
+
const max = Math.max(1, ...series.flatMap((s) => [...s.values]));
|
|
163
|
+
const toneOf = (s: BarSeries, i: number) => s.tone ?? PALETTE[i % PALETTE.length]!;
|
|
164
|
+
return (
|
|
165
|
+
<div className="udb-barchart" style={{ height }}>
|
|
166
|
+
{labels.map((label, i) => (
|
|
167
|
+
<div
|
|
168
|
+
key={label}
|
|
169
|
+
className="udb-barchart__col"
|
|
170
|
+
onMouseEnter={() => setHover(i)}
|
|
171
|
+
onMouseLeave={() => setHover(null)}
|
|
172
|
+
>
|
|
173
|
+
<div className="udb-barchart__bars">
|
|
174
|
+
{series.map((s, si) => (
|
|
175
|
+
<div
|
|
176
|
+
key={s.key}
|
|
177
|
+
className="udb-barchart__bar"
|
|
178
|
+
style={{
|
|
179
|
+
height: `${Math.max(2, ((s.values[i] ?? 0) / max) * 100)}%`,
|
|
180
|
+
background: toneOf(s, si),
|
|
181
|
+
opacity: hover === null || hover === i ? 1 : 0.4,
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
{hover === i && (
|
|
187
|
+
<div className="udb-barchart__tip">
|
|
188
|
+
<div className="udb-barchart__tiptitle">{formatLabel(label)}</div>
|
|
189
|
+
{series.map((s, si) => (
|
|
190
|
+
<div key={s.key} className="udb-barchart__tiprow">
|
|
191
|
+
<span className="udb-barchart__tiplabel">
|
|
192
|
+
<span className="udb-chartlegend__dot" style={{ background: toneOf(s, si) }} />
|
|
193
|
+
{s.label}
|
|
194
|
+
</span>
|
|
195
|
+
<b>{formatValue(s.values[i] ?? 0)}</b>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface DonutSegment {
|
|
207
|
+
label: string;
|
|
208
|
+
value: number;
|
|
209
|
+
tone?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function Donut({
|
|
213
|
+
segments,
|
|
214
|
+
size = 148,
|
|
215
|
+
thickness = 18,
|
|
216
|
+
centerValue,
|
|
217
|
+
centerLabel,
|
|
218
|
+
formatValue = (v: number) => String(v),
|
|
219
|
+
}: {
|
|
220
|
+
segments: DonutSegment[];
|
|
221
|
+
size?: number;
|
|
222
|
+
thickness?: number;
|
|
223
|
+
centerValue?: string;
|
|
224
|
+
centerLabel?: string;
|
|
225
|
+
formatValue?: (v: number) => string;
|
|
226
|
+
}) {
|
|
227
|
+
const [active, setActive] = useState<number | null>(null);
|
|
228
|
+
const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
|
|
229
|
+
const radius = (size - thickness) / 2;
|
|
230
|
+
const circumference = 2 * Math.PI * radius;
|
|
231
|
+
let offset = 0;
|
|
232
|
+
return (
|
|
233
|
+
<div className="udb-donut">
|
|
234
|
+
<div className="udb-donut__wrap" style={{ width: size, height: size }}>
|
|
235
|
+
<svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
|
|
236
|
+
<circle
|
|
237
|
+
cx={size / 2}
|
|
238
|
+
cy={size / 2}
|
|
239
|
+
r={radius}
|
|
240
|
+
fill="none"
|
|
241
|
+
stroke="var(--u-color-surface-muted)"
|
|
242
|
+
strokeWidth={thickness}
|
|
243
|
+
/>
|
|
244
|
+
{segments.map((seg, i) => {
|
|
245
|
+
const dash = (seg.value / total) * circumference;
|
|
246
|
+
const el = (
|
|
247
|
+
<circle
|
|
248
|
+
key={seg.label}
|
|
249
|
+
cx={size / 2}
|
|
250
|
+
cy={size / 2}
|
|
251
|
+
r={radius}
|
|
252
|
+
fill="none"
|
|
253
|
+
stroke={seg.tone ?? PALETTE[i % PALETTE.length]!}
|
|
254
|
+
strokeWidth={active === i ? thickness + 3 : thickness}
|
|
255
|
+
strokeDasharray={`${dash} ${circumference - dash}`}
|
|
256
|
+
strokeDashoffset={-offset}
|
|
257
|
+
strokeLinecap="butt"
|
|
258
|
+
opacity={active === null || active === i ? 1 : 0.35}
|
|
259
|
+
className="udb-donut__seg"
|
|
260
|
+
onMouseEnter={() => setActive(i)}
|
|
261
|
+
onMouseLeave={() => setActive(null)}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
offset += dash;
|
|
265
|
+
return el;
|
|
266
|
+
})}
|
|
267
|
+
</svg>
|
|
268
|
+
<div className="udb-donut__center">
|
|
269
|
+
<span className="udb-donut__value">
|
|
270
|
+
{active !== null ? formatValue(segments[active]!.value) : centerValue}
|
|
271
|
+
</span>
|
|
272
|
+
<span className="udb-donut__label">{active !== null ? segments[active]!.label : centerLabel}</span>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="udb-donut__list">
|
|
276
|
+
{segments.map((seg, i) => (
|
|
277
|
+
<div
|
|
278
|
+
key={seg.label}
|
|
279
|
+
className="udb-donut__item"
|
|
280
|
+
onMouseEnter={() => setActive(i)}
|
|
281
|
+
onMouseLeave={() => setActive(null)}
|
|
282
|
+
>
|
|
283
|
+
<span className="udb-donut__itemlabel">
|
|
284
|
+
<span className="udb-donut__dot" style={{ background: seg.tone ?? PALETTE[i % PALETTE.length]! }} />
|
|
285
|
+
{seg.label}
|
|
286
|
+
</span>
|
|
287
|
+
<span className="udb-donut__itemvalue">
|
|
288
|
+
{formatValue(seg.value)}
|
|
289
|
+
<span className="udb-donut__pct">{Math.round((seg.value / total) * 100)}%</span>
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface LineSeries {
|
|
299
|
+
key: string;
|
|
300
|
+
label: string;
|
|
301
|
+
values: number[];
|
|
302
|
+
tone?: string;
|
|
303
|
+
kind?: 'line' | 'bars';
|
|
304
|
+
axis?: 'left' | 'right';
|
|
305
|
+
dashed?: boolean;
|
|
306
|
+
area?: boolean;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface LineChartPoint {
|
|
310
|
+
label: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function LineChart({
|
|
314
|
+
points,
|
|
315
|
+
series,
|
|
316
|
+
height = 220,
|
|
317
|
+
formatLeft = (v: number) => String(Math.round(v)),
|
|
318
|
+
formatRight = (v: number) => String(Math.round(v)),
|
|
319
|
+
yLabelLeft,
|
|
320
|
+
yLabelRight,
|
|
321
|
+
}: {
|
|
322
|
+
points: LineChartPoint[];
|
|
323
|
+
series: LineSeries[];
|
|
324
|
+
height?: number;
|
|
325
|
+
formatLeft?: (v: number) => string;
|
|
326
|
+
formatRight?: (v: number) => string;
|
|
327
|
+
yLabelLeft?: string;
|
|
328
|
+
yLabelRight?: string;
|
|
329
|
+
}) {
|
|
330
|
+
const [hover, setHover] = useState<number | null>(null);
|
|
331
|
+
const w = 720;
|
|
332
|
+
const padLeft = 52;
|
|
333
|
+
const padRight = series.some((s) => s.axis === 'right') ? 46 : 14;
|
|
334
|
+
const padTop = 12;
|
|
335
|
+
const padBottom = 24;
|
|
336
|
+
const innerW = w - padLeft - padRight;
|
|
337
|
+
const innerH = height - padTop - padBottom;
|
|
338
|
+
const n = points.length;
|
|
339
|
+
|
|
340
|
+
const toneOf = (s: LineSeries, i: number) => s.tone ?? PALETTE[i % PALETTE.length]!;
|
|
341
|
+
|
|
342
|
+
const leftMax = useMemo(
|
|
343
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis !== 'right').flatMap((s) => s.values))),
|
|
344
|
+
[series],
|
|
345
|
+
);
|
|
346
|
+
const rightMax = useMemo(
|
|
347
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis === 'right').flatMap((s) => s.values))),
|
|
348
|
+
[series],
|
|
349
|
+
);
|
|
350
|
+
const leftTicks = useMemo(() => gridTicks(Math.max(...series.filter((s) => s.axis !== 'right').flatMap((s) => s.values), 1)), [series]);
|
|
351
|
+
|
|
352
|
+
const xAt = (i: number) => padLeft + (n <= 1 ? innerW / 2 : (i / (n - 1)) * innerW);
|
|
353
|
+
const yLeft = (v: number) => padTop + innerH - (v / leftMax) * innerH;
|
|
354
|
+
const yRight = (v: number) => padTop + innerH - (v / rightMax) * innerH;
|
|
355
|
+
const labelEvery = Math.max(1, Math.ceil(n / 8));
|
|
356
|
+
|
|
357
|
+
if (n === 0) return null;
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div className="udb-linechart">
|
|
361
|
+
<svg
|
|
362
|
+
viewBox={`0 0 ${w} ${height}`}
|
|
363
|
+
className="udb-linechart__svg"
|
|
364
|
+
style={{ height }}
|
|
365
|
+
onMouseLeave={() => setHover(null)}
|
|
366
|
+
onMouseMove={(e) => {
|
|
367
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
368
|
+
const x = ((e.clientX - rect.left) / rect.width) * w;
|
|
369
|
+
const idx = Math.round(((x - padLeft) / innerW) * (n - 1));
|
|
370
|
+
setHover(Math.max(0, Math.min(n - 1, idx)));
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
{leftTicks.map((v, i) => (
|
|
374
|
+
<g key={i}>
|
|
375
|
+
<line x1={padLeft} x2={w - padRight} y1={yLeft(v)} y2={yLeft(v)} className="udb-linechart__grid" />
|
|
376
|
+
<text x={padLeft - 6} y={yLeft(v) + 3} textAnchor="end" className="udb-linechart__axis">
|
|
377
|
+
{formatLeft(v)}
|
|
378
|
+
</text>
|
|
379
|
+
</g>
|
|
380
|
+
))}
|
|
381
|
+
{series.some((s) => s.axis === 'right') && (
|
|
382
|
+
<g>
|
|
383
|
+
{leftTicks.map((v, i) => (
|
|
384
|
+
<text key={i} x={w - padRight + 6} y={yLeft(v) + 3} textAnchor="start" className="udb-linechart__axis">
|
|
385
|
+
{formatRight((v / leftMax) * rightMax)}
|
|
386
|
+
</text>
|
|
387
|
+
))}
|
|
388
|
+
</g>
|
|
389
|
+
)}
|
|
390
|
+
{points.map((p, i) =>
|
|
391
|
+
i % labelEvery === 0 ? (
|
|
392
|
+
<text key={i} x={xAt(i)} y={height - 8} textAnchor="middle" className="udb-linechart__axis">
|
|
393
|
+
{p.label}
|
|
394
|
+
</text>
|
|
395
|
+
) : null,
|
|
396
|
+
)}
|
|
397
|
+
{series.map((s, si) => {
|
|
398
|
+
const yFn = s.axis === 'right' ? yRight : yLeft;
|
|
399
|
+
if (s.kind === 'bars') {
|
|
400
|
+
const barW = Math.max(3, (innerW / Math.max(1, n - 1)) * 0.5);
|
|
401
|
+
return (
|
|
402
|
+
<g key={s.key}>
|
|
403
|
+
{s.values.slice(0, n).map((v, i) => (
|
|
404
|
+
<rect
|
|
405
|
+
key={i}
|
|
406
|
+
x={xAt(i) - barW / 2}
|
|
407
|
+
y={yFn(v)}
|
|
408
|
+
width={barW}
|
|
409
|
+
height={Math.max(0, padTop + innerH - yFn(v))}
|
|
410
|
+
rx={1.5}
|
|
411
|
+
fill={toneOf(s, si)}
|
|
412
|
+
opacity={hover === null || hover === i ? 0.85 : 0.35}
|
|
413
|
+
/>
|
|
414
|
+
))}
|
|
415
|
+
</g>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const path = s.values
|
|
419
|
+
.slice(0, n)
|
|
420
|
+
.map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i).toFixed(1)},${yFn(v).toFixed(1)}`)
|
|
421
|
+
.join(' ');
|
|
422
|
+
const areaPath = `${path} L${xAt(n - 1).toFixed(1)},${padTop + innerH} L${xAt(0).toFixed(1)},${padTop + innerH} Z`;
|
|
423
|
+
return (
|
|
424
|
+
<g key={s.key}>
|
|
425
|
+
{s.area !== false && !s.dashed && <path d={areaPath} fill={toneOf(s, si)} opacity={0.07} />}
|
|
426
|
+
<path
|
|
427
|
+
d={path}
|
|
428
|
+
fill="none"
|
|
429
|
+
stroke={toneOf(s, si)}
|
|
430
|
+
strokeWidth={2}
|
|
431
|
+
strokeLinejoin="round"
|
|
432
|
+
strokeLinecap="round"
|
|
433
|
+
strokeDasharray={s.dashed ? '4 4' : undefined}
|
|
434
|
+
/>
|
|
435
|
+
{s.values.slice(0, n).map((v, i) => (
|
|
436
|
+
<circle
|
|
437
|
+
key={i}
|
|
438
|
+
cx={xAt(i)}
|
|
439
|
+
cy={yFn(v)}
|
|
440
|
+
r={hover === i ? 3.4 : 0}
|
|
441
|
+
fill="var(--u-color-surface)"
|
|
442
|
+
stroke={toneOf(s, si)}
|
|
443
|
+
strokeWidth={2}
|
|
444
|
+
/>
|
|
445
|
+
))}
|
|
446
|
+
</g>
|
|
447
|
+
);
|
|
448
|
+
})}
|
|
449
|
+
{hover !== null && (
|
|
450
|
+
<line
|
|
451
|
+
x1={xAt(hover)}
|
|
452
|
+
x2={xAt(hover)}
|
|
453
|
+
y1={padTop}
|
|
454
|
+
y2={padTop + innerH}
|
|
455
|
+
className="udb-linechart__crosshair"
|
|
456
|
+
/>
|
|
457
|
+
)}
|
|
458
|
+
{yLabelLeft && (
|
|
459
|
+
<text x={4} y={10} className="udb-linechart__axis">
|
|
460
|
+
{yLabelLeft}
|
|
461
|
+
</text>
|
|
462
|
+
)}
|
|
463
|
+
{yLabelRight && (
|
|
464
|
+
<text x={w - 4} y={10} textAnchor="end" className="udb-linechart__axis">
|
|
465
|
+
{yLabelRight}
|
|
466
|
+
</text>
|
|
467
|
+
)}
|
|
468
|
+
</svg>
|
|
469
|
+
{hover !== null && (
|
|
470
|
+
<div
|
|
471
|
+
className="udb-linechart__tip"
|
|
472
|
+
style={{
|
|
473
|
+
left: `${(xAt(hover) / w) * 100}%`,
|
|
474
|
+
transform: hover > n / 2 ? 'translateX(calc(-100% - 10px))' : 'translateX(10px)',
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<div className="udb-linechart__tiptitle">{points[hover]!.label}</div>
|
|
478
|
+
{series.map((s, si) => (
|
|
479
|
+
<div key={s.key} className="udb-linechart__tiprow">
|
|
480
|
+
<span>
|
|
481
|
+
<span className="udb-linechart__swatch" style={{ background: toneOf(s, si) }} />
|
|
482
|
+
{s.label}
|
|
483
|
+
</span>
|
|
484
|
+
<b>{(s.axis === 'right' ? formatRight : formatLeft)(s.values[hover]!)}</b>
|
|
485
|
+
</div>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
<div className="udb-linechart__legendhost">
|
|
490
|
+
<Legend items={series.map((s, si) => ({ label: s.label, color: toneOf(s, si) }))} />
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export { PALETTE, useTip, niceMax, gridTicks } from './utils';
|
|
2
2
|
export type { TipRow, TipState } from './utils';
|
|
3
|
-
export { ChartTip, ChartCard, Legend, Sparkline, HBars } from './components';
|
|
4
|
-
export type {
|
|
3
|
+
export { ChartTip, ChartCard, Legend, Sparkline, HBars, BarChart, Donut, LineChart } from './components';
|
|
4
|
+
export type {
|
|
5
|
+
ChartCardProps,
|
|
6
|
+
LegendItem,
|
|
7
|
+
HBarRow,
|
|
8
|
+
BarSeries,
|
|
9
|
+
DonutSegment,
|
|
10
|
+
LineSeries,
|
|
11
|
+
LineChartPoint,
|
|
12
|
+
} from './components';
|
package/src/styles/dashboard.css
CHANGED
|
@@ -2908,3 +2908,248 @@
|
|
|
2908
2908
|
text-align: right;
|
|
2909
2909
|
font-variant-numeric: tabular-nums;
|
|
2910
2910
|
}
|
|
2911
|
+
|
|
2912
|
+
.udb-barchart {
|
|
2913
|
+
display: flex;
|
|
2914
|
+
align-items: flex-end;
|
|
2915
|
+
gap: 3px;
|
|
2916
|
+
width: 100%;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
.udb-barchart__col {
|
|
2920
|
+
position: relative;
|
|
2921
|
+
display: flex;
|
|
2922
|
+
flex: 1;
|
|
2923
|
+
align-items: flex-end;
|
|
2924
|
+
min-width: 0;
|
|
2925
|
+
height: 100%;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
.udb-barchart__bars {
|
|
2929
|
+
display: flex;
|
|
2930
|
+
align-items: flex-end;
|
|
2931
|
+
justify-content: center;
|
|
2932
|
+
gap: 2px;
|
|
2933
|
+
width: 100%;
|
|
2934
|
+
height: 100%;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
.udb-barchart__bars .udb-barchart__bar {
|
|
2938
|
+
flex: 1;
|
|
2939
|
+
width: auto;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
.udb-barchart__bar {
|
|
2943
|
+
width: 100%;
|
|
2944
|
+
min-height: 2px;
|
|
2945
|
+
border-radius: 3px 3px 0 0;
|
|
2946
|
+
transition: opacity 150ms ease, height 300ms ease;
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
.udb-barchart__tip {
|
|
2950
|
+
position: absolute;
|
|
2951
|
+
bottom: calc(100% + 6px);
|
|
2952
|
+
left: 50%;
|
|
2953
|
+
z-index: 10;
|
|
2954
|
+
padding: 4px 8px;
|
|
2955
|
+
white-space: nowrap;
|
|
2956
|
+
pointer-events: none;
|
|
2957
|
+
border: 1px solid var(--u-color-border);
|
|
2958
|
+
border-radius: var(--u-radius-sm);
|
|
2959
|
+
background: var(--u-color-surface);
|
|
2960
|
+
box-shadow: var(--u-shadow-sm);
|
|
2961
|
+
font-size: var(--u-font-size-xs);
|
|
2962
|
+
color: var(--u-color-muted);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
.udb-barchart__tiptitle {
|
|
2966
|
+
font-weight: var(--u-font-weight-medium);
|
|
2967
|
+
color: var(--u-color-text);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
.udb-barchart__tiprow {
|
|
2971
|
+
display: flex;
|
|
2972
|
+
align-items: center;
|
|
2973
|
+
justify-content: space-between;
|
|
2974
|
+
gap: 10px;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
.udb-barchart__tiplabel {
|
|
2978
|
+
display: inline-flex;
|
|
2979
|
+
align-items: center;
|
|
2980
|
+
gap: 5px;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
.udb-barchart__tip b {
|
|
2984
|
+
margin-right: 4px;
|
|
2985
|
+
font-weight: var(--u-font-weight-medium);
|
|
2986
|
+
color: var(--u-color-text);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
.udb-donut {
|
|
2990
|
+
display: flex;
|
|
2991
|
+
align-items: center;
|
|
2992
|
+
gap: 20px;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
.udb-donut__wrap {
|
|
2996
|
+
position: relative;
|
|
2997
|
+
flex-shrink: 0;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
.udb-donut__seg {
|
|
3001
|
+
cursor: pointer;
|
|
3002
|
+
transition: stroke-width 150ms ease, opacity 150ms ease;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
.udb-donut__center {
|
|
3006
|
+
position: absolute;
|
|
3007
|
+
inset: 0;
|
|
3008
|
+
display: flex;
|
|
3009
|
+
flex-direction: column;
|
|
3010
|
+
align-items: center;
|
|
3011
|
+
justify-content: center;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
.udb-donut__value {
|
|
3015
|
+
font-size: var(--u-font-size-lg);
|
|
3016
|
+
font-weight: var(--u-font-weight-bold);
|
|
3017
|
+
color: var(--u-color-text);
|
|
3018
|
+
font-variant-numeric: tabular-nums;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
.udb-donut__label {
|
|
3022
|
+
margin-top: 2px;
|
|
3023
|
+
font-size: var(--u-font-size-xs);
|
|
3024
|
+
color: var(--u-color-muted);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
.udb-donut__list {
|
|
3028
|
+
display: flex;
|
|
3029
|
+
min-width: 0;
|
|
3030
|
+
flex: 1;
|
|
3031
|
+
flex-direction: column;
|
|
3032
|
+
gap: 6px;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
.udb-donut__item {
|
|
3036
|
+
display: flex;
|
|
3037
|
+
align-items: center;
|
|
3038
|
+
justify-content: space-between;
|
|
3039
|
+
gap: 8px;
|
|
3040
|
+
padding: 4px 6px;
|
|
3041
|
+
border-radius: var(--u-radius-sm);
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
.udb-donut__item:hover {
|
|
3045
|
+
background: var(--u-color-surface-muted);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
.udb-donut__itemlabel {
|
|
3049
|
+
display: flex;
|
|
3050
|
+
min-width: 0;
|
|
3051
|
+
align-items: center;
|
|
3052
|
+
gap: 8px;
|
|
3053
|
+
font-size: var(--u-font-size-sm);
|
|
3054
|
+
color: var(--u-color-text);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
.udb-donut__dot {
|
|
3058
|
+
width: 8px;
|
|
3059
|
+
height: 8px;
|
|
3060
|
+
flex-shrink: 0;
|
|
3061
|
+
border-radius: 3px;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
.udb-donut__itemvalue {
|
|
3065
|
+
flex-shrink: 0;
|
|
3066
|
+
font-size: var(--u-font-size-sm);
|
|
3067
|
+
font-weight: var(--u-font-weight-medium);
|
|
3068
|
+
color: var(--u-color-text);
|
|
3069
|
+
font-variant-numeric: tabular-nums;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
.udb-donut__pct {
|
|
3073
|
+
margin-left: 6px;
|
|
3074
|
+
color: var(--u-color-muted);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
.udb-linechart {
|
|
3078
|
+
position: relative;
|
|
3079
|
+
width: 100%;
|
|
3080
|
+
user-select: none;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
.udb-linechart__svg {
|
|
3084
|
+
display: block;
|
|
3085
|
+
width: 100%;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
.udb-linechart__axis {
|
|
3089
|
+
fill: var(--u-color-muted);
|
|
3090
|
+
font-size: var(--u-font-size-xs);
|
|
3091
|
+
font-family: var(--u-font-sans);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
.udb-linechart__grid {
|
|
3095
|
+
stroke: var(--u-color-border);
|
|
3096
|
+
stroke-width: 1;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
.udb-linechart__crosshair {
|
|
3100
|
+
stroke: var(--u-color-border-strong);
|
|
3101
|
+
stroke-width: 1;
|
|
3102
|
+
stroke-dasharray: 3 3;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
.udb-linechart__tip {
|
|
3106
|
+
position: absolute;
|
|
3107
|
+
top: 8px;
|
|
3108
|
+
z-index: 10;
|
|
3109
|
+
min-width: 132px;
|
|
3110
|
+
padding: 8px;
|
|
3111
|
+
pointer-events: none;
|
|
3112
|
+
border: 1px solid var(--u-color-border);
|
|
3113
|
+
border-radius: var(--u-radius-md);
|
|
3114
|
+
background: var(--u-color-surface);
|
|
3115
|
+
box-shadow: var(--u-shadow-md);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
.udb-linechart__tiptitle {
|
|
3119
|
+
margin-bottom: 4px;
|
|
3120
|
+
font-size: var(--u-font-size-xs);
|
|
3121
|
+
color: var(--u-color-muted);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
.udb-linechart__tiprow {
|
|
3125
|
+
display: flex;
|
|
3126
|
+
align-items: center;
|
|
3127
|
+
justify-content: space-between;
|
|
3128
|
+
gap: 12px;
|
|
3129
|
+
font-size: var(--u-font-size-sm);
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
.udb-linechart__tiprow span {
|
|
3133
|
+
display: flex;
|
|
3134
|
+
align-items: center;
|
|
3135
|
+
gap: 6px;
|
|
3136
|
+
color: var(--u-color-muted);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
.udb-linechart__tiprow b {
|
|
3140
|
+
font-weight: var(--u-font-weight-medium);
|
|
3141
|
+
color: var(--u-color-text);
|
|
3142
|
+
font-variant-numeric: tabular-nums;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
.udb-linechart__swatch {
|
|
3146
|
+
display: inline-block;
|
|
3147
|
+
width: 12px;
|
|
3148
|
+
height: 3px;
|
|
3149
|
+
border-radius: 999px;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
.udb-linechart__legendhost {
|
|
3153
|
+
margin-top: 4px;
|
|
3154
|
+
padding: 0 8px;
|
|
3155
|
+
}
|