@spatika/react 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -3
- package/README.md +1 -1
- package/dist/composites/EventCalendar.js +3 -3
- package/dist/composites/GlassCard.d.ts +1 -1
- package/dist/composites/RichTextEditor.d.ts +1 -4
- package/dist/composites/RichTextEditor.d.ts.map +1 -1
- package/dist/composites/RichTextEditor.js +12 -19
- package/dist/composites/SurfaceCard.d.ts +1 -1
- package/dist/composites/Tag.d.ts +2 -2
- package/dist/index.d.ts +8 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/breakpoints.d.ts +17 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +23 -0
- package/dist/lib/create-theme.d.ts +162 -0
- package/dist/lib/create-theme.d.ts.map +1 -0
- package/dist/lib/create-theme.js +138 -0
- package/dist/lib/scheduler.js +2 -2
- package/dist/lib/themes.d.ts +17 -7
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +22 -8
- package/dist/lib/use-media-query.d.ts +9 -0
- package/dist/lib/use-media-query.d.ts.map +1 -1
- package/dist/lib/use-media-query.js +28 -1
- package/dist/primitives/Alert.d.ts +1 -1
- package/dist/primitives/Badge.d.ts +2 -2
- package/dist/primitives/Button.d.ts +2 -2
- package/dist/primitives/Container.d.ts +2 -2
- package/dist/primitives/Fab.d.ts +4 -4
- package/dist/primitives/Grid.d.ts +2 -1
- package/dist/primitives/Grid.d.ts.map +1 -1
- package/dist/primitives/Grid.js +10 -2
- package/dist/primitives/IconButton.d.ts +2 -2
- package/dist/primitives/ToggleButton.d.ts +1 -1
- package/dist/theme/SpatikaThemeProvider.d.ts +4 -5
- package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -1
- package/dist/theme/SpatikaThemeProvider.js +28 -13
- package/package.json +4 -2
- package/skills/spatika-ui/SKILL.md +25 -6
- package/src/composites/EventCalendar.test.tsx +293 -0
- package/src/composites/EventCalendar.tsx +3 -3
- package/src/composites/RichTextEditor.test.tsx +51 -0
- package/src/composites/RichTextEditor.tsx +20 -84
- package/src/composites/chart-ui.test.tsx +21 -0
- package/src/composites/scheduler.test.tsx +0 -258
- package/src/index.test.ts +6 -0
- package/src/index.ts +138 -72
- package/src/lib/breakpoints.test.ts +29 -0
- package/src/lib/breakpoints.ts +28 -0
- package/src/lib/create-theme.test.ts +79 -0
- package/src/lib/create-theme.ts +249 -0
- package/src/lib/scheduler.test.ts +10 -0
- package/src/lib/scheduler.ts +2 -2
- package/src/lib/themes.test.ts +17 -0
- package/src/lib/themes.ts +43 -12
- package/src/lib/use-media-query.test.tsx +80 -0
- package/src/lib/use-media-query.ts +35 -1
- package/src/primitives/Grid.test.tsx +55 -0
- package/src/primitives/Grid.tsx +13 -3
- package/src/primitives/mui-more.test.tsx +17 -0
- package/src/theme/SpatikaThemeProvider.test.tsx +38 -0
- package/src/theme/SpatikaThemeProvider.tsx +43 -15
- package/src/composites/ChartDataGrid.test.tsx +0 -57
- package/src/composites/ChartDataGrid.tsx +0 -196
- package/src/composites/MapChart.test.tsx +0 -39
- package/src/composites/MapChart.tsx +0 -130
- package/src/composites/ScatterWebGL.test.tsx +0 -25
- package/src/composites/ScatterWebGL.tsx +0 -79
- package/src/composites/cartesian-charts.test.tsx +0 -136
- package/src/composites/cartesian-charts.tsx +0 -1355
- package/src/composites/chart-composition.test.tsx +0 -68
- package/src/composites/chart-composition.tsx +0 -1039
- package/src/composites/chart-interaction.ts +0 -136
- package/src/composites/chart-ui.tsx +0 -336
- package/src/composites/charts-3d.test.tsx +0 -25
- package/src/composites/charts-3d.tsx +0 -318
- package/src/composites/charts.test.tsx +0 -254
- package/src/composites/flow-charts.test.tsx +0 -98
- package/src/composites/flow-charts.tsx +0 -641
- package/src/composites/radial-charts.test.tsx +0 -89
- package/src/composites/radial-charts.tsx +0 -643
- package/src/lib/chart-export.test.ts +0 -20
- package/src/lib/chart-export.ts +0 -99
- package/src/lib/charts-platform.test.ts +0 -93
- package/src/lib/charts.test.ts +0 -132
- package/src/lib/charts.ts +0 -1021
- package/src/lib/geo.test.ts +0 -42
- package/src/lib/geo.ts +0 -138
- package/src/lib/iso.test.ts +0 -22
- package/src/lib/iso.ts +0 -102
- package/src/lib/webgl-scatter.test.ts +0 -36
- package/src/lib/webgl-scatter.ts +0 -230
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useRef,
|
|
4
|
-
useState,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
type RefObject,
|
|
7
|
-
} from "react";
|
|
8
|
-
import { cn } from "../lib/cn";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_CHART_MARGIN,
|
|
11
|
-
chartColor,
|
|
12
|
-
formatAxisValue,
|
|
13
|
-
formatChartNumber,
|
|
14
|
-
type ChartAxisConfig,
|
|
15
|
-
type ChartMargin,
|
|
16
|
-
} from "../lib/charts";
|
|
17
|
-
import {
|
|
18
|
-
type ChartHover,
|
|
19
|
-
type ChartTooltipRenderer,
|
|
20
|
-
} from "./chart-interaction";
|
|
21
|
-
|
|
22
|
-
export type { ChartHover, ChartTooltipItem } from "./chart-interaction";
|
|
23
|
-
|
|
24
|
-
export type ChartLegendItem = {
|
|
25
|
-
id: string;
|
|
26
|
-
label: string;
|
|
27
|
-
color: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type ChartFrameProps = {
|
|
31
|
-
slot: string;
|
|
32
|
-
className?: string;
|
|
33
|
-
width?: number;
|
|
34
|
-
height?: number;
|
|
35
|
-
/** When true, plot height follows the parent instead of the numeric `height`. */
|
|
36
|
-
fillHeight?: boolean;
|
|
37
|
-
margin?: ChartMargin;
|
|
38
|
-
legend?: ChartLegendItem[];
|
|
39
|
-
hiddenIds?: Set<string>;
|
|
40
|
-
onToggleSeries?: (id: string) => void;
|
|
41
|
-
hover?: ChartHover;
|
|
42
|
-
renderTooltip?: ChartTooltipRenderer;
|
|
43
|
-
"aria-label"?: string;
|
|
44
|
-
children: (plot: { width: number; height: number; m: Required<ChartMargin> }) => ReactNode;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export function resolveMargin(margin?: ChartMargin): Required<ChartMargin> {
|
|
48
|
-
return {
|
|
49
|
-
top: margin?.top ?? DEFAULT_CHART_MARGIN.top,
|
|
50
|
-
right: margin?.right ?? DEFAULT_CHART_MARGIN.right,
|
|
51
|
-
bottom: margin?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
|
|
52
|
-
left: margin?.left ?? DEFAULT_CHART_MARGIN.left,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function useElementSize(
|
|
57
|
-
ref: RefObject<HTMLElement | null>,
|
|
58
|
-
fallbackWidth: number,
|
|
59
|
-
fallbackHeight: number,
|
|
60
|
-
) {
|
|
61
|
-
const [size, setSize] = useState({ width: fallbackWidth, height: fallbackHeight });
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
const el = ref.current;
|
|
65
|
-
if (!el) return;
|
|
66
|
-
const apply = () => {
|
|
67
|
-
setSize({
|
|
68
|
-
width: el.clientWidth || fallbackWidth,
|
|
69
|
-
height: el.clientHeight || fallbackHeight,
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
apply();
|
|
73
|
-
if (typeof ResizeObserver === "undefined") return;
|
|
74
|
-
const observer = new ResizeObserver(apply);
|
|
75
|
-
observer.observe(el);
|
|
76
|
-
return () => observer.disconnect();
|
|
77
|
-
}, [fallbackWidth, fallbackHeight, ref]);
|
|
78
|
-
|
|
79
|
-
return size;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function ChartFrame({
|
|
83
|
-
slot,
|
|
84
|
-
className,
|
|
85
|
-
width,
|
|
86
|
-
height = 280,
|
|
87
|
-
fillHeight = false,
|
|
88
|
-
margin,
|
|
89
|
-
legend,
|
|
90
|
-
hiddenIds,
|
|
91
|
-
onToggleSeries,
|
|
92
|
-
hover,
|
|
93
|
-
renderTooltip,
|
|
94
|
-
"aria-label": ariaLabel,
|
|
95
|
-
children,
|
|
96
|
-
}: ChartFrameProps) {
|
|
97
|
-
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
98
|
-
const measured = useElementSize(surfaceRef, width ?? 320, height);
|
|
99
|
-
const plotWidth = width ?? measured.width;
|
|
100
|
-
const plotHeight = fillHeight ? measured.height : height;
|
|
101
|
-
const m = resolveMargin(margin);
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div
|
|
105
|
-
data-slot={slot}
|
|
106
|
-
className={cn("spk-chart", fillHeight && "spk-chart--fill", className)}
|
|
107
|
-
role="img"
|
|
108
|
-
aria-label={ariaLabel}
|
|
109
|
-
>
|
|
110
|
-
{legend?.length ? (
|
|
111
|
-
<ChartLegend items={legend} hiddenIds={hiddenIds} onToggle={onToggleSeries} />
|
|
112
|
-
) : null}
|
|
113
|
-
<div
|
|
114
|
-
ref={surfaceRef}
|
|
115
|
-
className="spk-chart-surface"
|
|
116
|
-
style={fillHeight ? undefined : { height: plotHeight }}
|
|
117
|
-
>
|
|
118
|
-
{plotWidth > 0 && plotHeight > 0 ? children({ width: plotWidth, height: plotHeight, m }) : null}
|
|
119
|
-
<ChartTooltip hover={hover ?? null} boundsWidth={plotWidth} render={renderTooltip} />
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function ChartLegend({
|
|
126
|
-
items,
|
|
127
|
-
hiddenIds,
|
|
128
|
-
onToggle,
|
|
129
|
-
}: {
|
|
130
|
-
items: ChartLegendItem[];
|
|
131
|
-
hiddenIds?: Set<string>;
|
|
132
|
-
onToggle?: (id: string) => void;
|
|
133
|
-
}) {
|
|
134
|
-
return (
|
|
135
|
-
<div className="spk-chart-legend">
|
|
136
|
-
{items.map((item) => (
|
|
137
|
-
<button
|
|
138
|
-
key={item.id}
|
|
139
|
-
type="button"
|
|
140
|
-
className="spk-chart-legend-item"
|
|
141
|
-
data-hidden={hiddenIds?.has(item.id) ? "true" : undefined}
|
|
142
|
-
onClick={() => onToggle?.(item.id)}
|
|
143
|
-
>
|
|
144
|
-
<span className="spk-chart-swatch" style={{ background: item.color }} />
|
|
145
|
-
{item.label}
|
|
146
|
-
</button>
|
|
147
|
-
))}
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function ChartTooltip({
|
|
153
|
-
hover,
|
|
154
|
-
boundsWidth,
|
|
155
|
-
render,
|
|
156
|
-
}: {
|
|
157
|
-
hover: ChartHover;
|
|
158
|
-
boundsWidth: number;
|
|
159
|
-
render?: ChartTooltipRenderer;
|
|
160
|
-
}) {
|
|
161
|
-
if (!hover || !hover.items.length) return null;
|
|
162
|
-
const custom = render?.(hover);
|
|
163
|
-
if (render && custom == null) return null;
|
|
164
|
-
const left = clampTooltip(hover.x, boundsWidth);
|
|
165
|
-
return (
|
|
166
|
-
<div className="spk-chart-tooltip" style={{ left, top: hover.y }} role="tooltip">
|
|
167
|
-
{render ? (
|
|
168
|
-
custom
|
|
169
|
-
) : (
|
|
170
|
-
<>
|
|
171
|
-
{hover.title ? <p className="spk-chart-tooltip-title">{hover.title}</p> : null}
|
|
172
|
-
<ul>
|
|
173
|
-
{hover.items.map((item) => (
|
|
174
|
-
<li key={item.label}>
|
|
175
|
-
<span className="spk-chart-swatch" style={{ background: item.color }} />
|
|
176
|
-
<span>{item.label}</span>
|
|
177
|
-
<strong>{item.value}</strong>
|
|
178
|
-
</li>
|
|
179
|
-
))}
|
|
180
|
-
</ul>
|
|
181
|
-
</>
|
|
182
|
-
)}
|
|
183
|
-
</div>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function clampTooltip(x: number, width: number) {
|
|
188
|
-
return clampNum(x, 72, Math.max(72, width - 72));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function clampNum(value: number, min: number, max: number) {
|
|
192
|
-
return Math.min(max, Math.max(min, value));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function CartesianGrid({
|
|
196
|
-
ticks,
|
|
197
|
-
scale,
|
|
198
|
-
left,
|
|
199
|
-
right,
|
|
200
|
-
top,
|
|
201
|
-
bottom,
|
|
202
|
-
}: {
|
|
203
|
-
ticks: number[];
|
|
204
|
-
scale: (value: number) => number;
|
|
205
|
-
left: number;
|
|
206
|
-
right: number;
|
|
207
|
-
top: number;
|
|
208
|
-
bottom: number;
|
|
209
|
-
}) {
|
|
210
|
-
return (
|
|
211
|
-
<g data-slot="chart-grid">
|
|
212
|
-
{ticks.map((tick) => (
|
|
213
|
-
<line
|
|
214
|
-
key={tick}
|
|
215
|
-
className="spk-chart-grid"
|
|
216
|
-
x1={left}
|
|
217
|
-
x2={right}
|
|
218
|
-
y1={scale(tick)}
|
|
219
|
-
y2={scale(tick)}
|
|
220
|
-
/>
|
|
221
|
-
))}
|
|
222
|
-
<line className="spk-chart-axis" x1={left} x2={right} y1={bottom} y2={bottom} />
|
|
223
|
-
<line className="spk-chart-axis" x1={left} x2={left} y1={top} y2={bottom} />
|
|
224
|
-
</g>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function ValueAxis({
|
|
229
|
-
ticks,
|
|
230
|
-
scale,
|
|
231
|
-
left,
|
|
232
|
-
width,
|
|
233
|
-
}: {
|
|
234
|
-
ticks: number[];
|
|
235
|
-
scale: (value: number) => number;
|
|
236
|
-
left: number;
|
|
237
|
-
width?: number;
|
|
238
|
-
}) {
|
|
239
|
-
return (
|
|
240
|
-
<g data-slot="chart-y-axis">
|
|
241
|
-
{ticks.map((tick) => (
|
|
242
|
-
<text
|
|
243
|
-
key={tick}
|
|
244
|
-
className="spk-chart-tick"
|
|
245
|
-
x={left - 8}
|
|
246
|
-
y={scale(tick)}
|
|
247
|
-
textAnchor="end"
|
|
248
|
-
dominantBaseline="middle"
|
|
249
|
-
>
|
|
250
|
-
{formatChartNumber(tick)}
|
|
251
|
-
</text>
|
|
252
|
-
))}
|
|
253
|
-
{width ? (
|
|
254
|
-
<text
|
|
255
|
-
className="spk-chart-axis-label"
|
|
256
|
-
x={12}
|
|
257
|
-
y={width / 2}
|
|
258
|
-
transform={`rotate(-90 12 ${width / 2})`}
|
|
259
|
-
textAnchor="middle"
|
|
260
|
-
/>
|
|
261
|
-
) : null}
|
|
262
|
-
</g>
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function CategoryAxis({
|
|
267
|
-
labels,
|
|
268
|
-
position,
|
|
269
|
-
y,
|
|
270
|
-
}: {
|
|
271
|
-
labels: Array<string | number | Date>;
|
|
272
|
-
position: (index: number) => number;
|
|
273
|
-
y: number;
|
|
274
|
-
}) {
|
|
275
|
-
const maxLabels = labels.length > 12 ? Math.ceil(labels.length / 8) : 1;
|
|
276
|
-
return (
|
|
277
|
-
<g data-slot="chart-x-axis">
|
|
278
|
-
{labels.map((label, index) => {
|
|
279
|
-
if (index % maxLabels !== 0) return null;
|
|
280
|
-
return (
|
|
281
|
-
<text
|
|
282
|
-
key={`${index}-${String(label)}`}
|
|
283
|
-
className="spk-chart-tick"
|
|
284
|
-
x={position(index)}
|
|
285
|
-
y={y + 16}
|
|
286
|
-
textAnchor="middle"
|
|
287
|
-
>
|
|
288
|
-
{formatAxisValue(label)}
|
|
289
|
-
</text>
|
|
290
|
-
);
|
|
291
|
-
})}
|
|
292
|
-
</g>
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function useHiddenSeries(ids: string[]) {
|
|
297
|
-
const [hidden, setHidden] = useState<Set<string>>(new Set());
|
|
298
|
-
const toggle = (id: string) => {
|
|
299
|
-
setHidden((current) => {
|
|
300
|
-
const next = new Set(current);
|
|
301
|
-
if (next.has(id)) next.delete(id);
|
|
302
|
-
else next.add(id);
|
|
303
|
-
return next;
|
|
304
|
-
});
|
|
305
|
-
};
|
|
306
|
-
const visible = ids.filter((id) => !hidden.has(id));
|
|
307
|
-
return { hidden, toggle, visible };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function seriesMeta(
|
|
311
|
-
series: Array<{ id?: string; label?: string; color?: string }>,
|
|
312
|
-
colors?: string[],
|
|
313
|
-
): ChartLegendItem[] {
|
|
314
|
-
return series.map((item, index) => ({
|
|
315
|
-
id: item.id ?? item.label ?? `series-${index}`,
|
|
316
|
-
label: item.label ?? item.id ?? `Series ${index + 1}`,
|
|
317
|
-
color: item.color ?? chartColor(index, colors),
|
|
318
|
-
}));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function useChartHover() {
|
|
322
|
-
const [hover, setHover] = useState<ChartHover>(null);
|
|
323
|
-
return { hover, setHover, clear: () => setHover(null) };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function categoryLabels(axis?: ChartAxisConfig, length = 0): Array<string | number | Date> {
|
|
327
|
-
if (axis?.data?.length) return axis.data;
|
|
328
|
-
return Array.from({ length }, (_, i) => i + 1);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function formatSeriesValue(value: number | [number, number] | undefined) {
|
|
332
|
-
if (value == null) return "—";
|
|
333
|
-
if (Array.isArray(value)) return `${formatChartNumber(value[0])} – ${formatChartNumber(value[1])}`;
|
|
334
|
-
return formatChartNumber(value);
|
|
335
|
-
}
|
|
336
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { render } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { BarChart3D, PieChart3D } from "./charts-3d";
|
|
4
|
-
|
|
5
|
-
describe("charts-3d", () => {
|
|
6
|
-
it("renders isometric bars and an extruded pie", () => {
|
|
7
|
-
const { container } = render(
|
|
8
|
-
<div>
|
|
9
|
-
<BarChart3D
|
|
10
|
-
width={240}
|
|
11
|
-
height={160}
|
|
12
|
-
xAxis={[{ data: ["A", "B"] }]}
|
|
13
|
-
series={[{ label: "Views", data: [3, 5] }]}
|
|
14
|
-
/>
|
|
15
|
-
<PieChart3D
|
|
16
|
-
width={200}
|
|
17
|
-
height={160}
|
|
18
|
-
series={[{ data: [{ value: 2, label: "A" }, { value: 3, label: "B" }] }]}
|
|
19
|
-
/>
|
|
20
|
-
</div>,
|
|
21
|
-
);
|
|
22
|
-
expect(container.querySelector('[data-slot="bar-chart-3d"]')).toBeTruthy();
|
|
23
|
-
expect(container.querySelector('[data-slot="pie-chart-3d"]')).toBeTruthy();
|
|
24
|
-
});
|
|
25
|
-
});
|