@optilogic/charts 1.0.0-beta.8 → 1.0.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.cjs +3034 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +489 -192
- package/dist/index.d.ts +489 -192
- package/dist/index.js +3006 -175
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cartesian/area-chart.tsx +177 -0
- package/src/cartesian/bar-chart.tsx +217 -0
- package/src/cartesian/composed-chart.tsx +222 -0
- package/src/cartesian/line-chart.tsx +159 -0
- package/src/cartesian/scatter-chart.tsx +158 -0
- package/src/cartesian/waterfall-chart.tsx +171 -0
- package/src/dashboard/chart-builder.tsx +310 -0
- package/src/dashboard/chart-renderer.tsx +250 -0
- package/src/dashboard/kpi-card.tsx +121 -0
- package/src/dashboard/scenario-comparison.tsx +235 -0
- package/src/dashboard/sparkline.tsx +86 -0
- package/src/index.ts +50 -19
- package/src/radial/donut-chart.tsx +135 -0
- package/src/radial/pie-chart.tsx +153 -0
- package/src/radial/radar-chart.tsx +111 -0
- package/src/radial/radial-bar-chart.tsx +115 -0
- package/src/shared/chart-container.tsx +104 -0
- package/src/shared/chart-legend.tsx +57 -0
- package/src/shared/chart-tooltip.tsx +159 -0
- package/src/shared/colors.ts +37 -0
- package/src/shared/formatters.ts +51 -0
- package/src/shared/types.ts +66 -0
- package/src/shared/use-live-data.ts +83 -0
- package/src/specialized/funnel-chart.tsx +93 -0
- package/src/specialized/gantt-chart.tsx +416 -0
- package/src/specialized/heatmap-chart.tsx +250 -0
- package/src/specialized/sankey-chart.tsx +155 -0
- package/src/specialized/treemap-chart.tsx +121 -0
- package/src/bar-chart.tsx +0 -337
- package/src/line-chart.tsx +0 -266
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@optilogic/core";
|
|
3
|
+
|
|
4
|
+
export interface HeatmapCell {
|
|
5
|
+
x: string | number;
|
|
6
|
+
y: string | number;
|
|
7
|
+
value: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HeatmapColorScale {
|
|
11
|
+
min: string;
|
|
12
|
+
mid?: string;
|
|
13
|
+
max: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HeatmapChartProps {
|
|
17
|
+
data: HeatmapCell[];
|
|
18
|
+
xLabels: string[];
|
|
19
|
+
yLabels: string[];
|
|
20
|
+
colorScale?: HeatmapColorScale;
|
|
21
|
+
cellSize?: number;
|
|
22
|
+
showValues?: boolean;
|
|
23
|
+
valueFormatter?: (value: number) => string;
|
|
24
|
+
className?: string;
|
|
25
|
+
height?: number | string;
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
onCellClick?: (cell: HeatmapCell) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_SCALE: HeatmapColorScale = {
|
|
31
|
+
min: "hsl(var(--chart-9))",
|
|
32
|
+
mid: "hsl(var(--chart-4))",
|
|
33
|
+
max: "hsl(var(--chart-7))",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function interpolateColor(t: number, scale: HeatmapColorScale): string {
|
|
37
|
+
if (!scale.mid) {
|
|
38
|
+
return `color-mix(in srgb, ${scale.min} ${(1 - t) * 100}%, ${scale.max})`;
|
|
39
|
+
}
|
|
40
|
+
if (t <= 0.5) {
|
|
41
|
+
const t2 = t * 2;
|
|
42
|
+
return `color-mix(in srgb, ${scale.min} ${(1 - t2) * 100}%, ${scale.mid})`;
|
|
43
|
+
}
|
|
44
|
+
const t2 = (t - 0.5) * 2;
|
|
45
|
+
return `color-mix(in srgb, ${scale.mid} ${(1 - t2) * 100}%, ${scale.max})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const HeatmapChart = React.forwardRef<HTMLDivElement, HeatmapChartProps>(
|
|
49
|
+
function HeatmapChart(
|
|
50
|
+
{
|
|
51
|
+
data,
|
|
52
|
+
xLabels,
|
|
53
|
+
yLabels,
|
|
54
|
+
colorScale = DEFAULT_SCALE,
|
|
55
|
+
cellSize = 40,
|
|
56
|
+
showValues = true,
|
|
57
|
+
valueFormatter,
|
|
58
|
+
className,
|
|
59
|
+
height,
|
|
60
|
+
loading,
|
|
61
|
+
onCellClick,
|
|
62
|
+
},
|
|
63
|
+
ref,
|
|
64
|
+
) {
|
|
65
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
66
|
+
const [hoveredCell, setHoveredCell] = React.useState<HeatmapCell | null>(null);
|
|
67
|
+
const [tooltipPos, setTooltipPos] = React.useState({ x: 0, y: 0 });
|
|
68
|
+
|
|
69
|
+
const { valueMap, minVal, maxVal } = React.useMemo(() => {
|
|
70
|
+
const map = new Map<string, number>();
|
|
71
|
+
let lo = Infinity;
|
|
72
|
+
let hi = -Infinity;
|
|
73
|
+
for (const cell of data) {
|
|
74
|
+
map.set(`${cell.x}|${cell.y}`, cell.value);
|
|
75
|
+
if (cell.value < lo) lo = cell.value;
|
|
76
|
+
if (cell.value > hi) hi = cell.value;
|
|
77
|
+
}
|
|
78
|
+
return { valueMap: map, minVal: lo, maxVal: hi };
|
|
79
|
+
}, [data]);
|
|
80
|
+
|
|
81
|
+
const normalize = (v: number) =>
|
|
82
|
+
maxVal === minVal ? 0.5 : (v - minVal) / (maxVal - minVal);
|
|
83
|
+
|
|
84
|
+
const labelColWidth = 80;
|
|
85
|
+
const svgWidth = labelColWidth + xLabels.length * cellSize;
|
|
86
|
+
const svgHeight = 28 + yLabels.length * cellSize;
|
|
87
|
+
|
|
88
|
+
if (loading) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
ref={ref}
|
|
92
|
+
className={cn("w-full flex items-center justify-center", className)}
|
|
93
|
+
style={{ height: height ?? svgHeight }}
|
|
94
|
+
>
|
|
95
|
+
<div
|
|
96
|
+
className="animate-pulse"
|
|
97
|
+
style={{
|
|
98
|
+
width: "80%",
|
|
99
|
+
height: "60%",
|
|
100
|
+
borderRadius: 6,
|
|
101
|
+
background: "hsl(var(--muted))",
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
ref={(node) => {
|
|
111
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
112
|
+
if (typeof ref === "function") ref(node);
|
|
113
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
114
|
+
}}
|
|
115
|
+
className={cn("w-full overflow-auto relative", className)}
|
|
116
|
+
style={{ height }}
|
|
117
|
+
>
|
|
118
|
+
<svg width={svgWidth} height={svgHeight}>
|
|
119
|
+
{/* X-axis labels */}
|
|
120
|
+
{xLabels.map((label, i) => (
|
|
121
|
+
<text
|
|
122
|
+
key={`x-${i}`}
|
|
123
|
+
x={labelColWidth + i * cellSize + cellSize / 2}
|
|
124
|
+
y={14}
|
|
125
|
+
textAnchor="middle"
|
|
126
|
+
fontSize={9}
|
|
127
|
+
fill="hsl(var(--muted-foreground))"
|
|
128
|
+
>
|
|
129
|
+
{label}
|
|
130
|
+
</text>
|
|
131
|
+
))}
|
|
132
|
+
|
|
133
|
+
{/* Y-axis labels + cells */}
|
|
134
|
+
{yLabels.map((yLabel, yi) => (
|
|
135
|
+
<g key={`row-${yi}`}>
|
|
136
|
+
<text
|
|
137
|
+
x={labelColWidth - 8}
|
|
138
|
+
y={28 + yi * cellSize + cellSize / 2}
|
|
139
|
+
textAnchor="end"
|
|
140
|
+
dominantBaseline="central"
|
|
141
|
+
fontSize={9}
|
|
142
|
+
fill="hsl(var(--muted-foreground))"
|
|
143
|
+
>
|
|
144
|
+
{yLabel}
|
|
145
|
+
</text>
|
|
146
|
+
{xLabels.map((xLabel, xi) => {
|
|
147
|
+
const key = `${xLabel}|${yLabel}`;
|
|
148
|
+
const val = valueMap.get(key);
|
|
149
|
+
const t = val != null ? normalize(val) : 0;
|
|
150
|
+
const cx = labelColWidth + xi * cellSize;
|
|
151
|
+
const cy = 28 + yi * cellSize;
|
|
152
|
+
const cellData: HeatmapCell = { x: xLabel, y: yLabel, value: val ?? 0 };
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<g
|
|
156
|
+
key={key}
|
|
157
|
+
onMouseEnter={(e) => {
|
|
158
|
+
setHoveredCell(cellData);
|
|
159
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
160
|
+
setTooltipPos({
|
|
161
|
+
x: e.clientX - (rect?.left ?? 0) + (containerRef.current?.scrollLeft ?? 0),
|
|
162
|
+
y: e.clientY - (rect?.top ?? 0) + (containerRef.current?.scrollTop ?? 0),
|
|
163
|
+
});
|
|
164
|
+
}}
|
|
165
|
+
onMouseLeave={() => setHoveredCell(null)}
|
|
166
|
+
onClick={() => onCellClick?.(cellData)}
|
|
167
|
+
style={{ cursor: onCellClick ? "pointer" : "default" }}
|
|
168
|
+
>
|
|
169
|
+
<rect
|
|
170
|
+
x={cx + 1}
|
|
171
|
+
y={cy + 1}
|
|
172
|
+
width={cellSize - 2}
|
|
173
|
+
height={cellSize - 2}
|
|
174
|
+
rx={3}
|
|
175
|
+
fill={
|
|
176
|
+
val != null
|
|
177
|
+
? interpolateColor(t, colorScale)
|
|
178
|
+
: "hsl(var(--muted))"
|
|
179
|
+
}
|
|
180
|
+
fillOpacity={val != null ? 0.85 : 0.3}
|
|
181
|
+
stroke={
|
|
182
|
+
hoveredCell?.x === xLabel && hoveredCell?.y === yLabel
|
|
183
|
+
? "hsl(var(--foreground))"
|
|
184
|
+
: "none"
|
|
185
|
+
}
|
|
186
|
+
strokeWidth={1.5}
|
|
187
|
+
/>
|
|
188
|
+
{showValues && val != null && cellSize >= 32 && (
|
|
189
|
+
<text
|
|
190
|
+
x={cx + cellSize / 2}
|
|
191
|
+
y={cy + cellSize / 2}
|
|
192
|
+
textAnchor="middle"
|
|
193
|
+
dominantBaseline="central"
|
|
194
|
+
fontSize={Math.min(10, cellSize / 4.5)}
|
|
195
|
+
fill="hsl(var(--foreground))"
|
|
196
|
+
fontWeight={500}
|
|
197
|
+
style={{ pointerEvents: "none" }}
|
|
198
|
+
>
|
|
199
|
+
{valueFormatter ? valueFormatter(val) : val.toLocaleString()}
|
|
200
|
+
</text>
|
|
201
|
+
)}
|
|
202
|
+
</g>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</g>
|
|
206
|
+
))}
|
|
207
|
+
</svg>
|
|
208
|
+
|
|
209
|
+
{/* Tooltip */}
|
|
210
|
+
{hoveredCell && (
|
|
211
|
+
<div
|
|
212
|
+
style={{
|
|
213
|
+
position: "absolute",
|
|
214
|
+
left: tooltipPos.x + 12,
|
|
215
|
+
top: tooltipPos.y - 10,
|
|
216
|
+
background: "hsl(var(--card))",
|
|
217
|
+
border: "1px solid hsl(var(--border))",
|
|
218
|
+
borderRadius: 6,
|
|
219
|
+
padding: "6px 10px",
|
|
220
|
+
fontSize: 11,
|
|
221
|
+
lineHeight: 1.5,
|
|
222
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
223
|
+
pointerEvents: "none",
|
|
224
|
+
zIndex: 50,
|
|
225
|
+
maxWidth: 200,
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<div style={{ color: "hsl(var(--muted-foreground))" }}>
|
|
229
|
+
{String(hoveredCell.x)} / {String(hoveredCell.y)}
|
|
230
|
+
</div>
|
|
231
|
+
<div
|
|
232
|
+
style={{
|
|
233
|
+
fontWeight: 600,
|
|
234
|
+
color: "hsl(var(--foreground))",
|
|
235
|
+
fontVariantNumeric: "tabular-nums",
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{valueFormatter
|
|
239
|
+
? valueFormatter(hoveredCell.value)
|
|
240
|
+
: hoveredCell.value.toLocaleString()}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
HeatmapChart.displayName = "HeatmapChart";
|
|
250
|
+
export { HeatmapChart };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Sankey, Tooltip, Layer, Rectangle } from "recharts";
|
|
3
|
+
import { getChartColor } from "../shared/colors";
|
|
4
|
+
import { resolveTooltipProps } from "../shared/chart-tooltip";
|
|
5
|
+
import { ChartContainer } from "../shared/chart-container";
|
|
6
|
+
import type { BaseChartProps, ChartTooltipProp } from "../shared/types";
|
|
7
|
+
|
|
8
|
+
export interface SankeyNode {
|
|
9
|
+
name: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SankeyLink {
|
|
14
|
+
source: number;
|
|
15
|
+
target: number;
|
|
16
|
+
value: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SankeyChartData {
|
|
20
|
+
nodes: SankeyNode[];
|
|
21
|
+
links: SankeyLink[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SankeyChartProps extends BaseChartProps {
|
|
25
|
+
data: SankeyChartData;
|
|
26
|
+
nodeWidth?: number;
|
|
27
|
+
nodePadding?: number;
|
|
28
|
+
tooltip?: ChartTooltipProp;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SankeyNodeRendererProps {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
index: number;
|
|
38
|
+
payload: { name: string; color?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function SankeyNodeRenderer({
|
|
42
|
+
x,
|
|
43
|
+
y,
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
index,
|
|
47
|
+
payload,
|
|
48
|
+
}: SankeyNodeRendererProps) {
|
|
49
|
+
const fill = payload.color ?? getChartColor(index);
|
|
50
|
+
return (
|
|
51
|
+
<Layer key={`node-${index}`}>
|
|
52
|
+
<Rectangle
|
|
53
|
+
x={x}
|
|
54
|
+
y={y}
|
|
55
|
+
width={width}
|
|
56
|
+
height={height}
|
|
57
|
+
fill={fill}
|
|
58
|
+
fillOpacity={0.9}
|
|
59
|
+
/>
|
|
60
|
+
<text
|
|
61
|
+
x={x + width + 6}
|
|
62
|
+
y={y + height / 2}
|
|
63
|
+
textAnchor="start"
|
|
64
|
+
dominantBaseline="central"
|
|
65
|
+
fontSize={10}
|
|
66
|
+
fill="hsl(var(--foreground))"
|
|
67
|
+
>
|
|
68
|
+
{payload.name}
|
|
69
|
+
</text>
|
|
70
|
+
</Layer>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SankeyLinkRendererProps {
|
|
75
|
+
sourceX: number;
|
|
76
|
+
sourceY: number;
|
|
77
|
+
sourceControlX: number;
|
|
78
|
+
targetX: number;
|
|
79
|
+
targetY: number;
|
|
80
|
+
targetControlX: number;
|
|
81
|
+
linkWidth: number;
|
|
82
|
+
index: number;
|
|
83
|
+
payload: { source: { color?: string }; target: unknown };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function SankeyLinkRenderer({
|
|
87
|
+
sourceX,
|
|
88
|
+
sourceY,
|
|
89
|
+
sourceControlX,
|
|
90
|
+
targetX,
|
|
91
|
+
targetY,
|
|
92
|
+
targetControlX,
|
|
93
|
+
linkWidth,
|
|
94
|
+
index,
|
|
95
|
+
payload,
|
|
96
|
+
}: SankeyLinkRendererProps) {
|
|
97
|
+
const sourceColor =
|
|
98
|
+
(payload.source as { color?: string }).color ?? getChartColor(index);
|
|
99
|
+
return (
|
|
100
|
+
<Layer key={`link-${index}`}>
|
|
101
|
+
<path
|
|
102
|
+
d={`
|
|
103
|
+
M${sourceX},${sourceY}
|
|
104
|
+
C${sourceControlX},${sourceY} ${targetControlX},${targetY} ${targetX},${targetY}
|
|
105
|
+
`}
|
|
106
|
+
fill="none"
|
|
107
|
+
stroke={sourceColor}
|
|
108
|
+
strokeWidth={linkWidth}
|
|
109
|
+
strokeOpacity={0.3}
|
|
110
|
+
/>
|
|
111
|
+
</Layer>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const SankeyChart = React.forwardRef<HTMLDivElement, SankeyChartProps>(
|
|
116
|
+
(
|
|
117
|
+
{
|
|
118
|
+
data,
|
|
119
|
+
nodeWidth = 10,
|
|
120
|
+
nodePadding = 20,
|
|
121
|
+
tooltip = true,
|
|
122
|
+
className,
|
|
123
|
+
height = "100%",
|
|
124
|
+
margin = { top: 8, right: 80, left: 8, bottom: 8 },
|
|
125
|
+
loading,
|
|
126
|
+
},
|
|
127
|
+
ref,
|
|
128
|
+
) => {
|
|
129
|
+
const tooltipProps = resolveTooltipProps(tooltip);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<ChartContainer
|
|
133
|
+
ref={ref}
|
|
134
|
+
className={className}
|
|
135
|
+
height={height}
|
|
136
|
+
loading={loading}
|
|
137
|
+
empty={!data?.nodes?.length}
|
|
138
|
+
>
|
|
139
|
+
<Sankey
|
|
140
|
+
data={data}
|
|
141
|
+
nodeWidth={nodeWidth}
|
|
142
|
+
nodePadding={nodePadding}
|
|
143
|
+
margin={margin}
|
|
144
|
+
node={(SankeyNodeRenderer as never)}
|
|
145
|
+
link={(SankeyLinkRenderer as never)}
|
|
146
|
+
>
|
|
147
|
+
{tooltipProps && <Tooltip {...tooltipProps} />}
|
|
148
|
+
</Sankey>
|
|
149
|
+
</ChartContainer>
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
SankeyChart.displayName = "SankeyChart";
|
|
155
|
+
export { SankeyChart };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Treemap, Tooltip } from "recharts";
|
|
3
|
+
import { getChartColor } from "../shared/colors";
|
|
4
|
+
import { resolveTooltipProps } from "../shared/chart-tooltip";
|
|
5
|
+
import { ChartContainer } from "../shared/chart-container";
|
|
6
|
+
import type { BaseChartProps, ChartTooltipProp } from "../shared/types";
|
|
7
|
+
|
|
8
|
+
export interface TreemapDatum {
|
|
9
|
+
name: string;
|
|
10
|
+
value?: number;
|
|
11
|
+
color?: string;
|
|
12
|
+
children?: TreemapDatum[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TreemapChartProps extends BaseChartProps {
|
|
16
|
+
data: TreemapDatum[];
|
|
17
|
+
dataKey?: string;
|
|
18
|
+
aspectRatio?: number;
|
|
19
|
+
tooltip?: ChartTooltipProp;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TreemapContentProps {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
index: number;
|
|
29
|
+
name: string;
|
|
30
|
+
depth: number;
|
|
31
|
+
color?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function TreemapContent({
|
|
35
|
+
x,
|
|
36
|
+
y,
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
index,
|
|
40
|
+
name,
|
|
41
|
+
depth,
|
|
42
|
+
color,
|
|
43
|
+
}: TreemapContentProps) {
|
|
44
|
+
const fill = color ?? getChartColor(index);
|
|
45
|
+
const showLabel = width > 40 && height > 20;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<g>
|
|
49
|
+
<rect
|
|
50
|
+
x={x}
|
|
51
|
+
y={y}
|
|
52
|
+
width={width}
|
|
53
|
+
height={height}
|
|
54
|
+
fill={fill}
|
|
55
|
+
fillOpacity={depth === 1 ? 0.85 : 0.6}
|
|
56
|
+
stroke="hsl(var(--card))"
|
|
57
|
+
strokeWidth={2}
|
|
58
|
+
rx={2}
|
|
59
|
+
/>
|
|
60
|
+
{showLabel && (
|
|
61
|
+
<text
|
|
62
|
+
x={x + width / 2}
|
|
63
|
+
y={y + height / 2}
|
|
64
|
+
textAnchor="middle"
|
|
65
|
+
dominantBaseline="central"
|
|
66
|
+
fontSize={Math.min(11, width / 6)}
|
|
67
|
+
fill="hsl(var(--card-foreground))"
|
|
68
|
+
fontWeight={500}
|
|
69
|
+
style={{ pointerEvents: "none" }}
|
|
70
|
+
>
|
|
71
|
+
{name.length > width / 7 ? `${name.slice(0, Math.floor(width / 7))}…` : name}
|
|
72
|
+
</text>
|
|
73
|
+
)}
|
|
74
|
+
</g>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const TreemapChart = React.forwardRef<HTMLDivElement, TreemapChartProps>(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
data,
|
|
82
|
+
dataKey = "value",
|
|
83
|
+
aspectRatio = 4 / 3,
|
|
84
|
+
tooltip = true,
|
|
85
|
+
animate = true,
|
|
86
|
+
live = false,
|
|
87
|
+
className,
|
|
88
|
+
height = "100%",
|
|
89
|
+
margin,
|
|
90
|
+
loading,
|
|
91
|
+
},
|
|
92
|
+
ref,
|
|
93
|
+
) => {
|
|
94
|
+
const tooltipProps = resolveTooltipProps(tooltip);
|
|
95
|
+
const isAnimated = live ? false : animate;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<ChartContainer
|
|
99
|
+
ref={ref}
|
|
100
|
+
className={className}
|
|
101
|
+
height={height}
|
|
102
|
+
loading={loading}
|
|
103
|
+
empty={!data?.length}
|
|
104
|
+
>
|
|
105
|
+
<Treemap
|
|
106
|
+
data={data}
|
|
107
|
+
dataKey={dataKey}
|
|
108
|
+
aspectRatio={aspectRatio}
|
|
109
|
+
isAnimationActive={isAnimated}
|
|
110
|
+
animationDuration={isAnimated ? 400 : 0}
|
|
111
|
+
content={<TreemapContent x={0} y={0} width={0} height={0} index={0} name="" depth={1} />}
|
|
112
|
+
>
|
|
113
|
+
{tooltipProps && <Tooltip {...tooltipProps} />}
|
|
114
|
+
</Treemap>
|
|
115
|
+
</ChartContainer>
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
TreemapChart.displayName = "TreemapChart";
|
|
121
|
+
export { TreemapChart };
|