@optilogic/charts 1.0.0-beta.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/index.cjs +3034 -174
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +489 -192
  4. package/dist/index.d.ts +489 -192
  5. package/dist/index.js +3006 -175
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/cartesian/area-chart.tsx +177 -0
  9. package/src/cartesian/bar-chart.tsx +217 -0
  10. package/src/cartesian/composed-chart.tsx +222 -0
  11. package/src/cartesian/line-chart.tsx +159 -0
  12. package/src/cartesian/scatter-chart.tsx +158 -0
  13. package/src/cartesian/waterfall-chart.tsx +171 -0
  14. package/src/dashboard/chart-builder.tsx +310 -0
  15. package/src/dashboard/chart-renderer.tsx +250 -0
  16. package/src/dashboard/kpi-card.tsx +121 -0
  17. package/src/dashboard/scenario-comparison.tsx +235 -0
  18. package/src/dashboard/sparkline.tsx +86 -0
  19. package/src/index.ts +50 -19
  20. package/src/radial/donut-chart.tsx +135 -0
  21. package/src/radial/pie-chart.tsx +153 -0
  22. package/src/radial/radar-chart.tsx +111 -0
  23. package/src/radial/radial-bar-chart.tsx +115 -0
  24. package/src/shared/chart-container.tsx +104 -0
  25. package/src/shared/chart-legend.tsx +57 -0
  26. package/src/shared/chart-tooltip.tsx +159 -0
  27. package/src/shared/colors.ts +37 -0
  28. package/src/shared/formatters.ts +51 -0
  29. package/src/shared/types.ts +66 -0
  30. package/src/shared/use-live-data.ts +83 -0
  31. package/src/specialized/funnel-chart.tsx +93 -0
  32. package/src/specialized/gantt-chart.tsx +416 -0
  33. package/src/specialized/heatmap-chart.tsx +250 -0
  34. package/src/specialized/sankey-chart.tsx +155 -0
  35. package/src/specialized/treemap-chart.tsx +121 -0
  36. package/src/bar-chart.tsx +0 -337
  37. package/src/line-chart.tsx +0 -266
@@ -0,0 +1,235 @@
1
+ import * as React from "react";
2
+ import { cn } from "@optilogic/core";
3
+ import { LineChart } from "../cartesian/line-chart";
4
+ import { BarChart } from "../cartesian/bar-chart";
5
+ import { RadarChart } from "../radial/radar-chart";
6
+ import type { ChartTooltipProp } from "../shared/types";
7
+
8
+ export interface ScenarioData {
9
+ name: string;
10
+ color?: string;
11
+ data: Record<string, unknown>[];
12
+ }
13
+
14
+ export type ScenarioComparisonMode =
15
+ | "side-by-side"
16
+ | "overlay"
17
+ | "delta"
18
+ | "scorecard";
19
+
20
+ export interface ScenarioComparisonProps {
21
+ scenarios: ScenarioData[];
22
+ /** Data keys to compare */
23
+ metrics: string[];
24
+ /** Category axis key */
25
+ categoryKey: string;
26
+ mode?: ScenarioComparisonMode;
27
+ /** Index of the baseline scenario for delta calculations */
28
+ baselineIndex?: number;
29
+ tooltip?: ChartTooltipProp;
30
+ className?: string;
31
+ height?: number | string;
32
+ }
33
+
34
+ function buildOverlayData(
35
+ scenarios: ScenarioData[],
36
+ categoryKey: string,
37
+ metrics: string[],
38
+ ): { data: Record<string, unknown>[]; series: { dataKey: string; name: string; color?: string; strokeDasharray?: string }[] } {
39
+ const merged = new Map<string, Record<string, unknown>>();
40
+ for (const scenario of scenarios) {
41
+ for (const row of scenario.data) {
42
+ const key = String(row[categoryKey]);
43
+ if (!merged.has(key)) merged.set(key, { [categoryKey]: row[categoryKey] });
44
+ const target = merged.get(key)!;
45
+ for (const m of metrics) {
46
+ target[`${scenario.name}_${m}`] = row[m];
47
+ }
48
+ }
49
+ }
50
+ const data = Array.from(merged.values());
51
+ const series = scenarios.flatMap((s, si) =>
52
+ metrics.map((m) => ({
53
+ dataKey: `${s.name}_${m}`,
54
+ name: `${s.name} – ${m}`,
55
+ color: s.color,
56
+ strokeDasharray: si > 0 ? "5 3" : undefined,
57
+ })),
58
+ );
59
+ return { data, series };
60
+ }
61
+
62
+ function buildDeltaData(
63
+ scenarios: ScenarioData[],
64
+ baselineIndex: number,
65
+ categoryKey: string,
66
+ metrics: string[],
67
+ ): { data: Record<string, unknown>[]; series: { dataKey: string; name: string }[] } {
68
+ const baseline = scenarios[baselineIndex];
69
+ if (!baseline) return { data: [], series: [] };
70
+
71
+ const baseMap = new Map<string, Record<string, unknown>>();
72
+ for (const row of baseline.data) {
73
+ baseMap.set(String(row[categoryKey]), row);
74
+ }
75
+
76
+ const comparisons = scenarios.filter((_, i) => i !== baselineIndex);
77
+ const merged = new Map<string, Record<string, unknown>>();
78
+
79
+ for (const scenario of comparisons) {
80
+ for (const row of scenario.data) {
81
+ const key = String(row[categoryKey]);
82
+ if (!merged.has(key)) merged.set(key, { [categoryKey]: row[categoryKey] });
83
+ const target = merged.get(key)!;
84
+ const baseRow = baseMap.get(key);
85
+ for (const m of metrics) {
86
+ const base = Number(baseRow?.[m] ?? 0);
87
+ const val = Number(row[m] ?? 0);
88
+ target[`${scenario.name}_${m}`] = val - base;
89
+ }
90
+ }
91
+ }
92
+
93
+ const data = Array.from(merged.values());
94
+ const series = comparisons.flatMap((s) =>
95
+ metrics.map((m) => ({
96
+ dataKey: `${s.name}_${m}`,
97
+ name: `${s.name} Δ ${m}`,
98
+ })),
99
+ );
100
+ return { data, series };
101
+ }
102
+
103
+ function buildScorecardData(
104
+ scenarios: ScenarioData[],
105
+ metrics: string[],
106
+ ): { data: Record<string, unknown>[]; series: { dataKey: string; name: string; color?: string; strokeDasharray?: string }[] } {
107
+ const data: Record<string, unknown>[] = metrics.map((m) => {
108
+ const row: Record<string, unknown> = { metric: m };
109
+ for (const s of scenarios) {
110
+ const values = s.data.map((d) => Number(d[m] ?? 0));
111
+ const avg = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
112
+ row[s.name] = Math.round(avg * 10) / 10;
113
+ }
114
+ return row;
115
+ });
116
+ const series = scenarios.map((s, i) => ({
117
+ dataKey: s.name,
118
+ name: s.name,
119
+ color: s.color,
120
+ strokeDasharray: i > 0 ? "5 3" : undefined,
121
+ }));
122
+ return { data, series };
123
+ }
124
+
125
+ const ScenarioComparison = React.memo(function ScenarioComparison({
126
+ scenarios,
127
+ metrics,
128
+ categoryKey,
129
+ mode = "overlay",
130
+ baselineIndex = 0,
131
+ tooltip = true,
132
+ className,
133
+ height = 350,
134
+ }: ScenarioComparisonProps) {
135
+ if (!scenarios.length || !metrics.length) return null;
136
+
137
+ if (mode === "side-by-side") {
138
+ return (
139
+ <div
140
+ className={cn("grid gap-4", className)}
141
+ style={{
142
+ gridTemplateColumns: `repeat(${scenarios.length}, 1fr)`,
143
+ }}
144
+ >
145
+ {scenarios.map((scenario) => (
146
+ <div key={scenario.name} style={{ height }}>
147
+ <div
148
+ style={{
149
+ fontSize: 12,
150
+ fontWeight: 600,
151
+ color: "hsl(var(--foreground))",
152
+ marginBottom: 6,
153
+ }}
154
+ >
155
+ {scenario.name}
156
+ </div>
157
+ <LineChart
158
+ data={scenario.data}
159
+ series={metrics.map((m) => ({
160
+ dataKey: m,
161
+ name: m,
162
+ color: scenario.color,
163
+ }))}
164
+ xAxis={{ dataKey: categoryKey }}
165
+ tooltip={tooltip}
166
+ legend={metrics.length > 1}
167
+ height="calc(100% - 24px)"
168
+ />
169
+ </div>
170
+ ))}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ if (mode === "overlay") {
176
+ const { data, series } = buildOverlayData(scenarios, categoryKey, metrics);
177
+ return (
178
+ <div className={className} style={{ height }}>
179
+ <LineChart
180
+ data={data}
181
+ series={series}
182
+ xAxis={{ dataKey: categoryKey }}
183
+ tooltip={tooltip}
184
+ legend
185
+ height="100%"
186
+ />
187
+ </div>
188
+ );
189
+ }
190
+
191
+ if (mode === "delta") {
192
+ const { data, series } = buildDeltaData(
193
+ scenarios,
194
+ baselineIndex,
195
+ categoryKey,
196
+ metrics,
197
+ );
198
+ return (
199
+ <div className={className} style={{ height }}>
200
+ <BarChart
201
+ data={data}
202
+ series={series.map((s) => ({ ...s }))}
203
+ xAxis={{ dataKey: categoryKey }}
204
+ tooltip={tooltip}
205
+ legend
206
+ height="100%"
207
+ />
208
+ </div>
209
+ );
210
+ }
211
+
212
+ if (mode === "scorecard") {
213
+ const { data, series } = buildScorecardData(scenarios, metrics);
214
+ return (
215
+ <div className={className} style={{ height }}>
216
+ <RadarChart
217
+ data={data}
218
+ series={series.map((s) => ({
219
+ ...s,
220
+ fillOpacity: 0.1,
221
+ }))}
222
+ categoryKey="metric"
223
+ tooltip={tooltip}
224
+ legend
225
+ height="100%"
226
+ />
227
+ </div>
228
+ );
229
+ }
230
+
231
+ return null;
232
+ });
233
+
234
+ ScenarioComparison.displayName = "ScenarioComparison";
235
+ export { ScenarioComparison };
@@ -0,0 +1,86 @@
1
+ import * as React from "react";
2
+ import {
3
+ LineChart,
4
+ Line,
5
+ AreaChart,
6
+ Area,
7
+ BarChart,
8
+ Bar,
9
+ ResponsiveContainer,
10
+ } from "recharts";
11
+ import { getChartColor } from "../shared/colors";
12
+
13
+ export type SparklineVariant = "line" | "area" | "bar";
14
+
15
+ export interface SparklineProps {
16
+ data: number[];
17
+ variant?: SparklineVariant;
18
+ color?: string;
19
+ width?: number | string;
20
+ height?: number;
21
+ strokeWidth?: number;
22
+ className?: string;
23
+ }
24
+
25
+ const Sparkline = React.memo(function Sparkline({
26
+ data,
27
+ variant = "line",
28
+ color,
29
+ width = "100%",
30
+ height = 24,
31
+ strokeWidth = 1.5,
32
+ className,
33
+ }: SparklineProps) {
34
+ const chartColor = color ?? getChartColor(0);
35
+
36
+ const points = React.useMemo(
37
+ () => data.map((v, i) => ({ i, v })),
38
+ [data],
39
+ );
40
+
41
+ if (!points.length) return null;
42
+
43
+ return (
44
+ <div className={className} style={{ width, height }}>
45
+ <ResponsiveContainer width="100%" height="100%">
46
+ {variant === "bar" ? (
47
+ <BarChart data={points} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
48
+ <Bar dataKey="v" fill={chartColor} fillOpacity={0.7} isAnimationActive={false} />
49
+ </BarChart>
50
+ ) : variant === "area" ? (
51
+ <AreaChart data={points} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
52
+ <defs>
53
+ <linearGradient id="sparkArea" x1="0" y1="0" x2="0" y2="1">
54
+ <stop offset="5%" stopColor={chartColor} stopOpacity={0.3} />
55
+ <stop offset="95%" stopColor={chartColor} stopOpacity={0.05} />
56
+ </linearGradient>
57
+ </defs>
58
+ <Area
59
+ type="monotone"
60
+ dataKey="v"
61
+ stroke={chartColor}
62
+ strokeWidth={strokeWidth}
63
+ fill="url(#sparkArea)"
64
+ isAnimationActive={false}
65
+ dot={false}
66
+ />
67
+ </AreaChart>
68
+ ) : (
69
+ <LineChart data={points} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
70
+ <Line
71
+ type="monotone"
72
+ dataKey="v"
73
+ stroke={chartColor}
74
+ strokeWidth={strokeWidth}
75
+ dot={false}
76
+ isAnimationActive={false}
77
+ />
78
+ </LineChart>
79
+ )}
80
+ </ResponsiveContainer>
81
+ </div>
82
+ );
83
+ });
84
+
85
+ Sparkline.displayName = "Sparkline";
86
+ export { Sparkline };
package/src/index.ts CHANGED
@@ -1,21 +1,52 @@
1
+ // ── Shared ────────────────────────────────────────────────────────────────────
2
+ export { CHART_COLORS, getChartColor, getSemanticColor, type SemanticColorName } from "./shared/colors";
3
+ export { ChartTooltipContent, resolveTooltipProps, type ChartTooltipContentProps } from "./shared/chart-tooltip";
4
+ export { ChartLegendContent, resolveLegendConfig } from "./shared/chart-legend";
5
+ export { ChartContainer, type ChartContainerProps } from "./shared/chart-container";
6
+ export { useLiveData, type UseLiveDataConfig, type UseLiveDataReturn } from "./shared/use-live-data";
1
7
  export {
2
- LineChart,
3
- CHART_COLORS,
4
- getChartColor,
5
- type LineChartProps,
6
- type LineChartSeries,
7
- type LineChartXAxis,
8
- type LineChartYAxis,
9
- type LineChartGrid,
10
- type LineChartLegend,
11
- } from "./line-chart";
8
+ formatCompact,
9
+ formatCurrency,
10
+ formatPercent,
11
+ formatUnit,
12
+ formatDuration,
13
+ } from "./shared/formatters";
14
+ export type {
15
+ ChartXAxis,
16
+ ChartYAxis,
17
+ ChartGrid,
18
+ ChartLegend,
19
+ ChartMargin,
20
+ ChartTooltipConfig,
21
+ ChartTooltipProp,
22
+ TooltipFormatter,
23
+ BaseChartProps,
24
+ } from "./shared/types";
12
25
 
13
- export {
14
- BarChart,
15
- type BarChartProps,
16
- type BarChartSeries,
17
- type BarChartXAxis,
18
- type BarChartYAxis,
19
- type BarChartGrid,
20
- type BarChartLegend,
21
- } from "./bar-chart";
26
+ // ── Cartesian Charts ──────────────────────────────────────────────────────────
27
+ export { LineChart, type LineChartProps, type LineChartSeries } from "./cartesian/line-chart";
28
+ export { BarChart, type BarChartProps, type BarChartSeries } from "./cartesian/bar-chart";
29
+ export { AreaChart, type AreaChartProps, type AreaChartSeries } from "./cartesian/area-chart";
30
+ export { ScatterChart, type ScatterChartProps, type ScatterChartSeries } from "./cartesian/scatter-chart";
31
+ export { ComposedChart, type ComposedChartProps, type ComposedChartSeries } from "./cartesian/composed-chart";
32
+ export { WaterfallChart, type WaterfallChartProps, type WaterfallItem } from "./cartesian/waterfall-chart";
33
+
34
+ // ── Radial Charts ─────────────────────────────────────────────────────────────
35
+ export { PieChart, type PieChartProps, type PieChartDatum } from "./radial/pie-chart";
36
+ export { DonutChart, type DonutChartProps, type DonutChartDatum } from "./radial/donut-chart";
37
+ export { RadarChart, type RadarChartProps, type RadarChartSeries } from "./radial/radar-chart";
38
+ export { RadialBarChart, type RadialBarChartProps, type RadialBarDatum } from "./radial/radial-bar-chart";
39
+
40
+ // ── Specialized Charts ────────────────────────────────────────────────────────
41
+ export { SankeyChart, type SankeyChartProps, type SankeyChartData, type SankeyNode, type SankeyLink } from "./specialized/sankey-chart";
42
+ export { TreemapChart, type TreemapChartProps, type TreemapDatum } from "./specialized/treemap-chart";
43
+ export { FunnelChart, type FunnelChartProps, type FunnelDatum } from "./specialized/funnel-chart";
44
+ export { GanttChart, type GanttChartProps, type GanttTask, type GanttMilestone, type GanttTimeScale } from "./specialized/gantt-chart";
45
+ export { HeatmapChart, type HeatmapChartProps, type HeatmapCell, type HeatmapColorScale } from "./specialized/heatmap-chart";
46
+
47
+ // ── Dashboard & Analytics ─────────────────────────────────────────────────────
48
+ export { Sparkline, type SparklineProps, type SparklineVariant } from "./dashboard/sparkline";
49
+ export { KPICard, type KPICardProps } from "./dashboard/kpi-card";
50
+ export { ScenarioComparison, type ScenarioComparisonProps, type ScenarioData, type ScenarioComparisonMode } from "./dashboard/scenario-comparison";
51
+ export { ChartBuilder, type ChartBuilderProps } from "./dashboard/chart-builder";
52
+ export { ChartRenderer, type ChartRendererProps, type ChartConfig, type ChartConfigSeries } from "./dashboard/chart-renderer";
@@ -0,0 +1,135 @@
1
+ import * as React from "react";
2
+ import {
3
+ PieChart as RechartsPieChart,
4
+ Pie,
5
+ Cell,
6
+ Tooltip,
7
+ Legend,
8
+ } from "recharts";
9
+ import { getChartColor } from "../shared/colors";
10
+ import { resolveTooltipProps } from "../shared/chart-tooltip";
11
+ import { ChartLegendContent, resolveLegendConfig } from "../shared/chart-legend";
12
+ import { ChartContainer } from "../shared/chart-container";
13
+ import type { BaseChartProps, ChartLegend, ChartTooltipProp } from "../shared/types";
14
+
15
+ export interface DonutChartDatum {
16
+ name: string;
17
+ value: number;
18
+ color?: string;
19
+ }
20
+
21
+ export interface DonutChartProps extends BaseChartProps {
22
+ data: DonutChartDatum[];
23
+ innerRadius?: number | string;
24
+ outerRadius?: number | string;
25
+ paddingAngle?: number;
26
+ legend?: boolean | ChartLegend;
27
+ tooltip?: ChartTooltipProp;
28
+ /** Content rendered in the center of the donut */
29
+ centerLabel?: string;
30
+ centerValue?: string;
31
+ loading?: boolean;
32
+ }
33
+
34
+ const DonutChart = React.forwardRef<HTMLDivElement, DonutChartProps>(
35
+ (
36
+ {
37
+ data,
38
+ innerRadius = "60%",
39
+ outerRadius = "80%",
40
+ paddingAngle = 2,
41
+ legend = false,
42
+ tooltip = true,
43
+ centerLabel,
44
+ centerValue,
45
+ animate = true,
46
+ live = false,
47
+ className,
48
+ height = "100%",
49
+ margin = { top: 8, right: 8, left: 8, bottom: 8 },
50
+ loading,
51
+ },
52
+ ref,
53
+ ) => {
54
+ const legendConfig = resolveLegendConfig(legend);
55
+ const tooltipProps = resolveTooltipProps(tooltip);
56
+ const isAnimated = live ? false : animate;
57
+
58
+ return (
59
+ <ChartContainer
60
+ ref={ref}
61
+ className={className}
62
+ height={height}
63
+ loading={loading}
64
+ empty={!data?.length}
65
+ >
66
+ <RechartsPieChart margin={margin}>
67
+ <Pie
68
+ data={data}
69
+ dataKey="value"
70
+ nameKey="name"
71
+ cx="50%"
72
+ cy="50%"
73
+ innerRadius={innerRadius}
74
+ outerRadius={outerRadius}
75
+ paddingAngle={paddingAngle}
76
+ startAngle={90}
77
+ endAngle={-270}
78
+ isAnimationActive={isAnimated}
79
+ animationDuration={isAnimated ? 400 : 0}
80
+ stroke="hsl(var(--card))"
81
+ strokeWidth={2}
82
+ >
83
+ {data.map((entry, index) => (
84
+ <Cell
85
+ key={entry.name}
86
+ fill={getChartColor(index, entry.color)}
87
+ />
88
+ ))}
89
+ </Pie>
90
+ {(centerLabel || centerValue) && (
91
+ <text
92
+ x="50%"
93
+ y="50%"
94
+ textAnchor="middle"
95
+ dominantBaseline="central"
96
+ >
97
+ {centerValue && (
98
+ <tspan
99
+ x="50%"
100
+ dy={centerLabel ? "-0.5em" : "0"}
101
+ fontSize={20}
102
+ fontWeight={700}
103
+ fill="hsl(var(--foreground))"
104
+ >
105
+ {centerValue}
106
+ </tspan>
107
+ )}
108
+ {centerLabel && (
109
+ <tspan
110
+ x="50%"
111
+ dy={centerValue ? "1.4em" : "0"}
112
+ fontSize={11}
113
+ fill="hsl(var(--muted-foreground))"
114
+ >
115
+ {centerLabel}
116
+ </tspan>
117
+ )}
118
+ </text>
119
+ )}
120
+ {tooltipProps && <Tooltip {...tooltipProps} />}
121
+ {legendConfig && (
122
+ <Legend
123
+ verticalAlign={legendConfig.position ?? "bottom"}
124
+ align={legendConfig.align ?? "center"}
125
+ content={<ChartLegendContent />}
126
+ />
127
+ )}
128
+ </RechartsPieChart>
129
+ </ChartContainer>
130
+ );
131
+ },
132
+ );
133
+
134
+ DonutChart.displayName = "DonutChart";
135
+ export { DonutChart };
@@ -0,0 +1,153 @@
1
+ import * as React from "react";
2
+ import {
3
+ PieChart as RechartsPieChart,
4
+ Pie,
5
+ Cell,
6
+ Tooltip,
7
+ Legend,
8
+ } from "recharts";
9
+ import { getChartColor } from "../shared/colors";
10
+ import { resolveTooltipProps } from "../shared/chart-tooltip";
11
+ import { ChartLegendContent, resolveLegendConfig } from "../shared/chart-legend";
12
+ import { ChartContainer } from "../shared/chart-container";
13
+ import type { BaseChartProps, ChartLegend, ChartTooltipProp } from "../shared/types";
14
+
15
+ export interface PieChartDatum {
16
+ name: string;
17
+ value: number;
18
+ color?: string;
19
+ }
20
+
21
+ export interface PieChartProps extends BaseChartProps {
22
+ data: PieChartDatum[];
23
+ innerRadius?: number | string;
24
+ outerRadius?: number | string;
25
+ paddingAngle?: number;
26
+ startAngle?: number;
27
+ endAngle?: number;
28
+ legend?: boolean | ChartLegend;
29
+ tooltip?: ChartTooltipProp;
30
+ /** Label rendered on each slice */
31
+ label?: boolean | ((entry: PieChartDatum) => string);
32
+ loading?: boolean;
33
+ }
34
+
35
+ const RADIAN = Math.PI / 180;
36
+
37
+ function defaultRenderLabel({
38
+ cx,
39
+ cy,
40
+ midAngle,
41
+ innerRadius,
42
+ outerRadius,
43
+ percent,
44
+ }: {
45
+ cx: number;
46
+ cy: number;
47
+ midAngle: number;
48
+ innerRadius: number;
49
+ outerRadius: number;
50
+ percent: number;
51
+ }) {
52
+ const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
53
+ const x = cx + radius * Math.cos(-midAngle * RADIAN);
54
+ const y = cy + radius * Math.sin(-midAngle * RADIAN);
55
+ if (percent < 0.04) return null;
56
+ return (
57
+ <text
58
+ x={x}
59
+ y={y}
60
+ fill="hsl(var(--card-foreground))"
61
+ textAnchor="middle"
62
+ dominantBaseline="central"
63
+ fontSize={10}
64
+ fontWeight={600}
65
+ >
66
+ {`${(percent * 100).toFixed(0)}%`}
67
+ </text>
68
+ );
69
+ }
70
+
71
+ const PieChart = React.forwardRef<HTMLDivElement, PieChartProps>(
72
+ (
73
+ {
74
+ data,
75
+ innerRadius = 0,
76
+ outerRadius = "80%",
77
+ paddingAngle = 2,
78
+ startAngle = 90,
79
+ endAngle = -270,
80
+ legend = false,
81
+ tooltip = true,
82
+ label = false,
83
+ animate = true,
84
+ live = false,
85
+ className,
86
+ height = "100%",
87
+ margin = { top: 8, right: 8, left: 8, bottom: 8 },
88
+ loading,
89
+ },
90
+ ref,
91
+ ) => {
92
+ const legendConfig = resolveLegendConfig(legend);
93
+ const tooltipProps = resolveTooltipProps(tooltip);
94
+ const isAnimated = live ? false : animate;
95
+
96
+ const renderLabel =
97
+ label === true
98
+ ? defaultRenderLabel
99
+ : typeof label === "function"
100
+ ? (entry: Record<string, unknown>) =>
101
+ (label as (e: PieChartDatum) => string)(entry as unknown as PieChartDatum)
102
+ : undefined;
103
+
104
+ return (
105
+ <ChartContainer
106
+ ref={ref}
107
+ className={className}
108
+ height={height}
109
+ loading={loading}
110
+ empty={!data?.length}
111
+ >
112
+ <RechartsPieChart margin={margin}>
113
+ <Pie
114
+ data={data}
115
+ dataKey="value"
116
+ nameKey="name"
117
+ cx="50%"
118
+ cy="50%"
119
+ innerRadius={innerRadius}
120
+ outerRadius={outerRadius}
121
+ paddingAngle={paddingAngle}
122
+ startAngle={startAngle}
123
+ endAngle={endAngle}
124
+ isAnimationActive={isAnimated}
125
+ animationDuration={isAnimated ? 400 : 0}
126
+ label={renderLabel}
127
+ labelLine={false}
128
+ stroke="hsl(var(--card))"
129
+ strokeWidth={2}
130
+ >
131
+ {data.map((entry, index) => (
132
+ <Cell
133
+ key={entry.name}
134
+ fill={getChartColor(index, entry.color)}
135
+ />
136
+ ))}
137
+ </Pie>
138
+ {tooltipProps && <Tooltip {...tooltipProps} />}
139
+ {legendConfig && (
140
+ <Legend
141
+ verticalAlign={legendConfig.position ?? "bottom"}
142
+ align={legendConfig.align ?? "center"}
143
+ content={<ChartLegendContent />}
144
+ />
145
+ )}
146
+ </RechartsPieChart>
147
+ </ChartContainer>
148
+ );
149
+ },
150
+ );
151
+
152
+ PieChart.displayName = "PieChart";
153
+ export { PieChart };