@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,114 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface ArcNode {
4
+ id: string
5
+ label: string
6
+ color?: string
7
+ }
8
+
9
+ export interface ArcLink {
10
+ source: string
11
+ target: string
12
+ value?: number
13
+ color?: string
14
+ }
15
+
16
+ export interface ArcDiagramProps {
17
+ nodes: ArcNode[]
18
+ links: ArcLink[]
19
+ height?: number
20
+ ariaLabel?: string
21
+ }
22
+
23
+ const PALETTE = ['#006FFF', '#00C875', '#F5A623', '#A78BFA', '#FF7B7B', '#00C8C8', '#FB923C']
24
+ const VW = 400
25
+ const PAD = { t: 12, r: 20, b: 36, l: 20 }
26
+
27
+ export function ArcDiagram({
28
+ nodes,
29
+ links,
30
+ height = 180,
31
+ ariaLabel = 'Arc diagram',
32
+ }: ArcDiagramProps) {
33
+ const layout = useMemo(() => {
34
+ const n = nodes.length
35
+ if (!n) return null
36
+ const chartW = VW - PAD.l - PAD.r
37
+ const baseY = height - PAD.b
38
+ const maxArcH = baseY - PAD.t
39
+
40
+ const xMap: Record<string, number> = {}
41
+ const colorMap: Record<string, string> = {}
42
+
43
+ const dots = nodes.map((node, i) => {
44
+ const x = n > 1 ? PAD.l + (i / (n - 1)) * chartW : VW / 2
45
+ xMap[node.id] = x
46
+ const color = node.color ?? PALETTE[i % PALETTE.length]
47
+ colorMap[node.id] = color
48
+ return { x, y: baseY, color, label: node.label }
49
+ })
50
+
51
+ const arcs = links.flatMap((link) => {
52
+ const x1 = xMap[link.source]
53
+ const x2 = xMap[link.target]
54
+ if (x1 == null || x2 == null) return []
55
+ const midX = (x1 + x2) / 2
56
+ const dist = Math.abs(x2 - x1)
57
+ const arcH = Math.min(dist * 0.45, maxArcH * 0.9)
58
+ const ctl = baseY - arcH
59
+ const color = link.color ?? colorMap[link.source] ?? PALETTE[0]
60
+ const sw = link.value != null ? Math.max(1, Math.min(4, link.value / 10)) : 1.5
61
+ return [{ d: `M ${x1} ${baseY} Q ${midX} ${ctl} ${x2} ${baseY}`, color, sw }]
62
+ })
63
+
64
+ return { dots, arcs, baseY }
65
+ }, [nodes, links, height])
66
+
67
+ if (!layout) return null
68
+
69
+ return (
70
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
71
+ <svg
72
+ viewBox={`0 0 ${VW} ${height}`}
73
+ style={{ width: '100%', height: 'auto', display: 'block' }}
74
+ aria-hidden="true"
75
+ focusable="false"
76
+ >
77
+ <line
78
+ x1={PAD.l}
79
+ y1={layout.baseY}
80
+ x2={VW - PAD.r}
81
+ y2={layout.baseY}
82
+ stroke="rgba(255,255,255,0.08)"
83
+ strokeWidth={1}
84
+ />
85
+ {layout.arcs.map((arc, i) => (
86
+ <path
87
+ key={i}
88
+ d={arc.d}
89
+ fill="none"
90
+ stroke={arc.color}
91
+ strokeWidth={arc.sw}
92
+ strokeOpacity={0.6}
93
+ />
94
+ ))}
95
+ {layout.dots.map((dot, i) => (
96
+ <circle key={i} cx={dot.x} cy={dot.y} r={5} fill={dot.color} />
97
+ ))}
98
+ {layout.dots.map((dot, i) => (
99
+ <text
100
+ key={i}
101
+ x={dot.x}
102
+ y={layout.baseY + 18}
103
+ textAnchor="middle"
104
+ fontSize={9}
105
+ fill="#A4A7B5"
106
+ fontFamily="inherit"
107
+ >
108
+ {dot.label}
109
+ </text>
110
+ ))}
111
+ </svg>
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,101 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface AreaChartSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface AreaChartProps {
10
+ series: AreaChartSeries[]
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
+ export function AreaChart({
20
+ series,
21
+ labels = [],
22
+ height = 160,
23
+ ariaLabel = 'Area chart',
24
+ }: AreaChartProps) {
25
+ const { areas, gridLines, chartW } = useMemo(() => {
26
+ const n = series[0]?.values.length ?? 0
27
+ if (!n) return { areas: [], gridLines: [], chartW: VW - PAD.l - PAD.r }
28
+
29
+ const totals = Array.from({ length: n }, (_, i) =>
30
+ series.reduce((acc, s) => acc + (s.values[i] ?? 0), 0),
31
+ )
32
+ const maxV = Math.max(...totals, 1)
33
+ const chartW = VW - PAD.l - PAD.r
34
+ const chartH = height - PAD.t - PAD.b
35
+
36
+ const toX = (i: number) => PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2)
37
+ const toY = (v: number) => PAD.t + chartH - (v / maxV) * chartH
38
+
39
+ const gridLines = [0, 0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
40
+
41
+ const areas = series.map((s, j) => {
42
+ const bottoms = Array.from({ length: n }, (_, i) =>
43
+ series.slice(0, j).reduce((acc, sr) => acc + (sr.values[i] ?? 0), 0),
44
+ )
45
+ const tops = bottoms.map((b, i) => b + (s.values[i] ?? 0))
46
+
47
+ const topPts = tops.map((v, i) => `${toX(i).toFixed(1)},${toY(v).toFixed(1)}`)
48
+ const botPts = bottoms.map((v, i) => `${toX(i).toFixed(1)},${toY(v).toFixed(1)}`).reverse()
49
+
50
+ return {
51
+ color: s.color,
52
+ label: s.label,
53
+ d: `M ${topPts.join(' L ')} L ${botPts.join(' L ')} Z`,
54
+ }
55
+ })
56
+
57
+ return { areas, gridLines, chartW }
58
+ }, [series, height])
59
+
60
+ return (
61
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
62
+ <svg
63
+ viewBox={`0 0 ${VW} ${height}`}
64
+ style={{ width: '100%', height: 'auto', display: 'block' }}
65
+ aria-hidden="true"
66
+ focusable="false"
67
+ >
68
+ {gridLines.map((y, i) => (
69
+ <line
70
+ key={i}
71
+ x1={PAD.l}
72
+ y1={y}
73
+ x2={VW - PAD.r}
74
+ y2={y}
75
+ stroke="rgba(255,255,255,0.06)"
76
+ strokeWidth={1}
77
+ />
78
+ ))}
79
+ {areas.map((s) => (
80
+ <path key={s.label} d={s.d} fill={s.color} fillOpacity={0.75} />
81
+ ))}
82
+ {labels.map((lbl, i) => {
83
+ const x = PAD.l + (labels.length > 1 ? (i / (labels.length - 1)) * chartW : chartW / 2)
84
+ return (
85
+ <text
86
+ key={i}
87
+ x={x}
88
+ y={height - 4}
89
+ textAnchor="middle"
90
+ fill="#6E7079"
91
+ fontSize={9}
92
+ fontFamily="inherit"
93
+ >
94
+ {lbl}
95
+ </text>
96
+ )
97
+ })}
98
+ </svg>
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,37 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ export type AvatarSize = 'sm' | 'md' | 'lg'
4
+ export type AvatarStatus = 'online' | 'offline' | 'away'
5
+
6
+ export interface AvatarProps {
7
+ initials?: string
8
+ src?: string
9
+ alt?: string
10
+ size?: AvatarSize
11
+ status?: AvatarStatus
12
+ className?: string
13
+ style?: CSSProperties
14
+ }
15
+
16
+ export function Avatar({
17
+ initials = '?',
18
+ src,
19
+ alt,
20
+ size = 'md',
21
+ status,
22
+ className = '',
23
+ style,
24
+ }: AvatarProps) {
25
+ const label = alt ?? (src ? '' : initials)
26
+ return (
27
+ <span
28
+ className={`avatar avatar-${size}${className ? ` ${className}` : ''}`}
29
+ role="img"
30
+ aria-label={label}
31
+ style={style}
32
+ >
33
+ {src ? <img src={src} alt="" /> : initials}
34
+ {status && <span className={`avatar-status avatar-status-${status}`} aria-hidden="true" />}
35
+ </span>
36
+ )
37
+ }
@@ -0,0 +1,36 @@
1
+ import { Avatar, type AvatarProps } from './Avatar.js'
2
+
3
+ export interface AvatarGroupProps {
4
+ avatars: AvatarProps[]
5
+ max?: number
6
+ size?: AvatarProps['size']
7
+ ariaLabel?: string
8
+ }
9
+
10
+ export function AvatarGroup({
11
+ avatars,
12
+ max = 5,
13
+ size = 'md',
14
+ ariaLabel = 'Avatar group',
15
+ }: AvatarGroupProps) {
16
+ const visible = avatars.slice(0, max)
17
+ const overflow = avatars.length - visible.length
18
+ return (
19
+ <span className="avatar-group" role="list" aria-label={ariaLabel}>
20
+ {visible.map((av, i) => (
21
+ <span key={i} role="listitem" className="avatar-group-item">
22
+ <Avatar {...av} size={size} />
23
+ </span>
24
+ ))}
25
+ {overflow > 0 && (
26
+ <span
27
+ role="listitem"
28
+ className={`avatar avatar-${size} avatar-overflow`}
29
+ aria-label={`${overflow} more`}
30
+ >
31
+ +{overflow}
32
+ </span>
33
+ )}
34
+ </span>
35
+ )
36
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type BadgeColor = 'danger' | 'warn' | 'info' | 'success' | 'neutral'
4
+
5
+ export interface BadgeProps {
6
+ children: ReactNode
7
+ count?: number
8
+ dot?: boolean
9
+ max?: number
10
+ showZero?: boolean
11
+ color?: BadgeColor
12
+ className?: string
13
+ }
14
+
15
+ export function Badge({
16
+ children,
17
+ count,
18
+ dot = false,
19
+ max = 99,
20
+ showZero = false,
21
+ color = 'danger',
22
+ className = '',
23
+ }: BadgeProps) {
24
+ const show = dot || (count !== undefined && (showZero || count > 0))
25
+ const label = count !== undefined ? (count > max ? `${max}+` : String(count)) : ''
26
+ return (
27
+ <span className={`badge-wrapper${className ? ` ${className}` : ''}`}>
28
+ {children}
29
+ {show && (
30
+ <span className={`badge badge-${color}${dot ? ' badge-dot' : ''}`} aria-hidden="true">
31
+ {!dot && label}
32
+ </span>
33
+ )}
34
+ </span>
35
+ )
36
+ }
@@ -0,0 +1,49 @@
1
+ import { type ReactNode } from 'react'
2
+ import { CloseIcon } from '../icons.js'
3
+
4
+ export type BannerVariant = 'info' | 'success' | 'warn' | 'danger'
5
+
6
+ export interface BannerAction {
7
+ label: string
8
+ onClick: () => void
9
+ }
10
+
11
+ export interface BannerProps {
12
+ variant?: BannerVariant
13
+ title?: string
14
+ children: ReactNode
15
+ action?: BannerAction
16
+ onDismiss?: () => void
17
+ }
18
+
19
+ export function Banner({ variant = 'info', title, children, action, onDismiss }: BannerProps) {
20
+ const isUrgent = variant === 'danger' || variant === 'warn'
21
+ return (
22
+ <div
23
+ className={`banner banner--${variant}`}
24
+ role={isUrgent ? 'alert' : 'status'}
25
+ aria-live={isUrgent ? 'assertive' : 'polite'}
26
+ aria-atomic="true"
27
+ >
28
+ <div className="banner__body">
29
+ {title && <span className="banner__title">{title}</span>}
30
+ <span>{children}</span>
31
+ </div>
32
+ {action && (
33
+ <button type="button" className="banner__action" onClick={action.onClick}>
34
+ {action.label}
35
+ </button>
36
+ )}
37
+ {onDismiss && (
38
+ <button
39
+ type="button"
40
+ className="banner__dismiss icon-btn"
41
+ onClick={onDismiss}
42
+ aria-label="Dismiss banner"
43
+ >
44
+ <CloseIcon aria-hidden="true" />
45
+ </button>
46
+ )}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,106 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface BarChartSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface BarChartProps {
10
+ series: BarChartSeries[]
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
+ export function BarChart({
20
+ series,
21
+ labels = [],
22
+ height = 160,
23
+ ariaLabel = 'Bar chart',
24
+ }: BarChartProps) {
25
+ const { bars, gridLines, groupW } = useMemo(() => {
26
+ if (!series.length || !series[0].values.length) return { bars: [], gridLines: [], groupW: 0 }
27
+ const n = series[0].values.length
28
+ const m = series.length
29
+ const maxV = Math.max(...series.flatMap((s) => s.values)) || 1
30
+ const chartW = VW - PAD.l - PAD.r
31
+ const chartH = height - PAD.t - PAD.b
32
+ const bot = PAD.t + chartH
33
+ const gw = chartW / n
34
+ const gap = gw * 0.15
35
+ const barW = (gw - gap * (m + 1)) / m
36
+ const gridLines = [0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
37
+ const bars = series.flatMap((s, si) =>
38
+ s.values.map((v, gi) => {
39
+ const x = PAD.l + gi * gw + gap * (si + 1) + barW * si
40
+ const bh = (v / maxV) * chartH
41
+ return {
42
+ x: x.toFixed(1),
43
+ y: (bot - bh).toFixed(1),
44
+ w: Math.max(barW, 1).toFixed(1),
45
+ h: bh.toFixed(1),
46
+ color: s.color,
47
+ key: `${si}-${gi}`,
48
+ }
49
+ }),
50
+ )
51
+ return { bars, gridLines, groupW: gw }
52
+ }, [series, height])
53
+
54
+ const chartW = VW - PAD.l - PAD.r
55
+ const n = series[0]?.values.length ?? 0
56
+ const gw = n > 0 ? chartW / n : chartW
57
+ const effectiveGroupW = groupW || gw
58
+
59
+ return (
60
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
61
+ <svg
62
+ viewBox={`0 0 ${VW} ${height}`}
63
+ style={{ width: '100%', height: 'auto', display: 'block' }}
64
+ aria-hidden="true"
65
+ focusable="false"
66
+ >
67
+ {gridLines.map((y, i) => (
68
+ <line
69
+ key={i}
70
+ x1={PAD.l}
71
+ y1={y}
72
+ x2={VW - PAD.r}
73
+ y2={y}
74
+ stroke="rgba(255,255,255,0.06)"
75
+ strokeWidth={1}
76
+ />
77
+ ))}
78
+ {bars.map((b) => (
79
+ <rect
80
+ key={b.key}
81
+ x={b.x}
82
+ y={b.y}
83
+ width={b.w}
84
+ height={b.h}
85
+ fill={b.color}
86
+ fillOpacity={0.9}
87
+ rx={2}
88
+ />
89
+ ))}
90
+ {labels.map((lbl, i) => (
91
+ <text
92
+ key={i}
93
+ x={(PAD.l + (i + 0.5) * effectiveGroupW).toFixed(1)}
94
+ y={height - 4}
95
+ textAnchor="middle"
96
+ fill="#6E7079"
97
+ fontSize={9}
98
+ fontFamily="inherit"
99
+ >
100
+ {lbl}
101
+ </text>
102
+ ))}
103
+ </svg>
104
+ </div>
105
+ )
106
+ }
@@ -0,0 +1,142 @@
1
+ export interface BeeswarmSeries {
2
+ label: string
3
+ points: number[]
4
+ color?: string
5
+ }
6
+
7
+ export interface BeeswarmChartProps {
8
+ series: BeeswarmSeries[]
9
+ yRange?: [number, number]
10
+ height?: number
11
+ unit?: string
12
+ dotRadius?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const PALETTE = ['#006FFF', '#00C875', '#FF7B7B', '#F5C26B', '#A78BFA', '#34D399']
17
+ const VW = 340
18
+ const PAD_L = 28
19
+ const PAD_R = 8
20
+ const PAD_T = 8
21
+ const PAD_B = 24
22
+
23
+ function swarmedPositions(
24
+ values: number[],
25
+ tyFn: (v: number) => number,
26
+ r: number,
27
+ halfWidth: number,
28
+ ): { x: number; y: number }[] {
29
+ const pts: { x: number; y: number }[] = []
30
+ const sorted = [...values].sort((a, b) => a - b)
31
+ const step = r * 2 + 0.5
32
+ for (const v of sorted) {
33
+ const cy = tyFn(v)
34
+ let chosen = 0
35
+ let found = false
36
+ for (let dist = 0; dist <= halfWidth && !found; dist += step) {
37
+ const candidates = dist === 0 ? [0] : [dist, -dist]
38
+ for (const cx of candidates) {
39
+ if (pts.every((p) => Math.hypot(cx - p.x, cy - p.y) >= r * 2)) {
40
+ chosen = cx
41
+ found = true
42
+ break
43
+ }
44
+ }
45
+ }
46
+ pts.push({ x: chosen, y: cy })
47
+ }
48
+ return pts
49
+ }
50
+
51
+ export function BeeswarmChart({
52
+ series,
53
+ yRange,
54
+ height = 200,
55
+ unit = '',
56
+ dotRadius = 4,
57
+ ariaLabel = 'Beeswarm chart',
58
+ }: BeeswarmChartProps) {
59
+ if (!series.length) return null
60
+
61
+ const PLOT_W = VW - PAD_L - PAD_R
62
+ const PLOT_H = height - PAD_T - PAD_B
63
+ const n = series.length
64
+ const colWidth = PLOT_W / n
65
+ const halfWidth = colWidth / 2 - dotRadius
66
+
67
+ const allPts = series.flatMap((s) => s.points)
68
+ const minV = yRange ? yRange[0] : Math.min(...allPts)
69
+ const maxV = yRange ? yRange[1] : Math.max(...allPts)
70
+ const vRange = maxV - minV || 1
71
+
72
+ const ty = (v: number) => PAD_T + (1 - (v - minV) / vRange) * PLOT_H
73
+ const colCx = (i: number) => PAD_L + (i + 0.5) * colWidth
74
+ const yTicks = [minV, (minV + maxV) / 2, maxV]
75
+
76
+ const swarmed = series.map((s) => swarmedPositions(s.points, ty, dotRadius, halfWidth))
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
+ {yTicks.map((v, i) => (
87
+ <g key={i}>
88
+ <line
89
+ x1={PAD_L}
90
+ y1={ty(v)}
91
+ x2={PAD_L + PLOT_W}
92
+ y2={ty(v)}
93
+ stroke="rgba(255,255,255,0.06)"
94
+ strokeWidth={1}
95
+ />
96
+ <text
97
+ x={PAD_L - 4}
98
+ y={ty(v) + 4}
99
+ fill="#6E7079"
100
+ fontSize={8}
101
+ textAnchor="end"
102
+ fontFamily="inherit"
103
+ >
104
+ {Math.round(v)}
105
+ {unit}
106
+ </text>
107
+ </g>
108
+ ))}
109
+
110
+ {series.map((s, si) => {
111
+ const cx = colCx(si)
112
+ const color = s.color ?? PALETTE[si % PALETTE.length]
113
+ const pts = swarmed[si]
114
+ return (
115
+ <g key={si}>
116
+ {pts.map((p, pi) => (
117
+ <circle
118
+ key={pi}
119
+ cx={cx + p.x}
120
+ cy={p.y}
121
+ r={dotRadius}
122
+ fill={color}
123
+ fillOpacity={0.75}
124
+ />
125
+ ))}
126
+ <text
127
+ x={cx}
128
+ y={height - 4}
129
+ fill="#6E7079"
130
+ fontSize={9}
131
+ textAnchor="middle"
132
+ fontFamily="inherit"
133
+ >
134
+ {s.label}
135
+ </text>
136
+ </g>
137
+ )
138
+ })}
139
+ </svg>
140
+ </div>
141
+ )
142
+ }