@spatika/react 1.3.0 → 1.4.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/README.md +6 -2
- package/dist/composites/EventCalendar.d.ts +13 -2
- package/dist/composites/EventCalendar.d.ts.map +1 -1
- package/dist/composites/EventCalendar.js +10 -24
- package/dist/composites/cartesian-charts.d.ts +12 -2
- package/dist/composites/cartesian-charts.d.ts.map +1 -1
- package/dist/composites/cartesian-charts.js +27 -13
- package/dist/composites/chart-composition.d.ts +19 -1
- package/dist/composites/chart-composition.d.ts.map +1 -1
- package/dist/composites/chart-composition.js +124 -62
- package/dist/composites/chart-interaction.d.ts +81 -0
- package/dist/composites/chart-interaction.d.ts.map +1 -0
- package/dist/composites/chart-interaction.js +56 -0
- package/dist/composites/chart-ui.d.ts +8 -13
- package/dist/composites/chart-ui.d.ts.map +1 -1
- package/dist/composites/chart-ui.js +9 -6
- package/dist/composites/flow-charts.d.ts +6 -1
- package/dist/composites/flow-charts.d.ts.map +1 -1
- package/dist/composites/flow-charts.js +28 -14
- package/dist/composites/radial-charts.d.ts +11 -1
- package/dist/composites/radial-charts.d.ts.map +1 -1
- package/dist/composites/radial-charts.js +33 -14
- package/dist/composites/scheduler-ui.d.ts +14 -1
- package/dist/composites/scheduler-ui.d.ts.map +1 -1
- package/dist/composites/scheduler-ui.js +23 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/lib/chips.d.ts +3 -0
- package/dist/lib/chips.d.ts.map +1 -1
- package/dist/lib/chips.js +10 -0
- package/dist/lib/scheduler.d.ts +12 -0
- package/dist/lib/scheduler.d.ts.map +1 -1
- package/dist/lib/scheduler.js +22 -0
- package/dist/primitives/NativeSelect.d.ts +10 -1
- package/dist/primitives/NativeSelect.d.ts.map +1 -1
- package/dist/primitives/NativeSelect.js +2 -2
- package/package.json +3 -3
- package/skills/spatika-ui/SKILL.md +4 -0
- package/src/composites/EventCalendar.tsx +35 -24
- package/src/composites/cartesian-charts.test.tsx +86 -2
- package/src/composites/cartesian-charts.tsx +84 -16
- package/src/composites/chart-composition.test.tsx +15 -0
- package/src/composites/chart-composition.tsx +229 -88
- package/src/composites/chart-interaction.ts +136 -0
- package/src/composites/chart-ui.tsx +35 -27
- package/src/composites/flow-charts.test.tsx +21 -2
- package/src/composites/flow-charts.tsx +59 -25
- package/src/composites/radial-charts.test.tsx +17 -2
- package/src/composites/radial-charts.tsx +38 -12
- package/src/composites/scheduler-ui.tsx +128 -26
- package/src/composites/scheduler.test.tsx +39 -0
- package/src/index.test.ts +4 -0
- package/src/index.ts +18 -1
- package/src/lib/chips.ts +16 -0
- package/src/lib/scheduler.test.ts +16 -0
- package/src/lib/scheduler.ts +34 -0
- package/src/primitives/NativeSelect.test.tsx +28 -0
- package/src/primitives/NativeSelect.tsx +14 -4
|
@@ -62,6 +62,14 @@ import {
|
|
|
62
62
|
seriesMeta,
|
|
63
63
|
type ChartHover,
|
|
64
64
|
} from "./chart-ui";
|
|
65
|
+
import {
|
|
66
|
+
bindChartMark,
|
|
67
|
+
seriesItemColor,
|
|
68
|
+
type ChartItemEvent,
|
|
69
|
+
type ChartMarkRenderContext,
|
|
70
|
+
type ChartReferenceLine,
|
|
71
|
+
type ChartTooltipRenderer,
|
|
72
|
+
} from "./chart-interaction";
|
|
65
73
|
|
|
66
74
|
export type ComposedSeriesType = "bar" | "line" | "area" | "scatter";
|
|
67
75
|
|
|
@@ -70,6 +78,8 @@ export type ComposedSeries = {
|
|
|
70
78
|
type?: ComposedSeriesType;
|
|
71
79
|
label?: string;
|
|
72
80
|
color?: string;
|
|
81
|
+
/** Per-category colors; each entry falls back to `color`. */
|
|
82
|
+
itemColors?: Array<string | null | undefined>;
|
|
73
83
|
data?: Array<number | null>;
|
|
74
84
|
dataKey?: string;
|
|
75
85
|
stack?: string;
|
|
@@ -85,6 +95,7 @@ type ResolvedSeries = {
|
|
|
85
95
|
type: ComposedSeriesType;
|
|
86
96
|
label: string;
|
|
87
97
|
color: string;
|
|
98
|
+
itemColors?: Array<string | null | undefined>;
|
|
88
99
|
data: Array<number | null>;
|
|
89
100
|
stack?: string;
|
|
90
101
|
yAxisId?: string;
|
|
@@ -135,6 +146,10 @@ type ChartContextValue = {
|
|
|
135
146
|
brush: { x0: number; x1: number } | null;
|
|
136
147
|
setBrush: (brush: { x0: number; x1: number } | null) => void;
|
|
137
148
|
exportChart: (format: "svg" | "png") => Promise<void>;
|
|
149
|
+
onItemClick?: (event: ChartItemEvent) => void;
|
|
150
|
+
renderMark?: (ctx: ChartMarkRenderContext) => ReactNode;
|
|
151
|
+
referenceLines?: ChartReferenceLine[];
|
|
152
|
+
showLabels?: boolean;
|
|
138
153
|
};
|
|
139
154
|
|
|
140
155
|
const ChartContext = createContext<ChartContextValue | null>(null);
|
|
@@ -175,6 +190,14 @@ export type ChartContainerProps = {
|
|
|
175
190
|
locale?: string;
|
|
176
191
|
children?: ReactNode;
|
|
177
192
|
"aria-label"?: string;
|
|
193
|
+
/** When true, plot height follows the parent instead of the numeric `height`. */
|
|
194
|
+
fillHeight?: boolean;
|
|
195
|
+
onItemClick?: (event: ChartItemEvent) => void;
|
|
196
|
+
renderTooltip?: ChartTooltipRenderer;
|
|
197
|
+
renderMark?: (ctx: ChartMarkRenderContext) => ReactNode;
|
|
198
|
+
referenceLines?: ChartReferenceLine[];
|
|
199
|
+
/** Draw the numeric value at the end of each bar. */
|
|
200
|
+
showLabels?: boolean;
|
|
178
201
|
};
|
|
179
202
|
|
|
180
203
|
function useElementSize(ref: RefObject<HTMLElement | null>, fallbackWidth: number, fallbackHeight: number) {
|
|
@@ -207,6 +230,7 @@ function resolveSeries(series: ComposedSeries[], dataset: ChartDataset | undefin
|
|
|
207
230
|
type,
|
|
208
231
|
label: item.label ?? item.dataKey ?? item.id ?? `Series ${index + 1}`,
|
|
209
232
|
color: item.color ?? chartColor(index, colors),
|
|
233
|
+
itemColors: item.itemColors,
|
|
210
234
|
data,
|
|
211
235
|
stack: item.stack,
|
|
212
236
|
yAxisId: item.yAxisId,
|
|
@@ -277,12 +301,19 @@ export function ChartContainer({
|
|
|
277
301
|
locale,
|
|
278
302
|
children,
|
|
279
303
|
"aria-label": ariaLabel,
|
|
304
|
+
fillHeight = false,
|
|
305
|
+
onItemClick,
|
|
306
|
+
renderTooltip,
|
|
307
|
+
renderMark,
|
|
308
|
+
referenceLines,
|
|
309
|
+
showLabels = false,
|
|
280
310
|
}: ChartContainerProps) {
|
|
281
311
|
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
282
312
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
283
313
|
const clipPathId = `spk-chart-clip-${useId().replace(/:/g, "")}`;
|
|
284
314
|
const measured = useElementSize(surfaceRef, width ?? 320, height);
|
|
285
315
|
const plotWidth = width ?? measured.width;
|
|
316
|
+
const plotHeight = fillHeight ? measured.height : height;
|
|
286
317
|
const hasRight = yAxis?.some((axis) => axis.position === "right");
|
|
287
318
|
const m = resolveMargin(
|
|
288
319
|
margin ?? (hasRight ? { ...DEFAULT_CHART_MARGIN, right: 48 } : undefined),
|
|
@@ -320,7 +351,7 @@ export function ChartContainer({
|
|
|
320
351
|
const [brush, setBrush] = useState<{ x0: number; x1: number } | null>(null);
|
|
321
352
|
const activeHighlight = selected ?? highlighted;
|
|
322
353
|
|
|
323
|
-
const plot = plotBox(plotWidth,
|
|
354
|
+
const plot = plotBox(plotWidth, plotHeight, m);
|
|
324
355
|
const visible = resolved.filter((item) => !hidden.has(item.id));
|
|
325
356
|
const leftSeries = visible.filter((item) => !item.yAxisId || item.yAxisId === (leftAxis?.id ?? item.yAxisId));
|
|
326
357
|
const rightSeries = visible.filter((item) => rightAxis && item.yAxisId === rightAxis.id);
|
|
@@ -372,7 +403,7 @@ export function ChartContainer({
|
|
|
372
403
|
const ctx: ChartContextValue = {
|
|
373
404
|
slot,
|
|
374
405
|
width: plotWidth,
|
|
375
|
-
height,
|
|
406
|
+
height: plotHeight,
|
|
376
407
|
margin: m,
|
|
377
408
|
plot,
|
|
378
409
|
categories,
|
|
@@ -409,6 +440,10 @@ export function ChartContainer({
|
|
|
409
440
|
brush,
|
|
410
441
|
setBrush,
|
|
411
442
|
exportChart: doExport,
|
|
443
|
+
onItemClick,
|
|
444
|
+
renderMark,
|
|
445
|
+
referenceLines,
|
|
446
|
+
showLabels,
|
|
412
447
|
};
|
|
413
448
|
|
|
414
449
|
const legend = seriesMeta(resolved, colors);
|
|
@@ -420,7 +455,7 @@ export function ChartContainer({
|
|
|
420
455
|
<ChartContext.Provider value={ctx}>
|
|
421
456
|
<div
|
|
422
457
|
data-slot={slot}
|
|
423
|
-
className={cn("spk-chart", animated && "spk-chart--animated", className)}
|
|
458
|
+
className={cn("spk-chart", animated && "spk-chart--animated", fillHeight && "spk-chart--fill", className)}
|
|
424
459
|
role="img"
|
|
425
460
|
aria-label={label}
|
|
426
461
|
>
|
|
@@ -428,9 +463,9 @@ export function ChartContainer({
|
|
|
428
463
|
{hideLegend ? null : (
|
|
429
464
|
<ChartLegend items={legend} hiddenIds={hidden} onToggle={toggleSeries} />
|
|
430
465
|
)}
|
|
431
|
-
<div ref={surfaceRef} className="spk-chart-surface" style={{ height }}>
|
|
432
|
-
{plotWidth > 0 ? children ?? <DefaultChartSurface /> : null}
|
|
433
|
-
<ChartTooltip hover={hover} boundsWidth={plotWidth} />
|
|
466
|
+
<div ref={surfaceRef} className="spk-chart-surface" style={fillHeight ? undefined : { height: plotHeight }}>
|
|
467
|
+
{plotWidth > 0 && plotHeight > 0 ? children ?? <DefaultChartSurface /> : null}
|
|
468
|
+
<ChartTooltip hover={hover} boundsWidth={plotWidth} render={renderTooltip} />
|
|
434
469
|
</div>
|
|
435
470
|
<ChartA11yTable />
|
|
436
471
|
</div>
|
|
@@ -450,6 +485,7 @@ function DefaultChartSurface() {
|
|
|
450
485
|
<ChartsXAxis />
|
|
451
486
|
<ChartsYAxis />
|
|
452
487
|
{rightAxis ? <ChartsYAxis position="right" /> : null}
|
|
488
|
+
<ChartsReferenceLine />
|
|
453
489
|
<ChartsBrush />
|
|
454
490
|
</ChartSurface>
|
|
455
491
|
);
|
|
@@ -609,50 +645,81 @@ export function BarPlot() {
|
|
|
609
645
|
const barW = (ctx.bandwidth - gap * (n - 1)) / n;
|
|
610
646
|
const zero = ctx.yAt(0);
|
|
611
647
|
return (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
648
|
+
<>
|
|
649
|
+
<PlotClip slot="bar-plot">
|
|
650
|
+
{bars.map((item, s) =>
|
|
651
|
+
item.data.map((value, i) => {
|
|
652
|
+
if (value == null) return null;
|
|
653
|
+
let x = ctx.xAt(i);
|
|
654
|
+
let y1: number;
|
|
655
|
+
let y2: number;
|
|
656
|
+
if (stacks) {
|
|
657
|
+
const seg = stacks[s]![i]!;
|
|
658
|
+
y1 = ctx.yAt(seg.end, item.yAxisId);
|
|
659
|
+
y2 = ctx.yAt(seg.start, item.yAxisId);
|
|
660
|
+
} else {
|
|
661
|
+
x = ctx.xAt(i) + s * (barW + gap);
|
|
662
|
+
y1 = ctx.yAt(value, item.yAxisId);
|
|
663
|
+
y2 = zero;
|
|
664
|
+
}
|
|
665
|
+
const y = Math.min(y1, y2);
|
|
666
|
+
const h = Math.max(1, Math.abs(y2 - y1));
|
|
667
|
+
const opacity = markOpacity(ctx.highlightScope, ctx.highlighted, item.id, i);
|
|
668
|
+
const color = seriesItemColor(item.color, item.itemColors, i);
|
|
669
|
+
const event: ChartItemEvent = {
|
|
670
|
+
seriesId: item.id,
|
|
671
|
+
seriesLabel: item.label,
|
|
672
|
+
dataIndex: i,
|
|
673
|
+
category: ctx.categories[i] ?? i,
|
|
674
|
+
value,
|
|
675
|
+
color,
|
|
676
|
+
};
|
|
677
|
+
return (
|
|
678
|
+
<path
|
|
679
|
+
key={`${item.id}-${i}`}
|
|
680
|
+
d={roundedRectPath(x, y, ctx.stacked ? ctx.bandwidth : barW, h, ctx.borderRadius)}
|
|
681
|
+
fill={color}
|
|
682
|
+
opacity={opacity}
|
|
683
|
+
{...bindChartMark({
|
|
684
|
+
event,
|
|
685
|
+
hover: {
|
|
686
|
+
title: String(ctx.categories[i]),
|
|
687
|
+
items: [{ color, label: item.label, value: formatSeriesValue(value) }],
|
|
688
|
+
},
|
|
689
|
+
onItemClick: ctx.onItemClick,
|
|
690
|
+
setHover: ctx.setHover,
|
|
691
|
+
clearHover: () => ctx.setHover(null),
|
|
692
|
+
onHighlight: ctx.setHighlighted,
|
|
693
|
+
})}
|
|
694
|
+
/>
|
|
695
|
+
);
|
|
696
|
+
}),
|
|
697
|
+
)}
|
|
698
|
+
</PlotClip>
|
|
699
|
+
{ctx.showLabels ? (
|
|
700
|
+
<g data-slot="bar-labels" pointerEvents="none">
|
|
701
|
+
{bars.map((item, s) =>
|
|
702
|
+
item.data.map((value, i) => {
|
|
703
|
+
if (value == null) return null;
|
|
704
|
+
const x = ctx.stacked ? ctx.xAt(i) : ctx.xAt(i) + s * (barW + gap);
|
|
705
|
+
const y = ctx.yAt(value, item.yAxisId);
|
|
706
|
+
const width = ctx.stacked ? ctx.bandwidth : barW;
|
|
707
|
+
return (
|
|
708
|
+
<text
|
|
709
|
+
key={`${item.id}-${i}-label`}
|
|
710
|
+
className="spk-chart-label"
|
|
711
|
+
x={x + width / 2}
|
|
712
|
+
y={y - 4}
|
|
713
|
+
textAnchor="middle"
|
|
714
|
+
>
|
|
715
|
+
{formatSeriesValue(value)}
|
|
716
|
+
</text>
|
|
717
|
+
);
|
|
718
|
+
}),
|
|
719
|
+
)}
|
|
720
|
+
</g>
|
|
721
|
+
) : null}
|
|
722
|
+
</>
|
|
656
723
|
);
|
|
657
724
|
}
|
|
658
725
|
|
|
@@ -705,29 +772,40 @@ function LineLikePlot({ types, slot }: { types: ComposedSeriesType[]; slot: stri
|
|
|
705
772
|
className="spk-chart-mark"
|
|
706
773
|
/>
|
|
707
774
|
{item.showMark
|
|
708
|
-
? pts.map((pt) =>
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
775
|
+
? pts.map((pt) => {
|
|
776
|
+
const color = seriesItemColor(item.color, item.itemColors, pt.i);
|
|
777
|
+
const event: ChartItemEvent = {
|
|
778
|
+
seriesId: item.id,
|
|
779
|
+
seriesLabel: item.label,
|
|
780
|
+
dataIndex: pt.i,
|
|
781
|
+
category: ctx.categories[pt.i] ?? pt.i,
|
|
782
|
+
value: pt.value,
|
|
783
|
+
color,
|
|
784
|
+
};
|
|
785
|
+
const mark = ctx.renderMark?.({ ...event, x: pt.x, y: pt.y }) ?? (
|
|
786
|
+
<circle cx={pt.x} cy={pt.y} r={3.4} fill={color} />
|
|
787
|
+
);
|
|
788
|
+
return (
|
|
789
|
+
<g
|
|
790
|
+
key={pt.i}
|
|
791
|
+
{...bindChartMark({
|
|
792
|
+
event,
|
|
793
|
+
hover: {
|
|
794
|
+
x: pt.x,
|
|
795
|
+
y: pt.y,
|
|
796
|
+
title: String(ctx.categories[pt.i]),
|
|
797
|
+
items: [{ color, label: item.label, value: formatSeriesValue(pt.value) }],
|
|
798
|
+
},
|
|
799
|
+
onItemClick: ctx.onItemClick,
|
|
800
|
+
setHover: ctx.setHover,
|
|
801
|
+
clearHover: () => ctx.setHover(null),
|
|
802
|
+
onHighlight: ctx.setHighlighted,
|
|
803
|
+
})}
|
|
804
|
+
>
|
|
805
|
+
{mark}
|
|
806
|
+
</g>
|
|
807
|
+
);
|
|
808
|
+
})
|
|
731
809
|
: null}
|
|
732
810
|
</g>
|
|
733
811
|
);
|
|
@@ -763,31 +841,37 @@ export function ScatterPlot() {
|
|
|
763
841
|
const cx = applyZoom(xScale(point.x), ctx.plot.left, ctx.plot.iw, ctx.zoom.x);
|
|
764
842
|
const cy = ctx.yAt(point.y, item.yAxisId);
|
|
765
843
|
const opacity = markOpacity(ctx.highlightScope, ctx.highlighted, item.id, i);
|
|
844
|
+
const color = seriesItemColor(item.color, item.itemColors, i);
|
|
845
|
+
const event: ChartItemEvent = {
|
|
846
|
+
seriesId: item.id,
|
|
847
|
+
seriesLabel: item.label,
|
|
848
|
+
dataIndex: i,
|
|
849
|
+
category: point.id ?? i,
|
|
850
|
+
value: point.y,
|
|
851
|
+
color,
|
|
852
|
+
};
|
|
766
853
|
return (
|
|
767
854
|
<circle
|
|
768
855
|
key={`${item.id}-${point.id ?? i}`}
|
|
769
856
|
cx={cx}
|
|
770
857
|
cy={cy}
|
|
771
858
|
r={4.5}
|
|
772
|
-
fill={
|
|
859
|
+
fill={color}
|
|
773
860
|
opacity={opacity}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
ctx.setHover({
|
|
778
|
-
x: event.nativeEvent.offsetX,
|
|
779
|
-
y: event.nativeEvent.offsetY,
|
|
861
|
+
{...bindChartMark({
|
|
862
|
+
event,
|
|
863
|
+
hover: {
|
|
780
864
|
title: item.label,
|
|
781
865
|
items: [
|
|
782
|
-
{ color
|
|
783
|
-
{ color
|
|
866
|
+
{ color, label: "x", value: formatChartNumber(point.x, ctx.locale) },
|
|
867
|
+
{ color, label: "y", value: formatChartNumber(point.y, ctx.locale) },
|
|
784
868
|
],
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
ctx.
|
|
789
|
-
ctx.
|
|
790
|
-
}}
|
|
869
|
+
},
|
|
870
|
+
onItemClick: ctx.onItemClick,
|
|
871
|
+
setHover: ctx.setHover,
|
|
872
|
+
clearHover: () => ctx.setHover(null),
|
|
873
|
+
onHighlight: ctx.setHighlighted,
|
|
874
|
+
})}
|
|
791
875
|
/>
|
|
792
876
|
);
|
|
793
877
|
});
|
|
@@ -813,6 +897,63 @@ export function ChartsBrush() {
|
|
|
813
897
|
);
|
|
814
898
|
}
|
|
815
899
|
|
|
900
|
+
/** Horizontal (value) or vertical (category index) guide overlay. */
|
|
901
|
+
export function ChartsReferenceLine() {
|
|
902
|
+
const { referenceLines, yAt, xAt, plot, bandwidth } = useChartContext();
|
|
903
|
+
if (!referenceLines?.length) return null;
|
|
904
|
+
return (
|
|
905
|
+
<g data-slot="chart-reference-lines">
|
|
906
|
+
{referenceLines.map((line, index) => {
|
|
907
|
+
const color = line.color ?? "var(--primary)";
|
|
908
|
+
const dash = line.strokeDasharray ?? "4 4";
|
|
909
|
+
if (line.y != null) {
|
|
910
|
+
const y = yAt(line.y, line.yAxisId);
|
|
911
|
+
return (
|
|
912
|
+
<g key={`y-${index}`}>
|
|
913
|
+
<line
|
|
914
|
+
className="spk-chart-ref-line"
|
|
915
|
+
x1={plot.left}
|
|
916
|
+
x2={plot.right}
|
|
917
|
+
y1={y}
|
|
918
|
+
y2={y}
|
|
919
|
+
stroke={color}
|
|
920
|
+
strokeDasharray={dash}
|
|
921
|
+
/>
|
|
922
|
+
{line.label ? (
|
|
923
|
+
<text className="spk-chart-ref-label" x={plot.right - 4} y={y - 4} textAnchor="end">
|
|
924
|
+
{line.label}
|
|
925
|
+
</text>
|
|
926
|
+
) : null}
|
|
927
|
+
</g>
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (line.xIndex != null) {
|
|
931
|
+
const x = xAt(line.xIndex) + bandwidth / 2;
|
|
932
|
+
return (
|
|
933
|
+
<g key={`x-${index}`}>
|
|
934
|
+
<line
|
|
935
|
+
className="spk-chart-ref-line"
|
|
936
|
+
x1={x}
|
|
937
|
+
x2={x}
|
|
938
|
+
y1={plot.top}
|
|
939
|
+
y2={plot.bottom}
|
|
940
|
+
stroke={color}
|
|
941
|
+
strokeDasharray={dash}
|
|
942
|
+
/>
|
|
943
|
+
{line.label ? (
|
|
944
|
+
<text className="spk-chart-ref-label" x={x + 4} y={plot.top + 12}>
|
|
945
|
+
{line.label}
|
|
946
|
+
</text>
|
|
947
|
+
) : null}
|
|
948
|
+
</g>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
})}
|
|
953
|
+
</g>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
816
957
|
export function ChartsLegend() {
|
|
817
958
|
const { series, hidden, toggleSeries } = useChartContext();
|
|
818
959
|
return (
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ChartTooltipItem = {
|
|
5
|
+
color: string;
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ChartHover = {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
title?: string;
|
|
14
|
+
items: ChartTooltipItem[];
|
|
15
|
+
dataIndex?: number;
|
|
16
|
+
seriesId?: string;
|
|
17
|
+
category?: string | number | Date;
|
|
18
|
+
} | null;
|
|
19
|
+
|
|
20
|
+
/** Payload fired when a chart mark (bar, slice, point, cell) is activated. */
|
|
21
|
+
export type ChartItemEvent = {
|
|
22
|
+
seriesId: string;
|
|
23
|
+
seriesLabel: string;
|
|
24
|
+
dataIndex: number;
|
|
25
|
+
category: string | number | Date;
|
|
26
|
+
value: number | null;
|
|
27
|
+
color: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Geometry plus item identity, used by `renderMark` on line and area series. */
|
|
31
|
+
export type ChartMarkRenderContext = ChartItemEvent & {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Overlay guide on a cartesian plot.
|
|
38
|
+
* Set `y` for a horizontal value line, or `xIndex` for a vertical category line.
|
|
39
|
+
*/
|
|
40
|
+
export type ChartReferenceLine = {
|
|
41
|
+
y?: number;
|
|
42
|
+
xIndex?: number;
|
|
43
|
+
yAxisId?: string;
|
|
44
|
+
color?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
strokeDasharray?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ChartTooltipRenderer = (hover: NonNullable<ChartHover>) => ReactNode;
|
|
50
|
+
|
|
51
|
+
export type ChartMarkBind = {
|
|
52
|
+
className: string;
|
|
53
|
+
role?: "button";
|
|
54
|
+
tabIndex?: number;
|
|
55
|
+
"aria-label"?: string;
|
|
56
|
+
onClick?: (event: { stopPropagation(): void }) => void;
|
|
57
|
+
onKeyDown?: (event: { key: string; preventDefault(): void }) => void;
|
|
58
|
+
onMouseEnter?: (event: { nativeEvent: { offsetX: number; offsetY: number } }) => void;
|
|
59
|
+
onMouseLeave?: () => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Per-category color, falling back to the series token. */
|
|
63
|
+
export function seriesItemColor(
|
|
64
|
+
color: string,
|
|
65
|
+
itemColors: Array<string | null | undefined> | undefined,
|
|
66
|
+
index: number,
|
|
67
|
+
): string {
|
|
68
|
+
return itemColors?.[index] || color;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function chartItemAriaLabel(event: ChartItemEvent): string {
|
|
72
|
+
const category = String(event.category);
|
|
73
|
+
if (event.seriesLabel && event.seriesLabel !== category) {
|
|
74
|
+
return `${event.seriesLabel} ${category}`;
|
|
75
|
+
}
|
|
76
|
+
return category;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pointer, keyboard, and hover bindings for an SVG mark.
|
|
81
|
+
* Adds a button role only when `onItemClick` is set so legends stay the sole named buttons.
|
|
82
|
+
*/
|
|
83
|
+
export function bindChartMark({
|
|
84
|
+
event,
|
|
85
|
+
hover,
|
|
86
|
+
onItemClick,
|
|
87
|
+
setHover,
|
|
88
|
+
clearHover,
|
|
89
|
+
onHighlight,
|
|
90
|
+
}: {
|
|
91
|
+
event: ChartItemEvent;
|
|
92
|
+
hover: Pick<NonNullable<ChartHover>, "title" | "items"> & Partial<NonNullable<ChartHover>>;
|
|
93
|
+
onItemClick?: (event: ChartItemEvent) => void;
|
|
94
|
+
setHover: (hover: ChartHover) => void;
|
|
95
|
+
clearHover: () => void;
|
|
96
|
+
onHighlight?: (item: { seriesId: string; dataIndex: number } | null) => void;
|
|
97
|
+
}): ChartMarkBind {
|
|
98
|
+
const clickable = Boolean(onItemClick);
|
|
99
|
+
const applyHover = (x: number, y: number) => {
|
|
100
|
+
onHighlight?.({ seriesId: event.seriesId, dataIndex: event.dataIndex });
|
|
101
|
+
setHover({
|
|
102
|
+
x: hover.x ?? x,
|
|
103
|
+
y: hover.y ?? y,
|
|
104
|
+
title: hover.title,
|
|
105
|
+
items: hover.items,
|
|
106
|
+
dataIndex: event.dataIndex,
|
|
107
|
+
seriesId: event.seriesId,
|
|
108
|
+
category: event.category,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
className: cn("spk-chart-mark", clickable && "spk-chart-mark--interactive"),
|
|
113
|
+
role: clickable ? "button" : undefined,
|
|
114
|
+
tabIndex: clickable ? 0 : undefined,
|
|
115
|
+
"aria-label": clickable ? chartItemAriaLabel(event) : undefined,
|
|
116
|
+
onClick: clickable
|
|
117
|
+
? (native) => {
|
|
118
|
+
native.stopPropagation();
|
|
119
|
+
onItemClick?.(event);
|
|
120
|
+
}
|
|
121
|
+
: undefined,
|
|
122
|
+
onKeyDown: clickable
|
|
123
|
+
? (native) => {
|
|
124
|
+
if (native.key === "Enter" || native.key === " ") {
|
|
125
|
+
native.preventDefault();
|
|
126
|
+
onItemClick?.(event);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
: undefined,
|
|
130
|
+
onMouseEnter: (native) => applyHover(native.nativeEvent.offsetX, native.nativeEvent.offsetY),
|
|
131
|
+
onMouseLeave: () => {
|
|
132
|
+
onHighlight?.(null);
|
|
133
|
+
clearHover();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|