@mijn-ui/react-charts 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.
package/dist/index.js ADDED
@@ -0,0 +1,1025 @@
1
+ "use client";
2
+
3
+ // src/chart-utils.ts
4
+ var HUE_BG_MAP = {
5
+ brand: "bg-bg-brand",
6
+ red: "bg-bg-red",
7
+ orange: "bg-bg-orange",
8
+ amber: "bg-bg-amber",
9
+ yellow: "bg-bg-yellow",
10
+ lime: "bg-bg-lime",
11
+ green: "bg-bg-green",
12
+ emerald: "bg-bg-emerald",
13
+ teal: "bg-bg-teal",
14
+ cyan: "bg-bg-cyan",
15
+ sky: "bg-bg-sky",
16
+ blue: "bg-bg-blue",
17
+ indigo: "bg-bg-indigo",
18
+ violet: "bg-bg-violet",
19
+ purple: "bg-bg-purple",
20
+ fuchsia: "bg-bg-fuchsia",
21
+ pink: "bg-bg-pink",
22
+ rose: "bg-bg-rose",
23
+ gray: "bg-bg-gray"
24
+ };
25
+ var HUE_BG_SUBTLE_MAP = {
26
+ brand: "bg-bg-brand-subtle",
27
+ red: "bg-bg-red-subtle",
28
+ orange: "bg-bg-orange-subtle",
29
+ amber: "bg-bg-amber-subtle",
30
+ yellow: "bg-bg-yellow-subtle",
31
+ lime: "bg-bg-lime-subtle",
32
+ green: "bg-bg-green-subtle",
33
+ emerald: "bg-bg-emerald-subtle",
34
+ teal: "bg-bg-teal-subtle",
35
+ cyan: "bg-bg-cyan-subtle",
36
+ sky: "bg-bg-sky-subtle",
37
+ blue: "bg-bg-blue-subtle",
38
+ indigo: "bg-bg-indigo-subtle",
39
+ violet: "bg-bg-violet-subtle",
40
+ purple: "bg-bg-purple-subtle",
41
+ fuchsia: "bg-bg-fuchsia-subtle",
42
+ pink: "bg-bg-pink-subtle",
43
+ rose: "bg-bg-rose-subtle",
44
+ gray: "bg-bg-gray-subtle"
45
+ };
46
+ var HUE_STROKE_MAP = {
47
+ brand: "stroke-fg-brand",
48
+ red: "stroke-fg-red",
49
+ orange: "stroke-fg-orange",
50
+ amber: "stroke-fg-amber",
51
+ yellow: "stroke-fg-yellow",
52
+ lime: "stroke-fg-lime",
53
+ green: "stroke-fg-green",
54
+ emerald: "stroke-fg-emerald",
55
+ teal: "stroke-fg-teal",
56
+ cyan: "stroke-fg-cyan",
57
+ sky: "stroke-fg-sky",
58
+ blue: "stroke-fg-blue",
59
+ indigo: "stroke-fg-indigo",
60
+ violet: "stroke-fg-violet",
61
+ purple: "stroke-fg-purple",
62
+ fuchsia: "stroke-fg-fuchsia",
63
+ pink: "stroke-fg-pink",
64
+ rose: "stroke-fg-rose",
65
+ gray: "stroke-fg-gray"
66
+ };
67
+ var HUE_TEXT_MAP = {
68
+ brand: "text-fg-brand",
69
+ red: "text-fg-red",
70
+ orange: "text-fg-orange",
71
+ amber: "text-fg-amber",
72
+ yellow: "text-fg-yellow",
73
+ lime: "text-fg-lime",
74
+ green: "text-fg-green",
75
+ emerald: "text-fg-emerald",
76
+ teal: "text-fg-teal",
77
+ cyan: "text-fg-cyan",
78
+ sky: "text-fg-sky",
79
+ blue: "text-fg-blue",
80
+ indigo: "text-fg-indigo",
81
+ violet: "text-fg-violet",
82
+ purple: "text-fg-purple",
83
+ fuchsia: "text-fg-fuchsia",
84
+ pink: "text-fg-pink",
85
+ rose: "text-fg-rose",
86
+ gray: "text-fg-gray"
87
+ };
88
+ var HUE_ON_BG_FILL_MAP = {
89
+ brand: "fill-on-bg-brand",
90
+ red: "fill-on-bg-red",
91
+ orange: "fill-on-bg-orange",
92
+ amber: "fill-on-bg-amber",
93
+ yellow: "fill-on-bg-yellow",
94
+ lime: "fill-on-bg-lime",
95
+ green: "fill-on-bg-green",
96
+ emerald: "fill-on-bg-emerald",
97
+ teal: "fill-on-bg-teal",
98
+ cyan: "fill-on-bg-cyan",
99
+ sky: "fill-on-bg-sky",
100
+ blue: "fill-on-bg-blue",
101
+ indigo: "fill-on-bg-indigo",
102
+ violet: "fill-on-bg-violet",
103
+ purple: "fill-on-bg-purple",
104
+ fuchsia: "fill-on-bg-fuchsia",
105
+ pink: "fill-on-bg-pink",
106
+ rose: "fill-on-bg-rose",
107
+ gray: "fill-on-bg-gray"
108
+ };
109
+ var DEFAULT_PALETTE = [
110
+ "blue",
111
+ "emerald",
112
+ "amber",
113
+ "violet",
114
+ "rose",
115
+ "cyan",
116
+ "orange",
117
+ "teal",
118
+ "pink",
119
+ "lime",
120
+ "indigo",
121
+ "gray"
122
+ ];
123
+
124
+ // src/chart-tooltip.tsx
125
+ import {
126
+ cn,
127
+ createTVUnstyledSlots
128
+ } from "@mijn-ui/react-core";
129
+ import { tv } from "tailwind-variants";
130
+ import { jsx, jsxs } from "react/jsx-runtime";
131
+ var chartTooltipStyles = tv({
132
+ slots: {
133
+ base: "bg-bg-default border-outline-secondary pointer-events-none absolute z-10 rounded-md border px-2 py-1 text-xs whitespace-nowrap shadow-md",
134
+ title: "text-fg-secondary font-medium",
135
+ row: "flex items-center gap-1.5",
136
+ swatch: "rounded-2xs size-2 shrink-0",
137
+ label: "text-fg-secondary",
138
+ value: "text-fg-default ml-auto pl-2 font-medium"
139
+ }
140
+ });
141
+ var ChartTooltip = ({
142
+ className,
143
+ classNames,
144
+ unstyled,
145
+ heading,
146
+ items,
147
+ children,
148
+ ref,
149
+ ...props
150
+ }) => {
151
+ const { base, title, row, swatch, label, value } = createTVUnstyledSlots(
152
+ chartTooltipStyles(),
153
+ unstyled
154
+ );
155
+ return /* @__PURE__ */ jsxs(
156
+ "div",
157
+ {
158
+ "data-slot": "chart-tooltip",
159
+ ref,
160
+ role: "presentation",
161
+ className: base({ className: cn(classNames?.base, className) }),
162
+ ...props,
163
+ children: [
164
+ heading !== void 0 && /* @__PURE__ */ jsx(
165
+ "div",
166
+ {
167
+ "data-slot": "chart-tooltip-title",
168
+ className: title({ className: classNames?.title }),
169
+ children: heading
170
+ }
171
+ ),
172
+ items?.map((item, index) => /* @__PURE__ */ jsxs(
173
+ "div",
174
+ {
175
+ "data-slot": "chart-tooltip-row",
176
+ className: row({ className: classNames?.row }),
177
+ children: [
178
+ item.color && /* @__PURE__ */ jsx(
179
+ "span",
180
+ {
181
+ "data-slot": "chart-tooltip-swatch",
182
+ "aria-hidden": true,
183
+ className: swatch({
184
+ className: cn(
185
+ !unstyled && HUE_BG_MAP[item.color],
186
+ classNames?.swatch
187
+ )
188
+ })
189
+ }
190
+ ),
191
+ item.label !== void 0 && /* @__PURE__ */ jsx(
192
+ "span",
193
+ {
194
+ "data-slot": "chart-tooltip-label",
195
+ className: label({ className: classNames?.label }),
196
+ children: item.label
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsx(
200
+ "span",
201
+ {
202
+ "data-slot": "chart-tooltip-value",
203
+ className: value({ className: classNames?.value }),
204
+ children: item.value
205
+ }
206
+ )
207
+ ]
208
+ },
209
+ index
210
+ )),
211
+ children
212
+ ]
213
+ }
214
+ );
215
+ };
216
+
217
+ // src/chart-legend.tsx
218
+ import {
219
+ cn as cn2,
220
+ createTVUnstyledSlots as createTVUnstyledSlots2
221
+ } from "@mijn-ui/react-core";
222
+ import { tv as tv2 } from "tailwind-variants";
223
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
224
+ var chartLegendStyles = tv2({
225
+ slots: {
226
+ base: "flex gap-x-4 gap-y-1 text-xs",
227
+ item: "flex min-w-0 items-center gap-1.5",
228
+ swatch: "rounded-2xs size-2.5 shrink-0",
229
+ label: "text-fg-secondary truncate",
230
+ value: "text-fg-default font-medium"
231
+ },
232
+ variants: {
233
+ orientation: {
234
+ horizontal: { base: "flex-row flex-wrap items-center" },
235
+ vertical: { base: "flex-col items-start" }
236
+ },
237
+ clickable: {
238
+ true: { item: "cursor-pointer select-none transition-opacity hover:opacity-75" }
239
+ },
240
+ hidden: {
241
+ true: { item: "opacity-45", label: "line-through" }
242
+ }
243
+ },
244
+ defaultVariants: {
245
+ orientation: "horizontal"
246
+ }
247
+ });
248
+ var ChartLegendItem = ({
249
+ className,
250
+ classNames,
251
+ unstyled,
252
+ label: labelText,
253
+ color,
254
+ value: valueText,
255
+ hidden = false,
256
+ onToggle,
257
+ ref,
258
+ ...props
259
+ }) => {
260
+ const { item, swatch, label, value } = createTVUnstyledSlots2(
261
+ chartLegendStyles(),
262
+ unstyled
263
+ );
264
+ return /* @__PURE__ */ jsxs2(
265
+ "div",
266
+ {
267
+ "data-slot": "chart-legend-item",
268
+ ref,
269
+ ...onToggle && {
270
+ role: "button",
271
+ tabIndex: 0,
272
+ "aria-pressed": !hidden,
273
+ onClick: onToggle,
274
+ onKeyDown: (event) => {
275
+ if (event.key === "Enter" || event.key === " ") {
276
+ event.preventDefault();
277
+ onToggle();
278
+ }
279
+ }
280
+ },
281
+ className: cn2(
282
+ item({ clickable: !!onToggle, hidden }),
283
+ classNames?.item,
284
+ className
285
+ ),
286
+ ...props,
287
+ children: [
288
+ /* @__PURE__ */ jsx2(
289
+ "span",
290
+ {
291
+ "data-slot": "chart-legend-swatch",
292
+ "aria-hidden": true,
293
+ className: swatch({
294
+ className: cn2(!unstyled && HUE_BG_MAP[color], classNames?.swatch)
295
+ })
296
+ }
297
+ ),
298
+ /* @__PURE__ */ jsx2(
299
+ "span",
300
+ {
301
+ "data-slot": "chart-legend-label",
302
+ className: cn2(label({ hidden }), classNames?.label),
303
+ children: labelText
304
+ }
305
+ ),
306
+ valueText !== void 0 && /* @__PURE__ */ jsx2(
307
+ "span",
308
+ {
309
+ "data-slot": "chart-legend-value",
310
+ className: value({ className: classNames?.value }),
311
+ children: valueText
312
+ }
313
+ )
314
+ ]
315
+ }
316
+ );
317
+ };
318
+ var ChartLegend = ({
319
+ className,
320
+ classNames,
321
+ unstyled,
322
+ items,
323
+ orientation,
324
+ onItemToggle,
325
+ ref,
326
+ ...props
327
+ }) => {
328
+ const { base } = createTVUnstyledSlots2(
329
+ chartLegendStyles({ orientation }),
330
+ unstyled
331
+ );
332
+ return /* @__PURE__ */ jsx2(
333
+ "div",
334
+ {
335
+ "data-slot": "chart-legend",
336
+ ref,
337
+ className: base({ className: cn2(classNames?.base, className) }),
338
+ ...props,
339
+ children: items.map((item, index) => /* @__PURE__ */ jsx2(
340
+ ChartLegendItem,
341
+ {
342
+ unstyled,
343
+ classNames,
344
+ onToggle: onItemToggle ? () => onItemToggle(index, item) : void 0,
345
+ ...item
346
+ },
347
+ index
348
+ ))
349
+ }
350
+ );
351
+ };
352
+
353
+ // src/bar-chart.tsx
354
+ import {
355
+ cn as cn3,
356
+ createTVUnstyledSlots as createTVUnstyledSlots3,
357
+ useControlledState
358
+ } from "@mijn-ui/react-core";
359
+ import { tv as tv3 } from "tailwind-variants";
360
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
361
+ var barChartStyles = tv3({
362
+ slots: {
363
+ base: "flex w-full flex-col gap-2",
364
+ plot: "flex w-full items-end gap-2",
365
+ barGroup: "relative flex h-full min-w-0 flex-1 items-end justify-center",
366
+ bar: "rounded-t-xs relative w-full max-w-10 transition-opacity duration-200",
367
+ valueLabel: "text-fg-secondary pointer-events-none absolute -top-4 left-1/2 -translate-x-1/2 text-[10px] font-medium whitespace-nowrap",
368
+ labelGroup: "flex w-full gap-2",
369
+ label: "text-fg-tertiary min-w-0 flex-1 truncate text-center text-xs"
370
+ },
371
+ variants: {
372
+ size: {
373
+ sm: { plot: "h-[140px]" },
374
+ default: { plot: "h-[200px]" },
375
+ lg: { plot: "h-[260px]" }
376
+ },
377
+ dimmed: {
378
+ true: { bar: "opacity-40" }
379
+ },
380
+ clickable: {
381
+ true: { barGroup: "cursor-pointer" }
382
+ },
383
+ // Headroom so labels above full-height bars stay inside the plot.
384
+ withValues: {
385
+ true: { plot: "pt-4" }
386
+ }
387
+ },
388
+ defaultVariants: {
389
+ size: "default"
390
+ }
391
+ });
392
+ var BarChart = ({
393
+ className,
394
+ classNames,
395
+ unstyled,
396
+ data,
397
+ valueFormatter = (value) => String(value),
398
+ emphasizeLast = false,
399
+ color = "brand",
400
+ colors,
401
+ size,
402
+ showValues = false,
403
+ showTooltip = true,
404
+ activeIndex: activeIndexProp,
405
+ onActiveIndexChange,
406
+ onValueClick,
407
+ ref,
408
+ ...props
409
+ }) => {
410
+ const [activeIndex, setActiveIndex] = useControlledState(
411
+ activeIndexProp,
412
+ null,
413
+ onActiveIndexChange
414
+ );
415
+ const { base, plot, barGroup, bar, valueLabel, labelGroup, label } = createTVUnstyledSlots3(barChartStyles({ size }), unstyled);
416
+ const max = Math.max(...data.map((datum) => datum.value), 0);
417
+ const hueAt = (index) => data[index]?.color ?? (colors && colors.length > 0 ? colors[index % colors.length] : color);
418
+ return /* @__PURE__ */ jsxs3(
419
+ "div",
420
+ {
421
+ "data-slot": "chart-bar",
422
+ ref,
423
+ role: "img",
424
+ "aria-label": `Bar chart with ${data.length} bars`,
425
+ className: base({ className: cn3(classNames?.base, className) }),
426
+ ...props,
427
+ children: [
428
+ /* @__PURE__ */ jsx3(
429
+ "div",
430
+ {
431
+ "data-slot": "chart-bar-plot",
432
+ className: cn3(plot({ withValues: showValues }), classNames?.plot),
433
+ onMouseLeave: () => setActiveIndex(null),
434
+ children: data.map((datum, index) => {
435
+ const height = max > 0 ? Math.max(datum.value, 0) / max * 100 : 0;
436
+ const subtle = emphasizeLast && index !== data.length - 1;
437
+ return /* @__PURE__ */ jsxs3(
438
+ "div",
439
+ {
440
+ "data-slot": "chart-bar-group",
441
+ title: showTooltip ? void 0 : `${datum.label}: ${valueFormatter(datum.value)}`,
442
+ className: cn3(
443
+ barGroup({ clickable: !!onValueClick }),
444
+ classNames?.barGroup
445
+ ),
446
+ onMouseEnter: () => setActiveIndex(index),
447
+ onClick: onValueClick ? () => onValueClick(index) : void 0,
448
+ children: [
449
+ showTooltip && activeIndex === index && /* @__PURE__ */ jsx3(
450
+ ChartTooltip,
451
+ {
452
+ unstyled,
453
+ heading: datum.label,
454
+ items: [{ value: valueFormatter(datum.value), color: hueAt(index) }],
455
+ className: "bottom-full left-1/2 mb-1 -translate-x-1/2"
456
+ }
457
+ ),
458
+ /* @__PURE__ */ jsx3(
459
+ "div",
460
+ {
461
+ "data-slot": "chart-bar-fill",
462
+ className: cn3(
463
+ bar({
464
+ dimmed: activeIndex !== null && activeIndex !== index
465
+ }),
466
+ !unstyled && (subtle ? HUE_BG_SUBTLE_MAP[hueAt(index)] : HUE_BG_MAP[hueAt(index)]),
467
+ classNames?.bar
468
+ ),
469
+ style: { height: `${height}%` },
470
+ children: showValues && /* @__PURE__ */ jsx3(
471
+ "span",
472
+ {
473
+ "data-slot": "chart-bar-value",
474
+ className: valueLabel({
475
+ className: classNames?.valueLabel
476
+ }),
477
+ children: valueFormatter(datum.value)
478
+ }
479
+ )
480
+ }
481
+ )
482
+ ]
483
+ },
484
+ index
485
+ );
486
+ })
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsx3(
490
+ "div",
491
+ {
492
+ "data-slot": "chart-bar-label-group",
493
+ className: labelGroup({ className: classNames?.labelGroup }),
494
+ children: data.map((datum, index) => /* @__PURE__ */ jsx3(
495
+ "div",
496
+ {
497
+ "data-slot": "chart-bar-label",
498
+ className: label({ className: classNames?.label }),
499
+ children: datum.label
500
+ },
501
+ index
502
+ ))
503
+ }
504
+ )
505
+ ]
506
+ }
507
+ );
508
+ };
509
+
510
+ // src/grouped-bar-chart.tsx
511
+ import {
512
+ cn as cn4,
513
+ createTVUnstyledSlots as createTVUnstyledSlots4,
514
+ useControlledState as useControlledState2
515
+ } from "@mijn-ui/react-core";
516
+ import { tv as tv4 } from "tailwind-variants";
517
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
518
+ var groupedBarChartStyles = tv4({
519
+ slots: {
520
+ base: "flex w-full flex-col gap-3",
521
+ legend: "",
522
+ plot: "flex w-full items-end gap-2",
523
+ barGroup: "relative flex h-full min-w-0 flex-1 items-end justify-center gap-0.5",
524
+ bar: "rounded-t-xs relative w-full max-w-4 transition-opacity duration-200",
525
+ valueLabel: "text-fg-secondary pointer-events-none absolute -top-3.5 left-1/2 -translate-x-1/2 text-[9px] font-medium whitespace-nowrap",
526
+ labelGroup: "flex w-full gap-2",
527
+ label: "text-fg-tertiary min-w-0 flex-1 truncate text-center text-xs"
528
+ },
529
+ variants: {
530
+ size: {
531
+ sm: { plot: "h-[140px]" },
532
+ default: { plot: "h-[200px]" },
533
+ lg: { plot: "h-[260px]" }
534
+ },
535
+ dimmed: {
536
+ true: { bar: "opacity-40" }
537
+ },
538
+ clickable: {
539
+ true: { barGroup: "cursor-pointer" }
540
+ },
541
+ // Headroom so labels above full-height bars stay inside the plot.
542
+ withValues: {
543
+ true: { plot: "pt-3.5" }
544
+ }
545
+ },
546
+ defaultVariants: {
547
+ size: "default"
548
+ }
549
+ });
550
+ var GroupedBarChart = ({
551
+ className,
552
+ classNames,
553
+ unstyled,
554
+ data,
555
+ series,
556
+ colors = DEFAULT_PALETTE,
557
+ valueFormatter = (value) => String(value),
558
+ size,
559
+ showValues = false,
560
+ showTooltip = true,
561
+ toggleable = false,
562
+ hiddenSeries: hiddenSeriesProp,
563
+ onHiddenSeriesChange,
564
+ activeIndex: activeIndexProp,
565
+ onActiveIndexChange,
566
+ onValueClick,
567
+ ref,
568
+ ...props
569
+ }) => {
570
+ const [activeIndex, setActiveIndex] = useControlledState2(
571
+ activeIndexProp,
572
+ null,
573
+ onActiveIndexChange
574
+ );
575
+ const [hiddenSeries, setHiddenSeries] = useControlledState2(
576
+ hiddenSeriesProp,
577
+ [],
578
+ onHiddenSeriesChange
579
+ );
580
+ const { base, legend, plot, barGroup, bar, valueLabel, labelGroup, label } = createTVUnstyledSlots4(groupedBarChartStyles({ size }), unstyled);
581
+ const isHidden = (name) => hiddenSeries.includes(name);
582
+ const toggleSeries = (name) => setHiddenSeries(
583
+ isHidden(name) ? hiddenSeries.filter((hidden) => hidden !== name) : [...hiddenSeries, name]
584
+ );
585
+ const max = Math.max(
586
+ ...data.flatMap(
587
+ (datum) => datum.values.filter((_, seriesIndex) => !isHidden(series[seriesIndex] ?? ""))
588
+ ),
589
+ 0
590
+ );
591
+ const hueAt = (seriesIndex) => colors[seriesIndex % colors.length] || "gray";
592
+ return /* @__PURE__ */ jsxs4(
593
+ "div",
594
+ {
595
+ "data-slot": "chart-grouped-bar",
596
+ ref,
597
+ role: "img",
598
+ "aria-label": `Grouped bar chart with ${series.length} series`,
599
+ className: base({ className: cn4(classNames?.base, className) }),
600
+ ...props,
601
+ children: [
602
+ /* @__PURE__ */ jsx4(
603
+ ChartLegend,
604
+ {
605
+ unstyled,
606
+ items: series.map((name, seriesIndex) => ({
607
+ label: name,
608
+ color: hueAt(seriesIndex),
609
+ hidden: isHidden(name)
610
+ })),
611
+ onItemToggle: toggleable ? (_, item) => toggleSeries(item.label) : void 0,
612
+ className: legend({ className: classNames?.legend })
613
+ }
614
+ ),
615
+ /* @__PURE__ */ jsx4(
616
+ "div",
617
+ {
618
+ "data-slot": "chart-grouped-bar-plot",
619
+ className: cn4(plot({ withValues: showValues }), classNames?.plot),
620
+ onMouseLeave: () => setActiveIndex(null),
621
+ children: data.map((datum, index) => /* @__PURE__ */ jsxs4(
622
+ "div",
623
+ {
624
+ "data-slot": "chart-grouped-bar-group",
625
+ className: cn4(
626
+ barGroup({ clickable: !!onValueClick }),
627
+ classNames?.barGroup
628
+ ),
629
+ onMouseEnter: () => setActiveIndex(index),
630
+ onClick: onValueClick ? () => onValueClick(index) : void 0,
631
+ children: [
632
+ showTooltip && activeIndex === index && /* @__PURE__ */ jsx4(
633
+ ChartTooltip,
634
+ {
635
+ unstyled,
636
+ heading: datum.label,
637
+ items: datum.values.flatMap(
638
+ (value, seriesIndex) => isHidden(series[seriesIndex] ?? "") ? [] : [
639
+ {
640
+ label: series[seriesIndex] ?? "",
641
+ value: valueFormatter(value),
642
+ color: hueAt(seriesIndex)
643
+ }
644
+ ]
645
+ ),
646
+ className: "bottom-full left-1/2 mb-1 -translate-x-1/2"
647
+ }
648
+ ),
649
+ datum.values.map((value, seriesIndex) => {
650
+ if (isHidden(series[seriesIndex] ?? "")) return null;
651
+ const height = max > 0 ? Math.max(value, 0) / max * 100 : 0;
652
+ return /* @__PURE__ */ jsx4(
653
+ "div",
654
+ {
655
+ "data-slot": "chart-grouped-bar-fill",
656
+ title: showTooltip ? void 0 : `${datum.label} \xB7 ${series[seriesIndex] ?? ""}: ${valueFormatter(value)}`,
657
+ className: cn4(
658
+ bar({
659
+ dimmed: activeIndex !== null && activeIndex !== index
660
+ }),
661
+ !unstyled && HUE_BG_MAP[hueAt(seriesIndex)],
662
+ classNames?.bar
663
+ ),
664
+ style: { height: `${height}%` },
665
+ children: showValues && /* @__PURE__ */ jsx4(
666
+ "span",
667
+ {
668
+ "data-slot": "chart-grouped-bar-value",
669
+ className: valueLabel({
670
+ className: classNames?.valueLabel
671
+ }),
672
+ children: valueFormatter(value)
673
+ }
674
+ )
675
+ },
676
+ seriesIndex
677
+ );
678
+ })
679
+ ]
680
+ },
681
+ index
682
+ ))
683
+ }
684
+ ),
685
+ /* @__PURE__ */ jsx4(
686
+ "div",
687
+ {
688
+ "data-slot": "chart-grouped-bar-label-group",
689
+ className: labelGroup({ className: classNames?.labelGroup }),
690
+ children: data.map((datum, index) => /* @__PURE__ */ jsx4(
691
+ "div",
692
+ {
693
+ "data-slot": "chart-grouped-bar-label",
694
+ className: label({ className: classNames?.label }),
695
+ children: datum.label
696
+ },
697
+ index
698
+ ))
699
+ }
700
+ )
701
+ ]
702
+ }
703
+ );
704
+ };
705
+
706
+ // src/donut-chart.tsx
707
+ import * as React from "react";
708
+ import {
709
+ cn as cn5,
710
+ createTVUnstyledSlots as createTVUnstyledSlots5
711
+ } from "@mijn-ui/react-core";
712
+ import { tv as tv5 } from "tailwind-variants";
713
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
714
+ var donutChartStyles = tv5({
715
+ slots: {
716
+ base: "relative inline-flex items-center justify-center",
717
+ svg: "-rotate-90",
718
+ slice: "fill-none transition-opacity duration-200",
719
+ sliceLabel: "pointer-events-none select-none text-[2.6px] font-semibold"
720
+ },
721
+ variants: {
722
+ size: {
723
+ sm: { svg: "size-[120px]" },
724
+ default: { svg: "size-[148px]" },
725
+ lg: { svg: "size-[180px]" }
726
+ },
727
+ thickness: {
728
+ thin: { slice: "[stroke-width:3]" },
729
+ default: { slice: "[stroke-width:5]" },
730
+ thick: { slice: "[stroke-width:8]" }
731
+ }
732
+ },
733
+ defaultVariants: {
734
+ size: "default",
735
+ thickness: "default"
736
+ }
737
+ });
738
+ var RADIUS = 100 / (2 * Math.PI);
739
+ var CENTER = 20;
740
+ var SLICE_GAP = 0.5;
741
+ var MIN_LABELED_PERCENT = 6;
742
+ var DonutChart = ({
743
+ className,
744
+ classNames,
745
+ unstyled,
746
+ data,
747
+ valueFormatter = (value) => String(value),
748
+ size,
749
+ thickness,
750
+ showPercentages = false,
751
+ showTooltip = true,
752
+ ref,
753
+ ...props
754
+ }) => {
755
+ const [activeIndex, setActiveIndex] = React.useState(null);
756
+ const { base, svg, slice, sliceLabel } = createTVUnstyledSlots5(
757
+ donutChartStyles({ size, thickness }),
758
+ unstyled
759
+ );
760
+ const total = data.reduce((sum, datum) => sum + Math.max(datum.value, 0), 0);
761
+ const gap = data.length > 1 ? SLICE_GAP : 0;
762
+ let offset = 0;
763
+ let tooltip = null;
764
+ return /* @__PURE__ */ jsxs5(
765
+ "div",
766
+ {
767
+ "data-slot": "chart-donut",
768
+ ref,
769
+ className: base({ className: cn5(classNames?.base, className) }),
770
+ ...props,
771
+ children: [
772
+ /* @__PURE__ */ jsx5(
773
+ "svg",
774
+ {
775
+ "data-slot": "chart-donut-svg",
776
+ role: "img",
777
+ "aria-label": `Donut chart with ${data.length} slices`,
778
+ viewBox: "0 0 40 40",
779
+ className: svg({ className: classNames?.svg }),
780
+ onMouseLeave: () => setActiveIndex(null),
781
+ children: data.map((datum, index) => {
782
+ const percent = total > 0 ? Math.max(datum.value, 0) / total * 100 : 0;
783
+ const length = Math.max(percent - gap, 0);
784
+ const startOffset = offset;
785
+ const hue = datum.color ?? DEFAULT_PALETTE[index % DEFAULT_PALETTE.length];
786
+ const midAngle = (startOffset + percent / 2) / 100 * 2 * Math.PI;
787
+ const labelX = CENTER + RADIUS * Math.cos(midAngle);
788
+ const labelY = CENTER + RADIUS * Math.sin(midAngle);
789
+ offset += percent;
790
+ if (showTooltip && activeIndex === index) {
791
+ const visualX = CENTER + RADIUS * Math.cos(midAngle - Math.PI / 2);
792
+ const visualY = CENTER + RADIUS * Math.sin(midAngle - Math.PI / 2);
793
+ tooltip = /* @__PURE__ */ jsx5(
794
+ ChartTooltip,
795
+ {
796
+ unstyled,
797
+ heading: datum.label,
798
+ items: [
799
+ {
800
+ value: `${valueFormatter(datum.value)} \xB7 ${Math.round(percent)}%`,
801
+ color: hue
802
+ }
803
+ ],
804
+ className: "-translate-x-1/2 -translate-y-[130%]",
805
+ style: {
806
+ left: `${visualX / 40 * 100}%`,
807
+ top: `${visualY / 40 * 100}%`
808
+ }
809
+ }
810
+ );
811
+ }
812
+ return /* @__PURE__ */ jsxs5(React.Fragment, { children: [
813
+ /* @__PURE__ */ jsx5(
814
+ "circle",
815
+ {
816
+ "data-slot": "chart-donut-slice",
817
+ cx: CENTER,
818
+ cy: CENTER,
819
+ r: RADIUS,
820
+ strokeDasharray: `${length} ${100 - length}`,
821
+ strokeDashoffset: -(startOffset + gap / 2),
822
+ onMouseEnter: () => setActiveIndex(index),
823
+ className: cn5(
824
+ slice({
825
+ className: cn5(
826
+ !unstyled && HUE_STROKE_MAP[hue],
827
+ classNames?.slice
828
+ )
829
+ }),
830
+ activeIndex !== null && activeIndex !== index && "opacity-40"
831
+ ),
832
+ children: /* @__PURE__ */ jsx5("title", { children: `${datum.label}: ${valueFormatter(datum.value)} (${Math.round(percent)}%)` })
833
+ }
834
+ ),
835
+ showPercentages && percent >= MIN_LABELED_PERCENT && /* @__PURE__ */ jsx5(
836
+ "text",
837
+ {
838
+ "data-slot": "chart-donut-slice-label",
839
+ x: labelX,
840
+ y: labelY,
841
+ textAnchor: "middle",
842
+ dominantBaseline: "central",
843
+ transform: `rotate(90 ${labelX} ${labelY})`,
844
+ className: sliceLabel({
845
+ className: cn5(
846
+ !unstyled && HUE_ON_BG_FILL_MAP[hue],
847
+ classNames?.sliceLabel
848
+ )
849
+ }),
850
+ "aria-hidden": true,
851
+ children: `${Math.round(percent)}%`
852
+ }
853
+ )
854
+ ] }, index);
855
+ })
856
+ }
857
+ ),
858
+ tooltip
859
+ ]
860
+ }
861
+ );
862
+ };
863
+
864
+ // src/sparkline.tsx
865
+ import * as React2 from "react";
866
+ import {
867
+ cn as cn6,
868
+ createTVUnstyledSlots as createTVUnstyledSlots6
869
+ } from "@mijn-ui/react-core";
870
+ import { tv as tv6 } from "tailwind-variants";
871
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
872
+ var sparklineStyles = tv6({
873
+ slots: {
874
+ wrapper: "relative inline-flex shrink-0",
875
+ base: "shrink-0",
876
+ line: "stroke-2",
877
+ area: "stroke-none",
878
+ marker: "fill-current stroke-none"
879
+ }
880
+ });
881
+ var PADDING = 2;
882
+ var Sparkline = ({
883
+ className,
884
+ classNames,
885
+ unstyled,
886
+ data,
887
+ color = "brand",
888
+ showArea = false,
889
+ labels,
890
+ valueFormatter = (value) => String(value),
891
+ showTooltip = true,
892
+ width = 120,
893
+ height = 32,
894
+ ref,
895
+ ...props
896
+ }) => {
897
+ const gradientId = React2.useId();
898
+ const [hoveredIndex, setHoveredIndex] = React2.useState(null);
899
+ const { wrapper, base, line, area, marker } = createTVUnstyledSlots6(
900
+ sparklineStyles(),
901
+ unstyled
902
+ );
903
+ const min = Math.min(...data);
904
+ const max = Math.max(...data);
905
+ const range = max - min;
906
+ const coords = data.map((value, index) => {
907
+ const x = data.length > 1 ? PADDING + index / (data.length - 1) * (width - PADDING * 2) : width / 2;
908
+ const y = range > 0 ? height - PADDING - (value - min) / range * (height - PADDING * 2) : height / 2;
909
+ return { x: Math.round(x * 100) / 100, y: Math.round(y * 100) / 100 };
910
+ });
911
+ const linePath = coords.map((point, index) => `${index === 0 ? "M" : "L"}${point.x},${point.y}`).join(" ");
912
+ const areaPath = coords.length > 0 ? `${linePath} L${width - PADDING},${height} L${PADDING},${height} Z` : "";
913
+ const indexAt = (event) => {
914
+ if (data.length === 0) return null;
915
+ const rect = event.currentTarget.getBoundingClientRect();
916
+ const x = event.clientX - rect.left;
917
+ const ratio = (x - PADDING) / Math.max(width - PADDING * 2, 1);
918
+ return Math.min(
919
+ data.length - 1,
920
+ Math.max(0, Math.round(ratio * (data.length - 1)))
921
+ );
922
+ };
923
+ const hovered = showTooltip && hoveredIndex !== null && coords[hoveredIndex] ? { index: hoveredIndex, ...coords[hoveredIndex] } : null;
924
+ return /* @__PURE__ */ jsxs6(
925
+ "span",
926
+ {
927
+ "data-slot": "chart-sparkline-wrapper",
928
+ className: wrapper({ className: classNames?.wrapper }),
929
+ children: [
930
+ /* @__PURE__ */ jsxs6(
931
+ "svg",
932
+ {
933
+ "data-slot": "chart-sparkline",
934
+ ref,
935
+ role: "img",
936
+ "aria-label": `Sparkline with ${data.length} points`,
937
+ width,
938
+ height,
939
+ viewBox: `0 0 ${width} ${height}`,
940
+ onMouseMove: showTooltip ? (event) => setHoveredIndex(indexAt(event)) : void 0,
941
+ onMouseLeave: showTooltip ? () => setHoveredIndex(null) : void 0,
942
+ className: base({
943
+ className: cn6(
944
+ !unstyled && HUE_TEXT_MAP[color],
945
+ classNames?.base,
946
+ className
947
+ )
948
+ }),
949
+ ...props,
950
+ children: [
951
+ showArea && coords.length > 0 && /* @__PURE__ */ jsxs6(Fragment2, { children: [
952
+ /* @__PURE__ */ jsx6("defs", { children: /* @__PURE__ */ jsxs6("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
953
+ /* @__PURE__ */ jsx6("stop", { offset: "0%", stopColor: "currentColor", stopOpacity: 0.25 }),
954
+ /* @__PURE__ */ jsx6("stop", { offset: "100%", stopColor: "currentColor", stopOpacity: 0 })
955
+ ] }) }),
956
+ /* @__PURE__ */ jsx6(
957
+ "path",
958
+ {
959
+ "data-slot": "chart-sparkline-area",
960
+ d: areaPath,
961
+ fill: `url(#${gradientId})`,
962
+ className: area({ className: classNames?.area })
963
+ }
964
+ )
965
+ ] }),
966
+ coords.length > 0 && /* @__PURE__ */ jsx6(
967
+ "path",
968
+ {
969
+ "data-slot": "chart-sparkline-line",
970
+ d: linePath,
971
+ fill: "none",
972
+ stroke: "currentColor",
973
+ strokeLinecap: "round",
974
+ strokeLinejoin: "round",
975
+ className: line({ className: classNames?.line })
976
+ }
977
+ ),
978
+ hovered && /* @__PURE__ */ jsx6(
979
+ "circle",
980
+ {
981
+ "data-slot": "chart-sparkline-marker",
982
+ cx: hovered.x,
983
+ cy: hovered.y,
984
+ r: 2.5,
985
+ className: marker({ className: classNames?.marker })
986
+ }
987
+ )
988
+ ]
989
+ }
990
+ ),
991
+ hovered && /* @__PURE__ */ jsx6(
992
+ ChartTooltip,
993
+ {
994
+ unstyled,
995
+ heading: labels?.[hovered.index] ?? `#${hovered.index + 1}`,
996
+ items: [{ value: valueFormatter(data[hovered.index] ?? 0), color }],
997
+ className: "-translate-x-1/2 -translate-y-[130%]",
998
+ style: { left: hovered.x, top: hovered.y }
999
+ }
1000
+ )
1001
+ ]
1002
+ }
1003
+ );
1004
+ };
1005
+ export {
1006
+ BarChart,
1007
+ ChartLegend,
1008
+ ChartLegendItem,
1009
+ ChartTooltip,
1010
+ DEFAULT_PALETTE,
1011
+ DonutChart,
1012
+ GroupedBarChart,
1013
+ HUE_BG_MAP,
1014
+ HUE_BG_SUBTLE_MAP,
1015
+ HUE_ON_BG_FILL_MAP,
1016
+ HUE_STROKE_MAP,
1017
+ HUE_TEXT_MAP,
1018
+ Sparkline,
1019
+ barChartStyles,
1020
+ chartLegendStyles,
1021
+ chartTooltipStyles,
1022
+ donutChartStyles,
1023
+ groupedBarChartStyles,
1024
+ sparklineStyles
1025
+ };