@umituz/web-design-system 6.0.2 → 6.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.
Files changed (30) hide show
  1. package/dist/styles.css +1 -1
  2. package/package.json +3 -1
  3. package/src/__tests__/charts/ActivityHeatmap.test.tsx +48 -0
  4. package/src/__tests__/charts/BarChart.test.tsx +91 -0
  5. package/src/__tests__/charts/ChartPrimitives.test.tsx +90 -0
  6. package/src/__tests__/charts/Donut.test.tsx +51 -0
  7. package/src/__tests__/charts/LineChart.test.tsx +81 -0
  8. package/src/__tests__/charts/ssr.test.tsx +51 -0
  9. package/src/__tests__/layout-components.test.tsx +100 -0
  10. package/src/presentation/molecules/TabNav.tsx +46 -0
  11. package/src/presentation/molecules/index.ts +3 -0
  12. package/src/presentation/organisms/AuditLogList.tsx +58 -0
  13. package/src/presentation/organisms/charts/ActivityHeatmap.tsx +58 -0
  14. package/src/presentation/organisms/charts/BarChart.tsx +143 -0
  15. package/src/presentation/organisms/charts/ChartCard.tsx +39 -0
  16. package/src/presentation/organisms/charts/ChartTip.tsx +18 -0
  17. package/src/presentation/organisms/charts/Donut.tsx +100 -0
  18. package/src/presentation/organisms/charts/HBars.tsx +31 -0
  19. package/src/presentation/organisms/charts/Legend.tsx +21 -0
  20. package/src/presentation/organisms/charts/LineChart.tsx +244 -0
  21. package/src/presentation/organisms/charts/Sparkline.tsx +35 -0
  22. package/src/presentation/organisms/charts/index.ts +17 -0
  23. package/src/presentation/organisms/charts/utils.ts +57 -0
  24. package/src/presentation/organisms/index.ts +6 -0
  25. package/src/presentation/templates/PageHeader.tsx +103 -33
  26. package/src/presentation/templates/SectionHeader.tsx +54 -0
  27. package/src/presentation/templates/index.ts +4 -1
  28. package/src/styles/app.css +0 -1
  29. package/src/styles/charts.css +455 -0
  30. package/src/styles/compile.css +1 -0
@@ -0,0 +1,143 @@
1
+ import { useState } from 'react';
2
+ import { seriesColor } from './utils';
3
+
4
+ export interface BarSeries {
5
+ key?: string;
6
+ label: string;
7
+ values?: readonly number[];
8
+ data?: readonly number[];
9
+ tone?: string;
10
+ color?: string;
11
+ accents?: readonly (string | undefined)[];
12
+ }
13
+
14
+ export interface BarChartGoalLine {
15
+ value: number;
16
+ label?: string;
17
+ tone?: string;
18
+ }
19
+
20
+ export interface BarChartProps {
21
+ labels: readonly string[];
22
+ series: BarSeries[];
23
+ stacking?: 'grouped' | 'stacked';
24
+ totalLabel?: string;
25
+ height?: number;
26
+ formatValue?: (v: number) => string;
27
+ formatLabel?: (l: string) => string;
28
+ goalLine?: BarChartGoalLine;
29
+ }
30
+
31
+ export function BarChart({
32
+ labels,
33
+ series,
34
+ stacking = 'grouped',
35
+ totalLabel = 'Σ',
36
+ height = 130,
37
+ formatValue = (v: number) => String(v),
38
+ formatLabel = (l: string) => l,
39
+ goalLine,
40
+ }: BarChartProps) {
41
+ const [hover, setHover] = useState<number | null>(null);
42
+ const vals = (s: BarSeries) => s.values ?? s.data ?? [];
43
+ const columnTotals = labels.map((_, i) => series.reduce((sum, s) => sum + (vals(s)[i] ?? 0), 0));
44
+ const max =
45
+ stacking === 'stacked'
46
+ ? Math.max(1, ...columnTotals, goalLine?.value ?? 0)
47
+ : Math.max(1, ...series.flatMap((s) => vals(s)), goalLine?.value ?? 0);
48
+ const toneOf = (s: BarSeries, i: number) => s.tone ?? s.color ?? seriesColor(i);
49
+ const accentOf = (s: BarSeries, si: number, i: number) => s.accents?.[i] ?? toneOf(s, si);
50
+ const goalPct = goalLine !== undefined ? Math.min(100, (goalLine.value / max) * 100) : null;
51
+ return (
52
+ <div className="udb-barchart" style={{ height }}>
53
+ {goalLine !== undefined && goalPct !== null && (
54
+ <div
55
+ className="udb-barchart__goal"
56
+ style={{
57
+ bottom: `${goalPct}%`,
58
+ ...(goalLine.tone !== undefined ? { borderTopColor: goalLine.tone } : {}),
59
+ }}
60
+ title={goalLine.label ?? formatValue(goalLine.value)}
61
+ aria-hidden="true"
62
+ >
63
+ {goalLine.label !== undefined && (
64
+ <span
65
+ className="udb-barchart__goallabel"
66
+ style={
67
+ goalLine.tone !== undefined
68
+ ? { color: goalLine.tone, borderColor: goalLine.tone }
69
+ : undefined
70
+ }
71
+ >
72
+ {goalLine.label}
73
+ </span>
74
+ )}
75
+ </div>
76
+ )}
77
+ {labels.map((label, i) => (
78
+ <div
79
+ key={label}
80
+ className="udb-barchart__col"
81
+ onMouseEnter={() => setHover(i)}
82
+ onMouseLeave={() => setHover(null)}
83
+ >
84
+ {stacking === 'stacked' ? (
85
+ <div className="udb-barchart__bars udb-barchart__bars--stacked">
86
+ {series.map((s, si) => {
87
+ const v = vals(s)[i] ?? 0;
88
+ return (
89
+ <div
90
+ key={s.key ?? s.label}
91
+ className="udb-barchart__bar"
92
+ style={{
93
+ height: `${(v / max) * 100}%`,
94
+ background: accentOf(s, si, i),
95
+ opacity: hover === null || hover === i ? 1 : 0.4,
96
+ ...(v === 0 ? { minHeight: 0 } : {}),
97
+ }}
98
+ />
99
+ );
100
+ })}
101
+ </div>
102
+ ) : (
103
+ <div className="udb-barchart__bars">
104
+ {series.map((s, si) => (
105
+ <div
106
+ key={s.key ?? s.label}
107
+ className="udb-barchart__bar"
108
+ style={{
109
+ height: `${Math.max(2, ((vals(s)[i] ?? 0) / max) * 100)}%`,
110
+ background: accentOf(s, si, i),
111
+ opacity: hover === null || hover === i ? 1 : 0.4,
112
+ }}
113
+ />
114
+ ))}
115
+ </div>
116
+ )}
117
+ {hover === i && (
118
+ <div className="udb-barchart__tip">
119
+ <div className="udb-barchart__tiptitle">{formatLabel(label)}</div>
120
+ {series.map((s, si) => (
121
+ <div key={s.key ?? s.label} className="udb-barchart__tiprow">
122
+ <span className="udb-barchart__tiplabel">
123
+ <span className="udb-chartlegend__dot" style={{ background: accentOf(s, si, i) }} />
124
+ {s.label}
125
+ </span>
126
+ <b>{formatValue(vals(s)[i] ?? 0)}</b>
127
+ </div>
128
+ ))}
129
+ {stacking === 'stacked' && (
130
+ <div className="udb-barchart__tiprow udb-barchart__tiprow--total">
131
+ <span className="udb-barchart__tiplabel">
132
+ <b>{totalLabel}</b>
133
+ </span>
134
+ <b>{formatValue(columnTotals[i] ?? 0)}</b>
135
+ </div>
136
+ )}
137
+ </div>
138
+ )}
139
+ </div>
140
+ ))}
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { TipState } from './utils';
3
+ import { ChartTip } from './ChartTip';
4
+
5
+ export interface ChartCardProps {
6
+ title: string;
7
+ subtitle?: string;
8
+ legend?: ReactNode;
9
+ actions?: ReactNode;
10
+ children: ReactNode;
11
+ tip: TipState | null;
12
+ hostRef?: React.Ref<HTMLDivElement>;
13
+ onMouseLeave?: () => void;
14
+ bare?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ export function ChartCard({ title, subtitle, legend, actions, children, tip, hostRef, onMouseLeave, bare, className }: ChartCardProps) {
19
+ return (
20
+ <div className={['udb-chartcard', className].filter(Boolean).join(' ')}>
21
+ <div className="udb-chartcard__head">
22
+ <div className="udb-chartcard__heading">
23
+ <div className="udb-chartcard__title">{title}</div>
24
+ {subtitle && <div className="udb-chartcard__sub">{subtitle}</div>}
25
+ </div>
26
+ <div className="udb-chartcard__tools">
27
+ {legend}
28
+ {actions}
29
+ </div>
30
+ </div>
31
+ <div className="udb-chartcard__body" style={bare ? { padding: 0 } : undefined}>
32
+ <div ref={hostRef} className="udb-chartcard__plot" onMouseLeave={onMouseLeave}>
33
+ {children}
34
+ {tip && <ChartTip tip={tip} />}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -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';