@umituz/web-design-system 6.0.2 → 6.1.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/dist/styles.css +1 -1
- package/package.json +2 -1
- package/src/__tests__/charts/ActivityHeatmap.test.tsx +48 -0
- package/src/__tests__/charts/BarChart.test.tsx +91 -0
- package/src/__tests__/charts/ChartPrimitives.test.tsx +90 -0
- package/src/__tests__/charts/Donut.test.tsx +51 -0
- package/src/__tests__/charts/LineChart.test.tsx +81 -0
- package/src/__tests__/layout-components.test.tsx +100 -0
- package/src/presentation/molecules/TabNav.tsx +46 -0
- package/src/presentation/molecules/index.ts +3 -0
- package/src/presentation/organisms/AuditLogList.tsx +58 -0
- package/src/presentation/organisms/charts/ActivityHeatmap.tsx +58 -0
- package/src/presentation/organisms/charts/BarChart.tsx +143 -0
- package/src/presentation/organisms/charts/ChartCard.tsx +39 -0
- package/src/presentation/organisms/charts/ChartTip.tsx +18 -0
- package/src/presentation/organisms/charts/Donut.tsx +100 -0
- package/src/presentation/organisms/charts/HBars.tsx +31 -0
- package/src/presentation/organisms/charts/Legend.tsx +21 -0
- package/src/presentation/organisms/charts/LineChart.tsx +244 -0
- package/src/presentation/organisms/charts/Sparkline.tsx +35 -0
- package/src/presentation/organisms/charts/index.ts +17 -0
- package/src/presentation/organisms/charts/utils.ts +57 -0
- package/src/presentation/organisms/index.ts +6 -0
- package/src/presentation/templates/PageHeader.tsx +103 -33
- package/src/presentation/templates/SectionHeader.tsx +54 -0
- package/src/presentation/templates/index.ts +4 -1
- package/src/styles/charts.css +455 -0
- package/src/styles/compile.css +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TipState } from './utils';
|
|
2
|
+
|
|
3
|
+
export function ChartTip({ tip }: { tip: TipState }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="udb-charttip" style={{ left: tip.x, top: tip.y }}>
|
|
6
|
+
<div className="udb-charttip__title">{tip.title}</div>
|
|
7
|
+
{tip.rows.map((r, i) => (
|
|
8
|
+
<div key={i} className="udb-charttip__row">
|
|
9
|
+
<span className="udb-charttip__label">
|
|
10
|
+
{r.color && <span className="udb-chartlegend__dot" style={{ background: r.color }} />}
|
|
11
|
+
{r.label}
|
|
12
|
+
</span>
|
|
13
|
+
<b>{r.value}</b>
|
|
14
|
+
</div>
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { seriesColor } from './utils';
|
|
3
|
+
|
|
4
|
+
export interface DonutSegment {
|
|
5
|
+
label: string;
|
|
6
|
+
value: number;
|
|
7
|
+
tone?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DonutProps {
|
|
12
|
+
segments: DonutSegment[];
|
|
13
|
+
size?: number;
|
|
14
|
+
thickness?: number;
|
|
15
|
+
centerValue?: string;
|
|
16
|
+
centerLabel?: string;
|
|
17
|
+
formatValue?: (v: number) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Donut({
|
|
21
|
+
segments,
|
|
22
|
+
size = 148,
|
|
23
|
+
thickness = 18,
|
|
24
|
+
centerValue,
|
|
25
|
+
centerLabel,
|
|
26
|
+
formatValue = (v: number) => String(v),
|
|
27
|
+
}: DonutProps) {
|
|
28
|
+
const [active, setActive] = useState<number | null>(null);
|
|
29
|
+
const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
|
|
30
|
+
const radius = (size - thickness) / 2;
|
|
31
|
+
const circumference = 2 * Math.PI * radius;
|
|
32
|
+
|
|
33
|
+
const slices = segments.map((seg, i) => {
|
|
34
|
+
const dash = (seg.value / total) * circumference;
|
|
35
|
+
const offset = segments
|
|
36
|
+
.slice(0, i)
|
|
37
|
+
.reduce((sum, prev) => sum + (prev.value / total) * circumference, 0);
|
|
38
|
+
return { seg, i, dash, offset };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="udb-donut">
|
|
43
|
+
<div className="udb-donut__wrap" style={{ width: size, height: size }}>
|
|
44
|
+
<svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
|
|
45
|
+
<circle
|
|
46
|
+
cx={size / 2}
|
|
47
|
+
cy={size / 2}
|
|
48
|
+
r={radius}
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="var(--color-muted)"
|
|
51
|
+
strokeWidth={thickness}
|
|
52
|
+
/>
|
|
53
|
+
{slices.map(({ seg, i, dash, offset }) => (
|
|
54
|
+
<circle
|
|
55
|
+
key={seg.label}
|
|
56
|
+
cx={size / 2}
|
|
57
|
+
cy={size / 2}
|
|
58
|
+
r={radius}
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke={seg.tone ?? seg.color ?? seriesColor(i)}
|
|
61
|
+
strokeWidth={active === i ? thickness + 3 : thickness}
|
|
62
|
+
strokeDasharray={`${dash} ${circumference - dash}`}
|
|
63
|
+
strokeDashoffset={-offset}
|
|
64
|
+
strokeLinecap="butt"
|
|
65
|
+
opacity={active === null || active === i ? 1 : 0.35}
|
|
66
|
+
className="udb-donut__seg"
|
|
67
|
+
onMouseEnter={() => setActive(i)}
|
|
68
|
+
onMouseLeave={() => setActive(null)}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</svg>
|
|
72
|
+
<div className="udb-donut__center">
|
|
73
|
+
<span className="udb-donut__value">
|
|
74
|
+
{active !== null ? formatValue(segments[active]!.value) : centerValue}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="udb-donut__label">{active !== null ? segments[active]!.label : centerLabel}</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="udb-donut__list">
|
|
80
|
+
{segments.map((seg, i) => (
|
|
81
|
+
<div
|
|
82
|
+
key={seg.label}
|
|
83
|
+
className="udb-donut__item"
|
|
84
|
+
onMouseEnter={() => setActive(i)}
|
|
85
|
+
onMouseLeave={() => setActive(null)}
|
|
86
|
+
>
|
|
87
|
+
<span className="udb-donut__itemlabel">
|
|
88
|
+
<span className="udb-donut__dot" style={{ background: seg.tone ?? seg.color ?? seriesColor(i) }} />
|
|
89
|
+
{seg.label}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="udb-donut__itemvalue">
|
|
92
|
+
{formatValue(seg.value)}
|
|
93
|
+
<span className="udb-donut__pct">{Math.round((seg.value / total) * 100)}%</span>
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HBarRow {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
sub?: ReactNode;
|
|
6
|
+
value: number;
|
|
7
|
+
tone?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function HBars({ rows, formatValue = (n: number) => String(n) }: { rows: HBarRow[]; formatValue?: (n: number) => string }) {
|
|
11
|
+
const max = Math.max(1, ...rows.map((r) => r.value));
|
|
12
|
+
return (
|
|
13
|
+
<div className="udb-hbars">
|
|
14
|
+
{rows.map((r, i) => (
|
|
15
|
+
<div key={i} className="udb-hbars__row">
|
|
16
|
+
<div className="udb-hbars__label">
|
|
17
|
+
<div className="udb-hbars__name">{r.label}</div>
|
|
18
|
+
{r.sub && <div className="udb-hbars__sub">{r.sub}</div>}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="udb-hbars__track">
|
|
21
|
+
<div
|
|
22
|
+
className="udb-hbars__fill"
|
|
23
|
+
style={{ width: `${(r.value / max) * 100}%`, ...(r.tone ? { background: r.tone } : {}) }}
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
<span className="udb-hbars__value">{formatValue(r.value)}</span>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LegendItem {
|
|
2
|
+
label: string;
|
|
3
|
+
color: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Legend({ items }: { items: LegendItem[] }) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="udb-chartlegend">
|
|
11
|
+
{items.map((i) => (
|
|
12
|
+
<span key={i.label} className="udb-chartlegend__item">
|
|
13
|
+
<span className="udb-chartlegend__dot" style={{ background: i.color }} />
|
|
14
|
+
{i.label}
|
|
15
|
+
{i.value && <span className="udb-chartlegend__value">{i.value}</span>}
|
|
16
|
+
{i.hint && <span className="udb-chartlegend__hint">{i.hint}</span>}
|
|
17
|
+
</span>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useId, useMemo, useState } from 'react';
|
|
2
|
+
import { gridTicks, niceMax, seriesColor } from './utils';
|
|
3
|
+
import { Legend } from './Legend';
|
|
4
|
+
|
|
5
|
+
export interface LineSeries {
|
|
6
|
+
key?: string;
|
|
7
|
+
label: string;
|
|
8
|
+
values?: readonly number[];
|
|
9
|
+
data?: readonly number[];
|
|
10
|
+
tone?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
kind?: 'line' | 'bars';
|
|
13
|
+
axis?: 'left' | 'right';
|
|
14
|
+
dashed?: boolean;
|
|
15
|
+
area?: boolean;
|
|
16
|
+
areaFill?: 'flat' | 'gradient';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LineChartPoint {
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LineChartProps {
|
|
24
|
+
points?: readonly LineChartPoint[];
|
|
25
|
+
labels?: readonly string[];
|
|
26
|
+
series: LineSeries[];
|
|
27
|
+
band?: { from: number; to: number };
|
|
28
|
+
height?: number;
|
|
29
|
+
formatLeft?: (v: number) => string;
|
|
30
|
+
formatRight?: (v: number) => string;
|
|
31
|
+
formatValue?: (v: number) => string;
|
|
32
|
+
yLabelLeft?: string;
|
|
33
|
+
yLabelRight?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function LineChart({
|
|
37
|
+
points,
|
|
38
|
+
labels,
|
|
39
|
+
series,
|
|
40
|
+
band,
|
|
41
|
+
height = 220,
|
|
42
|
+
formatLeft,
|
|
43
|
+
formatRight,
|
|
44
|
+
formatValue,
|
|
45
|
+
yLabelLeft,
|
|
46
|
+
yLabelRight,
|
|
47
|
+
}: LineChartProps) {
|
|
48
|
+
const [hover, setHover] = useState<number | null>(null);
|
|
49
|
+
const uid = useId();
|
|
50
|
+
const pts = points ?? (labels ?? []).map((label) => ({ label }));
|
|
51
|
+
const fmtLeft = formatLeft ?? formatValue ?? ((v: number) => String(Math.round(v)));
|
|
52
|
+
const fmtRight = formatRight ?? formatValue ?? ((v: number) => String(Math.round(v)));
|
|
53
|
+
const vals = (s: LineSeries) => s.values ?? s.data ?? [];
|
|
54
|
+
const w = 720;
|
|
55
|
+
const padLeft = 52;
|
|
56
|
+
const padRight = series.some((s) => s.axis === 'right') ? 46 : 14;
|
|
57
|
+
const padTop = 12;
|
|
58
|
+
const padBottom = 24;
|
|
59
|
+
const innerW = w - padLeft - padRight;
|
|
60
|
+
const innerH = height - padTop - padBottom;
|
|
61
|
+
const n = pts.length;
|
|
62
|
+
|
|
63
|
+
const toneOf = (s: LineSeries, i: number) => s.tone ?? s.color ?? seriesColor(i);
|
|
64
|
+
|
|
65
|
+
const leftMax = useMemo(
|
|
66
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis !== 'right').flatMap((s) => vals(s)), ...(band ? [band.from, band.to] : []))),
|
|
67
|
+
[series, band],
|
|
68
|
+
);
|
|
69
|
+
const rightMax = useMemo(
|
|
70
|
+
() => niceMax(Math.max(1, ...series.filter((s) => s.axis === 'right').flatMap((s) => vals(s)))),
|
|
71
|
+
[series],
|
|
72
|
+
);
|
|
73
|
+
const leftTicks = useMemo(() => gridTicks(Math.max(...series.filter((s) => s.axis !== 'right').flatMap((s) => vals(s)), ...(band ? [band.from, band.to] : []), 1)), [series, band]);
|
|
74
|
+
|
|
75
|
+
const xAt = (i: number) => padLeft + (n <= 1 ? innerW / 2 : (i / (n - 1)) * innerW);
|
|
76
|
+
const yLeft = (v: number) => padTop + innerH - (v / leftMax) * innerH;
|
|
77
|
+
const yRight = (v: number) => padTop + innerH - (v / rightMax) * innerH;
|
|
78
|
+
const labelEvery = Math.max(1, Math.ceil(n / 8));
|
|
79
|
+
|
|
80
|
+
if (n === 0) return null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="udb-linechart">
|
|
84
|
+
<svg
|
|
85
|
+
viewBox={`0 0 ${w} ${height}`}
|
|
86
|
+
className="udb-linechart__svg"
|
|
87
|
+
style={{ height }}
|
|
88
|
+
onMouseLeave={() => setHover(null)}
|
|
89
|
+
onMouseMove={(e) => {
|
|
90
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
91
|
+
const x = ((e.clientX - rect.left) / rect.width) * w;
|
|
92
|
+
const idx = Math.round(((x - padLeft) / innerW) * (n - 1));
|
|
93
|
+
setHover(Math.max(0, Math.min(n - 1, idx)));
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{leftTicks.map((v, i) => (
|
|
97
|
+
<g key={i}>
|
|
98
|
+
<line x1={padLeft} x2={w - padRight} y1={yLeft(v)} y2={yLeft(v)} className="udb-linechart__grid" />
|
|
99
|
+
<text x={padLeft - 6} y={yLeft(v) + 3} textAnchor="end" className="udb-linechart__axis">
|
|
100
|
+
{fmtLeft(v)}
|
|
101
|
+
</text>
|
|
102
|
+
</g>
|
|
103
|
+
))}
|
|
104
|
+
{band && (
|
|
105
|
+
<rect
|
|
106
|
+
x={padLeft}
|
|
107
|
+
y={yLeft(band.to)}
|
|
108
|
+
width={innerW}
|
|
109
|
+
height={Math.max(0, yLeft(band.from) - yLeft(band.to))}
|
|
110
|
+
rx={2}
|
|
111
|
+
className="udb-linechart__band"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
{series.some((s) => s.axis === 'right') && (
|
|
115
|
+
<g>
|
|
116
|
+
{leftTicks.map((v, i) => (
|
|
117
|
+
<text key={i} x={w - padRight + 6} y={yLeft(v) + 3} textAnchor="start" className="udb-linechart__axis">
|
|
118
|
+
{fmtRight((v / leftMax) * rightMax)}
|
|
119
|
+
</text>
|
|
120
|
+
))}
|
|
121
|
+
</g>
|
|
122
|
+
)}
|
|
123
|
+
{pts.map((p, i) =>
|
|
124
|
+
i % labelEvery === 0 ? (
|
|
125
|
+
<text key={i} x={xAt(i)} y={height - 8} textAnchor="middle" className="udb-linechart__axis">
|
|
126
|
+
{p.label}
|
|
127
|
+
</text>
|
|
128
|
+
) : null,
|
|
129
|
+
)}
|
|
130
|
+
{series.map((s, si) => {
|
|
131
|
+
const yFn = s.axis === 'right' ? yRight : yLeft;
|
|
132
|
+
if (s.kind === 'bars') {
|
|
133
|
+
const barW = Math.max(3, (innerW / Math.max(1, n - 1)) * 0.5);
|
|
134
|
+
return (
|
|
135
|
+
<g key={s.key ?? s.label}>
|
|
136
|
+
{vals(s).slice(0, n).map((v, i) => (
|
|
137
|
+
<rect
|
|
138
|
+
key={i}
|
|
139
|
+
x={xAt(i) - barW / 2}
|
|
140
|
+
y={yFn(v)}
|
|
141
|
+
width={barW}
|
|
142
|
+
height={Math.max(0, padTop + innerH - yFn(v))}
|
|
143
|
+
rx={1.5}
|
|
144
|
+
fill={toneOf(s, si)}
|
|
145
|
+
opacity={hover === null || hover === i ? 0.85 : 0.35}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
</g>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const path = vals(s)
|
|
152
|
+
.slice(0, n)
|
|
153
|
+
.map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i).toFixed(1)},${yFn(v).toFixed(1)}`)
|
|
154
|
+
.join(' ');
|
|
155
|
+
const areaPath = `${path} L${xAt(n - 1).toFixed(1)},${padTop + innerH} L${xAt(0).toFixed(1)},${padTop + innerH} Z`;
|
|
156
|
+
const gradId = `lc${uid}-${si}`;
|
|
157
|
+
const showArea = s.area !== false && !s.dashed;
|
|
158
|
+
return (
|
|
159
|
+
<g key={s.key ?? s.label}>
|
|
160
|
+
{showArea &&
|
|
161
|
+
(s.areaFill === 'gradient' ? (
|
|
162
|
+
<defs>
|
|
163
|
+
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
164
|
+
<stop offset="0%" stopColor={toneOf(s, si)} stopOpacity="0.28" />
|
|
165
|
+
<stop offset="100%" stopColor={toneOf(s, si)} stopOpacity="0" />
|
|
166
|
+
</linearGradient>
|
|
167
|
+
</defs>
|
|
168
|
+
) : null)}
|
|
169
|
+
{showArea &&
|
|
170
|
+
(s.areaFill === 'gradient' ? (
|
|
171
|
+
<path d={areaPath} fill={`url(#${gradId})`} />
|
|
172
|
+
) : (
|
|
173
|
+
<path d={areaPath} fill={toneOf(s, si)} opacity={0.07} />
|
|
174
|
+
))}
|
|
175
|
+
<path
|
|
176
|
+
d={path}
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke={toneOf(s, si)}
|
|
179
|
+
strokeWidth={2}
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
strokeLinecap="round"
|
|
182
|
+
strokeDasharray={s.dashed ? '4 4' : undefined}
|
|
183
|
+
/>
|
|
184
|
+
{vals(s).slice(0, n).map((v, i) => (
|
|
185
|
+
<circle
|
|
186
|
+
key={i}
|
|
187
|
+
cx={xAt(i)}
|
|
188
|
+
cy={yFn(v)}
|
|
189
|
+
r={hover === i ? 3.4 : 0}
|
|
190
|
+
fill="var(--color-card)"
|
|
191
|
+
stroke={toneOf(s, si)}
|
|
192
|
+
strokeWidth={2}
|
|
193
|
+
/>
|
|
194
|
+
))}
|
|
195
|
+
</g>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
{hover !== null && (
|
|
199
|
+
<line
|
|
200
|
+
x1={xAt(hover)}
|
|
201
|
+
x2={xAt(hover)}
|
|
202
|
+
y1={padTop}
|
|
203
|
+
y2={padTop + innerH}
|
|
204
|
+
className="udb-linechart__crosshair"
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
{yLabelLeft && (
|
|
208
|
+
<text x={4} y={10} className="udb-linechart__axis">
|
|
209
|
+
{yLabelLeft}
|
|
210
|
+
</text>
|
|
211
|
+
)}
|
|
212
|
+
{yLabelRight && (
|
|
213
|
+
<text x={w - 4} y={10} textAnchor="end" className="udb-linechart__axis">
|
|
214
|
+
{yLabelRight}
|
|
215
|
+
</text>
|
|
216
|
+
)}
|
|
217
|
+
</svg>
|
|
218
|
+
|
|
219
|
+
{hover !== null && (
|
|
220
|
+
<div
|
|
221
|
+
className="udb-linechart__tip"
|
|
222
|
+
style={{
|
|
223
|
+
left: `${(xAt(hover) / w) * 100}%`,
|
|
224
|
+
transform: hover > n / 2 ? 'translateX(calc(-100% - 10px))' : 'translateX(10px)',
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<div className="udb-linechart__tiptitle">{pts[hover]!.label}</div>
|
|
228
|
+
{series.map((s, si) => (
|
|
229
|
+
<div key={s.key ?? s.label} className="udb-linechart__tiprow">
|
|
230
|
+
<span>
|
|
231
|
+
<span className="udb-linechart__swatch" style={{ background: toneOf(s, si) }} />
|
|
232
|
+
{s.label}
|
|
233
|
+
</span>
|
|
234
|
+
<b>{(s.axis === 'right' ? fmtRight : fmtLeft)(vals(s)[hover] ?? 0)}</b>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
<div className="udb-linechart__legendhost">
|
|
240
|
+
<Legend items={series.map((s, si) => ({ label: s.label, color: toneOf(s, si) }))} />
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
|
|
3
|
+
export function Sparkline({
|
|
4
|
+
data,
|
|
5
|
+
height = 34,
|
|
6
|
+
width = 110,
|
|
7
|
+
color = 'currentColor',
|
|
8
|
+
}: {
|
|
9
|
+
data: readonly number[];
|
|
10
|
+
height?: number;
|
|
11
|
+
width?: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
}) {
|
|
14
|
+
const id = useId();
|
|
15
|
+
if (data.length < 2) return null;
|
|
16
|
+
const min = Math.min(...data);
|
|
17
|
+
const max = Math.max(...data);
|
|
18
|
+
const span = max - min || 1;
|
|
19
|
+
const pts = data.map((v, i) => [(i / (data.length - 1)) * width, height - 3 - ((v - min) / span) * (height - 8)]);
|
|
20
|
+
const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0]!.toFixed(1)},${p[1]!.toFixed(1)}`).join(' ');
|
|
21
|
+
const area = `${line} L${width},${height} L0,${height} Z`;
|
|
22
|
+
return (
|
|
23
|
+
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden>
|
|
24
|
+
<defs>
|
|
25
|
+
<linearGradient id={`sp${id}`} x1="0" y1="0" x2="0" y2="1">
|
|
26
|
+
<stop offset="0%" stopColor={color} stopOpacity="0.28" />
|
|
27
|
+
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
|
28
|
+
</linearGradient>
|
|
29
|
+
</defs>
|
|
30
|
+
<path d={area} fill={`url(#sp${id})`} />
|
|
31
|
+
<path d={line} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
|
|
32
|
+
<circle cx={pts.at(-1)![0]} cy={pts.at(-1)![1]} r="2.4" fill={color} />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { ChartTip } from './ChartTip';
|
|
2
|
+
export { ChartCard } from './ChartCard';
|
|
3
|
+
export type { ChartCardProps } from './ChartCard';
|
|
4
|
+
export { Legend } from './Legend';
|
|
5
|
+
export type { LegendItem } from './Legend';
|
|
6
|
+
export { Sparkline } from './Sparkline';
|
|
7
|
+
export { HBars } from './HBars';
|
|
8
|
+
export type { HBarRow } from './HBars';
|
|
9
|
+
export { BarChart } from './BarChart';
|
|
10
|
+
export type { BarSeries, BarChartGoalLine, BarChartProps } from './BarChart';
|
|
11
|
+
export { Donut } from './Donut';
|
|
12
|
+
export type { DonutSegment, DonutProps } from './Donut';
|
|
13
|
+
export { LineChart } from './LineChart';
|
|
14
|
+
export type { LineSeries, LineChartPoint, LineChartProps } from './LineChart';
|
|
15
|
+
export { ActivityHeatmap } from './ActivityHeatmap';
|
|
16
|
+
export type { HeatmapCell, ActivityHeatmapProps } from './ActivityHeatmap';
|
|
17
|
+
export * from './utils';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const PALETTE = ['#357344', '#b06e22', '#3e5f80', '#7a6ba8', '#8a5a3b', '#5e7d5a', '#a8b5a0', '#c9862f'];
|
|
4
|
+
|
|
5
|
+
export const SERIES_COLORS = [
|
|
6
|
+
'var(--udb-chart-1, #357344)',
|
|
7
|
+
'var(--udb-chart-2, #b06e22)',
|
|
8
|
+
'var(--udb-chart-3, #3e5f80)',
|
|
9
|
+
'var(--udb-chart-4, #7a6ba8)',
|
|
10
|
+
'var(--udb-chart-5, #8a5a3b)',
|
|
11
|
+
'var(--udb-chart-6, #5e7d5a)',
|
|
12
|
+
'var(--udb-chart-7, #a8b5a0)',
|
|
13
|
+
'var(--udb-chart-8, #c9862f)',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function seriesColor(index: number): string {
|
|
17
|
+
return SERIES_COLORS[index % SERIES_COLORS.length]!;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TipRow {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
color?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TipState {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
title: string;
|
|
30
|
+
rows: TipRow[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTip() {
|
|
34
|
+
const [tip, setTip] = useState<TipState | null>(null);
|
|
35
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const show = (e: React.MouseEvent, title: string, rows: TipRow[]) => {
|
|
37
|
+
const rect = hostRef.current?.getBoundingClientRect();
|
|
38
|
+
if (!rect) return;
|
|
39
|
+
setTip({ x: e.clientX - rect.left, y: e.clientY - rect.top, title, rows });
|
|
40
|
+
};
|
|
41
|
+
const hide = () => setTip(null);
|
|
42
|
+
return { tip, hostRef, show, hide };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function niceMax(max: number): number {
|
|
46
|
+
if (max <= 0) return 1;
|
|
47
|
+
const exp = Math.floor(Math.log10(max));
|
|
48
|
+
const base = Math.pow(10, exp);
|
|
49
|
+
const frac = max / base;
|
|
50
|
+
const nice = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10;
|
|
51
|
+
return nice * base;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function gridTicks(max: number, count = 4): number[] {
|
|
55
|
+
const nice = niceMax(max);
|
|
56
|
+
return Array.from({ length: count + 1 }, (_, i) => (nice / count) * i);
|
|
57
|
+
}
|
|
@@ -185,3 +185,9 @@ export type { StatsGridProps, StatItem } from './StatsGrid';
|
|
|
185
185
|
|
|
186
186
|
export { DescriptionList } from './DescriptionList';
|
|
187
187
|
export type { DescriptionListProps, DescriptionItem } from './DescriptionList';
|
|
188
|
+
|
|
189
|
+
export { AuditLogList } from './AuditLogList';
|
|
190
|
+
export type { AuditLogListProps, AuditEntry } from './AuditLogList';
|
|
191
|
+
|
|
192
|
+
// Visual Data Visualization & Charts
|
|
193
|
+
export * from './charts';
|