doom-design-system 0.4.12 → 0.4.13
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/components/Chart/Chart.d.ts +14 -2
- package/dist/components/Chart/Chart.js +25 -288
- package/dist/components/Chart/Chart.module.css +11 -17
- package/dist/components/Chart/ChartContext.d.ts +27 -0
- package/dist/components/Chart/ChartContext.js +9 -0
- package/dist/components/Chart/ChartFooter.d.ts +8 -0
- package/dist/components/Chart/ChartFooter.js +17 -0
- package/dist/components/Chart/ChartHeader.d.ts +9 -0
- package/dist/components/Chart/ChartHeader.js +14 -0
- package/dist/components/Chart/ChartLegend.d.ts +10 -0
- package/dist/components/Chart/ChartLegend.js +30 -0
- package/dist/components/Chart/ChartPlot.d.ts +12 -0
- package/dist/components/Chart/ChartPlot.js +223 -0
- package/dist/components/Chart/ChartRoot.d.ts +6 -0
- package/dist/components/Chart/ChartRoot.js +120 -0
- package/dist/components/Chart/renderers.d.ts +3 -3
- package/dist/components/Chart/renderers.js +26 -13
- package/dist/components/Chart/types.d.ts +2 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { ChartFooter } from "./ChartFooter";
|
|
2
|
+
import { ChartHeader } from "./ChartHeader";
|
|
3
|
+
import { ChartLegend } from "./ChartLegend";
|
|
4
|
+
import { ChartPlot } from "./ChartPlot";
|
|
5
|
+
import { ChartRoot } from "./ChartRoot";
|
|
1
6
|
import { ChartProps } from "./types";
|
|
2
|
-
export type { ChartConfig, ChartProps, DrawContext, LegendConfig, LegendItem,
|
|
3
|
-
|
|
7
|
+
export type { ChartConfig, ChartProps, DrawContext, LegendConfig, LegendItem, } from "./types";
|
|
8
|
+
declare function ChartInternal<T>(props: ChartProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare const Chart: typeof ChartInternal & {
|
|
10
|
+
Root: typeof ChartRoot;
|
|
11
|
+
Header: typeof ChartHeader;
|
|
12
|
+
Footer: typeof ChartFooter;
|
|
13
|
+
Legend: typeof ChartLegend;
|
|
14
|
+
Plot: typeof ChartPlot;
|
|
15
|
+
};
|
|
@@ -1,292 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import clsx from "clsx";
|
|
4
|
-
import { pointer, select } from "d3-selection";
|
|
5
|
-
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
6
|
-
import { Card } from "../Card/Card.js";
|
|
7
3
|
import { Flex, Stack } from "../Layout/Layout.js";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"var(--error)",
|
|
18
|
-
];
|
|
19
|
-
import { createScales, drawAxes, drawBars, drawGrid, drawLineArea, setupGradient, } from "./renderers.js";
|
|
20
|
-
export function Chart({ data, type = "line", variant = "default", x, y, d3Config, className, style, renderTooltip, flat, title, subtitle, legend, render, withFrame = true, onValueChange, }) {
|
|
21
|
-
var _a, _b, _c, _d;
|
|
22
|
-
// Constants
|
|
23
|
-
const MOBILE_ASPECT_RATIO = 0.75; // 4:3 aspect ratio for mobile
|
|
24
|
-
const TOOLTIP_OFFSET = 20;
|
|
25
|
-
const HIDE_DELAY_MS = 16;
|
|
26
|
-
const MOBILE_BREAKPOINT = 480;
|
|
27
|
-
const svgRef = useRef(null);
|
|
28
|
-
const containerRef = useRef(null);
|
|
29
|
-
const wrapperRef = useRef(null);
|
|
30
|
-
const tooltipRef = useRef(null);
|
|
31
|
-
const tooltipPosRef = useRef({ x: 0, y: 0, isTouch: false });
|
|
32
|
-
const hideTimeoutRef = useRef(null);
|
|
33
|
-
const gradientId = React.useId().replace(/:/g, "");
|
|
34
|
-
const [activeData, setActiveData] = useState(null);
|
|
35
|
-
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
36
|
-
const [containerWidth, setContainerWidth] = useState(0);
|
|
37
|
-
const isMobile = containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT;
|
|
38
|
-
// Determine legend position based on device
|
|
39
|
-
const legendPosition = isMobile
|
|
40
|
-
? ((_b = (_a = legend === null || legend === void 0 ? void 0 : legend.position) === null || _a === void 0 ? void 0 : _a.mobile) !== null && _b !== void 0 ? _b : "bottom")
|
|
41
|
-
: ((_d = (_c = legend === null || legend === void 0 ? void 0 : legend.position) === null || _c === void 0 ? void 0 : _c.default) !== null && _d !== void 0 ? _d : "top");
|
|
42
|
-
// Calculate auto height for mobile if not explicitly set
|
|
43
|
-
const autoHeight = useMemo(() => {
|
|
44
|
-
if (d3Config === null || d3Config === void 0 ? void 0 : d3Config.height) {
|
|
45
|
-
return d3Config.height;
|
|
46
|
-
}
|
|
47
|
-
// On mobile, use aspect ratio for balanced proportions
|
|
48
|
-
if (isMobile && containerWidth > 0) {
|
|
49
|
-
return Math.round(containerWidth * MOBILE_ASPECT_RATIO);
|
|
50
|
-
}
|
|
51
|
-
return 400; // Default height
|
|
52
|
-
}, [d3Config === null || d3Config === void 0 ? void 0 : d3Config.height, isMobile, containerWidth]);
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(activeData !== null && activeData !== void 0 ? activeData : null);
|
|
55
|
-
}, [activeData, onValueChange]);
|
|
56
|
-
// Effective y-axis label (hidden on mobile for space)
|
|
57
|
-
const effectiveYAxisLabel = isMobile ? undefined : d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel;
|
|
58
|
-
const config = useMemo(() => (Object.assign(Object.assign({ margin: {
|
|
59
|
-
top: isMobile ? 10 : 20,
|
|
60
|
-
right: isMobile ? 10 : 20,
|
|
61
|
-
bottom: isMobile ? 40 : (d3Config === null || d3Config === void 0 ? void 0 : d3Config.xAxisLabel) ? 60 : 30,
|
|
62
|
-
// On mobile, use minimal left margin since y-axis label is hidden
|
|
63
|
-
left: isMobile ? 35 : (d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel) ? 90 : 60,
|
|
64
|
-
}, curve: undefined, showAxes: true, grid: false, withGradient: type === "area", showDots: false }, d3Config), {
|
|
65
|
-
// Override yAxisLabel for mobile
|
|
66
|
-
yAxisLabel: effectiveYAxisLabel })), [d3Config, type, isMobile, effectiveYAxisLabel]);
|
|
67
|
-
const calculateTooltipTransform = (tx, ty, isTouch = false) => {
|
|
68
|
-
if (!tooltipRef.current || !wrapperRef.current) {
|
|
69
|
-
return "";
|
|
70
|
-
}
|
|
71
|
-
const rect = wrapperRef.current.getBoundingClientRect();
|
|
72
|
-
const tooltipWidth = tooltipRef.current.offsetWidth;
|
|
73
|
-
const absX = rect.left + tx;
|
|
74
|
-
let xOffset = TOOLTIP_OFFSET;
|
|
75
|
-
// Check right edge
|
|
76
|
-
if (absX + tooltipWidth + TOOLTIP_OFFSET > window.innerWidth - 10) {
|
|
77
|
-
xOffset = -TOOLTIP_OFFSET - tooltipWidth;
|
|
78
|
-
}
|
|
79
|
-
// Check left edge (after potential flip)
|
|
80
|
-
if (rect.left + tx + xOffset < 10) {
|
|
81
|
-
xOffset = 10 - (rect.left + tx);
|
|
82
|
-
}
|
|
83
|
-
// Apply Y offset for touch to avoid finger occlusion
|
|
84
|
-
// Shift tooltip 50px above touch point to clear the finger
|
|
85
|
-
const effectiveY = isTouch ? ty - 50 : ty;
|
|
86
|
-
return `translate3d(${tx + xOffset}px, calc(${effectiveY}px - 50%), 0)`;
|
|
87
|
-
};
|
|
88
|
-
useLayoutEffect(() => {
|
|
89
|
-
if (activeData && tooltipRef.current && wrapperRef.current) {
|
|
90
|
-
const { x, y, isTouch } = tooltipPosRef.current;
|
|
91
|
-
const transform = calculateTooltipTransform(x, y, !!isTouch);
|
|
92
|
-
if (transform) {
|
|
93
|
-
tooltipRef.current.style.transform = transform;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}, [activeData]);
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
return () => {
|
|
99
|
-
if (hideTimeoutRef.current) {
|
|
100
|
-
clearTimeout(hideTimeoutRef.current);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}, []);
|
|
104
|
-
// Track container width for auto aspect ratio
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (!containerRef.current) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const containerObserver = new ResizeObserver((entries) => {
|
|
110
|
-
const entry = entries[0];
|
|
111
|
-
if (entry) {
|
|
112
|
-
setContainerWidth(entry.contentRect.width);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
containerObserver.observe(containerRef.current);
|
|
116
|
-
return () => containerObserver.disconnect();
|
|
117
|
-
}, []);
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!wrapperRef.current) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
123
|
-
const entry = entries[0];
|
|
124
|
-
if (entry) {
|
|
125
|
-
setDimensions({
|
|
126
|
-
width: entry.contentRect.width,
|
|
127
|
-
height: entry.contentRect.height,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
resizeObserver.observe(wrapperRef.current);
|
|
132
|
-
return () => resizeObserver.disconnect();
|
|
133
|
-
}, []);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (!svgRef.current ||
|
|
136
|
-
!data.length ||
|
|
137
|
-
dimensions.width === 0 ||
|
|
138
|
-
dimensions.height === 0) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const { width, height } = dimensions;
|
|
142
|
-
const margin = config.margin;
|
|
143
|
-
if (width <= 0 || height <= 0) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const svg = select(svgRef.current);
|
|
147
|
-
svg.selectAll("*").remove();
|
|
148
|
-
const g = svg
|
|
149
|
-
.attr("width", width)
|
|
150
|
-
.attr("height", height)
|
|
151
|
-
.append("g")
|
|
152
|
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
153
|
-
if (config.withGradient) {
|
|
154
|
-
setupGradient(svg, gradientId);
|
|
155
|
-
}
|
|
156
|
-
const { xScale, yScale, innerWidth, innerHeight } = createScales(data, width, height, margin, x, y, type);
|
|
157
|
-
if (innerWidth <= 0 || innerHeight <= 0) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (config.grid) {
|
|
161
|
-
drawGrid(g, yScale, innerWidth, styles.grid, isMobile);
|
|
162
|
-
}
|
|
163
|
-
if (config.showAxes) {
|
|
164
|
-
drawAxes(g, xScale, yScale, innerWidth, innerHeight, margin, config, styles, isMobile);
|
|
165
|
-
}
|
|
166
|
-
const updateTooltip = (tx, ty, data, isTouch = false) => {
|
|
167
|
-
if (hideTimeoutRef.current) {
|
|
168
|
-
clearTimeout(hideTimeoutRef.current);
|
|
169
|
-
hideTimeoutRef.current = null;
|
|
170
|
-
}
|
|
171
|
-
tooltipPosRef.current = { x: tx, y: ty, isTouch };
|
|
172
|
-
if (tooltipRef.current && wrapperRef.current) {
|
|
173
|
-
const transform = calculateTooltipTransform(tx, ty, isTouch);
|
|
174
|
-
if (transform) {
|
|
175
|
-
tooltipRef.current.style.transform = transform;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
setActiveData((prev) => (prev === data ? prev : data));
|
|
179
|
-
};
|
|
180
|
-
const showTooltip = (event, data) => {
|
|
181
|
-
const [px, py] = select(svgRef.current).select("g").empty()
|
|
182
|
-
? [0, 0]
|
|
183
|
-
: pointer(event, g.node());
|
|
184
|
-
const tx = px + margin.left;
|
|
185
|
-
const ty = py + margin.top;
|
|
186
|
-
updateTooltip(tx, ty, data);
|
|
187
|
-
};
|
|
188
|
-
const hideTooltip = () => {
|
|
189
|
-
hideTimeoutRef.current = setTimeout(() => setActiveData(null), HIDE_DELAY_MS);
|
|
190
|
-
};
|
|
191
|
-
const ctx = {
|
|
192
|
-
g,
|
|
193
|
-
data,
|
|
194
|
-
xScale,
|
|
195
|
-
yScale,
|
|
196
|
-
x,
|
|
197
|
-
y,
|
|
198
|
-
innerWidth,
|
|
199
|
-
innerHeight,
|
|
200
|
-
margin,
|
|
201
|
-
config,
|
|
202
|
-
styles,
|
|
203
|
-
gradientId,
|
|
204
|
-
setHoverState: (state) => {
|
|
205
|
-
var _a;
|
|
206
|
-
if (!state) {
|
|
207
|
-
hideTooltip();
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
updateTooltip(state.x, state.y, state.data, (_a = state.isTouch) !== null && _a !== void 0 ? _a : false);
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
showTooltip,
|
|
214
|
-
hideTooltip,
|
|
215
|
-
type,
|
|
216
|
-
isMobile,
|
|
217
|
-
resolveInteraction: (event) => {
|
|
218
|
-
if (event.cancelable && event.type.startsWith("touch")) {
|
|
219
|
-
event.preventDefault();
|
|
220
|
-
}
|
|
221
|
-
let el = null;
|
|
222
|
-
if (event.touches && event.touches.length > 0) {
|
|
223
|
-
const touch = event.touches[0];
|
|
224
|
-
el = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
el = event.currentTarget;
|
|
228
|
-
}
|
|
229
|
-
if (!el) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
const data = select(el).datum();
|
|
233
|
-
if (Array.isArray(data)) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
return data ? { element: el, data } : null;
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
if (render) {
|
|
240
|
-
render(ctx);
|
|
241
|
-
}
|
|
242
|
-
else if (type === "bar") {
|
|
243
|
-
drawBars(ctx);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
drawLineArea(ctx);
|
|
247
|
-
}
|
|
248
|
-
}, [data, config, type, gradientId, dimensions, render, isMobile, x, y]);
|
|
249
|
-
// Render legend items
|
|
250
|
-
// Render legend items
|
|
251
|
-
const renderLegend = (isBottom = false, isMobile = false, layout = "horizontal") => {
|
|
252
|
-
var _a;
|
|
253
|
-
const paddingRight = isMobile ? 28 : 30;
|
|
254
|
-
if (!((_a = legend === null || legend === void 0 ? void 0 : legend.data) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
// Standard chart (no custom render) supports only 1 series,
|
|
258
|
-
// so we limit the legend to 1 item to match the single curve/bar series.
|
|
259
|
-
// If a custom render function is provided, we respect all legend items.
|
|
260
|
-
const activeData = !render && legend.data.length > 1 ? legend.data.slice(0, 1) : legend.data;
|
|
261
|
-
const isVertical = layout === "vertical";
|
|
262
|
-
return (_jsx(Flex, { className: clsx(styles.legend, isBottom && styles.legendBottom), gap: isVertical ? 2 : 4, style: Object.assign(Object.assign({ flexWrap: "wrap", flexDirection: isVertical ? "column" : "row", alignItems: isVertical ? "flex-start" : "center" }, (!isBottom &&
|
|
263
|
-
!isVertical && {
|
|
264
|
-
paddingRight: config.margin.left - paddingRight,
|
|
265
|
-
})), (isBottom && !isMobile && { marginLeft: config.margin.left })), children: activeData.map((item, index) => (_jsxs(Flex, { align: "center", className: styles.legendItem, gap: 1, children: [_jsx("span", { className: styles.legendDot, style: {
|
|
266
|
-
backgroundColor: item.color || LEGEND_PALETTE[index % LEGEND_PALETTE.length],
|
|
267
|
-
} }), _jsx(Text, { className: styles.legendLabel, variant: "small", children: item.label })] }, item.label))) }));
|
|
268
|
-
};
|
|
269
|
-
// Header with title/subtitle on left, legend on right (when position is top)
|
|
270
|
-
const renderHeader = (isMobile, legendPosition) => {
|
|
271
|
-
var _a;
|
|
272
|
-
const hasTitle = title || subtitle;
|
|
273
|
-
const hasLegendTop = legendPosition === "top" && ((_a = legend === null || legend === void 0 ? void 0 : legend.data) === null || _a === void 0 ? void 0 : _a.length);
|
|
274
|
-
let marginLeft = isMobile ? 28 : 30;
|
|
275
|
-
if (legendPosition && legendPosition === "left") {
|
|
276
|
-
marginLeft = config.margin.left;
|
|
277
|
-
}
|
|
278
|
-
if (!hasTitle && !hasLegendTop) {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
return (_jsxs(Flex, { align: "center", className: styles.chartHeader, justify: "space-between", style: {
|
|
282
|
-
marginLeft: config.margin.left - marginLeft,
|
|
283
|
-
flex: 1, // Allow header to fill available space or share it
|
|
284
|
-
}, children: [hasTitle ? (_jsxs(Stack, { gap: 2, children: [title && (_jsx("div", { children: typeof title === "string" ? (_jsx(Text, { style: { margin: 0 }, variant: "h5", children: title })) : (title) })), subtitle && (_jsx(Text, { className: styles.subtitle, variant: "small", children: subtitle }))] })) : (_jsx("div", {})), hasLegendTop && renderLegend(false, isMobile)] }));
|
|
285
|
-
};
|
|
286
|
-
return (_jsx("div", { ref: containerRef, className: clsx(styles.chartContainer, variant === "solid" && styles.solid, flat && styles.flat, isMobile && styles.mobile, !withFrame && styles.frameless, className), style: Object.assign({ height: autoHeight, width: config.width || "100%" }, style), children: _jsxs(Stack, { gap: isMobile ? 2 : 4, style: { flex: 1, minHeight: 0 }, children: [_jsx(Flex, { justify: "space-between", style: { width: "100%" }, children: renderHeader(isMobile, legendPosition) }), _jsxs(Flex, { gap: 8, style: { flex: 1, minHeight: 0 }, children: [legendPosition === "left" && (_jsx("div", { className: styles.legendVertical, children: renderLegend(false, isMobile, "vertical") })), _jsxs("div", { ref: wrapperRef, className: styles.responsiveWrapper, children: [_jsx("svg", { ref: svgRef, className: styles.chart, style: { display: "block", width: "100%", height: "100%" } }), activeData && (_jsx("div", { ref: tooltipRef, className: styles.tooltipWrapper, style: { pointerEvents: "none" }, children: renderTooltip ? (renderTooltip(activeData)) : (_jsxs(Card, { className: styles.tooltipCard, children: [_jsx("div", { style: { marginBottom: 4 }, children: _jsx(Text, { style: {
|
|
287
|
-
color: "var(--muted-foreground)",
|
|
288
|
-
textTransform: "uppercase",
|
|
289
|
-
fontSize: "10px",
|
|
290
|
-
letterSpacing: "0.5px",
|
|
291
|
-
}, variant: "h6", children: x(activeData) }) }), _jsx("div", { children: _jsx(Text, { style: { margin: 0 }, variant: "h4", children: y(activeData) }) })] })) }))] }), legendPosition === "right" && (_jsx("div", { className: styles.legendVertical, children: renderLegend(false, isMobile, "vertical") }))] }), legendPosition === "bottom" && renderLegend(true, isMobile)] }) }));
|
|
4
|
+
import { useChartContext } from "./ChartContext.js";
|
|
5
|
+
import { ChartFooter } from "./ChartFooter.js";
|
|
6
|
+
import { ChartHeader } from "./ChartHeader.js";
|
|
7
|
+
import { ChartLegend } from "./ChartLegend.js";
|
|
8
|
+
import { ChartPlot } from "./ChartPlot.js";
|
|
9
|
+
import { ChartRoot } from "./ChartRoot.js";
|
|
10
|
+
function ChartInternal(props) {
|
|
11
|
+
const { title, subtitle, withLegend, type, render, renderTooltip } = props;
|
|
12
|
+
return (_jsx(ChartRoot, Object.assign({}, props, { children: _jsx(StandardChartLayout, { render: render, renderTooltip: renderTooltip, subtitle: subtitle, title: title, type: type, withLegend: withLegend }) })));
|
|
292
13
|
}
|
|
14
|
+
function StandardChartLayout({ title, subtitle, withLegend, type, render, renderTooltip, }) {
|
|
15
|
+
// Standard Layout:
|
|
16
|
+
// - Header (Title + Subtitle + Legend Top Right)
|
|
17
|
+
// - Plot
|
|
18
|
+
// - (No side/bottom legends in standard mode - usage Composition for that)
|
|
19
|
+
const { legendItems } = useChartContext();
|
|
20
|
+
const showLegend = withLegend && legendItems.length > 0;
|
|
21
|
+
return (_jsxs(Stack, { style: { flex: 1, minHeight: 0 }, children: [_jsx(Flex, { justify: "space-between", style: { width: "100%" }, children: _jsx(ChartHeader, { subtitle: subtitle, title: title, children: showLegend && (_jsx(ChartLegend, { align: "end", items: legendItems, layout: "horizontal" })) }) }), _jsx(Flex, { gap: 8, style: { flex: 1, minHeight: 0 }, children: _jsx(ChartPlot, { render: render, renderTooltip: renderTooltip, type: type }) })] }));
|
|
22
|
+
}
|
|
23
|
+
export const Chart = Object.assign(ChartInternal, {
|
|
24
|
+
Root: ChartRoot,
|
|
25
|
+
Header: ChartHeader,
|
|
26
|
+
Footer: ChartFooter,
|
|
27
|
+
Legend: ChartLegend,
|
|
28
|
+
Plot: ChartPlot,
|
|
29
|
+
});
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
}
|
|
19
19
|
.chart :global .tick text {
|
|
20
20
|
fill: var(--foreground);
|
|
21
|
-
font-size:
|
|
22
|
-
font-weight:
|
|
21
|
+
font-size: var(--text-xs);
|
|
22
|
+
font-weight: var(--font-bold);
|
|
23
23
|
text-transform: uppercase;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
.cursorLine {
|
|
60
60
|
stroke: var(--muted-foreground);
|
|
61
|
-
stroke-width:
|
|
61
|
+
stroke-width: var(--border-width);
|
|
62
62
|
stroke-dasharray: 4 4;
|
|
63
63
|
opacity: 0;
|
|
64
64
|
pointer-events: none;
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
border: var(--border-width) solid var(--card-border);
|
|
90
90
|
border-radius: var(--radius);
|
|
91
91
|
box-shadow: var(--shadow-hard);
|
|
92
|
-
padding:
|
|
92
|
+
padding: var(--spacing-lg);
|
|
93
93
|
overflow: visible;
|
|
94
94
|
display: flex;
|
|
95
95
|
flex-direction: column;
|
|
@@ -131,13 +131,7 @@
|
|
|
131
131
|
border-color: var(--solid-fg);
|
|
132
132
|
}
|
|
133
133
|
.chartContainer.mobile {
|
|
134
|
-
padding:
|
|
135
|
-
}
|
|
136
|
-
.chartContainer.mobile .axisLabel {
|
|
137
|
-
font-size: 0.75rem;
|
|
138
|
-
}
|
|
139
|
-
.chartContainer.mobile :global(.tick) text {
|
|
140
|
-
font-size: 0.75rem;
|
|
134
|
+
padding: var(--spacing-sm);
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
.responsiveWrapper {
|
|
@@ -159,15 +153,15 @@
|
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
.tooltipCard {
|
|
162
|
-
padding:
|
|
163
|
-
font-size:
|
|
156
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
157
|
+
font-size: var(--text-xs);
|
|
164
158
|
white-space: nowrap;
|
|
165
159
|
}
|
|
166
160
|
|
|
167
161
|
.axisLabel {
|
|
168
162
|
fill: var(--foreground);
|
|
169
|
-
font-size:
|
|
170
|
-
font-weight:
|
|
163
|
+
font-size: var(--text-xs);
|
|
164
|
+
font-weight: var(--font-bold);
|
|
171
165
|
text-transform: uppercase;
|
|
172
166
|
text-anchor: middle;
|
|
173
167
|
}
|
|
@@ -211,6 +205,6 @@
|
|
|
211
205
|
display: flex;
|
|
212
206
|
flex-direction: column;
|
|
213
207
|
align-self: center;
|
|
214
|
-
gap:
|
|
215
|
-
padding: 0
|
|
208
|
+
gap: var(--spacing-sm);
|
|
209
|
+
padding: 0 var(--spacing-sm);
|
|
216
210
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChartConfig, LegendItem } from "./types";
|
|
2
|
+
export interface ChartContextValue<T = unknown> {
|
|
3
|
+
data: T[];
|
|
4
|
+
dimensions: {
|
|
5
|
+
containerWidth: number;
|
|
6
|
+
isMobile: boolean;
|
|
7
|
+
};
|
|
8
|
+
config: ChartConfig & {
|
|
9
|
+
margin: {
|
|
10
|
+
top: number;
|
|
11
|
+
right: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
left: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
colorPalette: string[];
|
|
17
|
+
activeData: T | null;
|
|
18
|
+
setActiveData: (data: T | null) => void;
|
|
19
|
+
x: (d: T) => string | number;
|
|
20
|
+
y: (d: T) => number;
|
|
21
|
+
legendItems: LegendItem[];
|
|
22
|
+
type?: "line" | "area" | "bar";
|
|
23
|
+
render?: (context: any) => void;
|
|
24
|
+
registerSeries?: (item: LegendItem) => () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare const ChartContext: import("react").Context<ChartContextValue<unknown> | null>;
|
|
27
|
+
export declare function useChartContext<T = unknown>(): ChartContextValue<T>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const ChartContext = createContext(null);
|
|
3
|
+
export function useChartContext() {
|
|
4
|
+
const context = useContext(ChartContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error("Chart components must be used within a Chart.Root");
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ChartFooterProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
style?: React.CSSProperties;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
align?: "start" | "center" | "end" | "between";
|
|
7
|
+
}
|
|
8
|
+
export declare function ChartFooter({ className, style, children, align, }: ChartFooterProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Flex } from "../Layout/Layout.js";
|
|
5
|
+
import styles from "./Chart.module.css";
|
|
6
|
+
export function ChartFooter({ className, style, children, align = "center", }) {
|
|
7
|
+
if (!children) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const justifyMap = {
|
|
11
|
+
start: "flex-start",
|
|
12
|
+
center: "center",
|
|
13
|
+
end: "flex-end",
|
|
14
|
+
between: "space-between",
|
|
15
|
+
};
|
|
16
|
+
return (_jsx(Flex, { align: "center", className: clsx(styles.chartFooter, className), justify: justifyMap[align], style: Object.assign({ width: "100%", marginTop: "var(--spacing-md)" }, style), children: children }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ChartHeaderProps {
|
|
3
|
+
title?: React.ReactNode;
|
|
4
|
+
subtitle?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function ChartHeader({ title, subtitle, className, style, children, }: ChartHeaderProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Flex, Stack } from "../Layout/Layout.js";
|
|
5
|
+
import { Text } from "../Text/Text.js";
|
|
6
|
+
// Use relative import for styles to ensure sharing
|
|
7
|
+
import styles from "./Chart.module.css";
|
|
8
|
+
export function ChartHeader({ title, subtitle, className, style, children, }) {
|
|
9
|
+
const hasTitle = title || subtitle;
|
|
10
|
+
if (!hasTitle && !children) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return (_jsxs(Flex, { align: "center", className: clsx(styles.chartHeader, className), justify: "space-between", style: Object.assign({ width: "100%" }, style), children: [hasTitle ? (_jsxs(Stack, { gap: 2, children: [title && (_jsx("div", { children: typeof title === "string" ? (_jsx(Text, { style: { margin: 0 }, variant: "h5", children: title })) : (title) })), subtitle && (_jsx(Text, { className: styles.subtitle, variant: "small", children: subtitle }))] })) : (_jsx("div", {})), children] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { LegendItem } from "./types";
|
|
3
|
+
export interface ChartLegendProps {
|
|
4
|
+
items?: LegendItem[] | ((items: LegendItem[]) => LegendItem[]);
|
|
5
|
+
layout?: "horizontal" | "vertical";
|
|
6
|
+
align?: "start" | "center" | "end";
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
export declare function ChartLegend({ items, layout, align, className, style, }: ChartLegendProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { Flex } from "../Layout/Layout.js";
|
|
6
|
+
import { Text } from "../Text/Text.js";
|
|
7
|
+
import styles from "./Chart.module.css";
|
|
8
|
+
import { useChartContext } from "./ChartContext.js";
|
|
9
|
+
export function ChartLegend({ items, layout = "horizontal", align = "start", className, style, }) {
|
|
10
|
+
const { legendItems, type, render } = useChartContext();
|
|
11
|
+
const contextItems = useMemo(() => {
|
|
12
|
+
if (render || (type === "bar" && legendItems.length > 1)) {
|
|
13
|
+
return legendItems;
|
|
14
|
+
}
|
|
15
|
+
return legendItems.slice(0, 1);
|
|
16
|
+
}, [legendItems, type, render]);
|
|
17
|
+
const activeItems = typeof items === "function" ? items(contextItems) : (items !== null && items !== void 0 ? items : contextItems);
|
|
18
|
+
if (!(activeItems === null || activeItems === void 0 ? void 0 : activeItems.length)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const isVertical = layout === "vertical";
|
|
22
|
+
const alignMap = {
|
|
23
|
+
start: "flex-start",
|
|
24
|
+
center: "center",
|
|
25
|
+
end: "flex-end",
|
|
26
|
+
};
|
|
27
|
+
return (_jsx(Flex, { className: clsx(styles.legend, className), gap: isVertical ? 2 : 4, style: Object.assign({ flexWrap: "wrap", flexDirection: isVertical ? "column" : "row", justifyContent: isVertical ? "flex-start" : alignMap[align], alignItems: isVertical ? alignMap[align] : "center" }, style), children: activeItems.map((item, index) => (_jsxs(Flex, { align: "center", className: styles.legendItem, gap: 1, children: [_jsx("span", { className: styles.legendDot, style: {
|
|
28
|
+
backgroundColor: item.color,
|
|
29
|
+
} }), _jsx(Text, { className: styles.legendLabel, variant: "small", children: item.label })] }, item.label || index))) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChartProps } from "./types";
|
|
3
|
+
export interface ChartPlotProps<T> {
|
|
4
|
+
type?: ChartProps<T>["type"];
|
|
5
|
+
render?: ChartProps<T>["render"];
|
|
6
|
+
renderTooltip?: ChartProps<T>["renderTooltip"];
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
label?: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function ChartPlot<T>({ type, render, renderTooltip, className, style, label, color, }: ChartPlotProps<T>): import("react/jsx-runtime").JSX.Element;
|