@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,96 @@
1
+ export interface GanttTask {
2
+ label: string
3
+ start: number
4
+ end: number
5
+ color?: string
6
+ }
7
+
8
+ export interface GanttChartProps {
9
+ tasks: GanttTask[]
10
+ xLabels?: string[]
11
+ ariaLabel?: string
12
+ }
13
+
14
+ const VW = 340
15
+ const LABEL_W = 100
16
+ const PAD_R = 8
17
+ const TRACK_W = VW - LABEL_W - PAD_R
18
+ const ROW_H = 28
19
+ const BAR_H = 14
20
+ const PAD_T = 6
21
+ const AXIS_H = 18
22
+
23
+ export function GanttChart({ tasks, xLabels = [], ariaLabel = 'Gantt chart' }: GanttChartProps) {
24
+ const svgH = PAD_T + tasks.length * ROW_H + AXIS_H
25
+ const ticks = xLabels.length
26
+ ? xLabels.map((lbl, i) => ({ x: LABEL_W + (i / (xLabels.length - 1 || 1)) * TRACK_W, lbl }))
27
+ : []
28
+
29
+ const tx = (v: number) => LABEL_W + v * TRACK_W
30
+
31
+ return (
32
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
33
+ <svg
34
+ viewBox={`0 0 ${VW} ${svgH}`}
35
+ style={{ width: '100%', height: 'auto', display: 'block' }}
36
+ aria-hidden="true"
37
+ focusable="false"
38
+ >
39
+ {ticks.map(({ x, lbl }, i) => (
40
+ <g key={i}>
41
+ <line
42
+ x1={x}
43
+ y1={PAD_T}
44
+ x2={x}
45
+ y2={PAD_T + tasks.length * ROW_H}
46
+ stroke="rgba(255,255,255,0.06)"
47
+ strokeWidth={1}
48
+ />
49
+ <text
50
+ x={x}
51
+ y={PAD_T + tasks.length * ROW_H + 13}
52
+ fill="#6E7079"
53
+ fontSize={9}
54
+ textAnchor="middle"
55
+ fontFamily="inherit"
56
+ >
57
+ {lbl}
58
+ </text>
59
+ </g>
60
+ ))}
61
+
62
+ {tasks.map((task, i) => {
63
+ const cy = PAD_T + i * ROW_H + ROW_H / 2
64
+ const x1 = tx(task.start)
65
+ const x2 = tx(task.end)
66
+ const barW = Math.max(x2 - x1, 4)
67
+ const color = task.color ?? '#006FFF'
68
+
69
+ return (
70
+ <g key={i}>
71
+ <text
72
+ x={LABEL_W - 6}
73
+ y={cy + 4}
74
+ fill="#A4A7B5"
75
+ fontSize={10}
76
+ textAnchor="end"
77
+ fontFamily="inherit"
78
+ >
79
+ {task.label}
80
+ </text>
81
+ <rect
82
+ x={x1}
83
+ y={cy - BAR_H / 2}
84
+ width={barW}
85
+ height={BAR_H}
86
+ rx={3}
87
+ fill={color}
88
+ opacity={0.85}
89
+ />
90
+ </g>
91
+ )
92
+ })}
93
+ </svg>
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,71 @@
1
+ export interface GaugeProps {
2
+ value: number
3
+ label: string
4
+ color?: string
5
+ size?: number
6
+ ariaLabel?: string
7
+ }
8
+
9
+ export function Gauge({ value, label, color = '#006FFF', size = 120, ariaLabel }: GaugeProps) {
10
+ const sw = 10
11
+ const cx = size / 2
12
+ const cy = size / 2
13
+ const r = cx - sw / 2 - 2
14
+ const svgH = Math.round(size * 0.7)
15
+ const pathLen = Math.PI * r
16
+ const fillLen = (Math.min(100, Math.max(0, value)) / 100) * pathLen
17
+ const d = `M ${cx - r},${cy} A ${r},${r} 0 1 1 ${cx + r},${cy}`
18
+ const valY = cy + Math.round(size * 0.06)
19
+ const lblY = valY + Math.round(size * 0.13) + 2
20
+ const a11yLabel = ariaLabel ?? `${label} ${value}%`
21
+
22
+ return (
23
+ <div
24
+ role="meter"
25
+ aria-valuenow={value}
26
+ aria-valuemin={0}
27
+ aria-valuemax={100}
28
+ aria-label={a11yLabel}
29
+ style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center' }}
30
+ >
31
+ <svg width={size} height={svgH} aria-hidden="true" focusable="false">
32
+ <path
33
+ d={d}
34
+ fill="none"
35
+ stroke="rgba(255,255,255,0.08)"
36
+ strokeWidth={sw}
37
+ strokeLinecap="round"
38
+ />
39
+ <path
40
+ d={d}
41
+ fill="none"
42
+ stroke={color}
43
+ strokeWidth={sw}
44
+ strokeLinecap="round"
45
+ strokeDasharray={`${fillLen} ${pathLen}`}
46
+ />
47
+ <text
48
+ x={cx}
49
+ y={valY}
50
+ textAnchor="middle"
51
+ fill="#fff"
52
+ fontSize={Math.round(size * 0.2)}
53
+ fontWeight={600}
54
+ fontFamily="inherit"
55
+ >
56
+ {value}%
57
+ </text>
58
+ <text
59
+ x={cx}
60
+ y={lblY}
61
+ textAnchor="middle"
62
+ fill="#6E7079"
63
+ fontSize={Math.round(size * 0.1)}
64
+ fontFamily="inherit"
65
+ >
66
+ {label}
67
+ </text>
68
+ </svg>
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,125 @@
1
+ import { useState, useId } from 'react'
2
+
3
+ export type GroupedListItemStatus = 'danger' | 'warn' | 'success' | 'info' | 'neutral'
4
+
5
+ export interface GroupedListItem {
6
+ label: string
7
+ sublabel?: string
8
+ meta?: string
9
+ status?: GroupedListItemStatus
10
+ }
11
+
12
+ export interface GroupedListGroup {
13
+ label: string
14
+ items: GroupedListItem[]
15
+ defaultOpen?: boolean
16
+ color?: string
17
+ }
18
+
19
+ export interface GroupedListProps {
20
+ groups: GroupedListGroup[]
21
+ collapsible?: boolean
22
+ ariaLabel?: string
23
+ }
24
+
25
+ const STATUS_COLORS: Record<GroupedListItemStatus, string> = {
26
+ danger: '#F03A3A',
27
+ warn: '#F5A623',
28
+ success: '#00B070',
29
+ info: '#006FFF',
30
+ neutral: '#6E7079',
31
+ }
32
+
33
+ export function GroupedList({
34
+ groups,
35
+ collapsible = true,
36
+ ariaLabel = 'Grouped list',
37
+ }: GroupedListProps) {
38
+ const uid = useId()
39
+ const [open, setOpen] = useState<boolean[]>(() => groups.map((g) => g.defaultOpen !== false))
40
+
41
+ function toggle(i: number) {
42
+ setOpen((prev) => prev.map((v, j) => (j === i ? !v : v)))
43
+ }
44
+
45
+ return (
46
+ <div className="gl" aria-label={ariaLabel}>
47
+ {groups.map((group, gi) => {
48
+ const isOpen = open[gi]
49
+ const panelId = `${uid}-gl-${gi}`
50
+ return (
51
+ <div key={gi} className="gl-group">
52
+ {collapsible ? (
53
+ <button
54
+ type="button"
55
+ className="gl-header"
56
+ aria-expanded={isOpen}
57
+ aria-controls={panelId}
58
+ onClick={() => toggle(gi)}
59
+ >
60
+ {group.color && (
61
+ <span
62
+ className="gl-color-dot"
63
+ style={{ background: group.color }}
64
+ aria-hidden="true"
65
+ />
66
+ )}
67
+ <span className="gl-title">{group.label}</span>
68
+ <span className="gl-count">{group.items.length}</span>
69
+ <svg
70
+ className={`gl-chevron${isOpen ? ' gl-chevron--open' : ''}`}
71
+ width="12"
72
+ height="12"
73
+ viewBox="0 0 12 12"
74
+ aria-hidden="true"
75
+ focusable={false}
76
+ >
77
+ <path
78
+ d="M2 4l4 4 4-4"
79
+ stroke="currentColor"
80
+ strokeWidth="1.5"
81
+ fill="none"
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ />
85
+ </svg>
86
+ </button>
87
+ ) : (
88
+ <div className="gl-header gl-header--static">
89
+ {group.color && (
90
+ <span
91
+ className="gl-color-dot"
92
+ style={{ background: group.color }}
93
+ aria-hidden="true"
94
+ />
95
+ )}
96
+ <span className="gl-title">{group.label}</span>
97
+ <span className="gl-count">{group.items.length}</span>
98
+ </div>
99
+ )}
100
+ {(!collapsible || isOpen) && (
101
+ <ul id={panelId} className="gl-items">
102
+ {group.items.map((item, ii) => (
103
+ <li key={ii} className="gl-item">
104
+ {item.status && (
105
+ <span
106
+ className="gl-item-dot"
107
+ style={{ background: STATUS_COLORS[item.status] }}
108
+ aria-label={item.status}
109
+ />
110
+ )}
111
+ <div className="gl-item-body">
112
+ <div className="gl-item-label">{item.label}</div>
113
+ {item.sublabel && <div className="gl-item-sublabel">{item.sublabel}</div>}
114
+ </div>
115
+ {item.meta && <span className="gl-item-meta">{item.meta}</span>}
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ )}
120
+ </div>
121
+ )
122
+ })}
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ export interface HealthBarProps {
4
+ value: number
5
+ fillStyle?: CSSProperties
6
+ label?: string
7
+ valueText?: string
8
+ }
9
+
10
+ export function HealthBar({ value, fillStyle, label, valueText }: HealthBarProps) {
11
+ const clamped = Math.max(0, Math.min(100, value))
12
+ return (
13
+ <div
14
+ className="hb"
15
+ role="progressbar"
16
+ aria-valuenow={Math.round(clamped)}
17
+ aria-valuemin={0}
18
+ aria-valuemax={100}
19
+ aria-label={label}
20
+ aria-valuetext={valueText}
21
+ >
22
+ <div className="hb-fill" style={{ width: `${clamped}%`, ...fillStyle }} />
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,106 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface HeatMapProps {
4
+ data: number[][]
5
+ colors?: string[]
6
+ xLabels?: string[]
7
+ height?: number
8
+ cellGap?: number
9
+ ariaLabel?: string
10
+ }
11
+
12
+ const VW = 400
13
+ const PAD_BASE = { t: 4, r: 4, l: 4 }
14
+ const DEFAULT_COLORS = ['#1B2D5A', '#3F7BC4', '#7FB6FF', '#F5C26B', '#F5A623', '#FF7B7B']
15
+
16
+ function hexToRgb(hex: string): [number, number, number] {
17
+ const n = parseInt(hex.slice(1), 16)
18
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
19
+ }
20
+
21
+ function interpolateColor(colors: string[], t: number): string {
22
+ if (colors.length === 1) return colors[0]
23
+ const scaled = Math.max(0, Math.min(1, t)) * (colors.length - 1)
24
+ const i = Math.min(Math.floor(scaled), colors.length - 2)
25
+ const f = scaled - i
26
+ const [r1, g1, b1] = hexToRgb(colors[i])
27
+ const [r2, g2, b2] = hexToRgb(colors[i + 1])
28
+ return `rgb(${Math.round(r1 + f * (r2 - r1))},${Math.round(g1 + f * (g2 - g1))},${Math.round(b1 + f * (b2 - b1))})`
29
+ }
30
+
31
+ export function HeatMap({
32
+ data,
33
+ colors = DEFAULT_COLORS,
34
+ xLabels = [],
35
+ height = 160,
36
+ cellGap = 1,
37
+ ariaLabel = 'Heat map',
38
+ }: HeatMapProps) {
39
+ const { rects, labelNodes } = useMemo(() => {
40
+ const rows = data.length
41
+ const cols = data[0]?.length ?? 0
42
+ if (!rows || !cols) return { rects: [], labelNodes: [] }
43
+
44
+ const padB = xLabels.length ? 16 : 4
45
+ const chartW = VW - PAD_BASE.l - PAD_BASE.r
46
+ const chartH = height - PAD_BASE.t - padB
47
+ const cellW = (chartW - (cols - 1) * cellGap) / cols
48
+ const cellH = (chartH - (rows - 1) * cellGap) / rows
49
+
50
+ const rects: { key: string; x: number; y: number; w: number; h: number; fill: string }[] = []
51
+ for (let row = 0; row < rows; row++) {
52
+ for (let col = 0; col < cols; col++) {
53
+ rects.push({
54
+ key: `${row}-${col}`,
55
+ x: PAD_BASE.l + col * (cellW + cellGap),
56
+ y: PAD_BASE.t + row * (cellH + cellGap),
57
+ w: cellW,
58
+ h: cellH,
59
+ fill: interpolateColor(colors, data[row][col]),
60
+ })
61
+ }
62
+ }
63
+
64
+ const labelNodes = xLabels.map((lbl, i) => {
65
+ const x = PAD_BASE.l + (xLabels.length > 1 ? (i / (xLabels.length - 1)) * chartW : chartW / 2)
66
+ return { lbl, x, y: height - 2 }
67
+ })
68
+
69
+ return { rects, labelNodes }
70
+ }, [data, colors, xLabels, height, cellGap])
71
+
72
+ return (
73
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
74
+ <svg
75
+ viewBox={`0 0 ${VW} ${height}`}
76
+ style={{ width: '100%', height: 'auto', display: 'block' }}
77
+ aria-hidden="true"
78
+ focusable="false"
79
+ >
80
+ {rects.map((r) => (
81
+ <rect
82
+ key={r.key}
83
+ x={r.x.toFixed(2)}
84
+ y={r.y.toFixed(2)}
85
+ width={r.w.toFixed(2)}
86
+ height={r.h.toFixed(2)}
87
+ fill={r.fill}
88
+ />
89
+ ))}
90
+ {labelNodes.map(({ lbl, x, y }, i) => (
91
+ <text
92
+ key={i}
93
+ x={x.toFixed(1)}
94
+ y={y}
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,152 @@
1
+ export interface HexbinPoint {
2
+ x: number
3
+ y: number
4
+ }
5
+
6
+ export interface HexbinChartProps {
7
+ points: HexbinPoint[]
8
+ xRange?: [number, number]
9
+ yRange?: [number, number]
10
+ height?: number
11
+ hexRadius?: number
12
+ ariaLabel?: string
13
+ }
14
+
15
+ const ROOT3 = Math.sqrt(3)
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
+
22
+ function pixelToHex(px: number, py: number, R: number): [number, number] {
23
+ const q = ((ROOT3 / 3) * px - (1 / 3) * py) / R
24
+ const r = ((2 / 3) * py) / R
25
+ const s = -q - r
26
+ let rq = Math.round(q),
27
+ rr = Math.round(r),
28
+ rs = Math.round(s)
29
+ const dq = Math.abs(rq - q),
30
+ dr = Math.abs(rr - r),
31
+ ds = Math.abs(rs - s)
32
+ if (dq > dr && dq > ds) rq = -rr - rs
33
+ else if (dr > ds) rr = -rq - rs
34
+ else rs = -rq - rr
35
+ return [rq, rr]
36
+ }
37
+
38
+ function hexCenter(q: number, r: number, R: number, ox: number, oy: number): [number, number] {
39
+ return [ROOT3 * R * (q + r / 2) + ox, 1.5 * R * r + oy]
40
+ }
41
+
42
+ function hexPath(cx: number, cy: number, R: number): string {
43
+ const pts = Array.from({ length: 6 }, (_, k) => {
44
+ const a = Math.PI / 6 + (Math.PI / 3) * k
45
+ return `${(cx + R * Math.cos(a)).toFixed(2)},${(cy + R * Math.sin(a)).toFixed(2)}`
46
+ })
47
+ return `M${pts.join('L')}Z`
48
+ }
49
+
50
+ export function HexbinChart({
51
+ points,
52
+ xRange,
53
+ yRange,
54
+ height = 200,
55
+ hexRadius = 10,
56
+ ariaLabel = 'Hexbin chart',
57
+ }: HexbinChartProps) {
58
+ if (!points.length) return null
59
+
60
+ const PLOT_W = VW - PAD_L - PAD_R
61
+ const PLOT_H = height - PAD_T - PAD_B
62
+
63
+ const xs = points.map((p) => p.x)
64
+ const ys = points.map((p) => p.y)
65
+ const xMin = xRange?.[0] ?? Math.min(...xs)
66
+ const xMax = xRange?.[1] ?? Math.max(...xs)
67
+ const yMin = yRange?.[0] ?? Math.min(...ys)
68
+ const yMax = yRange?.[1] ?? Math.max(...ys)
69
+ const xSpan = xMax - xMin || 1
70
+ const ySpan = yMax - yMin || 1
71
+
72
+ const bins = new Map<string, { q: number; r: number; count: number }>()
73
+ for (const pt of points) {
74
+ const px = ((pt.x - xMin) / xSpan) * PLOT_W
75
+ const py = (1 - (pt.y - yMin) / ySpan) * PLOT_H
76
+ const [q, r] = pixelToHex(px, py, hexRadius)
77
+ const key = `${q},${r}`
78
+ const bin = bins.get(key)
79
+ if (bin) bin.count++
80
+ else bins.set(key, { q, r, count: 1 })
81
+ }
82
+
83
+ const maxCount = Math.max(...[...bins.values()].map((b) => b.count))
84
+ const yTicks = [yMin, (yMin + yMax) / 2, yMax]
85
+ const xTicks = [xMin, (xMin + xMax) / 2, xMax]
86
+ const ty = (v: number) => PAD_T + (1 - (v - yMin) / ySpan) * PLOT_H
87
+ const tx = (v: number) => PAD_L + ((v - xMin) / xSpan) * PLOT_W
88
+
89
+ return (
90
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
91
+ <svg
92
+ viewBox={`0 0 ${VW} ${height}`}
93
+ style={{ width: '100%', height: 'auto', display: 'block' }}
94
+ aria-hidden="true"
95
+ focusable="false"
96
+ >
97
+ {yTicks.map((v, i) => (
98
+ <g key={i}>
99
+ <line
100
+ x1={PAD_L}
101
+ y1={ty(v)}
102
+ x2={PAD_L + PLOT_W}
103
+ y2={ty(v)}
104
+ stroke="rgba(255,255,255,0.06)"
105
+ strokeWidth={1}
106
+ />
107
+ <text
108
+ x={PAD_L - 4}
109
+ y={ty(v) + 3}
110
+ fill="#6E7079"
111
+ fontSize={8}
112
+ textAnchor="end"
113
+ fontFamily="inherit"
114
+ >
115
+ {Math.round(v)}
116
+ </text>
117
+ </g>
118
+ ))}
119
+
120
+ {[...bins.values()].map(({ q, r, count }) => {
121
+ const [cx, cy] = hexCenter(q, r, hexRadius, PAD_L, PAD_T)
122
+ if (cx < PAD_L - hexRadius * 2 || cx > PAD_L + PLOT_W + hexRadius * 2) return null
123
+ if (cy < PAD_T - hexRadius * 2 || cy > PAD_T + PLOT_H + hexRadius * 2) return null
124
+ const alpha = (0.15 + 0.85 * (count / maxCount)).toFixed(3)
125
+ return (
126
+ <path
127
+ key={`${q},${r}`}
128
+ d={hexPath(cx, cy, hexRadius - 0.5)}
129
+ fill={`rgba(0,111,255,${alpha})`}
130
+ stroke="rgba(0,111,255,0.2)"
131
+ strokeWidth={0.5}
132
+ />
133
+ )
134
+ })}
135
+
136
+ {xTicks.map((v, i) => (
137
+ <text
138
+ key={i}
139
+ x={tx(v)}
140
+ y={height - 4}
141
+ fill="#6E7079"
142
+ fontSize={8}
143
+ textAnchor="middle"
144
+ fontFamily="inherit"
145
+ >
146
+ {Math.round(v)}
147
+ </text>
148
+ ))}
149
+ </svg>
150
+ </div>
151
+ )
152
+ }
@@ -0,0 +1,121 @@
1
+ export interface HistogramBin {
2
+ x0: number
3
+ x1: number
4
+ count: number
5
+ }
6
+
7
+ export interface HistogramProps {
8
+ bins: HistogramBin[]
9
+ height?: number
10
+ color?: string
11
+ xUnit?: string
12
+ ariaLabel?: string
13
+ }
14
+
15
+ const VW = 320
16
+ const PAD_L = 36
17
+ const PAD_R = 8
18
+ const PAD_T = 8
19
+ const PAD_B = 28
20
+
21
+ export function Histogram({
22
+ bins,
23
+ height = 160,
24
+ color = '#006FFF',
25
+ xUnit = '',
26
+ ariaLabel = 'Histogram',
27
+ }: HistogramProps) {
28
+ const PLOT_W = VW - PAD_L - PAD_R
29
+ const PLOT_H = height - PAD_T - PAD_B
30
+ const maxCount = Math.max(...bins.map((b) => b.count), 1)
31
+ const barW = PLOT_W / bins.length
32
+
33
+ const yTicks = [0, 1, 2, 3].map((i) => Math.round((maxCount * i) / 3))
34
+
35
+ const xStep = bins.length <= 6 ? 1 : Math.ceil(bins.length / 5)
36
+ const xLabels: { x: number; label: string }[] = []
37
+ bins.forEach((bin, i) => {
38
+ if (i % xStep === 0) {
39
+ xLabels.push({ x: PAD_L + i * barW, label: String(bin.x0) + xUnit })
40
+ }
41
+ })
42
+ xLabels.push({
43
+ x: PAD_L + PLOT_W,
44
+ label: String(bins[bins.length - 1].x1) + xUnit,
45
+ })
46
+
47
+ return (
48
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
49
+ <svg
50
+ viewBox={`0 0 ${VW} ${height}`}
51
+ style={{ width: '100%', height: 'auto', display: 'block' }}
52
+ aria-hidden="true"
53
+ focusable="false"
54
+ >
55
+ {yTicks.map((tick, i) => {
56
+ const ty = PAD_T + PLOT_H - (tick / maxCount) * PLOT_H
57
+ return (
58
+ <g key={i}>
59
+ <line
60
+ x1={PAD_L}
61
+ y1={ty}
62
+ x2={PAD_L + PLOT_W}
63
+ y2={ty}
64
+ stroke="rgba(255,255,255,0.06)"
65
+ strokeWidth={1}
66
+ />
67
+ <text
68
+ x={PAD_L - 4}
69
+ y={ty + 4}
70
+ fill="#6E7079"
71
+ fontSize={9}
72
+ textAnchor="end"
73
+ fontFamily="inherit"
74
+ >
75
+ {tick}
76
+ </text>
77
+ </g>
78
+ )
79
+ })}
80
+
81
+ {bins.map((bin, i) => {
82
+ const bh = (bin.count / maxCount) * PLOT_H
83
+ return (
84
+ <rect
85
+ key={i}
86
+ x={PAD_L + i * barW + 1}
87
+ y={PAD_T + PLOT_H - bh}
88
+ width={Math.max(barW - 2, 1)}
89
+ height={bh}
90
+ fill={color}
91
+ opacity={0.85}
92
+ />
93
+ )
94
+ })}
95
+
96
+ <line
97
+ x1={PAD_L}
98
+ y1={PAD_T + PLOT_H}
99
+ x2={PAD_L + PLOT_W}
100
+ y2={PAD_T + PLOT_H}
101
+ stroke="rgba(255,255,255,0.12)"
102
+ strokeWidth={1}
103
+ />
104
+
105
+ {xLabels.map((lbl, i) => (
106
+ <text
107
+ key={i}
108
+ x={lbl.x}
109
+ y={PAD_T + PLOT_H + 14}
110
+ fill="#6E7079"
111
+ fontSize={9}
112
+ textAnchor="middle"
113
+ fontFamily="inherit"
114
+ >
115
+ {lbl.label}
116
+ </text>
117
+ ))}
118
+ </svg>
119
+ </div>
120
+ )
121
+ }