banhaten 0.4.0 → 0.6.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 (214) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/CONTRIBUTING.md +6 -4
  3. package/README.md +4 -2
  4. package/docs/design-spacing.md +13 -0
  5. package/docs/design-system/README.md +2 -0
  6. package/docs/design-system/appearances/default.md +1 -1
  7. package/docs/design-system/appearances/rounded.md +1 -1
  8. package/docs/design-system/appearances/sharp.md +1 -1
  9. package/docs/design-system/component-showcase-consistency-report.md +5 -5
  10. package/docs/design-system/components/README.md +15 -0
  11. package/docs/design-system/components/accordion.md +1 -1
  12. package/docs/design-system/components/activity-feed.md +1 -1
  13. package/docs/design-system/components/activity-heatmap.md +1 -1
  14. package/docs/design-system/components/agent-input.md +85 -0
  15. package/docs/design-system/components/alert-dialog.md +1 -1
  16. package/docs/design-system/components/alert.md +10 -4
  17. package/docs/design-system/components/app-header.md +97 -0
  18. package/docs/design-system/components/approval-card.md +134 -0
  19. package/docs/design-system/components/attribute.md +14 -2
  20. package/docs/design-system/components/avatar.md +1 -1
  21. package/docs/design-system/components/badge.md +1 -1
  22. package/docs/design-system/components/banner.md +1 -1
  23. package/docs/design-system/components/breadcrumbs.md +8 -3
  24. package/docs/design-system/components/button-group.md +1 -1
  25. package/docs/design-system/components/button.md +8 -5
  26. package/docs/design-system/components/card.md +15 -1
  27. package/docs/design-system/components/catalog-components.md +1 -1
  28. package/docs/design-system/components/chart.md +95 -24
  29. package/docs/design-system/components/checkbox.md +1 -1
  30. package/docs/design-system/components/collapsible.md +20 -6
  31. package/docs/design-system/components/command.md +1 -1
  32. package/docs/design-system/components/context-menu.md +1 -1
  33. package/docs/design-system/components/date-picker.md +1 -1
  34. package/docs/design-system/components/file-upload.md +1 -1
  35. package/docs/design-system/components/hover-card.md +1 -1
  36. package/docs/design-system/components/inline-citations.md +93 -0
  37. package/docs/design-system/components/input-group.md +1 -1
  38. package/docs/design-system/components/input.md +59 -3
  39. package/docs/design-system/components/item.md +1 -1
  40. package/docs/design-system/components/kbd.md +6 -4
  41. package/docs/design-system/components/menu.md +35 -4
  42. package/docs/design-system/components/menubar.md +1 -1
  43. package/docs/design-system/components/modal.md +2 -2
  44. package/docs/design-system/components/native-select.md +1 -1
  45. package/docs/design-system/components/navigation-menu.md +1 -1
  46. package/docs/design-system/components/onboarding-step-list-item.md +1 -1
  47. package/docs/design-system/components/page-header.md +58 -17
  48. package/docs/design-system/components/pagination.md +2 -2
  49. package/docs/design-system/components/popover.md +1 -1
  50. package/docs/design-system/components/progress-slider.md +1 -1
  51. package/docs/design-system/components/radio-card.md +1 -1
  52. package/docs/design-system/components/radio-group.md +1 -1
  53. package/docs/design-system/components/resizable.md +1 -1
  54. package/docs/design-system/components/rolling-count.md +61 -0
  55. package/docs/design-system/components/scroll-area.md +1 -1
  56. package/docs/design-system/components/select.md +12 -2
  57. package/docs/design-system/components/sheet.md +1 -1
  58. package/docs/design-system/components/shimmer-text.md +67 -0
  59. package/docs/design-system/components/sidebar.md +75 -8
  60. package/docs/design-system/components/skeleton.md +2 -2
  61. package/docs/design-system/components/slideout.md +1 -1
  62. package/docs/design-system/components/slider.md +2 -2
  63. package/docs/design-system/components/social-button.md +2 -2
  64. package/docs/design-system/components/streaming-text.md +82 -0
  65. package/docs/design-system/components/table.md +103 -21
  66. package/docs/design-system/components/tabs.md +25 -6
  67. package/docs/design-system/components/tag.md +1 -1
  68. package/docs/design-system/components/task-list.md +95 -0
  69. package/docs/design-system/components/textarea.md +1 -1
  70. package/docs/design-system/components/thinking-reasoning.md +72 -0
  71. package/docs/design-system/components/thinking-state.md +67 -0
  72. package/docs/design-system/components/timeline.md +1 -1
  73. package/docs/design-system/components/toast.md +1 -1
  74. package/docs/design-system/components/toggle.md +2 -2
  75. package/docs/design-system/components/toolbar.md +11 -6
  76. package/docs/design-system/components/tooltip.md +6 -4
  77. package/docs/design-system/density-and-elevation.md +16 -16
  78. package/docs/design-system/design-system-test-missing-items.md +8 -8
  79. package/docs/design-system/installation.md +2 -0
  80. package/docs/design-system/tabs.md +2 -1
  81. package/docs/design-system/template-gap-audit.md +32 -0
  82. package/docs/design-system/templates.md +59 -0
  83. package/docs/design-system/token-governance.md +15 -2
  84. package/package.json +2 -2
  85. package/registry/components/accordion.tsx +2 -2
  86. package/registry/components/agent-input-parts.tsx +552 -0
  87. package/registry/components/agent-input-state.ts +156 -0
  88. package/registry/components/agent-input.css +307 -0
  89. package/registry/components/agent-input.tsx +382 -0
  90. package/registry/components/alert-dialog.tsx +2 -2
  91. package/registry/components/alert.tsx +23 -7
  92. package/registry/components/app-header.tsx +151 -0
  93. package/registry/components/approval-card.css +409 -0
  94. package/registry/components/approval-card.tsx +401 -0
  95. package/registry/components/approval-frame.tsx +108 -0
  96. package/registry/components/approval-questions.tsx +559 -0
  97. package/registry/components/attribute.tsx +48 -13
  98. package/registry/components/autocomplete.tsx +15 -68
  99. package/registry/components/avatar.tsx +2 -2
  100. package/registry/components/badge.tsx +8 -7
  101. package/registry/components/button-group.tsx +2 -2
  102. package/registry/components/button.tsx +19 -14
  103. package/registry/components/card.tsx +18 -5
  104. package/registry/components/chart-block-data.ts +177 -0
  105. package/registry/components/chart-block.css +171 -0
  106. package/registry/components/chart-block.tsx +260 -0
  107. package/registry/components/chart-interactions.ts +123 -0
  108. package/registry/components/chart-options.cartesian.ts +162 -0
  109. package/registry/components/chart-options.paint.ts +99 -0
  110. package/registry/components/chart-options.parts.ts +72 -0
  111. package/registry/components/chart-options.radial.ts +109 -0
  112. package/registry/components/chart-options.ts +44 -0
  113. package/registry/components/chart-options.types.ts +67 -0
  114. package/registry/components/chart.tsx +1 -1
  115. package/registry/components/checkbox.tsx +6 -6
  116. package/registry/components/collapsible.tsx +43 -3
  117. package/registry/components/command.tsx +1 -1
  118. package/registry/components/context-menu.tsx +1 -1
  119. package/registry/components/data-chart.tsx +40 -0
  120. package/registry/components/date-picker.tsx +9 -9
  121. package/registry/components/echart-theme.ts +246 -0
  122. package/registry/components/echart.css +49 -0
  123. package/registry/components/echart.tsx +178 -0
  124. package/registry/components/expanded/Breadcrumbs.tsx +5 -1
  125. package/registry/components/expanded/PageHeader.tsx +67 -24
  126. package/registry/components/expanded/Table.tsx +60 -22
  127. package/registry/components/expanded/activityFeed.css +2 -2
  128. package/registry/components/expanded/activityHeatmap.css +1 -1
  129. package/registry/components/expanded/banner.css +2 -2
  130. package/registry/components/expanded/breadcrumbs.css +19 -2
  131. package/registry/components/expanded/catalogComponentsShowcase.css +1 -1
  132. package/registry/components/expanded/fileUpload.css +7 -7
  133. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  134. package/registry/components/expanded/pageHeader.css +38 -44
  135. package/registry/components/expanded/slideout.css +4 -4
  136. package/registry/components/expanded/table.css +58 -9
  137. package/registry/components/expanded/timeline.css +1 -1
  138. package/registry/components/hover-card.tsx +1 -1
  139. package/registry/components/inline-citations.css +126 -0
  140. package/registry/components/inline-citations.tsx +292 -0
  141. package/registry/components/input-group.tsx +4 -4
  142. package/registry/components/input.tsx +150 -137
  143. package/registry/components/item.tsx +1 -1
  144. package/registry/components/kbd.tsx +3 -2
  145. package/registry/components/menu.tsx +117 -12
  146. package/registry/components/menubar.tsx +2 -2
  147. package/registry/components/modal.tsx +3 -3
  148. package/registry/components/native-select.tsx +4 -4
  149. package/registry/components/navigation-menu.tsx +2 -2
  150. package/registry/components/pagination.tsx +4 -4
  151. package/registry/components/popover.tsx +3 -3
  152. package/registry/components/progress-slider.tsx +1 -1
  153. package/registry/components/radio-card.tsx +7 -7
  154. package/registry/components/radio-group.tsx +2 -2
  155. package/registry/components/resizable.tsx +2 -2
  156. package/registry/components/rolling-count.css +37 -0
  157. package/registry/components/rolling-count.tsx +110 -0
  158. package/registry/components/scroll-area.tsx +1 -1
  159. package/registry/components/select.tsx +15 -10
  160. package/registry/components/sheet.tsx +2 -2
  161. package/registry/components/shimmer-text.css +69 -0
  162. package/registry/components/shimmer-text.tsx +43 -0
  163. package/registry/components/sidebar.tsx +528 -223
  164. package/registry/components/skeleton.tsx +1 -1
  165. package/registry/components/slider.tsx +2 -2
  166. package/registry/components/social-button.tsx +1 -1
  167. package/registry/components/streaming-text.css +45 -0
  168. package/registry/components/streaming-text.tsx +165 -0
  169. package/registry/components/table-elements.tsx +164 -5
  170. package/registry/components/tabs.css +32 -4
  171. package/registry/components/tabs.tsx +5 -1
  172. package/registry/components/tag.tsx +1 -1
  173. package/registry/components/task-list.css +328 -0
  174. package/registry/components/task-list.tsx +429 -0
  175. package/registry/components/textarea.tsx +4 -4
  176. package/registry/components/thinking-reasoning.css +178 -0
  177. package/registry/components/thinking-reasoning.tsx +253 -0
  178. package/registry/components/thinking-state.css +8 -0
  179. package/registry/components/thinking-state.tsx +46 -0
  180. package/registry/components/toast.tsx +1 -1
  181. package/registry/components/toggle.tsx +2 -2
  182. package/registry/components/toolbar.tsx +6 -6
  183. package/registry/components/tooltip.tsx +6 -4
  184. package/registry/examples/agent-input-demo.tsx +90 -0
  185. package/registry/examples/app-header-demo.tsx +40 -0
  186. package/registry/examples/approval-card-demo.tsx +83 -0
  187. package/registry/examples/button-demo.tsx +3 -0
  188. package/registry/examples/chart-block-demo.tsx +5 -0
  189. package/registry/examples/chart-demo.tsx +6 -21
  190. package/registry/examples/collection-filter-bar-demo.tsx +1 -1
  191. package/registry/examples/control-density-contract-demo.tsx +5 -1
  192. package/registry/examples/inline-citations-demo.tsx +31 -0
  193. package/registry/examples/input-actions-demo.tsx +36 -0
  194. package/registry/examples/menu-demo.tsx +1 -1
  195. package/registry/examples/popover-demo.tsx +1 -0
  196. package/registry/examples/rolling-count-demo.tsx +39 -0
  197. package/registry/examples/shimmer-text-demo.tsx +20 -0
  198. package/registry/examples/streaming-text-demo.tsx +28 -0
  199. package/registry/examples/task-list-demo.tsx +74 -0
  200. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  201. package/registry/examples/thinking-state-demo.tsx +13 -0
  202. package/registry/guidance/charts.LICENSE.txt +24 -0
  203. package/registry/hooks/use-controllable-state.ts +33 -0
  204. package/registry/hooks/use-listbox-navigation.ts +73 -0
  205. package/registry/hooks/use-motion-end.ts +55 -0
  206. package/registry/hooks/use-reduced-motion.ts +28 -0
  207. package/registry/index.json +1808 -190
  208. package/registry/styles/globals.css +992 -1088
  209. package/registry.json +385 -10
  210. package/schema/registry.schema.json +5 -1
  211. package/schema/tokens.schema.json +8 -4
  212. package/src/cli/file-transaction.js +15 -8
  213. package/src/cli/index.js +22 -29
  214. package/tokens/banhaten.tokens.json +490 -192
@@ -0,0 +1,162 @@
1
+ import type { BarSeriesOption, LineSeriesOption, PictorialBarSeriesOption } from "echarts/charts"
2
+ import type { EChartsOption } from "echarts"
3
+ import type { EChartTheme } from "./echart-theme"
4
+ import { areaPaint, barPaint, chartDecal, chartGradient, seriesColors } from "./chart-options.paint"
5
+ import { chartNumber, chartSeries, type ChartCurve, type ChartSeries, type ChartSpec } from "./chart-options.types"
6
+ import { cartesianTooltip, dotItemStyle } from "./chart-options.parts"
7
+
8
+ function curveOption(curve: ChartCurve | undefined): Pick<LineSeriesOption, "smooth" | "smoothMonotone" | "step"> {
9
+ if (curve === "step") return { step: "middle" }
10
+ if (!curve || curve === "linear") return { smooth: false }
11
+ return { smooth: curve === "bump" ? 0.5 : true, smoothMonotone: curve === "monotoneY" ? "y" : "x" }
12
+ }
13
+
14
+ function valuesFor(spec: ChartSpec, series: ChartSeries, all: ChartSeries[]) {
15
+ const percent = spec.stack === "percent" || spec.stack === "expanded"
16
+ return spec.data.map((row) => {
17
+ const value = chartNumber(row[series.key])
18
+ if (value === null || !percent) return value
19
+ const sum = all.reduce((total, entry) => total + Math.max(0, chartNumber(row[entry.key]) ?? 0), 0)
20
+ return sum === 0 ? 0 : Math.max(0, value) / sum * 100
21
+ })
22
+ }
23
+
24
+ function isBarSeries(spec: ChartSpec, entry: ChartSeries) {
25
+ return entry.type === "bar" || (spec.family === "bar" && !entry.type)
26
+ }
27
+
28
+ function barEndRadius(theme: EChartTheme, spec: ChartSpec, all: ChartSeries[], index: number, point: number, value: number | null): number | number[] {
29
+ if (!value) return 0
30
+ // Positive and negative stacks have independent exposed ends; missing values leave no join.
31
+ if (spec.stack && spec.stack !== "default" && all.slice(index + 1).some((entry) => {
32
+ const next = chartNumber(spec.data[point][entry.key])
33
+ return isBarSeries(spec, entry) && next !== null && next * value > 0
34
+ })) return 0
35
+ const radius = theme.markRadius
36
+ if (spec.horizontal) return (value > 0) !== theme.rtl ? [0, radius, radius, 0] : [radius, 0, 0, radius]
37
+ return value > 0 ? [radius, radius, 0, 0] : [0, 0, radius, radius]
38
+ }
39
+
40
+ function lineSeries(theme: EChartTheme, spec: ChartSpec, entry: ChartSeries, index: number, all: ChartSeries[]): LineSeriesOption[] {
41
+ const values = valuesFor(spec, entry, all)
42
+ const colors = seriesColors(theme, entry, index)
43
+ const isArea = entry.type === "area" || spec.family === "area"
44
+ // Forecast tails must not enter a stack as a duplicate series and change totals.
45
+ const hasBuffer = entry.buffer && values.length > 1 && (!spec.stack || spec.stack === "default")
46
+ const base: LineSeriesOption = {
47
+ id: entry.key,
48
+ name: entry.label ?? entry.key,
49
+ type: "line",
50
+ ...curveOption(entry.curve ?? spec.curve),
51
+ data: hasBuffer ? values.map((value, point) => point === values.length - 1 ? null : value) : values,
52
+ stack: spec.stack && spec.stack !== "default" ? "total" : undefined,
53
+ connectNulls: false,
54
+ showSymbol: entry.dots ?? Boolean(entry.dotStyle),
55
+ symbol: "circle",
56
+ symbolSize: entry.dotStyle === "border" ? theme.space.md : theme.space.sm,
57
+ lineStyle: { color: chartGradient(colors), width: theme.lineWidth, type: entry.stroke === "solid" || !entry.stroke ? "solid" : "dashed", cap: "round" },
58
+ itemStyle: dotItemStyle(theme, colors[0], entry.dotStyle),
59
+ areaStyle: isArea ? { color: areaPaint(theme, colors, entry.variant ?? spec.variant ?? "gradient"), opacity: 1 } : undefined,
60
+ emphasis: { focus: spec.hoverHighlight ? "series" : "none", scale: true, lineStyle: { width: theme.lineWidth }, itemStyle: dotItemStyle(theme, colors[0], entry.activeDotStyle ?? entry.dotStyle) },
61
+ blur: { lineStyle: { opacity: theme.opacity.muted }, areaStyle: { opacity: theme.opacity.muted }, itemStyle: { opacity: theme.opacity.muted } },
62
+ selectedMode: "single",
63
+ select: { itemStyle: { color: colors[0] } },
64
+ }
65
+ if (!hasBuffer) return [base]
66
+ return [base, {
67
+ ...base, id: `${entry.key}-buffer`,
68
+ data: values.map((value, point) => point >= values.length - 2 ? value : null),
69
+ stack: undefined, silent: true,
70
+ lineStyle: { ...base.lineStyle, type: "dashed" },
71
+ }]
72
+ }
73
+
74
+ function barSeries(theme: EChartTheme, spec: ChartSpec, entry: ChartSeries, index: number, all: ChartSeries[]): (BarSeriesOption | PictorialBarSeriesOption)[] {
75
+ const values = valuesFor(spec, entry, all)
76
+ const colors = seriesColors(theme, entry, index)
77
+ const variant = entry.variant ?? spec.variant ?? "default"
78
+ const totals = spec.data.map((row) => all.reduce((total, series) => total + (chartNumber(row[series.key]) ?? 0), 0))
79
+ const maximumIndex = totals.indexOf(Math.max(...totals))
80
+ const fill = barPaint(theme, colors, variant, Boolean(spec.horizontal))
81
+ const data = values.map((value, point) => ({
82
+ value,
83
+ itemStyle: {
84
+ color: spec.highlightMax && point !== maximumIndex ? theme.neutral : fill,
85
+ decal: chartDecal(theme, colors[0], entry.buffer && point === values.length - 1 ? "hatched" : variant),
86
+ borderWidth: entry.buffer && point === values.length - 1 ? theme.borderWidth : 0,
87
+ borderColor: colors[0],
88
+ borderRadius: variant === "blocks" || variant === "expandable" ? 0 : barEndRadius(theme, spec, all, index, point, value),
89
+ },
90
+ }))
91
+ const base: BarSeriesOption = {
92
+ id: entry.key, name: entry.label ?? entry.key, type: "bar", data,
93
+ stack: spec.stack && spec.stack !== "default" ? "total" : undefined,
94
+ barMaxWidth: theme.space.xxl,
95
+ barCategoryGap: "35%",
96
+ itemStyle: { color: fill, borderRadius: 0 },
97
+ emphasis: { focus: spec.hoverHighlight ? "series" : "none", itemStyle: { color: colors[0] }, label: { show: variant === "expandable", position: spec.horizontal ? "right" : "top", color: theme.foreground, fontFamily: theme.fontFamily, fontSize: theme.fontSize, fontWeight: theme.mediumWeight } },
98
+ blur: { itemStyle: { opacity: theme.opacity.muted } },
99
+ selectedMode: "single",
100
+ select: { itemStyle: { color: colors[0] } },
101
+ }
102
+ if (variant !== "blocks") return [base]
103
+ const max = spec.max ?? Math.max(1, ...values.map((value) => value ?? 0))
104
+ const blocks: PictorialBarSeriesOption = {
105
+ id: entry.key, name: base.name, type: "pictorialBar", data,
106
+ symbol: "rect", symbolRepeat: true, symbolClip: true,
107
+ symbolSize: spec.horizontal ? [theme.space.sm, theme.space.lg] : [theme.space.lg, theme.space.sm],
108
+ symbolMargin: theme.borderWidth, symbolBoundingData: max,
109
+ itemStyle: { color: colors[0] }, z: 2,
110
+ emphasis: { itemStyle: { color: colors[0] } },
111
+ }
112
+ const background: PictorialBarSeriesOption = {
113
+ ...blocks, id: `${entry.key}-track`, name: "", data: values.map(() => max),
114
+ symbolClip: false, silent: true, tooltip: { show: false },
115
+ itemStyle: { color: theme.neutral }, z: 0,
116
+ }
117
+ return [background, blocks]
118
+ }
119
+
120
+ export function cartesianOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
121
+ const all = chartSeries(spec)
122
+ const percent = spec.stack === "percent" || spec.stack === "expanded"
123
+ const category = {
124
+ type: "category" as const,
125
+ data: spec.data.map((row) => String(row[spec.categoryKey ?? "name"] ?? "")),
126
+ boundaryGap: spec.family === "bar" || spec.family === "composed",
127
+ inverse: spec.horizontal ? true : theme.rtl,
128
+ position: spec.horizontal && theme.rtl ? "right" as const : undefined,
129
+ axisTick: { show: false }, axisLine: { show: false },
130
+ axisLabel: { color: theme.muted, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize, margin: theme.space.md },
131
+ }
132
+ const value = {
133
+ type: "value" as const,
134
+ inverse: Boolean(spec.horizontal && theme.rtl),
135
+ position: !spec.horizontal && theme.rtl ? "right" as const : undefined,
136
+ max: percent ? 100 : spec.max,
137
+ axisLine: { show: false }, axisTick: { show: false },
138
+ axisLabel: { color: theme.muted, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize, formatter: percent ? "{value}%" : undefined },
139
+ splitLine: { lineStyle: { color: theme.border, width: theme.borderWidth, type: "dashed" as const } },
140
+ }
141
+ const series = all.flatMap<LineSeriesOption | BarSeriesOption | PictorialBarSeriesOption>((entry, index) => isBarSeries(spec, entry)
142
+ ? barSeries(theme, spec, entry, index, all)
143
+ : lineSeries(theme, spec, entry, index, all))
144
+ return {
145
+ grid: { left: theme.rtl ? theme.space.lg : theme.space.axis, right: theme.rtl ? theme.space.axis : theme.space.lg, top: spec.legend === false ? theme.space.lg : theme.space.axis, bottom: spec.brush ? theme.space.axis * 2 : theme.space.axis },
146
+ xAxis: spec.horizontal ? value : category,
147
+ yAxis: spec.horizontal ? category : value,
148
+ legend: { show: spec.legend !== false, data: all.map((entry) => entry.label ?? entry.key), top: 0, right: 0 },
149
+ tooltip: { trigger: "axis", formatter: cartesianTooltip(theme, spec), axisPointer: { type: "line", lineStyle: { color: theme.border, width: theme.borderWidth } } },
150
+ dataZoom: spec.brush ? [{
151
+ type: "slider", xAxisIndex: spec.horizontal ? undefined : 0, yAxisIndex: spec.horizontal ? 0 : undefined,
152
+ bottom: theme.space.xs, height: theme.space.xxl, start: 0, end: 100,
153
+ borderColor: theme.border, backgroundColor: theme.surface, fillerColor: theme.neutral,
154
+ handleStyle: { color: theme.surface, borderColor: theme.border },
155
+ moveHandleStyle: { color: theme.border },
156
+ textStyle: { color: theme.muted, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize },
157
+ dataBackground: { lineStyle: { color: theme.muted }, areaStyle: { color: theme.neutral } },
158
+ selectedDataBackground: { lineStyle: { color: theme.palette[0] }, areaStyle: { color: theme.neutral } },
159
+ }] : [],
160
+ series,
161
+ }
162
+ }
@@ -0,0 +1,99 @@
1
+ import { color, graphic } from "echarts/core"
2
+ import type { EChartTheme } from "./echart-theme"
3
+ import { chartColor, type ChartSeries, type ChartVariant } from "./chart-options.types"
4
+
5
+ export function chartAlpha(paint: string, opacity: number): string {
6
+ return color.modifyAlpha(paint, opacity) ?? paint
7
+ }
8
+
9
+ export function seriesColors(theme: EChartTheme, series: ChartSeries, index: number): string[] {
10
+ return (series.colorIndices?.length ? series.colorIndices : [series.colorIndex ?? index]).map((slot) => chartColor(theme, slot))
11
+ }
12
+
13
+ export function chartGradient(colors: string[], vertical = false) {
14
+ if (colors.length === 1) return colors[0]
15
+ return new graphic.LinearGradient(0, 0, vertical ? 0 : 1, vertical ? 1 : 0,
16
+ colors.map((paint, index) => ({ offset: index / (colors.length - 1), color: paint })))
17
+ }
18
+
19
+ export function areaPaint(theme: EChartTheme, colors: string[], variant: ChartVariant) {
20
+ if (["dotted", "lines", "hatched"].includes(variant) && typeof document !== "undefined") {
21
+ const tile = document.createElement("canvas")
22
+ tile.width = Math.max(1, Math.round(theme.space.md))
23
+ tile.height = tile.width
24
+ const context = tile.getContext("2d")
25
+ if (context) {
26
+ context.fillStyle = chartAlpha(colors[0], theme.opacity.soft)
27
+ context.fillRect(0, 0, tile.width, tile.height)
28
+ context.fillStyle = chartAlpha(colors[0], theme.opacity.area)
29
+ context.strokeStyle = context.fillStyle
30
+ context.lineWidth = theme.borderWidth
31
+ if (variant === "dotted") {
32
+ context.beginPath()
33
+ context.arc(tile.width / 2, tile.height / 2, theme.borderWidth, 0, Math.PI * 2)
34
+ context.fill()
35
+ } else if (variant === "lines") {
36
+ context.fillRect(0, 0, tile.width, theme.borderWidth)
37
+ } else {
38
+ for (const offset of [-tile.width, 0, tile.width]) {
39
+ context.beginPath()
40
+ context.moveTo(offset, tile.height)
41
+ context.lineTo(offset + tile.width, 0)
42
+ context.stroke()
43
+ }
44
+ }
45
+ return { image: tile, repeat: "repeat" as const }
46
+ }
47
+ }
48
+ if (variant === "gradient" || variant === "gradient-reverse") {
49
+ const reverse = variant === "gradient-reverse"
50
+ return new graphic.LinearGradient(0, 0, 0, 1, [
51
+ { offset: 0, color: chartAlpha(colors[0], reverse ? 0 : theme.opacity.area) },
52
+ { offset: 1, color: chartAlpha(colors.at(-1)!, reverse ? theme.opacity.area : 0) },
53
+ ])
54
+ }
55
+ return chartGradient(colors.map((paint) => chartAlpha(paint, theme.opacity.area)))
56
+ }
57
+
58
+ export function chartDecal(theme: EChartTheme, paint: string, variant: ChartVariant) {
59
+ if (!["hatched", "lines", "dotted"].includes(variant)) return undefined
60
+ return {
61
+ symbol: variant === "dotted" ? "circle" : "rect",
62
+ symbolSize: variant === "dotted" ? theme.opacity.area : 1,
63
+ color: paint,
64
+ backgroundColor: chartAlpha(paint, theme.opacity.soft),
65
+ dashArrayX: variant === "dotted" ? [theme.space.xs, theme.space.xs] : [1, 0],
66
+ dashArrayY: [theme.borderWidth, theme.space.xs],
67
+ rotation: variant === "hatched" ? -Math.PI / 4 : 0,
68
+ }
69
+ }
70
+
71
+ export function expandablePaint(paint: string, fraction: number, horizontal: boolean) {
72
+ const low = (1 - fraction) / 2
73
+ const high = 1 - low
74
+ const clear = chartAlpha(paint, 0)
75
+ return new graphic.LinearGradient(0, 0, horizontal ? 0 : 1, horizontal ? 1 : 0, [
76
+ { offset: 0, color: clear }, { offset: low, color: clear },
77
+ { offset: low, color: paint }, { offset: high, color: paint },
78
+ { offset: high, color: clear }, { offset: 1, color: clear },
79
+ ])
80
+ }
81
+
82
+ export function barPaint(theme: EChartTheme, colors: string[], variant: ChartVariant, horizontal: boolean, capFraction = theme.space.xs / theme.space.axis) {
83
+ if (variant === "expandable") return expandablePaint(colors[0], theme.opacity.soft, horizontal)
84
+ if (variant === "gradient") return new graphic.LinearGradient(0, 0, horizontal ? 1 : 0, horizontal ? 0 : 1, [
85
+ { offset: 0, color: colors[0] }, { offset: 1, color: chartAlpha(colors.at(-1)!, theme.opacity.soft) },
86
+ ])
87
+ if (variant === "duotone" || variant === "duotone-reverse" || variant === "stripped") {
88
+ const muted = chartAlpha(colors[0], theme.opacity.area)
89
+ const reverse = variant === "duotone-reverse" || (variant === "stripped" && horizontal)
90
+ const first = reverse ? muted : colors[0]
91
+ const second = reverse ? colors[0] : muted
92
+ const split = variant === "stripped" ? horizontal ? 1 - capFraction : capFraction : 0.5
93
+ return new graphic.LinearGradient(0, 0, horizontal ? 1 : 0, horizontal ? 0 : 1, [
94
+ { offset: 0, color: first }, { offset: split, color: first },
95
+ { offset: split, color: second }, { offset: 1, color: second },
96
+ ])
97
+ }
98
+ return chartGradient(colors, !horizontal)
99
+ }
@@ -0,0 +1,72 @@
1
+ import type { EChartsOption, LegendComponentOption, TooltipComponentOption } from "echarts"
2
+ import type { EChartTheme } from "./echart-theme"
3
+ import { chartAlpha } from "./chart-options.paint"
4
+ import { chartColor, chartNumber, chartSeries, type ChartDotStyle, type ChartSpec } from "./chart-options.types"
5
+
6
+ export function dotItemStyle(theme: EChartTheme, paint: string, style: ChartDotStyle = "default") {
7
+ return {
8
+ color: style === "colored-border" ? theme.surface : paint,
9
+ borderColor: style === "border" ? theme.surface : paint,
10
+ borderWidth: style === "border" ? theme.lineWidth : style === "colored-border" ? theme.borderWidth : 0,
11
+ }
12
+ }
13
+
14
+ function escapeHtml(value: string) {
15
+ return value.replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!)
16
+ }
17
+
18
+ /** Read the original row so split forecast series don't duplicate or hide tooltip values. */
19
+ export function cartesianTooltip(theme: EChartTheme, spec: ChartSpec): NonNullable<TooltipComponentOption["formatter"]> {
20
+ const entries = chartSeries(spec)
21
+ return (params) => {
22
+ const point = Array.isArray(params) ? params[0] : params
23
+ const row = spec.data[point.dataIndex]
24
+ if (!row) return ""
25
+ const title = escapeHtml(String(row[spec.categoryKey ?? "name"] ?? ""))
26
+ const rows = entries.map((entry) => {
27
+ const value = chartNumber(row[entry.key])
28
+ return `<div>${escapeHtml(entry.label ?? entry.key)}: <span style="font-weight:${theme.mediumWeight}">${value === null ? "—" : value.toLocaleString()}</span></div>`
29
+ })
30
+ return `<div>${title}</div>${rows.join("")}`
31
+ }
32
+ }
33
+
34
+ function styleLegend(theme: EChartTheme, spec: ChartSpec, legend: LegendComponentOption): LegendComponentOption {
35
+ const variant = spec.legendIndicator ?? "rounded-square"
36
+ const all = chartSeries(spec)
37
+ const data = legend.data ?? (spec.family === "pie" || spec.family === "radial"
38
+ ? spec.data.map((row) => String(row[spec.categoryKey ?? "name"] ?? ""))
39
+ : all.map((series) => series.label ?? series.key))
40
+ const outlined = variant.endsWith("outline")
41
+ const icon = variant.startsWith("circle") ? "circle" : variant.startsWith("rounded") ? "roundRect" : "rect"
42
+ return {
43
+ ...legend, icon, align: theme.rtl ? "right" : "left",
44
+ itemWidth: variant === "vertical-bar" ? theme.space.xs : variant === "horizontal-bar" ? theme.space.lg : theme.space.md,
45
+ itemHeight: variant === "horizontal-bar" ? theme.space.xs : variant === "vertical-bar" ? theme.space.lg : theme.space.md,
46
+ data: data.map((item, index) => {
47
+ const paint = chartColor(theme, all[index]?.colorIndex ?? index)
48
+ return { ...(typeof item === "string" ? { name: item } : item), icon, itemStyle: { color: outlined ? theme.surface : paint, borderColor: paint, borderWidth: outlined ? theme.borderWidth : 0 } }
49
+ }),
50
+ }
51
+ }
52
+
53
+ export function styleChartParts(theme: EChartTheme, spec: ChartSpec, option: EChartsOption): EChartsOption {
54
+ const originalTooltip = !Array.isArray(option.tooltip) ? option.tooltip ?? {} : {}
55
+ const tooltip: TooltipComponentOption = {
56
+ ...originalTooltip,
57
+ backgroundColor: spec.tooltipSurface === "translucent" ? chartAlpha(theme.surface, theme.translucentOpacity ?? theme.opacity.muted) : theme.surface,
58
+ borderColor: theme.border, borderWidth: theme.borderWidth,
59
+ borderRadius: theme.radii?.[spec.tooltipRadius ?? "lg"] ?? theme.radius,
60
+ textStyle: { color: theme.foreground, fontFamily: theme.fontFamily, fontSize: theme.fontSize, fontWeight: theme.fontWeight, lineHeight: theme.lineHeight },
61
+ axisPointer: { ...originalTooltip.axisPointer, show: spec.tooltipCursor !== false },
62
+ }
63
+ if (spec.tooltipPosition === "fixed") {
64
+ tooltip.position = (point, _params, _element, _rect, size) => [Math.max(0, Math.min(point[0] - size.contentSize[0] / 2, size.viewSize[0] - size.contentSize[0])), theme.space.lg]
65
+ }
66
+ return {
67
+ ...option, tooltip,
68
+ legend: Array.isArray(option.legend)
69
+ ? option.legend.map((legend) => styleLegend(theme, spec, legend))
70
+ : option.legend ? styleLegend(theme, spec, option.legend) : undefined,
71
+ }
72
+ }
@@ -0,0 +1,109 @@
1
+ import type { EChartsOption } from "echarts"
2
+ import type { BarSeriesOption, PieSeriesOption, RadarSeriesOption } from "echarts/charts"
3
+ import type { EChartTheme } from "./echart-theme"
4
+ import { chartAlpha, chartGradient, seriesColors } from "./chart-options.paint"
5
+ import { chartColor, chartNumber, chartSeries, type ChartSpec } from "./chart-options.types"
6
+ import { dotItemStyle } from "./chart-options.parts"
7
+
8
+ function labelStyle(theme: EChartTheme) {
9
+ return { color: theme.foreground, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize, fontWeight: theme.mediumWeight, lineHeight: theme.lineHeight }
10
+ }
11
+
12
+ export function pieOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
13
+ const entry = chartSeries(spec)[0]
14
+ const category = spec.categoryKey ?? "name"
15
+ const donut = ["donut", "padded", "petal"].includes(spec.variant ?? "")
16
+ const data = spec.data.flatMap((row, index) => {
17
+ const value = chartNumber(row[entry.key])
18
+ if (value === null || value < 0) return []
19
+ const colors = entry.colorIndices?.length
20
+ ? [chartColor(theme, index), chartColor(theme, index + 1)]
21
+ : [chartColor(theme, index + (entry.colorIndex ?? 0))]
22
+ return [{ name: String(row[category] ?? ""), value, itemStyle: { color: chartGradient(colors) } }]
23
+ })
24
+ const series: PieSeriesOption = {
25
+ id: entry.key, type: "pie", data,
26
+ radius: [donut ? "48%" : "0%", spec.labels === "outside" ? "58%" : "74%"],
27
+ center: ["50%", "50%"],
28
+ padAngle: spec.variant === "padded" ? theme.borderWidth * 2 : 0,
29
+ itemStyle: {
30
+ borderRadius: spec.variant === "petal" || spec.variant === "padded" ? theme.markRadius : 0,
31
+ borderColor: theme.surface,
32
+ borderWidth: spec.variant === "petal" ? theme.space.xs : spec.variant === "padded" ? theme.borderWidth : 0,
33
+ },
34
+ selectedMode: "single", selectedOffset: theme.space.md,
35
+ emphasis: { scale: true, scaleSize: theme.space.xs, label: labelStyle(theme) },
36
+ label: { ...labelStyle(theme), show: spec.labels === "inside" || spec.labels === "outside", position: spec.labels === "inside" ? "inside" : "outside", formatter: spec.labels === "inside" ? "{d}%" : "{b}", backgroundColor: spec.labels === "inside" ? theme.surface : undefined, padding: spec.labels === "inside" ? theme.space.xs : 0, borderRadius: theme.radius },
37
+ labelLine: { show: spec.labels === "outside", lineStyle: { color: theme.border, width: theme.borderWidth }, length: theme.space.md, length2: theme.space.md },
38
+ }
39
+ return { legend: { show: spec.legend !== false, bottom: 0 }, tooltip: { trigger: "item" }, series: [series] }
40
+ }
41
+
42
+ export function radialOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
43
+ const entry = chartSeries(spec)[0]
44
+ const rows = spec.data.filter((row) => chartNumber(row[entry.key]) !== null && Number(row[entry.key]) >= 0)
45
+ const categories = rows.map((row) => String(row[spec.categoryKey ?? "name"] ?? ""))
46
+ const max = spec.max && spec.max > 0 ? spec.max : Math.max(1, ...rows.map((row) => Number(row[entry.key])))
47
+ const semi = spec.variant === "semi"
48
+ const series: BarSeriesOption[] = rows.map((row, index) => ({
49
+ id: `${entry.key}-${index}`, name: categories[index], type: "bar", coordinateSystem: "polar", polarIndex: 0,
50
+ data: [{ name: categories[index], value: [Number(row[entry.key]), index] }],
51
+ encode: { angle: 0, radius: 1 },
52
+ // Every series occupies a different category. Sharing the stack keeps ECharts
53
+ // from allocating a progressively smaller grouped bar band to each ring.
54
+ stack: "rings", barWidth: theme.space.lg, roundCap: true,
55
+ showBackground: false, z: 3,
56
+ itemStyle: { color: chartGradient(entry.colorIndices?.length ? [chartColor(theme, index), chartColor(theme, index + 1)] : [chartColor(theme, index + (entry.colorIndex ?? 0))]) },
57
+ emphasis: { focus: "series" },
58
+ selectedMode: "single",
59
+ label: { ...labelStyle(theme), show: spec.labels === "inside", position: "middle", backgroundColor: theme.surface, padding: theme.space.xs, borderRadius: theme.radius },
60
+ }))
61
+ // Native polar backgrounds always draw 360 degrees. A second identical polar
62
+ // gives the explicit track its own bar band and respects the semi-circle sweep.
63
+ series.push({
64
+ id: "ring-tracks", type: "bar", coordinateSystem: "polar", polarIndex: 1,
65
+ data: rows.map((_row, index) => ({ value: [max, index] })), encode: { angle: 0, radius: 1 },
66
+ barWidth: theme.space.lg, roundCap: true, silent: true, animation: false,
67
+ itemStyle: { color: theme.neutral }, emphasis: { disabled: true },
68
+ tooltip: { show: false }, z: 1,
69
+ })
70
+ return {
71
+ polar: [0, 1].map(() => ({ radius: ["28%", "78%"], center: ["50%", semi ? "68%" : "46%"] })),
72
+ angleAxis: [0, 1].map((polarIndex) => ({ polarIndex, type: "value", min: 0, max, startAngle: semi ? 180 : 90, endAngle: semi ? 0 : -270, clockwise: true, show: false })),
73
+ radiusAxis: [0, 1].map((polarIndex) => ({ polarIndex, type: "category", data: categories, inverse: true, show: false })),
74
+ legend: { show: spec.legend !== false, bottom: 0, data: categories },
75
+ tooltip: { trigger: "item", valueFormatter: (value) => String(Array.isArray(value) ? value[0] : value) },
76
+ series,
77
+ }
78
+ }
79
+
80
+ export function radarOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
81
+ const all = chartSeries(spec)
82
+ const max = spec.max && spec.max > 0 ? spec.max : Math.max(1, ...spec.data.flatMap((row) => all.map((entry) => chartNumber(row[entry.key]) ?? 0)))
83
+ const series: RadarSeriesOption[] = all.flatMap((entry, index) => {
84
+ const values = spec.data.map((row) => chartNumber(row[entry.key]))
85
+ if (values.some((value) => value === null)) return []
86
+ const colors = seriesColors(theme, entry, index)
87
+ return [{
88
+ id: entry.key, name: entry.label ?? entry.key, type: "radar",
89
+ data: [{ name: entry.label ?? entry.key, value: values as number[] }],
90
+ lineStyle: { color: chartGradient(colors), width: theme.lineWidth },
91
+ areaStyle: entry.variant === "lines" || spec.variant === "lines" ? { opacity: 0 } : { color: chartGradient(colors.map((paint) => chartAlpha(paint, theme.opacity.area))), opacity: 1 },
92
+ symbol: entry.dots || entry.dotStyle ? "circle" : "none", symbolSize: entry.dotStyle === "border" ? theme.space.md : theme.space.sm,
93
+ itemStyle: dotItemStyle(theme, colors[0], entry.dotStyle),
94
+ emphasis: { focus: "self", itemStyle: dotItemStyle(theme, colors[0], entry.activeDotStyle ?? entry.dotStyle) },
95
+ }]
96
+ })
97
+ return {
98
+ radar: {
99
+ indicator: spec.data.map((row) => ({ name: String(row[spec.categoryKey ?? "name"] ?? ""), max })),
100
+ shape: spec.grid === "circle" ? "circle" : "polygon", radius: "65%",
101
+ axisName: { color: theme.muted, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize },
102
+ axisLine: { lineStyle: { color: theme.border, width: theme.borderWidth } },
103
+ splitLine: { lineStyle: { color: theme.border, width: theme.borderWidth } },
104
+ splitArea: { show: false },
105
+ },
106
+ legend: { show: spec.legend !== false, bottom: 0, data: all.map((entry) => entry.label ?? entry.key) },
107
+ tooltip: { trigger: "item" }, series,
108
+ }
109
+ }
@@ -0,0 +1,44 @@
1
+ import type { EChartsOption } from "echarts"
2
+ import type { EChartTheme } from "./echart-theme"
3
+ import { cartesianOption } from "./chart-options.cartesian"
4
+ import { pieOption, radarOption, radialOption } from "./chart-options.radial"
5
+ import { chartColor, chartNumber, type ChartSpec } from "./chart-options.types"
6
+ import { styleChartParts } from "./chart-options.parts"
7
+
8
+ export type { ChartCurve, ChartDatum, ChartDotStyle, ChartFamily, ChartLegendIndicator, ChartSeries, ChartSpec, ChartStroke, ChartVariant } from "./chart-options.types"
9
+
10
+ /** Build native ECharts options using only resolved Banhaten tokens. */
11
+ export function createChartOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
12
+ return styleChartParts(theme, spec, createFamilyOption(theme, spec))
13
+ }
14
+
15
+ function createFamilyOption(theme: EChartTheme, spec: ChartSpec): EChartsOption {
16
+ if (["area", "line", "bar", "composed"].includes(spec.family)) return cartesianOption(theme, spec)
17
+ if (spec.family === "pie") return pieOption(theme, spec)
18
+ if (spec.family === "radial") return radialOption(theme, spec)
19
+ if (spec.family === "radar") return radarOption(theme, spec)
20
+ const nodes = spec.nodes ?? spec.data.map((row) => ({ name: String(row[spec.categoryKey ?? "name"] ?? "") }))
21
+ const names = new Set(nodes.map((node) => node.name))
22
+ const resolveName = (value: string | number) => typeof value === "number" ? nodes[value]?.name : value
23
+ const links = (spec.links ?? []).flatMap((link) => {
24
+ const source = resolveName(link.source)
25
+ const target = resolveName(link.target)
26
+ const value = chartNumber(link.value)
27
+ return source && target && names.has(source) && names.has(target) && value !== null && value > 0
28
+ ? [{ source, target, value }]
29
+ : []
30
+ })
31
+ return {
32
+ tooltip: { trigger: "item" },
33
+ series: [{
34
+ id: "flow", type: "sankey", orient: spec.horizontal === false ? "vertical" : "horizontal",
35
+ left: theme.space.lg, right: theme.space.axis * 2, top: theme.space.lg, bottom: theme.space.lg,
36
+ data: nodes.map((node, index) => ({ name: node.name, itemStyle: { color: chartColor(theme, "colorIndex" in node ? Number(node.colorIndex) : index), borderWidth: 0 } })),
37
+ links, nodeWidth: spec.labels === "inside" ? theme.space.axis * 2 : theme.space.lg,
38
+ nodeGap: theme.space.xl, draggable: false,
39
+ lineStyle: { color: spec.variant === "solid" ? theme.palette[0] : spec.variant === "target" ? "target" : spec.variant === "gradient" ? "gradient" : "source", opacity: theme.opacity.area, curveness: 0.5 },
40
+ emphasis: { focus: "adjacency", lineStyle: { opacity: theme.opacity.muted } },
41
+ label: { show: spec.labels !== "none", position: spec.labels === "inside" ? "inside" : "right", color: theme.foreground, fontFamily: theme.fontFamily, fontSize: theme.labelFontSize, fontWeight: theme.fontWeight, distance: theme.space.sm, backgroundColor: spec.labels === "inside" ? theme.surface : undefined, padding: spec.labels === "inside" ? theme.space.xs : 0, borderRadius: theme.radius },
42
+ }],
43
+ }
44
+ }
@@ -0,0 +1,67 @@
1
+ import type { EChartTheme } from "./echart-theme"
2
+
3
+ export type ChartFamily = "area" | "line" | "bar" | "composed" | "radar" | "pie" | "radial" | "sankey"
4
+ export type ChartDatum = Record<string, string | number | null>
5
+ export type ChartVariant = "default" | "gradient" | "gradient-reverse" | "solid" | "dotted" | "lines" | "hatched" | "duotone" | "duotone-reverse" | "stripped" | "blocks" | "expandable" | "filled" | "donut" | "padded" | "petal" | "full" | "semi" | "source" | "target"
6
+ export type ChartCurve = "linear" | "smooth" | "bump" | "monotone" | "monotoneX" | "monotoneY" | "natural" | "step"
7
+ export type ChartStroke = "solid" | "dashed" | "animated-dashed"
8
+ export type ChartDotStyle = "default" | "border" | "colored-border"
9
+ export type ChartLegendIndicator = "square" | "circle" | "circle-outline" | "rounded-square" | "rounded-square-outline" | "vertical-bar" | "horizontal-bar"
10
+
11
+ /** Series colors are palette indices: application data cannot introduce off-brand colors. */
12
+ export interface ChartSeries {
13
+ key: string
14
+ label?: string
15
+ type?: "line" | "bar" | "area"
16
+ colorIndex?: number
17
+ colorIndices?: number[]
18
+ variant?: ChartVariant
19
+ stroke?: ChartStroke
20
+ curve?: ChartCurve
21
+ buffer?: boolean
22
+ dots?: boolean
23
+ dotStyle?: ChartDotStyle
24
+ activeDotStyle?: ChartDotStyle
25
+ }
26
+
27
+ export interface ChartSpec {
28
+ family: ChartFamily
29
+ data: ChartDatum[]
30
+ series?: ChartSeries[]
31
+ categoryKey?: string
32
+ variant?: ChartVariant
33
+ stack?: "default" | "stacked" | "percent" | "expanded"
34
+ curve?: ChartCurve
35
+ horizontal?: boolean
36
+ legend?: boolean
37
+ brush?: boolean
38
+ labels?: "none" | "inside" | "outside"
39
+ max?: number
40
+ grid?: "polygon" | "circle"
41
+ nodes?: { name: string; colorIndex?: number }[]
42
+ links?: { source: string | number; target: string | number; value: number }[]
43
+ highlightMax?: boolean
44
+ hoverHighlight?: boolean
45
+ hoverReveal?: boolean
46
+ legendIndicator?: ChartLegendIndicator
47
+ tooltipSurface?: "default" | "translucent"
48
+ tooltipRadius?: "sm" | "md" | "lg" | "xl"
49
+ tooltipCursor?: boolean
50
+ tooltipPosition?: "variable" | "fixed"
51
+ }
52
+
53
+ export function chartSeries(spec: ChartSpec): ChartSeries[] {
54
+ if (spec.series?.length) return spec.series
55
+ const key = Object.keys(spec.data[0] ?? {}).find((entry) =>
56
+ entry !== (spec.categoryKey ?? "name") && spec.data.some((row) => typeof row[entry] === "number"))
57
+ return [{ key: key ?? "value" }]
58
+ }
59
+
60
+ /** Missing values remain gaps; they never become a fabricated zero or infinity. */
61
+ export function chartNumber(value: unknown): number | null {
62
+ return typeof value === "number" && Number.isFinite(value) ? value : null
63
+ }
64
+
65
+ export function chartColor(theme: EChartTheme, index: number): string {
66
+ return theme.palette[((Math.trunc(index) % theme.palette.length) + theme.palette.length) % theme.palette.length] ?? theme.foreground
67
+ }
@@ -107,7 +107,7 @@ function ChartTooltipContent({
107
107
  <div
108
108
  data-slot="chart-tooltip"
109
109
  className={cn(
110
- "grid min-w-[var(--bh-space-17xl-288)] gap-[var(--bh-space-sm-6)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-xl-12)] text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-default)] shadow-[var(--shadow-popover)]",
110
+ "grid min-w-[var(--bh-space-17xl-288)] gap-[var(--bh-space-sm-6)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-xl-12)] text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-default)] shadow-[var(--elevation-popover)]",
111
111
  className
112
112
  )}
113
113
  {...forwardableDomProps(props)}
@@ -13,11 +13,11 @@ type CheckboxCheckedState = NonNullable<
13
13
  type CheckboxCardControlPosition = "start" | "end"
14
14
 
15
15
  const checkboxVariants = cva(
16
- "peer group/checkbox relative size-[var(--bh-checkbox-root-size)] shrink-0 rounded-[var(--bh-checkbox-control-radius)] outline-none transition-[box-shadow] focus-visible:[&>[data-slot=checkbox-control]]:shadow-[var(--shadow-checkbox-focus)] disabled:pointer-events-none data-[disabled]:pointer-events-none data-[state=checked]:hover:[&>[data-slot=checkbox-control]]:bg-[var(--bh-interactive-checkbox-hover)] data-[state=indeterminate]:hover:[&>[data-slot=checkbox-control]]:bg-[var(--bh-interactive-checkbox-hover)]"
16
+ "peer group/checkbox relative size-[var(--bh-checkbox-root-size)] shrink-0 rounded-[var(--bh-checkbox-control-radius)] outline-none transition-[box-shadow] focus-visible:[&>[data-slot=checkbox-control]]:shadow-[var(--elevation-checkbox-focus)] disabled:pointer-events-none data-[disabled]:pointer-events-none data-[state=checked]:hover:[&>[data-slot=checkbox-control]]:bg-[var(--bh-interactive-checkbox-hover)] data-[state=indeterminate]:hover:[&>[data-slot=checkbox-control]]:bg-[var(--bh-interactive-checkbox-hover)]"
17
17
  )
18
18
 
19
19
  const checkboxCardVariants = cva(
20
- "group/checkbox-card relative flex w-[var(--bh-checkbox-card-width)] max-w-full overflow-hidden rounded-[var(--bh-checkbox-card-radius)] bg-[var(--bh-bg-raised)] p-[var(--bh-checkbox-card-padding)] text-start shadow-[var(--shadow-checkbox-card)] outline-none transition-[background-color,box-shadow] [--bh-checkbox-card-border:var(--bh-border-subtle)] [--bh-checkbox-card-border-width-current:var(--bh-checkbox-card-border-width)] [--shadow-checkbox-card:inset_0px_0px_0px_var(--bh-checkbox-card-border-width-current,var(--bh-border-width-default))_var(--bh-checkbox-card-border,var(--bh-border-subtle)),var(--shadow-component-default)] hover:bg-[var(--bh-bg-raised-hover)] focus-visible:shadow-[var(--shadow-checkbox-card-focus)] data-[checked=true]:bg-[var(--bh-bg-raised)] data-[checked=true]:[--bh-checkbox-card-border:var(--bh-border-brand-strong)] data-[checked=true]:[--bh-checkbox-card-border-width-current:var(--bh-checkbox-card-border-width-active)] disabled:pointer-events-none disabled:bg-[var(--bh-bg-disabled)] disabled:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)] data-[disabled=true]:pointer-events-none data-[disabled=true]:bg-[var(--bh-bg-disabled)] data-[disabled=true]:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
20
+ "group/checkbox-card relative flex w-[var(--bh-checkbox-card-width)] max-w-full overflow-hidden rounded-[var(--bh-checkbox-card-radius)] bg-[var(--bh-bg-raised)] p-[var(--bh-checkbox-card-padding)] text-start shadow-[var(--elevation-checkbox-card)] outline-none transition-[background-color,box-shadow] [--bh-checkbox-card-border:var(--bh-border-subtle)] [--bh-checkbox-card-border-width-current:var(--bh-checkbox-card-border-width)] [--elevation-checkbox-card:inset_0px_0px_0px_var(--bh-checkbox-card-border-width-current,var(--bh-border-width-default))_var(--bh-checkbox-card-border,var(--bh-border-subtle)),var(--elevation-component-default)] hover:bg-[var(--bh-bg-raised-hover)] focus-visible:shadow-[var(--elevation-checkbox-card-focus)] data-[checked=true]:bg-[var(--bh-bg-raised)] data-[checked=true]:[--bh-checkbox-card-border:var(--bh-border-brand-strong)] data-[checked=true]:[--bh-checkbox-card-border-width-current:var(--bh-checkbox-card-border-width-active)] disabled:pointer-events-none disabled:bg-[var(--bh-bg-disabled)] disabled:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)] data-[disabled=true]:pointer-events-none data-[disabled=true]:bg-[var(--bh-bg-disabled)] data-[disabled=true]:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
21
21
  {
22
22
  variants: {
23
23
  controlPosition: {
@@ -53,11 +53,11 @@ const Checkbox = React.forwardRef<
53
53
  <span
54
54
  aria-hidden="true"
55
55
  data-slot="checkbox-control"
56
- className="pointer-events-none absolute top-1/2 left-1/2 size-[var(--bh-checkbox-control-size)] -translate-x-1/2 -translate-y-1/2 rounded-[var(--bh-checkbox-control-radius)] bg-[var(--bh-bg-default)] shadow-[var(--shadow-checkbox-control)] transition-[background-color,box-shadow] [--bh-checkbox-control-border:var(--bh-border-input)] [--shadow-checkbox-control:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-checkbox-control-border,var(--bh-border-input)),var(--shadow-component-default)] group-data-[state=checked]/checkbox:[--bh-checkbox-control-border:transparent] group-data-[state=checked]/checkbox:bg-[var(--bh-interactive-checkbox-default)] group-data-[state=indeterminate]/checkbox:[--bh-checkbox-control-border:transparent] group-data-[state=indeterminate]/checkbox:bg-[var(--bh-interactive-checkbox-default)] group-data-[disabled]/checkbox:bg-[var(--bh-bg-disabled)] group-data-[disabled]/checkbox:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]"
56
+ className="pointer-events-none absolute top-1/2 left-1/2 size-[var(--bh-checkbox-control-size)] -translate-x-1/2 -translate-y-1/2 rounded-[var(--bh-checkbox-control-radius)] bg-[var(--bh-bg-default)] shadow-[var(--elevation-checkbox-control)] transition-[background-color,box-shadow] [--bh-checkbox-control-border:var(--bh-border-input)] [--elevation-checkbox-control:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-checkbox-control-border,var(--bh-border-input)),var(--elevation-component-default)] group-data-[state=checked]/checkbox:[--bh-checkbox-control-border:transparent] group-data-[state=checked]/checkbox:bg-[var(--bh-interactive-checkbox-default)] group-data-[state=indeterminate]/checkbox:[--bh-checkbox-control-border:transparent] group-data-[state=indeterminate]/checkbox:bg-[var(--bh-interactive-checkbox-default)] group-data-[disabled]/checkbox:bg-[var(--bh-bg-disabled)] group-data-[disabled]/checkbox:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]"
57
57
  />
58
58
  <CheckboxPrimitive.Indicator
59
59
  data-slot="checkbox-indicator"
60
- className="pointer-events-none absolute top-1/2 left-1/2 flex size-[var(--bh-checkbox-icon-size)] -translate-x-1/2 -translate-y-1/2 items-center justify-center text-[var(--bh-content-on-color)] group-data-[disabled]/checkbox:text-[var(--bh-content-disabled)]"
60
+ className="pointer-events-none absolute top-1/2 left-1/2 flex size-[var(--bh-checkbox-icon-size)] -translate-x-1/2 -translate-y-1/2 items-center justify-center text-[var(--bh-interactive-checkbox-content)] group-data-[disabled]/checkbox:text-[var(--bh-content-disabled)]"
61
61
  >
62
62
  <CheckIcon
63
63
  aria-hidden="true"
@@ -224,12 +224,12 @@ function CheckboxCardIndicator({
224
224
  data-disabled={disabled ? "true" : undefined}
225
225
  data-slot="checkbox-control"
226
226
  data-state={checked ? "checked" : "unchecked"}
227
- className="pointer-events-none absolute top-1/2 left-1/2 size-[var(--bh-checkbox-control-size)] -translate-x-1/2 -translate-y-1/2 rounded-[var(--bh-checkbox-control-radius)] bg-[var(--bh-bg-default)] shadow-[var(--shadow-checkbox-control)] [--bh-checkbox-control-border:var(--bh-border-input)] [--shadow-checkbox-control:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-checkbox-control-border,var(--bh-border-input)),var(--shadow-component-default)] data-[state=checked]:[--bh-checkbox-control-border:transparent] data-[state=checked]:bg-[var(--bh-interactive-checkbox-default)] data-[disabled=true]:bg-[var(--bh-bg-disabled)] data-[disabled=true]:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]"
227
+ className="pointer-events-none absolute top-1/2 left-1/2 size-[var(--bh-checkbox-control-size)] -translate-x-1/2 -translate-y-1/2 rounded-[var(--bh-checkbox-control-radius)] bg-[var(--bh-bg-default)] shadow-[var(--elevation-checkbox-control)] [--bh-checkbox-control-border:var(--bh-border-input)] [--elevation-checkbox-control:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-checkbox-control-border,var(--bh-border-input)),var(--elevation-component-default)] data-[state=checked]:[--bh-checkbox-control-border:transparent] data-[state=checked]:bg-[var(--bh-interactive-checkbox-default)] data-[disabled=true]:bg-[var(--bh-bg-disabled)] data-[disabled=true]:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]"
228
228
  />
229
229
  {checked ? (
230
230
  <CheckIcon
231
231
  aria-hidden="true"
232
- className="absolute top-1/2 left-1/2 size-[var(--bh-checkbox-icon-size)] -translate-x-1/2 -translate-y-1/2 text-[var(--bh-content-on-color)]"
232
+ className="absolute top-1/2 left-1/2 size-[var(--bh-checkbox-icon-size)] -translate-x-1/2 -translate-y-1/2 text-[var(--bh-interactive-checkbox-content)]"
233
233
  focusable="false"
234
234
  strokeWidth="var(--bh-icon-stroke-300)"
235
235
  />