@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,47 @@
1
+ export interface StackedProgressSegment {
2
+ label: string
3
+ value: number
4
+ color: string
5
+ }
6
+
7
+ export interface StackedProgressProps {
8
+ segments: StackedProgressSegment[]
9
+ total?: number
10
+ ariaLabel?: string
11
+ showLegend?: boolean
12
+ }
13
+
14
+ export function StackedProgress({
15
+ segments,
16
+ total,
17
+ ariaLabel,
18
+ showLegend = true,
19
+ }: StackedProgressProps) {
20
+ const sum = total ?? segments.reduce((acc, s) => acc + s.value, 0)
21
+ const text =
22
+ ariaLabel ?? segments.map((s) => `${s.label} ${Math.round((s.value / sum) * 100)}%`).join(', ')
23
+ return (
24
+ <div className="sp" role="img" aria-label={text}>
25
+ <div className="sp-bar" aria-hidden="true">
26
+ {segments.map(({ label, value, color }) => (
27
+ <div
28
+ key={label}
29
+ className="sp-seg"
30
+ style={{ width: `${(value / sum) * 100}%`, background: color }}
31
+ />
32
+ ))}
33
+ </div>
34
+ {showLegend && (
35
+ <div className="sp-legend" aria-hidden="true">
36
+ {segments.map(({ label, value, color }) => (
37
+ <span key={label} className="sp-item">
38
+ <span className="sp-dot" style={{ background: color }} />
39
+ {label}
40
+ <span className="sp-pct">{Math.round((value / sum) * 100)}%</span>
41
+ </span>
42
+ ))}
43
+ </div>
44
+ )}
45
+ </div>
46
+ )
47
+ }
@@ -0,0 +1,103 @@
1
+ import { useId, useState } from 'react'
2
+
3
+ export interface StarRatingProps {
4
+ label: string
5
+ value?: number
6
+ defaultValue?: number
7
+ max?: number
8
+ readOnly?: boolean
9
+ size?: 'sm' | 'md'
10
+ onChange?: (value: number) => void
11
+ className?: string
12
+ }
13
+
14
+ function StarIcon({ filled }: { filled: boolean }) {
15
+ return (
16
+ <svg
17
+ aria-hidden="true"
18
+ focusable="false"
19
+ viewBox="0 0 24 24"
20
+ fill={filled ? 'currentColor' : 'none'}
21
+ stroke="currentColor"
22
+ strokeWidth="1.5"
23
+ >
24
+ <polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
25
+ </svg>
26
+ )
27
+ }
28
+
29
+ export function StarRating({
30
+ label,
31
+ value: valueProp,
32
+ defaultValue = 0,
33
+ max = 5,
34
+ readOnly = false,
35
+ size = 'md',
36
+ onChange,
37
+ className,
38
+ }: StarRatingProps) {
39
+ const id = useId()
40
+ const [internalValue, setInternalValue] = useState(defaultValue)
41
+ const [hovered, setHovered] = useState(0)
42
+ const value = valueProp !== undefined ? valueProp : internalValue
43
+ const active = hovered || value
44
+
45
+ const cls = [
46
+ 'star-rating',
47
+ size === 'sm' && 'star-rating--sm',
48
+ readOnly && 'star-rating--readonly',
49
+ className,
50
+ ]
51
+ .filter(Boolean)
52
+ .join(' ')
53
+
54
+ if (readOnly) {
55
+ return (
56
+ <span className={cls} role="img" aria-label={`${value} out of ${max} stars`}>
57
+ {Array.from({ length: max }, (_, i) => (
58
+ <span
59
+ key={i}
60
+ className={i < value ? 'star-icon star-icon--on' : 'star-icon'}
61
+ aria-hidden="true"
62
+ >
63
+ <StarIcon filled={i < value} />
64
+ </span>
65
+ ))}
66
+ </span>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <fieldset className={cls}>
72
+ <legend className="sr-only">{label}</legend>
73
+ {Array.from({ length: max }, (_, i) => {
74
+ const star = i + 1
75
+ const on = star <= active
76
+ return (
77
+ <label
78
+ key={star}
79
+ htmlFor={`${id}-${star}`}
80
+ className={on ? 'star-label star-label--on' : 'star-label'}
81
+ onMouseEnter={() => setHovered(star)}
82
+ onMouseLeave={() => setHovered(0)}
83
+ >
84
+ <input
85
+ type="radio"
86
+ id={`${id}-${star}`}
87
+ name={`${id}`}
88
+ value={String(star)}
89
+ checked={value === star}
90
+ onChange={() => {
91
+ if (valueProp === undefined) setInternalValue(star)
92
+ onChange?.(star)
93
+ }}
94
+ className="star-radio"
95
+ aria-label={`${star} out of ${max}`}
96
+ />
97
+ <StarIcon filled={on} />
98
+ </label>
99
+ )
100
+ })}
101
+ </fieldset>
102
+ )
103
+ }
@@ -0,0 +1,48 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+ import { Card } from './Card.js'
3
+
4
+ export interface StatProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {
5
+ label: string
6
+ value: ReactNode
7
+ unit?: string
8
+ sub?: string
9
+ delta?: string
10
+ deltaDir?: 'up' | 'down' | 'neutral'
11
+ span?: number
12
+ color?: string
13
+ }
14
+
15
+ export function Stat({
16
+ label,
17
+ value,
18
+ unit,
19
+ sub,
20
+ delta,
21
+ deltaDir = 'neutral',
22
+ span,
23
+ color,
24
+ ...rest
25
+ }: StatProps) {
26
+ const deltaClass = deltaDir === 'up' ? 'delta-up' : deltaDir === 'down' ? 'delta-down' : ''
27
+ return (
28
+ <Card span={span} {...rest}>
29
+ <h3>{label}</h3>
30
+ <div className="stat" style={color ? { color } : undefined}>
31
+ {value}
32
+ {unit && <span className="unit">{unit}</span>}
33
+ </div>
34
+ {(sub || delta) && (
35
+ <div className="submeta">
36
+ {sub}
37
+ {sub && delta && ' · '}
38
+ {delta && (
39
+ <>
40
+ <span className="sr-only">Trend: </span>
41
+ <span className={deltaClass || undefined}>{delta}</span>
42
+ </>
43
+ )}
44
+ </div>
45
+ )}
46
+ </Card>
47
+ )
48
+ }
@@ -0,0 +1,26 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ export interface StatusIndicatorProps {
4
+ color: string
5
+ text: string
6
+ textColor?: string
7
+ }
8
+
9
+ export function StatusIndicator({ color, text, textColor }: StatusIndicatorProps) {
10
+ const style: CSSProperties = {
11
+ display: 'inline-flex',
12
+ alignItems: 'center',
13
+ gap: 6,
14
+ color: textColor ?? color,
15
+ fontSize: 12,
16
+ }
17
+ return (
18
+ <span style={style}>
19
+ <span
20
+ aria-hidden="true"
21
+ style={{ width: 6, height: 6, borderRadius: '50%', background: color }}
22
+ />
23
+ {text}
24
+ </span>
25
+ )
26
+ }
@@ -0,0 +1,130 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface StepChartSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface StepChartProps {
10
+ series: StepChartSeries[]
11
+ labels?: string[]
12
+ height?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const VW = 400
17
+ const PAD = { t: 12, r: 8, b: 28, l: 8 }
18
+
19
+ function stepPath(xs: number[], ys: number[]): string {
20
+ if (!xs.length) return ''
21
+ let d = `M ${xs[0].toFixed(1)},${ys[0].toFixed(1)}`
22
+ for (let i = 1; i < xs.length; i++) {
23
+ d += ` H ${xs[i].toFixed(1)} V ${ys[i].toFixed(1)}`
24
+ }
25
+ return d
26
+ }
27
+
28
+ function stepArea(xs: number[], ys: number[], bot: number): string {
29
+ if (!xs.length) return ''
30
+ const b = bot.toFixed(1)
31
+ let d = `M ${xs[0].toFixed(1)},${b} V ${ys[0].toFixed(1)}`
32
+ for (let i = 1; i < xs.length; i++) {
33
+ d += ` H ${xs[i].toFixed(1)} V ${ys[i].toFixed(1)}`
34
+ }
35
+ d += ` H ${xs[xs.length - 1].toFixed(1)} V ${b} Z`
36
+ return d
37
+ }
38
+
39
+ export function StepChart({
40
+ series,
41
+ labels = [],
42
+ height = 160,
43
+ ariaLabel = 'Step chart',
44
+ }: StepChartProps) {
45
+ const { paths, areas, gridLines } = useMemo(() => {
46
+ const allValues = series.flatMap((s) => s.values)
47
+ if (!allValues.length) return { paths: [], areas: [], gridLines: [] }
48
+ const minV = Math.min(...allValues)
49
+ const maxV = Math.max(...allValues)
50
+ const range = maxV - minV || 1
51
+ const chartW = VW - PAD.l - PAD.r
52
+ const chartH = height - PAD.t - PAD.b
53
+ const n = series[0]?.values.length ?? 0
54
+
55
+ const toX = (i: number) => PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2)
56
+ const toY = (v: number) => PAD.t + chartH - ((v - minV) / range) * chartH
57
+
58
+ const gridLines = [0, 0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
59
+ const bot = PAD.t + chartH
60
+
61
+ const paths = series.map((s) => {
62
+ const xs = s.values.map((_, i) => toX(i))
63
+ const ys = s.values.map((v) => toY(v))
64
+ return { color: s.color, label: s.label, d: stepPath(xs, ys) }
65
+ })
66
+
67
+ const areas = series.map((s) => {
68
+ const xs = s.values.map((_, i) => toX(i))
69
+ const ys = s.values.map((v) => toY(v))
70
+ return { color: s.color, label: s.label, d: stepArea(xs, ys, bot) }
71
+ })
72
+
73
+ return { paths, areas, gridLines }
74
+ }, [series, height])
75
+
76
+ const chartW = VW - PAD.l - PAD.r
77
+
78
+ return (
79
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
80
+ <svg
81
+ viewBox={`0 0 ${VW} ${height}`}
82
+ style={{ width: '100%', height: 'auto', display: 'block' }}
83
+ aria-hidden="true"
84
+ focusable="false"
85
+ >
86
+ {gridLines.map((y, i) => (
87
+ <line
88
+ key={i}
89
+ x1={PAD.l}
90
+ y1={y}
91
+ x2={VW - PAD.r}
92
+ y2={y}
93
+ stroke="rgba(255,255,255,0.06)"
94
+ strokeWidth={1}
95
+ />
96
+ ))}
97
+ {areas.map((s) => (
98
+ <path key={s.label} d={s.d} fill={s.color} fillOpacity={0.1} />
99
+ ))}
100
+ {paths.map((s) => (
101
+ <path
102
+ key={s.label}
103
+ d={s.d}
104
+ fill="none"
105
+ stroke={s.color}
106
+ strokeWidth={2}
107
+ strokeLinejoin="round"
108
+ strokeLinecap="round"
109
+ />
110
+ ))}
111
+ {labels.map((lbl, i) => {
112
+ const x = PAD.l + (labels.length > 1 ? (i / (labels.length - 1)) * chartW : chartW / 2)
113
+ return (
114
+ <text
115
+ key={i}
116
+ x={x}
117
+ y={height - 4}
118
+ textAnchor="middle"
119
+ fill="#6E7079"
120
+ fontSize={9}
121
+ fontFamily="inherit"
122
+ >
123
+ {lbl}
124
+ </text>
125
+ )
126
+ })}
127
+ </svg>
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,51 @@
1
+ export interface StepperStep {
2
+ id: string
3
+ label: string
4
+ }
5
+
6
+ export interface StepperProps {
7
+ steps: StepperStep[]
8
+ active: string
9
+ className?: string
10
+ }
11
+
12
+ export function Stepper({ steps, active, className = '' }: StepperProps) {
13
+ const activeIdx = steps.findIndex((s) => s.id === active)
14
+ return (
15
+ <nav aria-label="Progress" className={`stepper ${className}`.trim()}>
16
+ <ol className="stepper__list">
17
+ {steps.map((step, idx) => {
18
+ const isActive = step.id === active
19
+ const isDone = idx < activeIdx
20
+ const state = isActive ? 'active' : isDone ? 'done' : 'upcoming'
21
+ return (
22
+ <li key={step.id} className={`stepper__item stepper__item--${state}`}>
23
+ <span className="stepper__num" aria-hidden="true">
24
+ {isDone ? (
25
+ <svg viewBox="0 0 12 12" width="12" height="12" fill="none">
26
+ <path
27
+ d="M2 6l3 3 5-5"
28
+ stroke="currentColor"
29
+ strokeWidth="1.5"
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ />
33
+ </svg>
34
+ ) : (
35
+ idx + 1
36
+ )}
37
+ </span>
38
+ {isDone && <span className="sr-only">completed: </span>}
39
+ <span
40
+ className="stepper__label"
41
+ {...(isActive ? { 'aria-current': 'step' as const } : {})}
42
+ >
43
+ {step.label}
44
+ </span>
45
+ </li>
46
+ )
47
+ })}
48
+ </ol>
49
+ </nav>
50
+ )
51
+ }
@@ -0,0 +1,97 @@
1
+ export interface StreamSeries {
2
+ label: string
3
+ values: number[]
4
+ color?: string
5
+ }
6
+
7
+ export interface StreamGraphProps {
8
+ labels: string[]
9
+ series: StreamSeries[]
10
+ ariaLabel?: string
11
+ }
12
+
13
+ const VW = 380
14
+ const VH = 120
15
+ const PAD_L = 8
16
+ const PAD_R = 8
17
+ const PAD_T = 8
18
+ const PAD_B = 20
19
+ const COLORS = ['#006FFF', '#00C8C8', '#F5A623', '#7FB6FF', '#A878F5', '#F56342']
20
+
21
+ export function StreamGraph({ labels, series, ariaLabel = 'Stream graph' }: StreamGraphProps) {
22
+ const n = labels.length
23
+ const TRACK_W = VW - PAD_L - PAD_R
24
+ const TRACK_H = VH - PAD_T - PAD_B
25
+
26
+ const totals = Array.from({ length: n }, (_, i) =>
27
+ series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0),
28
+ )
29
+ const maxTotal = Math.max(...totals, 1)
30
+
31
+ function xOf(i: number) {
32
+ return n > 1 ? PAD_L + (i / (n - 1)) * TRACK_W : PAD_L + TRACK_W / 2
33
+ }
34
+
35
+ const layers = series.map((s, si) => {
36
+ const tops: number[] = []
37
+ const bots: number[] = []
38
+ for (let i = 0; i < n; i++) {
39
+ const total = totals[i]
40
+ const baseline = (maxTotal - total) / 2
41
+ let bottom = baseline
42
+ for (let k = 0; k < si; k++) bottom += series[k].values[i] ?? 0
43
+ const top = bottom + (s.values[i] ?? 0)
44
+ bots[i] = PAD_T + (bottom / maxTotal) * TRACK_H
45
+ tops[i] = PAD_T + (top / maxTotal) * TRACK_H
46
+ }
47
+ return { tops, bots }
48
+ })
49
+
50
+ function buildArea(tops: number[], bots: number[]): string {
51
+ const fwd = tops.map((y, i) => {
52
+ const x = xOf(i)
53
+ if (i === 0) return `M${x},${y}`
54
+ const px = xOf(i - 1)
55
+ const cpX = (px + x) / 2
56
+ return `C${cpX},${tops[i - 1]} ${cpX},${y} ${x},${y}`
57
+ })
58
+ const bwd = [...bots].reverse().map((y, ri) => {
59
+ const i = bots.length - 1 - ri
60
+ const x = xOf(i)
61
+ if (ri === 0) return `L${x},${y}`
62
+ const nx = xOf(i + 1)
63
+ const cpX = (nx + x) / 2
64
+ return `C${cpX},${bots[i + 1]} ${cpX},${y} ${x},${y}`
65
+ })
66
+ return [...fwd, ...bwd, 'Z'].join(' ')
67
+ }
68
+
69
+ return (
70
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
71
+ <svg
72
+ viewBox={`0 0 ${VW} ${VH}`}
73
+ style={{ width: '100%', height: 'auto', display: 'block' }}
74
+ aria-hidden="true"
75
+ focusable="false"
76
+ >
77
+ {layers.map(({ tops, bots }, si) => {
78
+ const color = series[si].color ?? COLORS[si % COLORS.length]
79
+ return <path key={si} d={buildArea(tops, bots)} fill={color} opacity={0.82} />
80
+ })}
81
+ {labels.map((label, i) => (
82
+ <text
83
+ key={i}
84
+ x={xOf(i)}
85
+ y={VH - 5}
86
+ fill="#6E7079"
87
+ fontSize={9}
88
+ textAnchor={i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle'}
89
+ fontFamily="inherit"
90
+ >
91
+ {label}
92
+ </text>
93
+ ))}
94
+ </svg>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,76 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface StripeItem {
4
+ label: string
5
+ value: number
6
+ }
7
+
8
+ export interface StripeChartProps {
9
+ data: StripeItem[]
10
+ height?: number
11
+ colorLow?: string
12
+ colorHigh?: string
13
+ ariaLabel?: string
14
+ }
15
+
16
+ function hexToRgb(hex: string): [number, number, number] {
17
+ const h = hex.replace('#', '')
18
+ const n = parseInt(h, 16)
19
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
20
+ }
21
+
22
+ function lerp(a: number, b: number, t: number) {
23
+ return Math.round(a + (b - a) * t)
24
+ }
25
+
26
+ function interpColor(low: string, high: string, t: number): string {
27
+ const [r1, g1, b1] = hexToRgb(low)
28
+ const [r2, g2, b2] = hexToRgb(high)
29
+ return `rgb(${lerp(r1, r2, t)},${lerp(g1, g2, t)},${lerp(b1, b2, t)})`
30
+ }
31
+
32
+ const VW = 400
33
+
34
+ export function StripeChart({
35
+ data,
36
+ height = 48,
37
+ colorLow = '#0A2840',
38
+ colorHigh = '#FF4040',
39
+ ariaLabel = 'Stripe chart',
40
+ }: StripeChartProps) {
41
+ const stripes = useMemo(() => {
42
+ if (!data.length) return []
43
+ const vals = data.map((d) => d.value)
44
+ const min = Math.min(...vals)
45
+ const max = Math.max(...vals)
46
+ const range = max - min || 1
47
+ const sw = VW / data.length
48
+ return data.map((d, i) => ({
49
+ x: i * sw,
50
+ w: sw,
51
+ color: interpColor(colorLow, colorHigh, (d.value - min) / range),
52
+ label: d.label,
53
+ value: d.value,
54
+ }))
55
+ }, [data, colorLow, colorHigh])
56
+
57
+ return (
58
+ <div role="img" aria-label={ariaLabel} style={{ lineHeight: 0 }}>
59
+ <svg
60
+ viewBox={`0 0 ${VW} ${height}`}
61
+ width="100%"
62
+ height={height}
63
+ aria-hidden="true"
64
+ focusable="false"
65
+ preserveAspectRatio="none"
66
+ >
67
+ {stripes.map((s, i) => (
68
+ <g key={i}>
69
+ <rect x={s.x} y={0} width={s.w} height={height} fill={s.color} />
70
+ <title>{`${s.label}: ${s.value}`}</title>
71
+ </g>
72
+ ))}
73
+ </svg>
74
+ </div>
75
+ )
76
+ }