@w5-ui/react 0.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.
Files changed (164) hide show
  1. package/README.md +9 -0
  2. package/package.json +59 -0
  3. package/src/components/Accordion.tsx +65 -0
  4. package/src/components/ActionMenu.tsx +131 -0
  5. package/src/components/ActivityFeed.tsx +77 -0
  6. package/src/components/Alert.tsx +33 -0
  7. package/src/components/AnnotatedTimeSeries.tsx +148 -0
  8. package/src/components/AppShell.tsx +158 -0
  9. package/src/components/ArcDiagram.tsx +114 -0
  10. package/src/components/AreaChart.tsx +101 -0
  11. package/src/components/Avatar.tsx +37 -0
  12. package/src/components/AvatarGroup.tsx +36 -0
  13. package/src/components/Badge.tsx +36 -0
  14. package/src/components/Banner.tsx +49 -0
  15. package/src/components/BarChart.tsx +106 -0
  16. package/src/components/BeeswarmChart.tsx +142 -0
  17. package/src/components/BoxPlot.tsx +142 -0
  18. package/src/components/Breadcrumb.tsx +44 -0
  19. package/src/components/BubbleChart.tsx +115 -0
  20. package/src/components/BulletChart.tsx +121 -0
  21. package/src/components/BumpChart.tsx +127 -0
  22. package/src/components/Button.tsx +61 -0
  23. package/src/components/CIDRInput.tsx +191 -0
  24. package/src/components/CalendarHeatmap.tsx +117 -0
  25. package/src/components/Callout.tsx +82 -0
  26. package/src/components/CandlestickChart.tsx +113 -0
  27. package/src/components/Card.tsx +27 -0
  28. package/src/components/Carousel.tsx +78 -0
  29. package/src/components/Checkbox.tsx +42 -0
  30. package/src/components/ChordDiagram.tsx +133 -0
  31. package/src/components/CirclePacking.tsx +189 -0
  32. package/src/components/CodeBlock.tsx +27 -0
  33. package/src/components/ColorPicker.tsx +120 -0
  34. package/src/components/ColumnToggle.tsx +106 -0
  35. package/src/components/Combobox.tsx +211 -0
  36. package/src/components/CommandPalette.tsx +168 -0
  37. package/src/components/ConfirmDialog.tsx +114 -0
  38. package/src/components/ContextMenu.tsx +127 -0
  39. package/src/components/ContextualHelp.tsx +69 -0
  40. package/src/components/ContourPlot.tsx +255 -0
  41. package/src/components/CopyButton.tsx +47 -0
  42. package/src/components/CorrelationMatrix.tsx +107 -0
  43. package/src/components/CountUp.tsx +70 -0
  44. package/src/components/CumulativeDistribution.tsx +155 -0
  45. package/src/components/DatePicker.tsx +315 -0
  46. package/src/components/DateRangePicker.tsx +327 -0
  47. package/src/components/Dendrogram.tsx +150 -0
  48. package/src/components/Donut.tsx +101 -0
  49. package/src/components/DotPlot.tsx +195 -0
  50. package/src/components/Drawer.tsx +81 -0
  51. package/src/components/DualAxisChart.tsx +209 -0
  52. package/src/components/DumbbellChart.tsx +102 -0
  53. package/src/components/DurationInput.tsx +144 -0
  54. package/src/components/EmptyState.tsx +40 -0
  55. package/src/components/ErrorBandChart.tsx +124 -0
  56. package/src/components/ExpandableRow.tsx +58 -0
  57. package/src/components/FileUpload.tsx +129 -0
  58. package/src/components/FilterBuilder.tsx +217 -0
  59. package/src/components/FlameGraph.tsx +119 -0
  60. package/src/components/ForceGraph.tsx +149 -0
  61. package/src/components/FunnelChart.tsx +79 -0
  62. package/src/components/GanttChart.tsx +96 -0
  63. package/src/components/Gauge.tsx +71 -0
  64. package/src/components/GroupedList.tsx +125 -0
  65. package/src/components/HealthBar.tsx +25 -0
  66. package/src/components/HeatMap.tsx +106 -0
  67. package/src/components/HexbinChart.tsx +152 -0
  68. package/src/components/Histogram.tsx +121 -0
  69. package/src/components/HorizonChart.tsx +110 -0
  70. package/src/components/HoverCard.tsx +52 -0
  71. package/src/components/IPInput.tsx +147 -0
  72. package/src/components/IcicleChart.tsx +118 -0
  73. package/src/components/InlineEdit.tsx +119 -0
  74. package/src/components/Input.tsx +94 -0
  75. package/src/components/InputGroup.tsx +21 -0
  76. package/src/components/JsonViewer.tsx +116 -0
  77. package/src/components/KVTable.tsx +29 -0
  78. package/src/components/KanbanBoard.tsx +171 -0
  79. package/src/components/Kbd.tsx +22 -0
  80. package/src/components/LineChart.tsx +112 -0
  81. package/src/components/LogViewer.tsx +102 -0
  82. package/src/components/LollipopChart.tsx +110 -0
  83. package/src/components/MACInput.tsx +164 -0
  84. package/src/components/MarimekkoChart.tsx +143 -0
  85. package/src/components/MatrixChart.tsx +118 -0
  86. package/src/components/Menubar.tsx +194 -0
  87. package/src/components/MirroredBarChart.tsx +148 -0
  88. package/src/components/Modal.tsx +87 -0
  89. package/src/components/MultiSelect.tsx +232 -0
  90. package/src/components/NightingaleChart.tsx +101 -0
  91. package/src/components/NotificationPanel.tsx +187 -0
  92. package/src/components/NumberInput.tsx +129 -0
  93. package/src/components/OTPInput.tsx +135 -0
  94. package/src/components/Pagination.tsx +73 -0
  95. package/src/components/ParallelCoordinates.tsx +149 -0
  96. package/src/components/ParetoChart.tsx +159 -0
  97. package/src/components/PasswordInput.tsx +112 -0
  98. package/src/components/PieChart.tsx +109 -0
  99. package/src/components/Pill.tsx +26 -0
  100. package/src/components/PolarHeatmap.tsx +118 -0
  101. package/src/components/Popover.tsx +103 -0
  102. package/src/components/ProgressBar.tsx +33 -0
  103. package/src/components/PunchCard.tsx +93 -0
  104. package/src/components/QuadrantChart.tsx +256 -0
  105. package/src/components/RadarChart.tsx +119 -0
  106. package/src/components/RadialBarChart.tsx +103 -0
  107. package/src/components/RadioGroup.tsx +69 -0
  108. package/src/components/RangeSlider.tsx +116 -0
  109. package/src/components/RankedList.tsx +61 -0
  110. package/src/components/ResizablePanel.tsx +122 -0
  111. package/src/components/RidgelinePlot.tsx +155 -0
  112. package/src/components/SankeyDiagram.tsx +156 -0
  113. package/src/components/ScatterPlot.tsx +96 -0
  114. package/src/components/SegmentedControl.tsx +78 -0
  115. package/src/components/Select.tsx +158 -0
  116. package/src/components/SelectionToolbar.tsx +55 -0
  117. package/src/components/Signal.tsx +19 -0
  118. package/src/components/Skeleton.tsx +37 -0
  119. package/src/components/SkipLink.tsx +19 -0
  120. package/src/components/Slider.tsx +74 -0
  121. package/src/components/SlopeChart.tsx +205 -0
  122. package/src/components/SortableList.tsx +130 -0
  123. package/src/components/SortableTable.tsx +46 -0
  124. package/src/components/Sparkline.tsx +50 -0
  125. package/src/components/SparklineMatrix.tsx +117 -0
  126. package/src/components/Spinner.tsx +8 -0
  127. package/src/components/SplitButton.tsx +151 -0
  128. package/src/components/Spoiler.tsx +47 -0
  129. package/src/components/StackedBarChart.tsx +145 -0
  130. package/src/components/StackedProgress.tsx +47 -0
  131. package/src/components/StarRating.tsx +103 -0
  132. package/src/components/Stat.tsx +48 -0
  133. package/src/components/StatusIndicator.tsx +26 -0
  134. package/src/components/StepChart.tsx +130 -0
  135. package/src/components/Stepper.tsx +51 -0
  136. package/src/components/StreamGraph.tsx +97 -0
  137. package/src/components/StripeChart.tsx +76 -0
  138. package/src/components/SunburstChart.tsx +142 -0
  139. package/src/components/SwitchPortGrid.tsx +59 -0
  140. package/src/components/Tabs.tsx +91 -0
  141. package/src/components/Tag.tsx +37 -0
  142. package/src/components/TagInput.tsx +99 -0
  143. package/src/components/Textarea.tsx +9 -0
  144. package/src/components/ThresholdAreaChart.tsx +113 -0
  145. package/src/components/TimePicker.tsx +144 -0
  146. package/src/components/TimeRange.tsx +152 -0
  147. package/src/components/Timeline.tsx +31 -0
  148. package/src/components/Toast.tsx +72 -0
  149. package/src/components/Toggle.tsx +68 -0
  150. package/src/components/ToggleGroup.tsx +57 -0
  151. package/src/components/Tooltip.tsx +25 -0
  152. package/src/components/TransferList.tsx +227 -0
  153. package/src/components/TreeMap.tsx +126 -0
  154. package/src/components/TreeView.tsx +221 -0
  155. package/src/components/UptimeTimeline.tsx +138 -0
  156. package/src/components/VennDiagram.tsx +151 -0
  157. package/src/components/ViolinPlot.tsx +158 -0
  158. package/src/components/VirtualList.tsx +59 -0
  159. package/src/components/WaffleChart.tsx +79 -0
  160. package/src/components/WaterfallChart.tsx +164 -0
  161. package/src/components/WordCloud.tsx +107 -0
  162. package/src/icons.tsx +241 -0
  163. package/src/index.ts +160 -0
  164. package/src/styles.css +2 -0
@@ -0,0 +1,255 @@
1
+ export interface ContourPoint {
2
+ x: number
3
+ y: number
4
+ }
5
+
6
+ export interface ContourPlotProps {
7
+ points: ContourPoint[]
8
+ xRange?: [number, number]
9
+ yRange?: [number, number]
10
+ height?: number
11
+ bandwidth?: number
12
+ color?: string
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const VW = 340
17
+ const PAD_L = 28
18
+ const PAD_R = 8
19
+ const PAD_T = 8
20
+ const PAD_B = 20
21
+ const GW = 38
22
+ const GH = 28
23
+ const LEVELS = [0.12, 0.3, 0.54, 0.78]
24
+
25
+ function buildGrid(pts: Array<{ gx: number; gy: number }>, bw: number): number[][] {
26
+ const bw2 = bw * bw
27
+ const g: number[][] = Array.from({ length: GH + 1 }, () =>
28
+ Array.from({ length: GW + 1 }, () => 0),
29
+ )
30
+ for (let r = 0; r <= GH; r++) {
31
+ for (let c = 0; c <= GW; c++) {
32
+ let d = 0
33
+ for (const p of pts) {
34
+ const dx = c - p.gx,
35
+ dy = r - p.gy
36
+ d += Math.exp((-0.5 * (dx * dx + dy * dy)) / bw2)
37
+ }
38
+ g[r][c] = d
39
+ }
40
+ }
41
+ let mx = 0
42
+ for (const row of g) for (const v of row) if (v > mx) mx = v
43
+ if (mx > 0) for (const row of g) for (let c = 0; c < row.length; c++) row[c] /= mx
44
+ return g
45
+ }
46
+
47
+ function edgeCross(v0: number, v1: number, t: number): number {
48
+ const d = v1 - v0
49
+ return Math.abs(d) < 1e-9 ? 0.5 : Math.max(0, Math.min(1, (t - v0) / d))
50
+ }
51
+
52
+ type P = [number, number]
53
+
54
+ function marchCell(tl: number, tr: number, bl: number, br: number, t: number): Array<[P, P]> {
55
+ const a = tl >= t ? 1 : 0
56
+ const b = tr >= t ? 1 : 0
57
+ const c = bl >= t ? 1 : 0
58
+ const d = br >= t ? 1 : 0
59
+ const T = (): P => [edgeCross(tl, tr, t), 0]
60
+ const R = (): P => [1, edgeCross(tr, br, t)]
61
+ const B = (): P => [edgeCross(bl, br, t), 1]
62
+ const L = (): P => [0, edgeCross(tl, bl, t)]
63
+ switch ((a << 3) | (b << 2) | (c << 1) | d) {
64
+ case 0:
65
+ case 15:
66
+ return []
67
+ case 1:
68
+ return [[R(), B()]]
69
+ case 2:
70
+ return [[B(), L()]]
71
+ case 3:
72
+ return [[L(), R()]]
73
+ case 4:
74
+ return [[T(), R()]]
75
+ case 5:
76
+ return [[T(), B()]]
77
+ case 6: {
78
+ const m = (tl + tr + bl + br) / 4
79
+ return m >= t
80
+ ? [
81
+ [T(), L()],
82
+ [R(), B()],
83
+ ]
84
+ : [
85
+ [T(), R()],
86
+ [L(), B()],
87
+ ]
88
+ }
89
+ case 7:
90
+ return [[T(), L()]]
91
+ case 8:
92
+ return [[T(), L()]]
93
+ case 9: {
94
+ const m = (tl + tr + bl + br) / 4
95
+ return m >= t
96
+ ? [
97
+ [T(), R()],
98
+ [L(), B()],
99
+ ]
100
+ : [
101
+ [T(), L()],
102
+ [R(), B()],
103
+ ]
104
+ }
105
+ case 10:
106
+ return [[T(), B()]]
107
+ case 11:
108
+ return [[T(), R()]]
109
+ case 12:
110
+ return [[L(), R()]]
111
+ case 13:
112
+ return [[B(), L()]]
113
+ case 14:
114
+ return [[R(), B()]]
115
+ }
116
+ return []
117
+ }
118
+
119
+ export function ContourPlot({
120
+ points,
121
+ xRange,
122
+ yRange,
123
+ height = 200,
124
+ bandwidth = 3.5,
125
+ color = '#006FFF',
126
+ ariaLabel = 'Contour plot',
127
+ }: ContourPlotProps) {
128
+ if (!points.length) return null
129
+
130
+ const PW = VW - PAD_L - PAD_R
131
+ const PH = height - PAD_T - PAD_B
132
+
133
+ const xs = points.map((p) => p.x)
134
+ const ys = points.map((p) => p.y)
135
+ const xMin = xRange?.[0] ?? Math.min(...xs)
136
+ const xMax = xRange?.[1] ?? Math.max(...xs)
137
+ const yMin = yRange?.[0] ?? Math.min(...ys)
138
+ const yMax = yRange?.[1] ?? Math.max(...ys)
139
+ const xSpan = xMax - xMin || 1
140
+ const ySpan = yMax - yMin || 1
141
+
142
+ const gridPts = points.map((p) => ({
143
+ gx: ((p.x - xMin) / xSpan) * GW,
144
+ gy: (1 - (p.y - yMin) / ySpan) * GH,
145
+ }))
146
+ const grid = buildGrid(gridPts, bandwidth)
147
+
148
+ const cellW = PW / GW
149
+ const cellH = PH / GH
150
+ const svgX = (col: number, lx: number) => PAD_L + (col + lx) * cellW
151
+ const svgY = (row: number, ly: number) => PAD_T + (row + ly) * cellH
152
+
153
+ const tyY = (v: number) => PAD_T + (1 - (v - yMin) / ySpan) * PH
154
+ const txX = (v: number) => PAD_L + ((v - xMin) / xSpan) * PW
155
+ const yTicks = [yMin, (yMin + yMax) / 2, yMax]
156
+ const xTicks = [xMin, (xMin + xMax) / 2, xMax]
157
+
158
+ return (
159
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
160
+ <svg
161
+ viewBox={`0 0 ${VW} ${height}`}
162
+ style={{ width: '100%', height: 'auto', display: 'block' }}
163
+ aria-hidden="true"
164
+ focusable="false"
165
+ >
166
+ {yTicks.map((v, i) => (
167
+ <g key={i}>
168
+ <line
169
+ x1={PAD_L}
170
+ y1={tyY(v)}
171
+ x2={PAD_L + PW}
172
+ y2={tyY(v)}
173
+ stroke="rgba(255,255,255,0.06)"
174
+ strokeWidth={1}
175
+ />
176
+ <text
177
+ x={PAD_L - 4}
178
+ y={tyY(v) + 3}
179
+ fill="#6E7079"
180
+ fontSize={8}
181
+ textAnchor="end"
182
+ fontFamily="inherit"
183
+ >
184
+ {Math.round(v)}
185
+ </text>
186
+ </g>
187
+ ))}
188
+
189
+ {points.map((p, i) => (
190
+ <circle
191
+ key={i}
192
+ cx={PAD_L + ((p.x - xMin) / xSpan) * PW}
193
+ cy={PAD_T + (1 - (p.y - yMin) / ySpan) * PH}
194
+ r={1.5}
195
+ fill={color}
196
+ opacity={0.2}
197
+ />
198
+ ))}
199
+
200
+ {LEVELS.map((t, li) => {
201
+ const opacity = 0.28 + li * 0.18
202
+ const sw = 0.55 + li * 0.28
203
+ const lines: Array<{ key: string; x1: number; y1: number; x2: number; y2: number }> = []
204
+ for (let r = 0; r < GH; r++) {
205
+ for (let col = 0; col < GW; col++) {
206
+ const segs = marchCell(
207
+ grid[r][col],
208
+ grid[r][col + 1],
209
+ grid[r + 1][col],
210
+ grid[r + 1][col + 1],
211
+ t,
212
+ )
213
+ segs.forEach(([[lx0, ly0], [lx1, ly1]], si) => {
214
+ lines.push({
215
+ key: `${r}-${col}-${si}`,
216
+ x1: svgX(col, lx0),
217
+ y1: svgY(r, ly0),
218
+ x2: svgX(col, lx1),
219
+ y2: svgY(r, ly1),
220
+ })
221
+ })
222
+ }
223
+ }
224
+ return lines.map((s) => (
225
+ <line
226
+ key={`${li}-${s.key}`}
227
+ x1={s.x1}
228
+ y1={s.y1}
229
+ x2={s.x2}
230
+ y2={s.y2}
231
+ stroke={color}
232
+ strokeWidth={sw}
233
+ opacity={opacity}
234
+ strokeLinecap="round"
235
+ />
236
+ ))
237
+ })}
238
+
239
+ {xTicks.map((v, i) => (
240
+ <text
241
+ key={i}
242
+ x={txX(v)}
243
+ y={height - 4}
244
+ fill="#6E7079"
245
+ fontSize={8}
246
+ textAnchor="middle"
247
+ fontFamily="inherit"
248
+ >
249
+ {Math.round(v)}
250
+ </text>
251
+ ))}
252
+ </svg>
253
+ </div>
254
+ )
255
+ }
@@ -0,0 +1,47 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ const CopyIcon = () => (
4
+ <svg viewBox="0 0 14 14" width="14" height="14" fill="none" aria-hidden="true" focusable={false}>
5
+ <rect x="4" y="1" width="9" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
6
+ <rect x="1" y="4" width="9" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
7
+ </svg>
8
+ )
9
+
10
+ const CheckIcon = () => (
11
+ <svg viewBox="0 0 14 14" width="14" height="14" fill="none" aria-hidden="true" focusable={false}>
12
+ <path
13
+ d="M2.5 7.5l3.5 3.5L12 3"
14
+ stroke="currentColor"
15
+ strokeWidth="1.5"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </svg>
20
+ )
21
+
22
+ export interface CopyButtonProps {
23
+ text: string
24
+ label?: string
25
+ }
26
+
27
+ export function CopyButton({ text, label = 'Copy' }: CopyButtonProps) {
28
+ const [copied, setCopied] = useState(false)
29
+
30
+ const handleCopy = useCallback(() => {
31
+ navigator.clipboard.writeText(text).then(() => {
32
+ setCopied(true)
33
+ setTimeout(() => setCopied(false), 2000)
34
+ })
35
+ }, [text])
36
+
37
+ return (
38
+ <button
39
+ type="button"
40
+ className={`copy-btn${copied ? ' copy-btn--done' : ''}`}
41
+ aria-label={copied ? 'Copied!' : label}
42
+ onClick={handleCopy}
43
+ >
44
+ {copied ? <CheckIcon /> : <CopyIcon />}
45
+ </button>
46
+ )
47
+ }
@@ -0,0 +1,107 @@
1
+ export interface CorrelationMatrixProps {
2
+ labels: string[]
3
+ data: number[][]
4
+ cellSize?: number
5
+ ariaLabel?: string
6
+ }
7
+
8
+ const LABEL_W = 68
9
+ const LABEL_H = 26
10
+ const DARK = [26, 29, 43] as const
11
+ const BLUE = [0, 111, 255] as const
12
+ const RED = [255, 59, 48] as const
13
+
14
+ function lerp(a: number, b: number, t: number): number {
15
+ return Math.round(a + (b - a) * t)
16
+ }
17
+
18
+ function cellColor(v: number): string {
19
+ const t = Math.max(0, Math.min(1, Math.abs(v)))
20
+ const [r0, g0, b0] = DARK
21
+ if (v >= 0) {
22
+ const [r1, g1, b1] = BLUE
23
+ return `rgb(${lerp(r0, r1, t)},${lerp(g0, g1, t)},${lerp(b0, b1, t)})`
24
+ }
25
+ const [r1, g1, b1] = RED
26
+ return `rgb(${lerp(r0, r1, t)},${lerp(g0, g1, t)},${lerp(b0, b1, t)})`
27
+ }
28
+
29
+ export function CorrelationMatrix({
30
+ labels,
31
+ data,
32
+ cellSize = 52,
33
+ ariaLabel = 'Correlation matrix',
34
+ }: CorrelationMatrixProps) {
35
+ const n = labels.length
36
+ const svgW = LABEL_W + n * cellSize
37
+ const svgH = LABEL_H + n * cellSize
38
+
39
+ return (
40
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
41
+ <svg
42
+ viewBox={`0 0 ${svgW} ${svgH}`}
43
+ style={{ width: '100%', height: 'auto', display: 'block' }}
44
+ aria-hidden="true"
45
+ focusable="false"
46
+ >
47
+ {labels.map((label, j) => (
48
+ <text
49
+ key={j}
50
+ x={LABEL_W + j * cellSize + cellSize / 2}
51
+ y={LABEL_H - 4}
52
+ textAnchor="middle"
53
+ fontSize={9}
54
+ fill="#A4A7B5"
55
+ fontFamily="inherit"
56
+ >
57
+ {label}
58
+ </text>
59
+ ))}
60
+ {labels.map((label, i) => (
61
+ <text
62
+ key={i}
63
+ x={LABEL_W - 4}
64
+ y={LABEL_H + i * cellSize + cellSize / 2}
65
+ textAnchor="end"
66
+ dominantBaseline="middle"
67
+ fontSize={9}
68
+ fill="#A4A7B5"
69
+ fontFamily="inherit"
70
+ >
71
+ {label}
72
+ </text>
73
+ ))}
74
+ {data.map((row, i) =>
75
+ row.map((v, j) => {
76
+ const x = LABEL_W + j * cellSize
77
+ const y = LABEL_H + i * cellSize
78
+ return (
79
+ <g key={`${i}-${j}`}>
80
+ <rect
81
+ x={x + 1}
82
+ y={y + 1}
83
+ width={cellSize - 2}
84
+ height={cellSize - 2}
85
+ fill={cellColor(v)}
86
+ rx={3}
87
+ />
88
+ <text
89
+ x={x + cellSize / 2}
90
+ y={y + cellSize / 2}
91
+ textAnchor="middle"
92
+ dominantBaseline="middle"
93
+ fontSize={10}
94
+ fill={Math.abs(v) >= 0.5 ? '#fff' : '#A4A7B5'}
95
+ fontFamily="inherit"
96
+ fontWeight={i === j ? '700' : '400'}
97
+ >
98
+ {v.toFixed(2)}
99
+ </text>
100
+ </g>
101
+ )
102
+ }),
103
+ )}
104
+ </svg>
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,70 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+
3
+ export interface CountUpProps {
4
+ from?: number
5
+ to: number
6
+ duration?: number
7
+ decimals?: number
8
+ prefix?: string
9
+ suffix?: string
10
+ separator?: string
11
+ className?: string
12
+ }
13
+
14
+ function easeOut(t: number): number {
15
+ return 1 - Math.pow(1 - t, 3)
16
+ }
17
+
18
+ function fmt(n: number, decimals: number, separator: string): string {
19
+ const fixed = n.toFixed(decimals)
20
+ if (!separator) return fixed
21
+ const [int, dec] = fixed.split('.')
22
+ const intFmt = int.replace(/\B(?=(\d{3})+(?!\d))/g, separator)
23
+ return dec !== undefined ? `${intFmt}.${dec}` : intFmt
24
+ }
25
+
26
+ export function CountUp({
27
+ from = 0,
28
+ to,
29
+ duration = 1200,
30
+ decimals = 0,
31
+ prefix = '',
32
+ suffix = '',
33
+ separator = ',',
34
+ className,
35
+ }: CountUpProps) {
36
+ const [current, setCurrent] = useState(from)
37
+ const rafRef = useRef<number>(0)
38
+ const reduced =
39
+ typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
40
+
41
+ useEffect(() => {
42
+ if (reduced) {
43
+ setCurrent(to)
44
+ return
45
+ }
46
+ const t0 = performance.now()
47
+ const range = to - from
48
+ function tick(now: number) {
49
+ const t = Math.min((now - t0) / duration, 1)
50
+ setCurrent(from + range * easeOut(t))
51
+ if (t < 1) rafRef.current = requestAnimationFrame(tick)
52
+ }
53
+ rafRef.current = requestAnimationFrame(tick)
54
+ return () => cancelAnimationFrame(rafRef.current)
55
+ }, [from, to, duration, reduced])
56
+
57
+ const display = fmt(current, decimals, separator)
58
+ const label = `${prefix}${fmt(to, decimals, separator)}${suffix}`
59
+
60
+ return (
61
+ <span className={className}>
62
+ <span aria-hidden="true">
63
+ {prefix}
64
+ {display}
65
+ {suffix}
66
+ </span>
67
+ <span className="sr-only">{label}</span>
68
+ </span>
69
+ )
70
+ }
@@ -0,0 +1,155 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface CdfSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface CumulativeDistributionProps {
10
+ series: CdfSeries[]
11
+ guides?: number[]
12
+ height?: number
13
+ unit?: string
14
+ ariaLabel?: string
15
+ }
16
+
17
+ const W = 400
18
+ const PL = 38
19
+ const PR = 12
20
+ const PT = 14
21
+ const PB = 28
22
+
23
+ export function CumulativeDistribution({
24
+ series,
25
+ guides = [50, 95],
26
+ height = 160,
27
+ unit = '',
28
+ ariaLabel = 'Cumulative distribution chart',
29
+ }: CumulativeDistributionProps) {
30
+ const layout = useMemo(() => {
31
+ const allVals = series.flatMap((s) => s.values)
32
+ if (!allVals.length) return null
33
+
34
+ const xMin = Math.min(...allVals)
35
+ const xMax = Math.max(...allVals)
36
+ const xRange = xMax - xMin || 1
37
+ const cW = W - PL - PR
38
+ const cH = height - PT - PB
39
+
40
+ const toX = (v: number) => PL + ((v - xMin) / xRange) * cW
41
+ const toY = (p: number) => PT + (1 - p) * cH
42
+
43
+ const paths = series.map((s) => {
44
+ const sorted = [...s.values].sort((a, b) => a - b)
45
+ const n = sorted.length
46
+ let d = `M${toX(xMin).toFixed(1)},${toY(0).toFixed(1)}`
47
+ sorted.forEach((v, i) => {
48
+ d += ` H${toX(v).toFixed(1)} V${toY((i + 1) / n).toFixed(1)}`
49
+ })
50
+ d += ` H${toX(xMax).toFixed(1)}`
51
+ return { label: s.label, color: s.color, d }
52
+ })
53
+
54
+ const yTicks = [0, 0.25, 0.5, 0.75, 1].map((p) => ({
55
+ y: toY(p),
56
+ label: `${Math.round(p * 100)}%`,
57
+ }))
58
+
59
+ const step = xRange / 4
60
+ const xTicks = Array.from({ length: 5 }, (_, i) => {
61
+ const v = xMin + step * i
62
+ return { x: toX(v), label: `${Math.round(v)}${unit}` }
63
+ })
64
+
65
+ const guideLines = guides.map((g) => ({ p: g, y: toY(g / 100) }))
66
+
67
+ return { paths, yTicks, xTicks, guideLines }
68
+ }, [series, guides, height, unit])
69
+
70
+ if (!layout) return null
71
+ const { paths, yTicks, xTicks, guideLines } = layout
72
+
73
+ return (
74
+ <div role="img" aria-label={ariaLabel} style={{ lineHeight: 0 }}>
75
+ <svg
76
+ viewBox={`0 0 ${W} ${height}`}
77
+ width="100%"
78
+ height={height}
79
+ aria-hidden="true"
80
+ focusable="false"
81
+ >
82
+ {yTicks.map((t) => (
83
+ <line
84
+ key={t.label}
85
+ x1={PL}
86
+ x2={W - PR}
87
+ y1={t.y}
88
+ y2={t.y}
89
+ stroke="rgba(255,255,255,0.06)"
90
+ strokeWidth={1}
91
+ />
92
+ ))}
93
+ {guideLines.map((g) => (
94
+ <g key={g.p}>
95
+ <line
96
+ x1={PL}
97
+ x2={W - PR}
98
+ y1={g.y}
99
+ y2={g.y}
100
+ stroke="rgba(255,255,255,0.18)"
101
+ strokeWidth={1}
102
+ strokeDasharray="4 3"
103
+ />
104
+ <text
105
+ x={W - PR - 2}
106
+ y={g.y - 3}
107
+ textAnchor="end"
108
+ fill="#6E7079"
109
+ fontSize={9}
110
+ fontFamily="inherit"
111
+ >
112
+ p{g.p}
113
+ </text>
114
+ </g>
115
+ ))}
116
+ {paths.map((s) => (
117
+ <path
118
+ key={s.label}
119
+ d={s.d}
120
+ fill="none"
121
+ stroke={s.color}
122
+ strokeWidth={2}
123
+ strokeLinejoin="round"
124
+ />
125
+ ))}
126
+ {yTicks.map((t) => (
127
+ <text
128
+ key={t.label}
129
+ x={PL - 4}
130
+ y={t.y + 4}
131
+ textAnchor="end"
132
+ fill="#6E7079"
133
+ fontSize={9}
134
+ fontFamily="inherit"
135
+ >
136
+ {t.label}
137
+ </text>
138
+ ))}
139
+ {xTicks.map((t, i) => (
140
+ <text
141
+ key={i}
142
+ x={t.x}
143
+ y={height - PB + 14}
144
+ textAnchor="middle"
145
+ fill="#6E7079"
146
+ fontSize={9}
147
+ fontFamily="inherit"
148
+ >
149
+ {t.label}
150
+ </text>
151
+ ))}
152
+ </svg>
153
+ </div>
154
+ )
155
+ }