ajo-ui 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 (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
package/src/chart.tsx ADDED
@@ -0,0 +1,1194 @@
1
+ import type { Children, IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callRef, clamp, dom, id as uniqueId, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { pointReference, position, type ReservedPositionArg } from './position'
5
+ import type { FixedArgs, OmitArg } from './utils'
6
+ import { text } from './utils'
7
+
8
+ type ChartTheme = 'dark' | 'light'
9
+
10
+ /** Labels, icons, and colors configured for each chart data key. */
11
+ export type ChartConfig = Record<
12
+ string,
13
+ {
14
+ /** Human label used by tooltip and legend. */
15
+ label?: Children
16
+ /** Optional icon rendered by tooltip and legend. */
17
+ icon?: Children | ((attrs: { class?: string }) => Children)
18
+ /** CSS color for this data key. */
19
+ color?: string
20
+ /** Theme-specific colors for this data key. */
21
+ theme?: Partial<Record<ChartTheme, string>>
22
+ }
23
+ >
24
+
25
+ /** Native SVG visualization supported by ChartContainer. */
26
+ export type ChartType =
27
+ | 'area'
28
+ | 'bar'
29
+ | 'line'
30
+ | 'pie'
31
+
32
+ /** One keyed data row consumed by the native chart primitives. */
33
+ export type ChartDatum = Record<string, unknown>
34
+
35
+ /** Data-key descriptor for one rendered chart series. */
36
+ export type ChartSeries = {
37
+ /** Object key used for numeric values. */
38
+ key: string
39
+ /** Human label. Defaults to config label or key. */
40
+ label?: Children
41
+ /** CSS color. Defaults to config color or a chart token. */
42
+ color?: string
43
+ }
44
+
45
+ /** Series shorthand accepted by ChartContainer. */
46
+ export type ChartSeriesInput = ChartSeries | string
47
+
48
+ /** Plot-area inset in SVG user units. */
49
+ export type ChartMargin = {
50
+ bottom: number
51
+ left: number
52
+ right: number
53
+ top: number
54
+ }
55
+
56
+ /** Resolved series value supplied to tooltip and legend formatters. */
57
+ export type ChartPayload = {
58
+ color: string
59
+ formattedValue: string
60
+ index: number
61
+ key: string
62
+ label: Children
63
+ row: ChartDatum
64
+ value: number
65
+ }
66
+
67
+ /** Active chart coordinate and payload exposed through chart context. */
68
+ export type ChartActive = {
69
+ index: number
70
+ items: ChartPayload[]
71
+ label: Children
72
+ x: number
73
+ y: number
74
+ }
75
+
76
+ /** Arguments for the accessible Chart data and context root. */
77
+ export type ChartContainerArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'children' | 'gap' | 'placement' | ReservedPositionArg> & {
78
+ /** Optional resolved chart identity override; otherwise derived from the DOM id or generated. */
79
+ chartId?: string
80
+ /** Chart series styling keyed by data key. */
81
+ config: ChartConfig
82
+ /** Rows to render when using the native SVG chart primitives. */
83
+ data?: ChartDatum[]
84
+ /** Key used for x-axis/category labels. */
85
+ xKey?: string
86
+ /** Native chart type rendered when no children are provided. */
87
+ type?: ChartType
88
+ /** Series keys or detailed series entries. Defaults to the config keys. */
89
+ series?: ChartSeriesInput[]
90
+ /** Accessible label for the chart image. */
91
+ label?: string
92
+ /** Accessible long description for the chart image. */
93
+ description?: string
94
+ /** SVG coordinate width used by native chart primitives. */
95
+ width?: number
96
+ /** SVG coordinate height used by native chart primitives. */
97
+ height?: number
98
+ /** SVG plot margins. */
99
+ margin?: Partial<ChartMargin>
100
+ /** Format category labels. */
101
+ formatLabel?: (value: unknown, row: ChartDatum, index: number) => Children
102
+ /** Format axis, accessible mark, and tooltip values. Defaults to the host locale. */
103
+ formatValue?: (value: number, key: string, row: ChartDatum, index: number) => string
104
+ /** Color palette used when a series has no configured color. */
105
+ palette: string[]
106
+ /** Classes supplied by the styled wrapper. */
107
+ class?: string
108
+ }> & FixedArgs<'gap' | 'placement' | ReservedPositionArg>
109
+
110
+ /** Arguments for native cartesian bar, line, and area plots. */
111
+ export type ChartPlotArgs = OmitArg<IntrinsicElements['svg'], 'children'> & {
112
+ /** Show grid lines. */
113
+ grid?: boolean
114
+ /** Show axes and labels. */
115
+ axis?: boolean
116
+ /** Classes supplied by the styled wrapper. */
117
+ class?: string
118
+ axisStroke?: string
119
+ axisStrokeOpacity?: string
120
+ barClass?: string
121
+ gridStroke?: string
122
+ pointClass?: string
123
+ pointFill?: string
124
+ } & FixedArgs<'children'>
125
+
126
+ /** Arguments for a native pie or donut plot. */
127
+ export type ChartPieArgs = ChartPlotArgs & {
128
+ /** Inner radius for donut charts, in SVG user units. */
129
+ innerRadius?: number
130
+ centerLabelClass?: string
131
+ centerLabelFill?: string
132
+ sliceStroke?: string
133
+ }
134
+
135
+ /** Arguments for the floating active-value tooltip. */
136
+ export type ChartTooltipArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'children' | 'gap' | 'placement' | ReservedPositionArg> & {
137
+ /** Custom tooltip content, commonly a tooltip-content component. */
138
+ content?: Children
139
+ /** Classes supplied by the styled wrapper. */
140
+ class?: string
141
+ }> & FixedArgs<'defaultIndex' | 'gap' | 'placement' | ReservedPositionArg>
142
+
143
+ type ClassResolver<State> = string | ((state: State) => string | undefined)
144
+
145
+ type TooltipContentState = {
146
+ indicator: 'dashed' | 'dot' | 'line'
147
+ nestLabel: boolean
148
+ }
149
+
150
+ /** Arguments for the default tooltip payload renderer. */
151
+ export type ChartTooltipContentArgs = OmitArg<IntrinsicElements['div'], 'children'> & {
152
+ /** Indicator shape shown beside each row. */
153
+ indicator?: 'dashed' | 'dot' | 'line'
154
+ /** Hide the category label. */
155
+ hideLabel?: boolean
156
+ /** Hide the color indicator. */
157
+ hideIndicator?: boolean
158
+ /** Additional class for the label row. */
159
+ labelClass?: string
160
+ /** Format the active label. */
161
+ labelFormatter?: (label: Children, payload: ChartPayload[]) => Children
162
+ /** Format each value row. */
163
+ formatter?: (value: number, key: string, item: ChartPayload, index: number) => Children
164
+ /** Classes supplied by the styled wrapper. */
165
+ class?: string
166
+ formattedValueClass?: string
167
+ iconClass?: string
168
+ iconWrapperClass?: string
169
+ indicatorClass?: ClassResolver<TooltipContentState>
170
+ itemClass?: ClassResolver<TooltipContentState>
171
+ itemLabelClass?: string
172
+ itemsClass?: string
173
+ nestedLabelClass?: string
174
+ valueLabelGroupClass?: string
175
+ valueRowClass?: ClassResolver<{ nestLabel: boolean }>
176
+ } & FixedArgs<'children'>
177
+
178
+ /** Arguments for the chart legend surface. */
179
+ export type ChartLegendArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'children'> & {
180
+ /** Custom legend content, commonly a legend-content component. */
181
+ content?: Children
182
+ /** Classes supplied by the styled wrapper. */
183
+ class?: string
184
+ }>
185
+
186
+ /** Arguments for the default series legend renderer. */
187
+ export type ChartLegendContentArgs = OmitArg<IntrinsicElements['div'], 'children'> & {
188
+ /** Hide configured icons and show color swatches instead. */
189
+ hideIcon?: boolean
190
+ /** Classes supplied by the styled wrapper. */
191
+ class?: string
192
+ iconClass?: string
193
+ iconWrapperClass?: string
194
+ itemClass?: string
195
+ swatchClass?: string
196
+ } & FixedArgs<'children'>
197
+
198
+ type SeriesEntry = ChartSeries & {
199
+ color: string
200
+ label: Children
201
+ }
202
+
203
+ type Point = {
204
+ x: number
205
+ y: number
206
+ }
207
+
208
+ type ChartReferencePoint = Point & {
209
+ svg: SVGSVGElement
210
+ }
211
+
212
+ type ChartContextValue = {
213
+ active: ChartActive | null
214
+ clearActive: () => void
215
+ config: ChartConfig
216
+ data: ChartDatum[]
217
+ description?: string
218
+ formatLabel: (value: unknown, row: ChartDatum, index: number) => Children
219
+ formatValue: (value: number, key: string, row: ChartDatum, index: number) => string
220
+ height: number
221
+ id: string
222
+ label?: string
223
+ margin: ChartMargin
224
+ palette: string[]
225
+ releasePlot: (plot: SVGSVGElement) => void
226
+ series: SeriesEntry[]
227
+ setActive: (active: ChartActive | null, at?: ChartReferencePoint) => void
228
+ setTooltip: (element: HTMLElement | null) => void
229
+ type: ChartType
230
+ width: number
231
+ xKey?: string
232
+ }
233
+
234
+ const ChartContext = context<ChartContextValue | null>(null)
235
+ /** Read the stable identity resolved by the nearest ChartContainer. */
236
+ export const ChartIdContext = context<string | null>(null)
237
+
238
+ const DEFAULT_MARGIN: ChartMargin = { bottom: 32, left: 40, right: 16, top: 16 }
239
+ const DEFAULT_WIDTH = 640
240
+ const DEFAULT_HEIGHT = 240
241
+
242
+ const resolveRootChartId = (value: unknown, fallback: string) =>
243
+ value ? `chart-${encodeURIComponent(String(value))}` : fallback
244
+
245
+ const resolve = <State,>(value: ClassResolver<State> | undefined, state: State) =>
246
+ typeof value === 'function' ? value(state) : value
247
+
248
+ const number = (value: unknown) => {
249
+ const next = Number(value)
250
+ return Number.isFinite(next) ? next : undefined
251
+ }
252
+
253
+ const colorFor = (
254
+ key: string,
255
+ index: number,
256
+ config: ChartConfig,
257
+ palette: string[],
258
+ override?: string,
259
+ ) =>
260
+ override ?? (config[key]?.color || config[key]?.theme ? `var(--color-${key})` : palette[index % palette.length])
261
+
262
+ const seriesEntries = (
263
+ config: ChartConfig,
264
+ series: ChartSeriesInput[] | undefined,
265
+ data: ChartDatum[] | undefined,
266
+ xKey: string | undefined,
267
+ palette: string[],
268
+ ) => {
269
+ const raw = series?.length
270
+ ? series
271
+ : Object.keys(config).length
272
+ ? Object.keys(config)
273
+ : Object.keys(data?.[0] ?? {}).filter(key => key !== xKey && number(data?.[0]?.[key]) != null)
274
+
275
+ return raw.map((item, index): SeriesEntry => {
276
+ const entry = typeof item === 'string' ? { key: item } : item
277
+ return {
278
+ ...entry,
279
+ color: colorFor(entry.key, index, config, palette, entry.color),
280
+ label: entry.label ?? config[entry.key]?.label ?? entry.key,
281
+ }
282
+ })
283
+ }
284
+
285
+ const defaultFormatLabel = (value: unknown, _row: ChartDatum, index: number) =>
286
+ value == null ? `Item ${index + 1}` : String(value)
287
+
288
+ let numberFormatter: Intl.NumberFormat | undefined
289
+
290
+ const defaultFormatValue = (value: number) =>
291
+ (numberFormatter ??= new Intl.NumberFormat()).format(value)
292
+
293
+ const labelFor = (chart: ChartContextValue, row: ChartDatum, index: number) =>
294
+ chart.formatLabel(chart.xKey ? row[chart.xKey] : undefined, row, index)
295
+
296
+ const payloadFor = (
297
+ chart: ChartContextValue,
298
+ index: number,
299
+ series = chart.series,
300
+ colors?: string[],
301
+ ) => {
302
+ const row = chart.data[index] ?? {}
303
+ return series
304
+ .map((entry, entryIndex) => {
305
+ const value = number(row[entry.key])
306
+ if (value == null) return undefined
307
+ const color = colors?.[entryIndex] ?? entry.color
308
+ return {
309
+ color,
310
+ formattedValue: chart.formatValue(value, entry.key, row, index),
311
+ index,
312
+ key: entry.key,
313
+ label: entry.label,
314
+ row,
315
+ value,
316
+ } satisfies ChartPayload
317
+ })
318
+ .filter(Boolean) as ChartPayload[]
319
+ }
320
+
321
+ const sameActiveKeys = (current: ChartActive | null, index: number, keys: string[]) =>
322
+ current?.index === index
323
+ && current.items.length === keys.length
324
+ && current.items.every((item, itemIndex) => item.key === keys[itemIndex])
325
+
326
+ const sameActiveTarget = (current: ChartActive | null, index: number, keys: string[]) =>
327
+ current?.index === index
328
+ && current.items.every(item => keys.includes(item.key))
329
+
330
+ const sameActiveValue = (current: ChartActive | null, next: ChartActive | null) => {
331
+ if (!current || !next) return current === next
332
+
333
+ return sameActiveKeys(current, next.index, next.items.map(item => item.key))
334
+ }
335
+
336
+ const extent = (chart: ChartContextValue) => {
337
+ const values = chart.data.flatMap(row => chart.series.map(entry => number(row[entry.key])).filter(value => value != null))
338
+ const min = Math.min(0, ...values)
339
+ const max = Math.max(0, ...values)
340
+ return min === max ? { max: max + 1, min: min - 1 } : { max, min }
341
+ }
342
+
343
+ const scaled = (value: number, min: number, max: number, top: number, bottom: number) =>
344
+ bottom - ((value - min) / (max - min)) * (bottom - top)
345
+
346
+ const plotBox = (chart: ChartContextValue) => ({
347
+ bottom: chart.height - chart.margin.bottom,
348
+ left: chart.margin.left,
349
+ right: chart.width - chart.margin.right,
350
+ top: chart.margin.top,
351
+ })
352
+
353
+ const clientPoint = (svg: SVGSVGElement, x: number, y: number) => {
354
+ if (!svg.isConnected) return null
355
+ const matrix = svg.getScreenCTM()
356
+ if (!matrix) return null
357
+ const point = {
358
+ x: matrix.a * x + matrix.c * y + matrix.e,
359
+ y: matrix.b * x + matrix.d * y + matrix.f,
360
+ }
361
+ return Number.isFinite(point.x) && Number.isFinite(point.y) ? point : null
362
+ }
363
+
364
+ const svgPoint = (svg: SVGSVGElement, clientX: number, clientY: number) => {
365
+ const matrix = svg.getScreenCTM()
366
+ if (!matrix) return null
367
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c
368
+ if (!Number.isFinite(determinant) || determinant === 0) return null
369
+ const x = clientX - matrix.e
370
+ const y = clientY - matrix.f
371
+ return {
372
+ x: (matrix.d * x - matrix.c * y) / determinant,
373
+ y: (-matrix.b * x + matrix.a * y) / determinant,
374
+ }
375
+ }
376
+
377
+ const referencePoint = (svg: SVGSVGElement, point: Point): ChartReferencePoint => ({
378
+ svg,
379
+ x: point.x,
380
+ y: point.y,
381
+ })
382
+
383
+ const plotRef = (chart: ChartContextValue, ref: unknown) => {
384
+ let current: SVGSVGElement | null = null
385
+ return (element: SVGSVGElement | null) => {
386
+ if (current && current !== element) chart.releasePlot(current)
387
+ current = element
388
+ callRef(ref, element)
389
+ }
390
+ }
391
+
392
+ const svgFromEvent = (event: Event) => {
393
+ const current = event.currentTarget as SVGElement | null
394
+ if (current instanceof SVGSVGElement) return current
395
+ if (current?.ownerSVGElement) return current.ownerSVGElement
396
+
397
+ const target = event.target as SVGElement | null
398
+ if (target instanceof SVGSVGElement) return target
399
+ return target?.ownerSVGElement ?? null
400
+ }
401
+
402
+ const renderIcon = (icon: ChartConfig[string]['icon'], classes: string | undefined) =>
403
+ typeof icon === 'function' ? icon({ class: classes }) : icon
404
+
405
+ const axis = (
406
+ chart: ChartContextValue,
407
+ type: Exclude<ChartType, 'pie'>,
408
+ yTicks: number[],
409
+ min: number,
410
+ max: number,
411
+ axisStroke?: string,
412
+ axisStrokeOpacity?: string,
413
+ gridStroke?: string,
414
+ ) => {
415
+ const box = plotBox(chart)
416
+ const groupWidth = (box.right - box.left) / chart.data.length
417
+ const xStep = chart.data.length > 1 ? (box.right - box.left) / (chart.data.length - 1) : 0
418
+ const rowCenter = (index: number) => type === 'bar'
419
+ ? box.left + groupWidth * index + groupWidth / 2
420
+ : chart.data.length > 1 ? box.left + xStep * index : (box.left + box.right) / 2
421
+
422
+ return (
423
+ <g data-slot="chart-axis">
424
+ <line x1={box.left} x2={box.right} y1={box.bottom} y2={box.bottom} stroke={axisStroke} stroke-opacity={axisStrokeOpacity} />
425
+ <line x1={box.left} x2={box.left} y1={box.top} y2={box.bottom} stroke={axisStroke} stroke-opacity={axisStrokeOpacity} />
426
+ {yTicks.map(tick => {
427
+ const y = scaled(tick, min, max, box.top, box.bottom)
428
+ return (
429
+ <g key={tick}>
430
+ <line x1={box.left} x2={box.right} y1={y} y2={y} stroke={gridStroke} />
431
+ <text x={box.left - 8} y={y + 3} fill="currentColor" style="text-anchor:end">
432
+ {chart.formatValue(tick, '', {}, 0)}
433
+ </text>
434
+ </g>
435
+ )
436
+ })}
437
+ {chart.data.map((row, index) => {
438
+ return (
439
+ <text key={index} x={rowCenter(index)} y={chart.height - 8} fill="currentColor" style="text-anchor:middle">
440
+ {text(labelFor(chart, row, index)).slice(0, 12)}
441
+ </text>
442
+ )
443
+ })}
444
+ </g>
445
+ )
446
+ }
447
+
448
+ const linePath = (points: Point[]) =>
449
+ points.map((point, index) => `${index ? 'L' : 'M'} ${point.x} ${point.y}`).join(' ')
450
+
451
+ const areaPath = (points: Point[], baseline: number) =>
452
+ points.length ? `${linePath(points)} L ${points[points.length - 1]!.x} ${baseline} L ${points[0]!.x} ${baseline} Z` : ''
453
+
454
+ const anglePoint = (center: number, radius: number, angle: number) => ({
455
+ x: center + radius * Math.cos(angle),
456
+ y: center + radius * Math.sin(angle),
457
+ })
458
+
459
+ const slicePath = (center: number, radius: number, innerRadius: number, start: number, end: number) => {
460
+ const outerStart = anglePoint(center, radius, start)
461
+ const outerEnd = anglePoint(center, radius, end)
462
+ const large = end - start > Math.PI ? 1 : 0
463
+
464
+ if (innerRadius > 0) {
465
+ const innerStart = anglePoint(center, innerRadius, start)
466
+ const innerEnd = anglePoint(center, innerRadius, end)
467
+ return [
468
+ `M ${outerStart.x} ${outerStart.y}`,
469
+ `A ${radius} ${radius} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y}`,
470
+ `L ${innerEnd.x} ${innerEnd.y}`,
471
+ `A ${innerRadius} ${innerRadius} 0 ${large} 0 ${innerStart.x} ${innerStart.y}`,
472
+ 'Z',
473
+ ].join(' ')
474
+ }
475
+
476
+ return [
477
+ `M ${center} ${center}`,
478
+ `L ${outerStart.x} ${outerStart.y}`,
479
+ `A ${radius} ${radius} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y}`,
480
+ 'Z',
481
+ ].join(' ')
482
+ }
483
+
484
+ type ChartContainerRootArgs = ChartContainerArgs & {
485
+ rootId?: unknown
486
+ }
487
+
488
+ const ChartContainerRoot: Stateful<ChartContainerRootArgs> = function* () {
489
+ let active: ChartActive | null = null
490
+ const fallbackId = uniqueId('chart')
491
+ let tooltip: HTMLElement | null = null
492
+ const root = dom(this) ? this : null
493
+ let activePoint: ChartReferencePoint | null = null
494
+ let lastClientPoint: Point | null = null
495
+ const reference = root ? pointReference(() => activePoint?.svg ?? root, () => {
496
+ const next = activePoint ? clientPoint(activePoint.svg, activePoint.x, activePoint.y) : null
497
+ if (next) lastClientPoint = next
498
+ if (lastClientPoint) return lastClientPoint
499
+ const rect = root.getBoundingClientRect()
500
+ return { x: rect.left, y: rect.top }
501
+ }) : null
502
+ const geometry = position(this, {
503
+ profile: 'chart',
504
+ boundary: () => root,
505
+ elements: () => ({ reference, floating: tooltip, arrow: null }),
506
+ })
507
+ let geometryScheduled = false
508
+ let geometryRestart = false
509
+
510
+ const report = (error: unknown) => {
511
+ if (this.signal.aborted) return
512
+ queueMicrotask(() => {
513
+ if (!this.signal.aborted) this.throw(error)
514
+ })
515
+ }
516
+ const scheduleGeometry = (restart = false) => {
517
+ geometryRestart ||= restart
518
+ if (geometryScheduled || this.signal.aborted) return
519
+ geometryScheduled = true
520
+ queueMicrotask(() => {
521
+ geometryScheduled = false
522
+ const shouldRestart = geometryRestart
523
+ geometryRestart = false
524
+ if (!active || !activePoint || !tooltip || this.signal.aborted) return
525
+ const task = shouldRestart ? geometry.start() : geometry.update()
526
+ void task.catch(report)
527
+ })
528
+ }
529
+ const setTooltip = (element: HTMLElement | null) => {
530
+ if (element === tooltip) return
531
+ geometry.stop()
532
+ tooltip = element
533
+ if (active && activePoint && element) scheduleGeometry(true)
534
+ }
535
+ const movePoint = (next: ChartReferencePoint | undefined) => {
536
+ if (!next) return 0
537
+ if (
538
+ activePoint?.svg === next.svg &&
539
+ activePoint.x === next.x &&
540
+ activePoint.y === next.y
541
+ ) return 0
542
+ const retargeted = activePoint != null && activePoint.svg !== next.svg
543
+ activePoint = next
544
+ return retargeted ? 2 : 1
545
+ }
546
+ const setActive = (next: ChartActive | null, at?: ChartReferencePoint) => {
547
+ const movement = next ? movePoint(at) : 0
548
+ const retargeted = movement === 2
549
+ if (retargeted) geometry.stop()
550
+ if (!next) {
551
+ activePoint = null
552
+ lastClientPoint = null
553
+ geometry.stop()
554
+ }
555
+ if (sameActiveValue(active, next)) {
556
+ if (next && movement) scheduleGeometry(movement === 2)
557
+ return
558
+ }
559
+
560
+ const first = active == null && next != null
561
+
562
+ this.next(() => active = next)
563
+
564
+ // The tooltip ref/content updates during the render above. Mount/retarget
565
+ // and point changes collapse into one current Adapter request.
566
+ if (next && at) scheduleGeometry(first || retargeted)
567
+ }
568
+ const clearActive = () => setActive(null)
569
+ const releasePlot = (plot: SVGSVGElement) => {
570
+ if (this.signal.aborted || activePoint?.svg !== plot) return
571
+ const releasedPoint = activePoint
572
+ geometry.stop()
573
+ activePoint = null
574
+ lastClientPoint = null
575
+ queueMicrotask(() => {
576
+ if (this.signal.aborted || activePoint) return
577
+ if (plot.isConnected) {
578
+ activePoint = releasedPoint
579
+ if (active && tooltip) scheduleGeometry(true)
580
+ return
581
+ }
582
+ if (active) clearActive()
583
+ })
584
+ }
585
+
586
+ for (const args of this) {
587
+ const chartId = args.chartId ?? resolveRootChartId(args.rootId, fallbackId)
588
+ const data = args.data ?? []
589
+ const width = args.width ?? DEFAULT_WIDTH
590
+ const height = args.height ?? DEFAULT_HEIGHT
591
+ const margin = { ...DEFAULT_MARGIN, ...(args.margin ?? {}) }
592
+ const type = args.type ?? 'bar'
593
+ const chart: ChartContextValue = {
594
+ active,
595
+ clearActive,
596
+ config: args.config,
597
+ data,
598
+ description: args.description,
599
+ formatLabel: args.formatLabel ?? defaultFormatLabel,
600
+ formatValue: args.formatValue ?? defaultFormatValue,
601
+ height,
602
+ id: chartId,
603
+ label: args.label,
604
+ margin,
605
+ palette: args.palette,
606
+ releasePlot,
607
+ series: seriesEntries(args.config, args.series, data, args.xKey, args.palette),
608
+ setActive,
609
+ setTooltip,
610
+ type,
611
+ width,
612
+ xKey: args.xKey,
613
+ }
614
+
615
+ ChartContext(chart)
616
+ ChartIdContext(chart.id)
617
+
618
+ yield <>{args.children}</>
619
+ }
620
+ }
621
+
622
+
623
+ /** Unstyled chart root provider for config, data, tooltip, and legend state. */
624
+ const ChartContainer: Stateless<ChartContainerArgs> = ({
625
+ chartId,
626
+ children,
627
+ class: classes,
628
+ config,
629
+ data,
630
+ description,
631
+ formatLabel,
632
+ formatValue,
633
+ height,
634
+ id,
635
+ label,
636
+ margin,
637
+ palette,
638
+ series,
639
+ type,
640
+ width,
641
+ xKey,
642
+ ...attrs
643
+ }) => {
644
+ return <ChartContainerRoot
645
+ {...rootAttrs(attrs)}
646
+ chartId={chartId}
647
+ config={config}
648
+ data={data}
649
+ description={description}
650
+ formatLabel={formatLabel}
651
+ formatValue={formatValue}
652
+ height={height}
653
+ margin={margin}
654
+ palette={palette}
655
+ rootId={id}
656
+ series={series}
657
+ type={type}
658
+ width={width}
659
+ xKey={xKey}
660
+ attr:class={classes}
661
+ attr:data-slot="chart"
662
+ attr:id={id}
663
+ label={label}
664
+ >
665
+ {children}
666
+ </ChartContainerRoot>
667
+ }
668
+
669
+ const ChartPlot: Stateless<ChartPlotArgs & { type: Exclude<ChartType, 'pie'> }> = ({
670
+ axis: showAxis = true,
671
+ axisStroke,
672
+ axisStrokeOpacity,
673
+ barClass,
674
+ class: classes,
675
+ grid: showGrid = true,
676
+ gridStroke,
677
+ pointClass,
678
+ pointFill,
679
+ ref,
680
+ type,
681
+ ...attrs
682
+ }) => {
683
+ const chart = ChartContext()
684
+ if (!chart || !chart.data.length || !chart.series.length) return null
685
+
686
+ const box = plotBox(chart)
687
+ const { max, min } = extent(chart)
688
+ const yTicks = Array.from({ length: 4 }, (_, index) => min + ((max - min) / 3) * index)
689
+ const groupWidth = (box.right - box.left) / chart.data.length
690
+ const xStep = chart.data.length > 1 ? (box.right - box.left) / (chart.data.length - 1) : 0
691
+ const baseline = scaled(0, min, max, box.top, box.bottom)
692
+ const rowCenter = (index: number) => type === 'bar'
693
+ ? box.left + groupWidth * index + groupWidth / 2
694
+ : chart.data.length > 1 ? box.left + xStep * index : (box.left + box.right) / 2
695
+ const seriesKeys = chart.series.map(entry => entry.key)
696
+ const rowY = (index: number) => Math.min(...chart.series.map(entry =>
697
+ scaled(number(chart.data[index]?.[entry.key]) ?? 0, min, max, box.top, box.bottom)))
698
+ const rowPoint = (index: number) => ({
699
+ x: rowCenter(index),
700
+ y: rowY(index),
701
+ })
702
+ const points = (entry: SeriesEntry) => chart.data.map((row, index) => ({
703
+ x: rowCenter(index),
704
+ y: scaled(number(row[entry.key]) ?? 0, min, max, box.top, box.bottom),
705
+ }))
706
+
707
+ const activate = (index: number, at?: ChartReferencePoint) => {
708
+ if (sameActiveTarget(chart.active, index, seriesKeys)) {
709
+ chart.setActive(chart.active, at)
710
+ return
711
+ }
712
+
713
+ const items = payloadFor(chart, index)
714
+ if (!items.length) {
715
+ chart.clearActive()
716
+ return
717
+ }
718
+
719
+ const point = rowPoint(index)
720
+ chart.setActive({
721
+ index,
722
+ items,
723
+ label: labelFor(chart, chart.data[index]!, index),
724
+ x: point.x,
725
+ y: point.y,
726
+ }, at)
727
+ }
728
+
729
+ const pointerMove = (event: PointerEvent) => {
730
+ const svg = event.currentTarget as SVGSVGElement
731
+ const cursor = svgPoint(svg, event.clientX, event.clientY)
732
+ if (!cursor) {
733
+ chart.clearActive()
734
+ return
735
+ }
736
+ const sx = cursor.x
737
+ const index = type === 'bar'
738
+ ? clamp(Math.floor((sx - box.left) / groupWidth), 0, chart.data.length - 1)
739
+ : xStep === 0 ? 0 : clamp(Math.round((sx - box.left) / xStep), 0, chart.data.length - 1)
740
+ const point = rowPoint(index)
741
+ activate(index, referencePoint(svg, point))
742
+ }
743
+
744
+ const focusIn = (event: FocusEvent) => {
745
+ const target = event.target as SVGElement | null
746
+ const index = Number(target?.getAttribute('data-chart-index'))
747
+ const svg = svgFromEvent(event)
748
+ if (!svg || !Number.isInteger(index)) return
749
+ const point = rowPoint(index)
750
+ activate(index, referencePoint(svg, point))
751
+ }
752
+
753
+ return (
754
+ <svg
755
+ {...attrs}
756
+ aria-describedby={chart.description ? `${chart.id}-description` : undefined}
757
+ aria-label={chart.label}
758
+ class={classes}
759
+ data-slot={`chart-${type}`}
760
+ height={chart.height}
761
+ role="img"
762
+ ref={plotRef(chart, ref)}
763
+ viewBox={`0 0 ${chart.width} ${chart.height}`}
764
+ width="100%"
765
+ xmlns="http://www.w3.org/2000/svg"
766
+ set:onfocusin={focusIn}
767
+ set:onpointerleave={chart.clearActive}
768
+ set:onpointermove={pointerMove}
769
+ >
770
+ {chart.label ? <title>{chart.label}</title> : null}
771
+ {chart.description ? <desc id={`${chart.id}-description`}>{chart.description}</desc> : null}
772
+ {showGrid ? axis(chart, type, yTicks, min, max, axisStroke, axisStrokeOpacity, gridStroke) : showAxis ? axis(chart, type, [], min, max, axisStroke, axisStrokeOpacity, gridStroke) : null}
773
+ {type === 'bar' ? chart.data.map((row, index) => {
774
+ const barWidth = clamp(groupWidth * 0.68 / chart.series.length, 6, 42)
775
+ const groupStart = box.left + groupWidth * index + (groupWidth - barWidth * chart.series.length) / 2
776
+
777
+ return chart.series.map((entry, seriesIndex) => {
778
+ const value = number(row[entry.key]) ?? 0
779
+ const sign = value === 0 ? 'zero' : value < 0 ? 'negative' : 'positive'
780
+ const y = scaled(Math.max(0, value), min, max, box.top, box.bottom)
781
+ const yZero = scaled(Math.min(0, value), min, max, box.top, box.bottom)
782
+ const height = Math.max(1, Math.abs(yZero - y))
783
+ const x = groupStart + seriesIndex * barWidth
784
+
785
+ return (
786
+ <rect
787
+ key={`${entry.key}-${index}`}
788
+ aria-label={`${text(labelFor(chart, row, index))} ${text(entry.label)} ${chart.formatValue(value, entry.key, row, index)}`}
789
+ class={barClass}
790
+ data-active={chart.active?.index === index ? 'true' : undefined}
791
+ data-chart-index={index}
792
+ data-chart-sign={sign}
793
+ data-chart-series={entry.key}
794
+ fill={entry.color}
795
+ focusable="true"
796
+ height={height}
797
+ style={`--chart-index:${index}`}
798
+ tabindex="0"
799
+ width={Math.max(2, barWidth - 2)}
800
+ x={x}
801
+ y={Math.min(y, yZero)}
802
+ set:onfocus={(event: FocusEvent) => {
803
+ const svg = svgFromEvent(event)
804
+ const point = rowPoint(index)
805
+ if (svg) activate(index, referencePoint(svg, point))
806
+ }}
807
+ set:onpointerenter={(event: PointerEvent) => {
808
+ const svg = svgFromEvent(event)
809
+ const point = rowPoint(index)
810
+ if (svg) activate(index, referencePoint(svg, point))
811
+ }}
812
+ />
813
+ )
814
+ })
815
+ }) : chart.series.map(entry => {
816
+ const entryPoints = points(entry)
817
+ return (
818
+ <g key={entry.key} data-chart-series={entry.key}>
819
+ {type === 'area' ? (
820
+ <path d={areaPath(entryPoints, baseline)} fill={entry.color} fill-opacity="0.18" />
821
+ ) : null}
822
+ <path d={linePath(entryPoints)} fill="none" pathLength={1} stroke={entry.color} stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
823
+ {entryPoints.map((point, index) => {
824
+ const row = chart.data[index]!
825
+ const value = number(row[entry.key]) ?? 0
826
+ return (
827
+ <circle
828
+ key={`${entry.key}-${index}`}
829
+ aria-label={`${text(labelFor(chart, row, index))} ${text(entry.label)} ${chart.formatValue(value, entry.key, row, index)}`}
830
+ class={pointClass}
831
+ cx={point.x}
832
+ cy={point.y}
833
+ data-active={chart.active?.index === index ? 'true' : undefined}
834
+ data-chart-index={index}
835
+ fill={pointFill}
836
+ focusable="true"
837
+ r="4"
838
+ style={`--chart-index:${index}`}
839
+ stroke={entry.color}
840
+ stroke-width="2"
841
+ tabindex="0"
842
+ set:onfocus={(event: FocusEvent) => {
843
+ const svg = svgFromEvent(event)
844
+ const activePoint = rowPoint(index)
845
+ if (svg) activate(index, referencePoint(svg, activePoint))
846
+ }}
847
+ set:onpointerenter={(event: PointerEvent) => {
848
+ const svg = svgFromEvent(event)
849
+ const activePoint = rowPoint(index)
850
+ if (svg) activate(index, referencePoint(svg, activePoint))
851
+ }}
852
+ />
853
+ )
854
+ })}
855
+ </g>
856
+ )
857
+ })}
858
+ </svg>
859
+ )
860
+ }
861
+
862
+ /** Unstyled SVG bar chart primitive for use inside ChartContainer. */
863
+ const ChartBar: Stateless<ChartPlotArgs> = attrs => <ChartPlot {...attrs} type="bar" />
864
+
865
+ /** Unstyled SVG line chart primitive for use inside ChartContainer. */
866
+ const ChartLine: Stateless<ChartPlotArgs> = attrs => <ChartPlot {...attrs} type="line" />
867
+
868
+ /** Unstyled SVG area chart primitive for use inside ChartContainer. */
869
+ const ChartArea: Stateless<ChartPlotArgs> = attrs => <ChartPlot {...attrs} type="area" />
870
+
871
+ /** Unstyled SVG pie/donut chart primitive for use inside ChartContainer. */
872
+ const ChartPie: Stateless<ChartPieArgs> = ({
873
+ centerLabelClass,
874
+ centerLabelFill,
875
+ class: classes,
876
+ innerRadius = 0,
877
+ ref,
878
+ sliceStroke,
879
+ ...attrs
880
+ }) => {
881
+ const chart = ChartContext()
882
+ if (!chart || !chart.data.length || !chart.series.length) return null
883
+
884
+ const entry = chart.series[0]!
885
+ const values = chart.data.map(row => Math.max(0, number(row[entry.key]) ?? 0))
886
+ const total = values.reduce((sum, value) => sum + value, 0) || 1
887
+ const size = Math.min(chart.width, chart.height)
888
+ const center = size / 2
889
+ const radius = center - 12
890
+ let start = -Math.PI / 2
891
+
892
+ const activate = (index: number, angle: number, color: string, at?: ChartReferencePoint) => {
893
+ if (sameActiveTarget(chart.active, index, [entry.key])) {
894
+ chart.setActive(chart.active, at)
895
+ return
896
+ }
897
+
898
+ const point = anglePoint(center, radius * 0.78, angle)
899
+ const row = chart.data[index]!
900
+ const value = values[index] ?? 0
901
+ chart.setActive({
902
+ index,
903
+ items: [{
904
+ color,
905
+ formattedValue: chart.formatValue(value, entry.key, row, index),
906
+ index,
907
+ key: entry.key,
908
+ label: labelFor(chart, row, index),
909
+ row,
910
+ value,
911
+ }],
912
+ label: labelFor(chart, row, index),
913
+ x: point.x,
914
+ y: point.y,
915
+ }, at)
916
+ }
917
+
918
+ const focusIn = (event: FocusEvent) => {
919
+ const target = event.target as SVGElement | null
920
+ const index = Number(target?.getAttribute('data-chart-index'))
921
+ if (!Number.isInteger(index)) return
922
+ const svg = svgFromEvent(event)
923
+ if (!svg) return
924
+ const totalBefore = values.slice(0, index).reduce((sum, value) => sum + value, 0)
925
+ const value = values[index] ?? 0
926
+ const start = -Math.PI / 2 + (totalBefore / total) * Math.PI * 2
927
+ const middle = start + ((value / total) * Math.PI * 2) / 2
928
+ const point = anglePoint(center, radius * 0.78, middle)
929
+ activate(index, middle, chart.palette[index % chart.palette.length], referencePoint(svg, point))
930
+ }
931
+
932
+ const pointerMove = (event: PointerEvent) => {
933
+ const svg = event.currentTarget as SVGSVGElement
934
+ const cursorPoint = svgPoint(svg, event.clientX, event.clientY)
935
+ if (!cursorPoint) {
936
+ chart.clearActive()
937
+ return
938
+ }
939
+ const { x, y } = cursorPoint
940
+ const distance = Math.hypot(x - center, y - center)
941
+ if (distance > radius || distance < innerRadius) {
942
+ chart.clearActive()
943
+ return
944
+ }
945
+
946
+ let angle = Math.atan2(y - center, x - center) + Math.PI / 2
947
+ if (angle < 0) angle += Math.PI * 2
948
+
949
+ let cursor = 0
950
+ for (let index = 0; index < values.length; index++) {
951
+ const span = ((values[index] ?? 0) / total) * Math.PI * 2
952
+ if (angle <= cursor + span || index === values.length - 1) {
953
+ const middle = -Math.PI / 2 + cursor + span / 2
954
+ const point = anglePoint(center, radius * 0.78, middle)
955
+ activate(index, middle, chart.palette[index % chart.palette.length], referencePoint(svg, point))
956
+ return
957
+ }
958
+ cursor += span
959
+ }
960
+ }
961
+
962
+ return (
963
+ <svg
964
+ {...attrs}
965
+ aria-describedby={chart.description ? `${chart.id}-description` : undefined}
966
+ aria-label={chart.label}
967
+ class={classes}
968
+ data-slot="chart-pie"
969
+ height={chart.height}
970
+ role="img"
971
+ ref={plotRef(chart, ref)}
972
+ viewBox={`0 0 ${size} ${size}`}
973
+ width="100%"
974
+ xmlns="http://www.w3.org/2000/svg"
975
+ set:onfocusin={focusIn}
976
+ set:onpointerleave={chart.clearActive}
977
+ set:onpointermove={pointerMove}
978
+ >
979
+ {chart.label ? <title>{chart.label}</title> : null}
980
+ {chart.description ? <desc id={`${chart.id}-description`}>{chart.description}</desc> : null}
981
+ {values.map((value, index) => {
982
+ const angle = (value / total) * Math.PI * 2
983
+ const end = start + angle
984
+ const middle = start + angle / 2
985
+ const color = chart.palette[index % chart.palette.length]
986
+ const path = slicePath(center, radius, innerRadius, start, end)
987
+ const row = chart.data[index]!
988
+ start = end
989
+
990
+ return (
991
+ <path
992
+ key={index}
993
+ aria-label={`${text(labelFor(chart, row, index))} ${chart.formatValue(value, entry.key, row, index)}`}
994
+ d={path}
995
+ data-active={chart.active?.index === index ? 'true' : undefined}
996
+ data-chart-index={index}
997
+ fill={color}
998
+ focusable="true"
999
+ style={`--chart-index:${index}`}
1000
+ stroke={sliceStroke}
1001
+ stroke-width="2"
1002
+ tabindex="0"
1003
+ set:onfocus={(event: FocusEvent) => {
1004
+ const svg = svgFromEvent(event)
1005
+ const point = anglePoint(center, radius * 0.78, middle)
1006
+ if (svg) activate(index, middle, color, referencePoint(svg, point))
1007
+ }}
1008
+ set:onpointerenter={(event: PointerEvent) => {
1009
+ const svg = svgFromEvent(event)
1010
+ const point = anglePoint(center, radius * 0.78, middle)
1011
+ if (svg) activate(index, middle, color, referencePoint(svg, point))
1012
+ }}
1013
+ />
1014
+ )
1015
+ })}
1016
+ {innerRadius > 0 ? (
1017
+ <text x={center} y={center} fill={centerLabelFill} class={centerLabelClass} style="text-anchor:middle;dominant-baseline:middle">
1018
+ {chart.formatValue(total, entry.key, {}, 0)}
1019
+ </text>
1020
+ ) : null}
1021
+ </svg>
1022
+ )
1023
+ }
1024
+
1025
+ /** Unstyled absolute tooltip layer for native chart primitives. */
1026
+ const ChartTooltip: Stateless<ChartTooltipArgs> = ({
1027
+ children,
1028
+ class: classes,
1029
+ content,
1030
+ ...attrs
1031
+ }) => {
1032
+ const chart = ChartContext()
1033
+ if (!chart) return null
1034
+
1035
+ if (!chart.active?.items.length) return null
1036
+
1037
+ return (
1038
+ <div
1039
+ key="chart-tooltip"
1040
+ {...attrs}
1041
+ class={classes}
1042
+ data-slot="chart-tooltip"
1043
+ ref={chart.setTooltip}
1044
+ style="left:0;top:0"
1045
+ >
1046
+ {content ?? children ?? <ChartTooltipContent />}
1047
+ </div>
1048
+ )
1049
+ }
1050
+
1051
+ /** Unstyled tooltip body for native chart payloads. */
1052
+ const ChartTooltipContent: Stateless<ChartTooltipContentArgs> = ({
1053
+ class: classes,
1054
+ formattedValueClass,
1055
+ formatter,
1056
+ hideIndicator,
1057
+ hideLabel,
1058
+ iconClass,
1059
+ iconWrapperClass,
1060
+ indicator = 'dot',
1061
+ indicatorClass,
1062
+ itemClass,
1063
+ itemLabelClass,
1064
+ itemsClass,
1065
+ labelClass,
1066
+ labelFormatter,
1067
+ nestedLabelClass,
1068
+ valueLabelGroupClass,
1069
+ valueRowClass,
1070
+ ...attrs
1071
+ }) => {
1072
+ const chart = ChartContext()
1073
+ const active = chart?.active
1074
+ if (!chart || !active?.items.length) return null
1075
+
1076
+ const nestLabel = active.items.length === 1 && indicator !== 'dot'
1077
+ const state = { indicator, nestLabel }
1078
+
1079
+ return (
1080
+ <div {...attrs} class={classes} data-slot="chart-tooltip-content">
1081
+ {!hideLabel && !nestLabel ? (
1082
+ <div class={labelClass} data-slot="chart-tooltip-label">
1083
+ {labelFormatter ? labelFormatter(active.label, active.items) : active.label}
1084
+ </div>
1085
+ ) : null}
1086
+ <div class={itemsClass}>
1087
+ {active.items.map((item, index) => {
1088
+ const config = chart.config[item.key]
1089
+ const icon = config?.icon
1090
+
1091
+ return (
1092
+ <div key={`${item.key}-${index}`} class={resolve(itemClass, state)} data-slot="chart-tooltip-item">
1093
+ {formatter ? formatter(item.value, item.key, item, index) : (
1094
+ <>
1095
+ {icon && !hideIndicator ? (
1096
+ <span aria-hidden="true" class={iconWrapperClass}>{renderIcon(icon, iconClass)}</span>
1097
+ ) : !hideIndicator ? (
1098
+ <span
1099
+ aria-hidden="true"
1100
+ class={resolve(indicatorClass, state)}
1101
+ style={`--chart-indicator:${item.color}`}
1102
+ />
1103
+ ) : null}
1104
+ <div class={resolve(valueRowClass, { nestLabel })}>
1105
+ <div class={valueLabelGroupClass}>
1106
+ {!hideLabel && nestLabel ? (
1107
+ <div class={nestedLabelClass}>
1108
+ {labelFormatter ? labelFormatter(active.label, active.items) : active.label}
1109
+ </div>
1110
+ ) : null}
1111
+ <span class={itemLabelClass}>{item.label}</span>
1112
+ </div>
1113
+ <span class={formattedValueClass}>{item.formattedValue}</span>
1114
+ </div>
1115
+ </>
1116
+ )}
1117
+ </div>
1118
+ )
1119
+ })}
1120
+ </div>
1121
+ </div>
1122
+ )
1123
+ }
1124
+
1125
+ /** Unstyled legend layer for native chart primitives. */
1126
+ const ChartLegend: Stateless<ChartLegendArgs> = ({
1127
+ children,
1128
+ class: classes,
1129
+ content,
1130
+ ...attrs
1131
+ }) => (
1132
+ <div key="chart-legend" {...attrs} class={classes} data-slot="chart-legend">
1133
+ {content ?? children ?? <ChartLegendContent />}
1134
+ </div>
1135
+ )
1136
+
1137
+ /** Unstyled legend content for native chart payloads. */
1138
+ const ChartLegendContent: Stateless<ChartLegendContentArgs> = ({
1139
+ class: classes,
1140
+ hideIcon,
1141
+ iconClass,
1142
+ iconWrapperClass,
1143
+ itemClass,
1144
+ swatchClass,
1145
+ ...attrs
1146
+ }) => {
1147
+ const chart = ChartContext()
1148
+ if (!chart) return null
1149
+
1150
+ const entries = chart.type === 'pie'
1151
+ ? chart.data.map((row, index) => ({
1152
+ color: chart.palette[index % chart.palette.length],
1153
+ icon: undefined,
1154
+ key: String(index),
1155
+ label: labelFor(chart, row, index),
1156
+ }))
1157
+ : chart.series.map(entry => ({
1158
+ color: entry.color,
1159
+ icon: chart.config[entry.key]?.icon,
1160
+ key: entry.key,
1161
+ label: entry.label,
1162
+ }))
1163
+
1164
+ return (
1165
+ <div
1166
+ {...attrs}
1167
+ class={classes}
1168
+ data-slot="chart-legend-content"
1169
+ >
1170
+ {entries.map(entry => (
1171
+ <div key={entry.key} class={itemClass} data-slot="chart-legend-item">
1172
+ {entry.icon && !hideIcon ? (
1173
+ <span aria-hidden="true" class={iconWrapperClass}>{renderIcon(entry.icon, iconClass)}</span>
1174
+ ) : (
1175
+ <span aria-hidden="true" class={swatchClass} style={`background:${entry.color}`} />
1176
+ )}
1177
+ <span>{entry.label}</span>
1178
+ </div>
1179
+ ))}
1180
+ </div>
1181
+ )
1182
+ }
1183
+
1184
+ export {
1185
+ ChartArea,
1186
+ ChartBar,
1187
+ ChartContainer,
1188
+ ChartLegend,
1189
+ ChartLegendContent,
1190
+ ChartLine,
1191
+ ChartPie,
1192
+ ChartTooltip,
1193
+ ChartTooltipContent,
1194
+ }