xertica-ui 2.1.2 → 2.1.4
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/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/bin/cli.ts +1 -1
- package/bin/generate-tokens.ts +13 -7
- package/components/assistant/xertica-assistant/index.ts +2 -0
- package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
- package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
- package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
- package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
- package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
- package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
- package/components/assistant/xertica-assistant/parts/index.ts +16 -0
- package/components/assistant/xertica-assistant/types.ts +139 -0
- package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
- package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
- package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
- package/components/hooks/index.ts +3 -0
- package/components/hooks/use-layout-shortcuts.ts +46 -0
- package/components/layout/sidebar/index.ts +2 -0
- package/components/layout/sidebar/sidebar.stories.tsx +160 -8
- package/components/layout/sidebar/sidebar.tsx +606 -497
- package/components/layout/sidebar/use-sidebar.ts +104 -0
- package/components/media/audio-player/AudioPlayer.tsx +131 -206
- package/components/media/audio-player/use-audio-player.ts +298 -0
- package/components/pages/home-page/HomePage.tsx +1 -1
- package/components/pages/template-content/TemplateContent.tsx +5 -5
- package/components/pages/template-page/TemplatePage.tsx +5 -5
- package/components/shared/CustomTooltipContent.tsx +52 -0
- package/components/shared/layout-constants.ts +1 -1
- package/components/ui/chart/chart.stories.tsx +966 -7
- package/components/ui/chart/chart.tsx +918 -45
- package/components/ui/file-upload/file-upload.stories.tsx +100 -0
- package/components/ui/file-upload/file-upload.tsx +14 -74
- package/components/ui/file-upload/index.ts +1 -0
- package/components/ui/file-upload/use-file-upload.ts +181 -0
- package/components/ui/pagination/index.ts +2 -0
- package/components/ui/pagination/pagination.stories.tsx +94 -0
- package/components/ui/pagination/use-pagination.ts +194 -0
- package/components/ui/rich-text-editor/index.ts +2 -0
- package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
- package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
- package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
- package/components/ui/stepper/index.ts +3 -1
- package/components/ui/stepper/stepper.stories.tsx +116 -0
- package/components/ui/stepper/stepper.tsx +4 -4
- package/components/ui/stepper/use-stepper.ts +137 -0
- package/components/ui/tree-view/index.ts +4 -1
- package/components/ui/tree-view/tree-view.stories.tsx +110 -4
- package/components/ui/tree-view/tree-view.tsx +17 -125
- package/components/ui/tree-view/use-tree-view.ts +229 -0
- package/contexts/AssistenteContext.tsx +17 -54
- package/contexts/BrandColorsContext.tsx +6 -17
- package/contexts/LayoutContext.tsx +5 -31
- package/dist/AssistantChart-BAudAfne.cjs +3591 -0
- package/dist/AssistantChart-BP8upjMk.js +3565 -0
- package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
- package/dist/AudioPlayer-DuKXrCfy.js +937 -0
- package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
- package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
- package/dist/LanguageContext-BwhwC3G2.js +657 -0
- package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
- package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
- package/dist/LayoutContext-dbQvdC4O.js +85 -0
- package/dist/ThemeContext-RTy1m2Uq.js +82 -0
- package/dist/ThemeContext-bSzuOit2.cjs +81 -0
- package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
- package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
- package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
- package/dist/XerticaProvider-siSt9uG2.js +40 -0
- package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
- package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
- package/dist/assistant.cjs.js +2 -1
- package/dist/assistant.es.js +3 -2
- package/dist/brand.cjs.js +2 -2
- package/dist/brand.es.js +2 -2
- package/dist/cli.js +14 -8
- package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
- package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
- package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
- package/dist/components/hooks/index.d.ts +3 -0
- package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
- package/dist/components/layout/sidebar/index.d.ts +2 -0
- package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
- package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
- package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
- package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
- package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
- package/dist/components/shared/layout-constants.d.ts +1 -1
- package/dist/components/ui/alert/alert.d.ts +1 -1
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts +2 -2
- package/dist/components/ui/chart/chart.d.ts +162 -5
- package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
- package/dist/components/ui/file-upload/index.d.ts +1 -0
- package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
- package/dist/components/ui/pagination/index.d.ts +2 -0
- package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
- package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
- package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
- package/dist/components/ui/stepper/index.d.ts +3 -1
- package/dist/components/ui/stepper/stepper.d.ts +2 -2
- package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
- package/dist/components/ui/tree-view/index.d.ts +4 -1
- package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
- package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
- package/dist/contexts/AssistenteContext.d.ts +10 -49
- package/dist/hooks.cjs.js +30 -10
- package/dist/hooks.es.js +25 -4
- package/dist/index.cjs.js +20 -9
- package/dist/index.es.js +38 -27
- package/dist/layout.cjs.js +82 -1
- package/dist/layout.es.js +83 -2
- package/dist/media.cjs.js +1 -1
- package/dist/media.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/rich-text-editor-BmsjY03B.js +2949 -0
- package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
- package/dist/sidebar-CVUGHOS_.cjs +756 -0
- package/dist/sidebar-CmvwjnVb.js +757 -0
- package/dist/ui.cjs.js +12 -2
- package/dist/ui.es.js +24 -14
- package/dist/use-audio-player-Bkh23vQ3.js +177 -0
- package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
- package/dist/utils/color-utils.d.ts +51 -0
- package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
- package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/ai-usage.md +28 -10
- package/docs/architecture-improvements.md +463 -0
- package/docs/architecture.md +77 -1
- package/docs/components/assistant-chart.md +1 -1
- package/docs/components/assistant.md +159 -0
- package/docs/components/audio-player.md +46 -0
- package/docs/components/branding.md +251 -0
- package/docs/components/chart.md +354 -39
- package/docs/components/code-block.md +108 -0
- package/docs/components/file-upload.md +119 -2
- package/docs/components/formatted-document.md +113 -0
- package/docs/components/hooks.md +430 -0
- package/docs/components/image-with-fallback.md +106 -0
- package/docs/components/map-layers.md +140 -0
- package/docs/components/modern-chat-input.md +163 -0
- package/docs/components/pages.md +351 -0
- package/docs/components/pagination.md +187 -0
- package/docs/components/rich-text-editor.md +164 -0
- package/docs/components/sidebar.md +153 -4
- package/docs/components/stepper.md +157 -12
- package/docs/components/tree-view.md +164 -6
- package/docs/doc-audit.md +223 -0
- package/docs/getting-started.md +155 -1
- package/docs/guidelines.md +14 -8
- package/docs/layout.md +2 -2
- package/docs/llms.md +29 -9
- package/docs/patterns/detail-page.md +276 -0
- package/docs/patterns/settings.md +346 -0
- package/docs/patterns/wizard.md +217 -0
- package/guidelines/Guidelines.md +5 -3
- package/llms.txt +1 -1
- package/package.json +10 -10
- package/styles/xertica/tokens.css +41 -12
- package/templates/CLAUDE.md +16 -6
- package/templates/guidelines/Guidelines.md +16 -4
- package/templates/package.json +3 -3
- package/templates/src/styles/xertica/tokens.css +39 -10
- package/utils/color-utils.ts +72 -0
|
@@ -71,6 +71,16 @@ export type ChartPeriod = {
|
|
|
71
71
|
|
|
72
72
|
export type ChartBarSize = "sm" | "md" | "lg" | "xl" | number;
|
|
73
73
|
|
|
74
|
+
/** Curve interpolation type for line and area charts */
|
|
75
|
+
export type ChartCurveType =
|
|
76
|
+
| "monotone"
|
|
77
|
+
| "linear"
|
|
78
|
+
| "step"
|
|
79
|
+
| "stepBefore"
|
|
80
|
+
| "stepAfter"
|
|
81
|
+
| "natural"
|
|
82
|
+
| "basis";
|
|
83
|
+
|
|
74
84
|
type ChartValueFormatter = (value: number | string) => string;
|
|
75
85
|
|
|
76
86
|
export type ChartErrorState = boolean | string | Error | React.ReactNode;
|
|
@@ -100,6 +110,9 @@ const defaultChartColors = [
|
|
|
100
110
|
"var(--chart-3)",
|
|
101
111
|
"var(--chart-4)",
|
|
102
112
|
"var(--chart-5)",
|
|
113
|
+
"var(--chart-6)",
|
|
114
|
+
"var(--chart-7)",
|
|
115
|
+
"var(--chart-8)",
|
|
103
116
|
];
|
|
104
117
|
|
|
105
118
|
const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
|
|
@@ -188,13 +201,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
|
188
201
|
([theme, prefix]) => `
|
|
189
202
|
${prefix} [data-chart=${id}] {
|
|
190
203
|
${colorConfig
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
.map(([key, itemConfig]) => {
|
|
205
|
+
const color =
|
|
206
|
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
207
|
+
itemConfig.color;
|
|
208
|
+
return color ? ` --color-${key}: ${color};` : null;
|
|
209
|
+
})
|
|
210
|
+
.join("\n")}
|
|
198
211
|
}
|
|
199
212
|
`,
|
|
200
213
|
)
|
|
@@ -275,7 +288,7 @@ function ChartTooltipContent({
|
|
|
275
288
|
return (
|
|
276
289
|
<div
|
|
277
290
|
className={cn(
|
|
278
|
-
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-
|
|
291
|
+
"border-border/50 bg-background/95 backdrop-blur-sm grid min-w-[8rem] items-start gap-1.5 rounded-xl border px-3 py-2 text-xs shadow-xl",
|
|
279
292
|
className,
|
|
280
293
|
)}
|
|
281
294
|
>
|
|
@@ -335,7 +348,7 @@ function ChartTooltipContent({
|
|
|
335
348
|
</span>
|
|
336
349
|
</div>
|
|
337
350
|
{item.value && (
|
|
338
|
-
<span className="text-foreground font-mono font-
|
|
351
|
+
<span className="text-foreground font-mono font-semibold tabular-nums">
|
|
339
352
|
{item.value.toLocaleString()}
|
|
340
353
|
</span>
|
|
341
354
|
)}
|
|
@@ -392,13 +405,13 @@ function ChartLegendContent({
|
|
|
392
405
|
<itemConfig.icon />
|
|
393
406
|
) : (
|
|
394
407
|
<div
|
|
395
|
-
className="h-2 w-2 shrink-0 rounded-
|
|
408
|
+
className="h-2 w-2 shrink-0 rounded-full bg-(--color-bg)"
|
|
396
409
|
style={{
|
|
397
410
|
"--color-bg": item.color || `var(--color-${key})`,
|
|
398
411
|
} as React.CSSProperties}
|
|
399
412
|
/>
|
|
400
413
|
)}
|
|
401
|
-
<span>{itemConfig?.label || item.value}</span>
|
|
414
|
+
<span className="text-muted-foreground text-xs">{itemConfig?.label || item.value}</span>
|
|
402
415
|
</div>
|
|
403
416
|
);
|
|
404
417
|
})}
|
|
@@ -418,8 +431,8 @@ function getPayloadConfigFromPayload(
|
|
|
418
431
|
|
|
419
432
|
const payloadPayload =
|
|
420
433
|
"payload" in payload &&
|
|
421
|
-
|
|
422
|
-
|
|
434
|
+
typeof payload.payload === "object" &&
|
|
435
|
+
payload.payload !== null
|
|
423
436
|
? payload.payload
|
|
424
437
|
: undefined;
|
|
425
438
|
|
|
@@ -494,6 +507,20 @@ function getChartConfigWithColors(
|
|
|
494
507
|
}, {});
|
|
495
508
|
}
|
|
496
509
|
|
|
510
|
+
/** Build a ChartConfig from a list of keys (no pre-existing config needed). */
|
|
511
|
+
function buildChartConfig(
|
|
512
|
+
keys: string[],
|
|
513
|
+
colors?: DashboardChartColors,
|
|
514
|
+
): ChartConfig {
|
|
515
|
+
return keys.reduce<ChartConfig>((cfg, key, index) => {
|
|
516
|
+
cfg[key] = {
|
|
517
|
+
label: key,
|
|
518
|
+
color: getSeriesColor(key, index, colors),
|
|
519
|
+
};
|
|
520
|
+
return cfg;
|
|
521
|
+
}, {});
|
|
522
|
+
}
|
|
523
|
+
|
|
497
524
|
function formatTick(value: number | string) {
|
|
498
525
|
if (typeof value !== "number") {
|
|
499
526
|
return value;
|
|
@@ -700,9 +727,51 @@ function ChartCard({
|
|
|
700
727
|
);
|
|
701
728
|
}
|
|
702
729
|
|
|
730
|
+
// ─── Gradient defs helper ────────────────────────────────────────────────────
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Renders SVG `<defs>` with linear gradients for each series key.
|
|
734
|
+
* Used internally by area charts when `gradientFill` is enabled.
|
|
735
|
+
*/
|
|
736
|
+
function AreaGradientDefs({
|
|
737
|
+
seriesKeys,
|
|
738
|
+
opacity = 0.3,
|
|
739
|
+
}: {
|
|
740
|
+
seriesKeys: string[];
|
|
741
|
+
opacity?: number;
|
|
742
|
+
}) {
|
|
743
|
+
return (
|
|
744
|
+
<defs>
|
|
745
|
+
{seriesKeys.map((key) => (
|
|
746
|
+
<linearGradient
|
|
747
|
+
key={key}
|
|
748
|
+
id={`gradient-${key}`}
|
|
749
|
+
x1="0"
|
|
750
|
+
y1="0"
|
|
751
|
+
x2="0"
|
|
752
|
+
y2="1"
|
|
753
|
+
>
|
|
754
|
+
<stop
|
|
755
|
+
offset="5%"
|
|
756
|
+
stopColor={`var(--color-${key})`}
|
|
757
|
+
stopOpacity={opacity}
|
|
758
|
+
/>
|
|
759
|
+
<stop
|
|
760
|
+
offset="95%"
|
|
761
|
+
stopColor={`var(--color-${key})`}
|
|
762
|
+
stopOpacity={0}
|
|
763
|
+
/>
|
|
764
|
+
</linearGradient>
|
|
765
|
+
))}
|
|
766
|
+
</defs>
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ─── DashboardBarChart ────────────────────────────────────────────────────────
|
|
771
|
+
|
|
703
772
|
export interface DashboardBarChartProps
|
|
704
773
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
705
|
-
|
|
774
|
+
ChartStateProps {
|
|
706
775
|
data: DashboardChartDatum[];
|
|
707
776
|
indexKey?: string;
|
|
708
777
|
series?: DashboardChartSeries[];
|
|
@@ -770,9 +839,14 @@ function DashboardBarChart({
|
|
|
770
839
|
className={cn("h-[320px] w-full", className)}
|
|
771
840
|
{...props}
|
|
772
841
|
>
|
|
773
|
-
<RechartsPrimitive.BarChart data={data} accessibilityLayer>
|
|
842
|
+
<RechartsPrimitive.BarChart data={data} accessibilityLayer barGap={4}>
|
|
774
843
|
{showGrid ? (
|
|
775
|
-
<RechartsPrimitive.CartesianGrid
|
|
844
|
+
<RechartsPrimitive.CartesianGrid
|
|
845
|
+
vertical={false}
|
|
846
|
+
strokeDasharray="3 3"
|
|
847
|
+
stroke="var(--border)"
|
|
848
|
+
strokeOpacity={0.5}
|
|
849
|
+
/>
|
|
776
850
|
) : null}
|
|
777
851
|
<RechartsPrimitive.XAxis
|
|
778
852
|
dataKey={indexKey}
|
|
@@ -786,7 +860,10 @@ function DashboardBarChart({
|
|
|
786
860
|
tickMargin={8}
|
|
787
861
|
tickFormatter={valueFormatter}
|
|
788
862
|
/>
|
|
789
|
-
<ChartTooltip
|
|
863
|
+
<ChartTooltip
|
|
864
|
+
cursor={{ fill: "var(--muted)", opacity: 0.4, radius: 4 }}
|
|
865
|
+
content={<ChartTooltipContent />}
|
|
866
|
+
/>
|
|
790
867
|
{showLegend ? (
|
|
791
868
|
<ChartLegend content={<ChartLegendContent />} />
|
|
792
869
|
) : null}
|
|
@@ -808,6 +885,9 @@ function DashboardBarChart({
|
|
|
808
885
|
radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
|
|
809
886
|
barSize={getBarSize(barSize)}
|
|
810
887
|
stackId={stacked ? item.stackId || "total" : item.stackId}
|
|
888
|
+
isAnimationActive
|
|
889
|
+
animationDuration={600}
|
|
890
|
+
animationEasing="ease-out"
|
|
811
891
|
/>
|
|
812
892
|
);
|
|
813
893
|
});
|
|
@@ -817,9 +897,11 @@ function DashboardBarChart({
|
|
|
817
897
|
);
|
|
818
898
|
}
|
|
819
899
|
|
|
900
|
+
// ─── DashboardLineChart ───────────────────────────────────────────────────────
|
|
901
|
+
|
|
820
902
|
export interface DashboardLineChartProps
|
|
821
903
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
822
|
-
|
|
904
|
+
ChartStateProps {
|
|
823
905
|
data: DashboardChartDatum[];
|
|
824
906
|
indexKey?: string;
|
|
825
907
|
series?: DashboardChartSeries[];
|
|
@@ -827,6 +909,8 @@ export interface DashboardLineChartProps
|
|
|
827
909
|
showDots?: boolean;
|
|
828
910
|
showGrid?: boolean;
|
|
829
911
|
showLegend?: boolean;
|
|
912
|
+
curveType?: ChartCurveType;
|
|
913
|
+
strokeWidth?: number;
|
|
830
914
|
valueFormatter?: ChartValueFormatter;
|
|
831
915
|
}
|
|
832
916
|
|
|
@@ -839,6 +923,8 @@ function DashboardLineChart({
|
|
|
839
923
|
showDots = false,
|
|
840
924
|
showGrid = true,
|
|
841
925
|
showLegend = true,
|
|
926
|
+
curveType = "monotone",
|
|
927
|
+
strokeWidth = 2,
|
|
842
928
|
valueFormatter = formatTick,
|
|
843
929
|
isLoading,
|
|
844
930
|
error,
|
|
@@ -887,7 +973,12 @@ function DashboardLineChart({
|
|
|
887
973
|
>
|
|
888
974
|
<RechartsPrimitive.LineChart data={data} accessibilityLayer>
|
|
889
975
|
{showGrid ? (
|
|
890
|
-
<RechartsPrimitive.CartesianGrid
|
|
976
|
+
<RechartsPrimitive.CartesianGrid
|
|
977
|
+
vertical={false}
|
|
978
|
+
strokeDasharray="3 3"
|
|
979
|
+
stroke="var(--border)"
|
|
980
|
+
strokeOpacity={0.5}
|
|
981
|
+
/>
|
|
891
982
|
) : null}
|
|
892
983
|
<RechartsPrimitive.XAxis
|
|
893
984
|
dataKey={indexKey}
|
|
@@ -908,13 +999,16 @@ function DashboardLineChart({
|
|
|
908
999
|
{chartSeries.map((item) => (
|
|
909
1000
|
<RechartsPrimitive.Line
|
|
910
1001
|
key={item.key}
|
|
911
|
-
type=
|
|
1002
|
+
type={curveType}
|
|
912
1003
|
dataKey={item.key}
|
|
913
1004
|
stroke={`var(--color-${item.key})`}
|
|
914
|
-
strokeWidth={
|
|
915
|
-
dot={showDots ? { r: 3, fill: `var(--color-${item.key})
|
|
916
|
-
activeDot={{ r: 5 }}
|
|
1005
|
+
strokeWidth={strokeWidth}
|
|
1006
|
+
dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
|
|
1007
|
+
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
917
1008
|
yAxisId={item.yAxisId}
|
|
1009
|
+
isAnimationActive
|
|
1010
|
+
animationDuration={600}
|
|
1011
|
+
animationEasing="ease-out"
|
|
918
1012
|
/>
|
|
919
1013
|
))}
|
|
920
1014
|
</RechartsPrimitive.LineChart>
|
|
@@ -922,9 +1016,11 @@ function DashboardLineChart({
|
|
|
922
1016
|
);
|
|
923
1017
|
}
|
|
924
1018
|
|
|
1019
|
+
// ─── HorizontalBarChart ───────────────────────────────────────────────────────
|
|
1020
|
+
|
|
925
1021
|
export interface HorizontalBarChartProps
|
|
926
1022
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
927
|
-
|
|
1023
|
+
ChartStateProps {
|
|
928
1024
|
data: DashboardChartDatum[];
|
|
929
1025
|
indexKey?: string;
|
|
930
1026
|
series?: DashboardChartSeries[];
|
|
@@ -1001,7 +1097,12 @@ function HorizontalBarChart({
|
|
|
1001
1097
|
margin={{ left: 8, right: 16 }}
|
|
1002
1098
|
>
|
|
1003
1099
|
{showGrid ? (
|
|
1004
|
-
<RechartsPrimitive.CartesianGrid
|
|
1100
|
+
<RechartsPrimitive.CartesianGrid
|
|
1101
|
+
horizontal={false}
|
|
1102
|
+
strokeDasharray="3 3"
|
|
1103
|
+
stroke="var(--border)"
|
|
1104
|
+
strokeOpacity={0.5}
|
|
1105
|
+
/>
|
|
1005
1106
|
) : null}
|
|
1006
1107
|
<RechartsPrimitive.XAxis
|
|
1007
1108
|
type="number"
|
|
@@ -1030,6 +1131,9 @@ function HorizontalBarChart({
|
|
|
1030
1131
|
radius={[0, 4, 4, 0]}
|
|
1031
1132
|
barSize={getBarSize(barSize)}
|
|
1032
1133
|
stackId={stacked ? item.stackId || "total" : item.stackId}
|
|
1134
|
+
isAnimationActive
|
|
1135
|
+
animationDuration={600}
|
|
1136
|
+
animationEasing="ease-out"
|
|
1033
1137
|
/>
|
|
1034
1138
|
))}
|
|
1035
1139
|
</RechartsPrimitive.BarChart>
|
|
@@ -1037,9 +1141,11 @@ function HorizontalBarChart({
|
|
|
1037
1141
|
);
|
|
1038
1142
|
}
|
|
1039
1143
|
|
|
1144
|
+
// ─── InteractiveTimeSeriesChart ───────────────────────────────────────────────
|
|
1145
|
+
|
|
1040
1146
|
export interface InteractiveTimeSeriesChartProps
|
|
1041
1147
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
1042
|
-
|
|
1148
|
+
ChartStateProps {
|
|
1043
1149
|
data: DashboardChartDatum[];
|
|
1044
1150
|
indexKey?: string;
|
|
1045
1151
|
series?: DashboardChartSeries[];
|
|
@@ -1053,6 +1159,10 @@ export interface InteractiveTimeSeriesChartProps
|
|
|
1053
1159
|
) => DashboardChartDatum[];
|
|
1054
1160
|
showGrid?: boolean;
|
|
1055
1161
|
showLegend?: boolean;
|
|
1162
|
+
showDots?: boolean;
|
|
1163
|
+
gradientFill?: boolean;
|
|
1164
|
+
curveType?: ChartCurveType;
|
|
1165
|
+
strokeWidth?: number;
|
|
1056
1166
|
valueFormatter?: ChartValueFormatter;
|
|
1057
1167
|
}
|
|
1058
1168
|
|
|
@@ -1068,6 +1178,10 @@ function InteractiveTimeSeriesChart({
|
|
|
1068
1178
|
filterData = defaultFilterData,
|
|
1069
1179
|
showGrid = true,
|
|
1070
1180
|
showLegend = false,
|
|
1181
|
+
showDots = false,
|
|
1182
|
+
gradientFill = true,
|
|
1183
|
+
curveType = "monotone",
|
|
1184
|
+
strokeWidth = 2,
|
|
1071
1185
|
valueFormatter = formatTick,
|
|
1072
1186
|
isLoading,
|
|
1073
1187
|
error,
|
|
@@ -1121,7 +1235,7 @@ function InteractiveTimeSeriesChart({
|
|
|
1121
1235
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1122
1236
|
{chartSeries.length > 1 ? (
|
|
1123
1237
|
<div
|
|
1124
|
-
className="inline-flex h-10 items-center justify-center rounded-
|
|
1238
|
+
className="inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
|
|
1125
1239
|
role="group"
|
|
1126
1240
|
aria-label="Selecionar metrica do grafico"
|
|
1127
1241
|
>
|
|
@@ -1137,7 +1251,7 @@ function InteractiveTimeSeriesChart({
|
|
|
1137
1251
|
aria-pressed={activeSeries === item.key}
|
|
1138
1252
|
onClick={() => setActiveSeries(item.key)}
|
|
1139
1253
|
className={cn(
|
|
1140
|
-
"h-8 px-3 text-sm font-medium",
|
|
1254
|
+
"h-8 px-3 text-sm font-medium transition-all",
|
|
1141
1255
|
activeSeries === item.key
|
|
1142
1256
|
? "bg-background text-foreground shadow-sm"
|
|
1143
1257
|
: "text-muted-foreground hover:text-foreground",
|
|
@@ -1171,8 +1285,19 @@ function InteractiveTimeSeriesChart({
|
|
|
1171
1285
|
{...props}
|
|
1172
1286
|
>
|
|
1173
1287
|
<RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
|
|
1288
|
+
{gradientFill && (
|
|
1289
|
+
<AreaGradientDefs
|
|
1290
|
+
seriesKeys={visibleSeries.map((s) => s.key)}
|
|
1291
|
+
opacity={0.4}
|
|
1292
|
+
/>
|
|
1293
|
+
)}
|
|
1174
1294
|
{showGrid ? (
|
|
1175
|
-
<RechartsPrimitive.CartesianGrid
|
|
1295
|
+
<RechartsPrimitive.CartesianGrid
|
|
1296
|
+
vertical={false}
|
|
1297
|
+
strokeDasharray="3 3"
|
|
1298
|
+
stroke="var(--border)"
|
|
1299
|
+
strokeOpacity={0.5}
|
|
1300
|
+
/>
|
|
1176
1301
|
) : null}
|
|
1177
1302
|
<RechartsPrimitive.XAxis
|
|
1178
1303
|
dataKey={indexKey}
|
|
@@ -1193,13 +1318,21 @@ function InteractiveTimeSeriesChart({
|
|
|
1193
1318
|
{visibleSeries.map((item) => (
|
|
1194
1319
|
<RechartsPrimitive.Area
|
|
1195
1320
|
key={item.key}
|
|
1196
|
-
type=
|
|
1321
|
+
type={curveType}
|
|
1197
1322
|
dataKey={item.key}
|
|
1198
1323
|
stroke={`var(--color-${item.key})`}
|
|
1199
|
-
fill={
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1324
|
+
fill={
|
|
1325
|
+
gradientFill
|
|
1326
|
+
? `url(#gradient-${item.key})`
|
|
1327
|
+
: `var(--color-${item.key})`
|
|
1328
|
+
}
|
|
1329
|
+
fillOpacity={gradientFill ? 1 : 0.16}
|
|
1330
|
+
strokeWidth={strokeWidth}
|
|
1331
|
+
dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
|
|
1332
|
+
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
1333
|
+
isAnimationActive
|
|
1334
|
+
animationDuration={600}
|
|
1335
|
+
animationEasing="ease-out"
|
|
1203
1336
|
/>
|
|
1204
1337
|
))}
|
|
1205
1338
|
</RechartsPrimitive.AreaChart>
|
|
@@ -1209,9 +1342,11 @@ function InteractiveTimeSeriesChart({
|
|
|
1209
1342
|
);
|
|
1210
1343
|
}
|
|
1211
1344
|
|
|
1345
|
+
// ─── ComboMetricChart ─────────────────────────────────────────────────────────
|
|
1346
|
+
|
|
1212
1347
|
export interface ComboMetricChartProps
|
|
1213
1348
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
1214
|
-
|
|
1349
|
+
ChartStateProps {
|
|
1215
1350
|
data: DashboardChartDatum[];
|
|
1216
1351
|
indexKey?: string;
|
|
1217
1352
|
series?: DashboardChartSeries[];
|
|
@@ -1219,6 +1354,8 @@ export interface ComboMetricChartProps
|
|
|
1219
1354
|
barSize?: ChartBarSize;
|
|
1220
1355
|
showGrid?: boolean;
|
|
1221
1356
|
showLegend?: boolean;
|
|
1357
|
+
gradientFill?: boolean;
|
|
1358
|
+
curveType?: ChartCurveType;
|
|
1222
1359
|
valueFormatter?: ChartValueFormatter;
|
|
1223
1360
|
}
|
|
1224
1361
|
|
|
@@ -1231,6 +1368,8 @@ function ComboMetricChart({
|
|
|
1231
1368
|
barSize = "md",
|
|
1232
1369
|
showGrid = true,
|
|
1233
1370
|
showLegend = true,
|
|
1371
|
+
gradientFill = false,
|
|
1372
|
+
curveType = "monotone",
|
|
1234
1373
|
valueFormatter = formatTick,
|
|
1235
1374
|
isLoading,
|
|
1236
1375
|
error,
|
|
@@ -1271,6 +1410,10 @@ function ComboMetricChart({
|
|
|
1271
1410
|
return chartState;
|
|
1272
1411
|
}
|
|
1273
1412
|
|
|
1413
|
+
const areaSeries = gradientFill
|
|
1414
|
+
? chartSeries.filter((s) => s.type === "area").map((s) => s.key)
|
|
1415
|
+
: [];
|
|
1416
|
+
|
|
1274
1417
|
return (
|
|
1275
1418
|
<ChartContainer
|
|
1276
1419
|
config={chartConfig}
|
|
@@ -1278,8 +1421,16 @@ function ComboMetricChart({
|
|
|
1278
1421
|
{...props}
|
|
1279
1422
|
>
|
|
1280
1423
|
<RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
|
|
1424
|
+
{gradientFill && areaSeries.length > 0 && (
|
|
1425
|
+
<AreaGradientDefs seriesKeys={areaSeries} opacity={0.35} />
|
|
1426
|
+
)}
|
|
1281
1427
|
{showGrid ? (
|
|
1282
|
-
<RechartsPrimitive.CartesianGrid
|
|
1428
|
+
<RechartsPrimitive.CartesianGrid
|
|
1429
|
+
vertical={false}
|
|
1430
|
+
strokeDasharray="3 3"
|
|
1431
|
+
stroke="var(--border)"
|
|
1432
|
+
strokeOpacity={0.5}
|
|
1433
|
+
/>
|
|
1283
1434
|
) : null}
|
|
1284
1435
|
<RechartsPrimitive.XAxis
|
|
1285
1436
|
dataKey={indexKey}
|
|
@@ -1301,24 +1452,36 @@ function ComboMetricChart({
|
|
|
1301
1452
|
item.type === "line" ? (
|
|
1302
1453
|
<RechartsPrimitive.Line
|
|
1303
1454
|
key={item.key}
|
|
1304
|
-
type=
|
|
1455
|
+
type={curveType}
|
|
1305
1456
|
dataKey={item.key}
|
|
1306
1457
|
stroke={`var(--color-${item.key})`}
|
|
1307
1458
|
strokeWidth={2}
|
|
1308
1459
|
dot={false}
|
|
1460
|
+
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
1309
1461
|
yAxisId={item.yAxisId}
|
|
1462
|
+
isAnimationActive
|
|
1463
|
+
animationDuration={600}
|
|
1464
|
+
animationEasing="ease-out"
|
|
1310
1465
|
/>
|
|
1311
1466
|
) : item.type === "area" ? (
|
|
1312
1467
|
<RechartsPrimitive.Area
|
|
1313
1468
|
key={item.key}
|
|
1314
|
-
type=
|
|
1469
|
+
type={curveType}
|
|
1315
1470
|
dataKey={item.key}
|
|
1316
1471
|
stroke={`var(--color-${item.key})`}
|
|
1317
|
-
fill={
|
|
1318
|
-
|
|
1472
|
+
fill={
|
|
1473
|
+
gradientFill
|
|
1474
|
+
? `url(#gradient-${item.key})`
|
|
1475
|
+
: `var(--color-${item.key})`
|
|
1476
|
+
}
|
|
1477
|
+
fillOpacity={gradientFill ? 1 : 0.12}
|
|
1319
1478
|
strokeWidth={2}
|
|
1320
1479
|
dot={false}
|
|
1480
|
+
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
1321
1481
|
yAxisId={item.yAxisId}
|
|
1482
|
+
isAnimationActive
|
|
1483
|
+
animationDuration={600}
|
|
1484
|
+
animationEasing="ease-out"
|
|
1322
1485
|
/>
|
|
1323
1486
|
) : (
|
|
1324
1487
|
<RechartsPrimitive.Bar
|
|
@@ -1328,6 +1491,9 @@ function ComboMetricChart({
|
|
|
1328
1491
|
radius={[4, 4, 0, 0]}
|
|
1329
1492
|
barSize={getBarSize(barSize)}
|
|
1330
1493
|
yAxisId={item.yAxisId}
|
|
1494
|
+
isAnimationActive
|
|
1495
|
+
animationDuration={600}
|
|
1496
|
+
animationEasing="ease-out"
|
|
1331
1497
|
/>
|
|
1332
1498
|
),
|
|
1333
1499
|
)}
|
|
@@ -1336,9 +1502,11 @@ function ComboMetricChart({
|
|
|
1336
1502
|
);
|
|
1337
1503
|
}
|
|
1338
1504
|
|
|
1505
|
+
// ─── DonutBreakdownChart ──────────────────────────────────────────────────────
|
|
1506
|
+
|
|
1339
1507
|
export interface DonutBreakdownChartProps
|
|
1340
1508
|
extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
|
|
1341
|
-
|
|
1509
|
+
ChartStateProps {
|
|
1342
1510
|
data: DashboardChartDatum[];
|
|
1343
1511
|
nameKey?: string;
|
|
1344
1512
|
valueKey?: string;
|
|
@@ -1346,6 +1514,10 @@ export interface DonutBreakdownChartProps
|
|
|
1346
1514
|
centerLabel?: React.ReactNode;
|
|
1347
1515
|
centerValue?: React.ReactNode;
|
|
1348
1516
|
showLegend?: boolean;
|
|
1517
|
+
/** Inner radius as percentage string (e.g. "58%") or number */
|
|
1518
|
+
innerRadius?: string | number;
|
|
1519
|
+
/** Outer radius as percentage string (e.g. "82%") or number */
|
|
1520
|
+
outerRadius?: string | number;
|
|
1349
1521
|
}
|
|
1350
1522
|
|
|
1351
1523
|
function DonutBreakdownChart({
|
|
@@ -1357,6 +1529,8 @@ function DonutBreakdownChart({
|
|
|
1357
1529
|
centerLabel,
|
|
1358
1530
|
centerValue,
|
|
1359
1531
|
showLegend = true,
|
|
1532
|
+
innerRadius = "58%",
|
|
1533
|
+
outerRadius = "82%",
|
|
1360
1534
|
isLoading,
|
|
1361
1535
|
error,
|
|
1362
1536
|
onRetry,
|
|
@@ -1416,11 +1590,14 @@ function DonutBreakdownChart({
|
|
|
1416
1590
|
data={data}
|
|
1417
1591
|
dataKey={valueKey}
|
|
1418
1592
|
nameKey={nameKey}
|
|
1419
|
-
innerRadius=
|
|
1420
|
-
outerRadius=
|
|
1421
|
-
paddingAngle={
|
|
1593
|
+
innerRadius={innerRadius}
|
|
1594
|
+
outerRadius={outerRadius}
|
|
1595
|
+
paddingAngle={3}
|
|
1422
1596
|
activeIndex={activeIndex}
|
|
1423
1597
|
onMouseEnter={(_, index) => setActiveIndex(index)}
|
|
1598
|
+
isAnimationActive
|
|
1599
|
+
animationDuration={600}
|
|
1600
|
+
animationEasing="ease-out"
|
|
1424
1601
|
>
|
|
1425
1602
|
{data.map((entry, index) => {
|
|
1426
1603
|
const key = String(entry[nameKey] || `segment-${index}`);
|
|
@@ -1429,7 +1606,8 @@ function DonutBreakdownChart({
|
|
|
1429
1606
|
<RechartsPrimitive.Cell
|
|
1430
1607
|
key={key}
|
|
1431
1608
|
fill={`var(--color-${key})`}
|
|
1432
|
-
opacity={index === activeIndex ? 1 : 0.
|
|
1609
|
+
opacity={index === activeIndex ? 1 : 0.7}
|
|
1610
|
+
stroke="transparent"
|
|
1433
1611
|
/>
|
|
1434
1612
|
);
|
|
1435
1613
|
})}
|
|
@@ -1477,6 +1655,696 @@ function DonutBreakdownChart({
|
|
|
1477
1655
|
);
|
|
1478
1656
|
}
|
|
1479
1657
|
|
|
1658
|
+
// ─── SparklineChart ───────────────────────────────────────────────────────────
|
|
1659
|
+
|
|
1660
|
+
export interface SparklineChartProps {
|
|
1661
|
+
/** Data array — each item must have the `dataKey` field */
|
|
1662
|
+
data: DashboardChartDatum[];
|
|
1663
|
+
/** Key to plot on the Y axis */
|
|
1664
|
+
dataKey: string;
|
|
1665
|
+
/** Color for the line/area — defaults to `var(--chart-1)` */
|
|
1666
|
+
color?: string;
|
|
1667
|
+
/** Show filled area under the line */
|
|
1668
|
+
filled?: boolean;
|
|
1669
|
+
/** Show the last data point as a dot */
|
|
1670
|
+
showEndDot?: boolean;
|
|
1671
|
+
/** Curve interpolation type */
|
|
1672
|
+
curveType?: ChartCurveType;
|
|
1673
|
+
/** Stroke width */
|
|
1674
|
+
strokeWidth?: number;
|
|
1675
|
+
className?: string;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Minimal inline sparkline chart — no axes, no grid, no tooltip.
|
|
1680
|
+
* Ideal for embedding inside stat cards or table cells.
|
|
1681
|
+
*
|
|
1682
|
+
* @ai-rules
|
|
1683
|
+
* - Use `filled` for area-style sparklines.
|
|
1684
|
+
* - Keep `className` height small (e.g. `h-12` or `h-16`).
|
|
1685
|
+
* - Do NOT wrap in `ChartCard` — it is designed to be inline.
|
|
1686
|
+
*/
|
|
1687
|
+
function SparklineChart({
|
|
1688
|
+
data,
|
|
1689
|
+
dataKey,
|
|
1690
|
+
color = "var(--chart-1)",
|
|
1691
|
+
filled = true,
|
|
1692
|
+
showEndDot = true,
|
|
1693
|
+
curveType = "monotone",
|
|
1694
|
+
strokeWidth = 2,
|
|
1695
|
+
className,
|
|
1696
|
+
}: SparklineChartProps) {
|
|
1697
|
+
const sparkId = React.useId().replace(/:/g, "");
|
|
1698
|
+
const lastPoint = data[data.length - 1];
|
|
1699
|
+
const lastValue = lastPoint ? lastPoint[dataKey] : undefined;
|
|
1700
|
+
|
|
1701
|
+
return (
|
|
1702
|
+
<div className={cn("relative h-12 w-full min-w-0", className)}>
|
|
1703
|
+
<RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
|
|
1704
|
+
<RechartsPrimitive.AreaChart
|
|
1705
|
+
data={data}
|
|
1706
|
+
margin={{ top: 4, right: showEndDot ? 8 : 0, bottom: 0, left: 0 }}
|
|
1707
|
+
>
|
|
1708
|
+
{filled && (
|
|
1709
|
+
<defs>
|
|
1710
|
+
<linearGradient
|
|
1711
|
+
id={`spark-gradient-${sparkId}`}
|
|
1712
|
+
x1="0"
|
|
1713
|
+
y1="0"
|
|
1714
|
+
x2="0"
|
|
1715
|
+
y2="1"
|
|
1716
|
+
>
|
|
1717
|
+
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
|
|
1718
|
+
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
|
1719
|
+
</linearGradient>
|
|
1720
|
+
</defs>
|
|
1721
|
+
)}
|
|
1722
|
+
<RechartsPrimitive.Area
|
|
1723
|
+
type={curveType}
|
|
1724
|
+
dataKey={dataKey}
|
|
1725
|
+
stroke={color}
|
|
1726
|
+
strokeWidth={strokeWidth}
|
|
1727
|
+
fill={filled ? `url(#spark-gradient-${sparkId})` : "none"}
|
|
1728
|
+
fillOpacity={1}
|
|
1729
|
+
dot={false}
|
|
1730
|
+
activeDot={false}
|
|
1731
|
+
isAnimationActive
|
|
1732
|
+
animationDuration={600}
|
|
1733
|
+
animationEasing="ease-out"
|
|
1734
|
+
/>
|
|
1735
|
+
</RechartsPrimitive.AreaChart>
|
|
1736
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
1737
|
+
{showEndDot && lastValue !== undefined && lastValue !== null && (
|
|
1738
|
+
<div
|
|
1739
|
+
className="pointer-events-none absolute right-0 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full ring-2 ring-background"
|
|
1740
|
+
style={{ backgroundColor: color }}
|
|
1741
|
+
/>
|
|
1742
|
+
)}
|
|
1743
|
+
</div>
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// ─── RadarChart ───────────────────────────────────────────────────────────────
|
|
1748
|
+
|
|
1749
|
+
export interface RadarMetricChartProps extends ChartStateProps {
|
|
1750
|
+
/** Data array — each item is one axis point on the radar */
|
|
1751
|
+
data: DashboardChartDatum[];
|
|
1752
|
+
/** Key in each datum used as the axis label */
|
|
1753
|
+
labelKey: string;
|
|
1754
|
+
/**
|
|
1755
|
+
* Series to render. Each entry maps to one `<Radar>` element.
|
|
1756
|
+
* Use a single entry for a simple radar, multiple for comparison.
|
|
1757
|
+
*/
|
|
1758
|
+
series: DashboardChartSeries[];
|
|
1759
|
+
/** Override colors per series key or as an ordered array */
|
|
1760
|
+
colors?: DashboardChartColors;
|
|
1761
|
+
/** Fill the radar polygon (default: true) */
|
|
1762
|
+
filled?: boolean;
|
|
1763
|
+
/** Fill opacity when `filled` is true (default: 0.25) */
|
|
1764
|
+
fillOpacity?: number;
|
|
1765
|
+
/** Show dots on each axis point (default: false) */
|
|
1766
|
+
showDots?: boolean;
|
|
1767
|
+
/** Show the polar grid lines (default: true) */
|
|
1768
|
+
showGrid?: boolean;
|
|
1769
|
+
/** Show the legend (default: true when multiple series) */
|
|
1770
|
+
showLegend?: boolean;
|
|
1771
|
+
/** Format axis tick values */
|
|
1772
|
+
valueFormatter?: ChartValueFormatter;
|
|
1773
|
+
className?: string;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function RadarMetricChart({
|
|
1777
|
+
data,
|
|
1778
|
+
labelKey,
|
|
1779
|
+
series,
|
|
1780
|
+
colors,
|
|
1781
|
+
filled = true,
|
|
1782
|
+
fillOpacity = 0.25,
|
|
1783
|
+
showDots = false,
|
|
1784
|
+
showGrid = true,
|
|
1785
|
+
showLegend,
|
|
1786
|
+
valueFormatter,
|
|
1787
|
+
isLoading,
|
|
1788
|
+
error,
|
|
1789
|
+
onRetry,
|
|
1790
|
+
retryLabel,
|
|
1791
|
+
emptyTitle,
|
|
1792
|
+
emptyDescription,
|
|
1793
|
+
errorTitle,
|
|
1794
|
+
errorDescription,
|
|
1795
|
+
loadingLabel,
|
|
1796
|
+
stateClassName,
|
|
1797
|
+
className,
|
|
1798
|
+
}: RadarMetricChartProps) {
|
|
1799
|
+
const chartConfig = React.useMemo(
|
|
1800
|
+
() => buildChartConfig(series.map((s) => s.key), colors),
|
|
1801
|
+
[series, colors],
|
|
1802
|
+
);
|
|
1803
|
+
|
|
1804
|
+
const hasData = data.length > 0 && series.length > 0;
|
|
1805
|
+
const chartState = getChartState(
|
|
1806
|
+
{ isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
|
|
1807
|
+
hasData,
|
|
1808
|
+
stateClassName,
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
const displayLegend = showLegend ?? series.length > 1;
|
|
1812
|
+
|
|
1813
|
+
return (
|
|
1814
|
+
<ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
|
|
1815
|
+
{chartState ?? (
|
|
1816
|
+
<RechartsPrimitive.RadarChart data={data} accessibilityLayer>
|
|
1817
|
+
{showGrid && (
|
|
1818
|
+
<RechartsPrimitive.PolarGrid
|
|
1819
|
+
stroke="var(--border)"
|
|
1820
|
+
strokeOpacity={0.6}
|
|
1821
|
+
/>
|
|
1822
|
+
)}
|
|
1823
|
+
<RechartsPrimitive.PolarAngleAxis
|
|
1824
|
+
dataKey={labelKey}
|
|
1825
|
+
tick={{ fill: "var(--muted-foreground)", fontSize: 12 }}
|
|
1826
|
+
/>
|
|
1827
|
+
<RechartsPrimitive.PolarRadiusAxis
|
|
1828
|
+
tick={false}
|
|
1829
|
+
axisLine={false}
|
|
1830
|
+
tickFormatter={valueFormatter}
|
|
1831
|
+
/>
|
|
1832
|
+
<ChartTooltip
|
|
1833
|
+
content={
|
|
1834
|
+
<ChartTooltipContent
|
|
1835
|
+
formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
|
|
1836
|
+
/>
|
|
1837
|
+
}
|
|
1838
|
+
/>
|
|
1839
|
+
{displayLegend && (
|
|
1840
|
+
<ChartLegend content={<ChartLegendContent />} />
|
|
1841
|
+
)}
|
|
1842
|
+
{series.map((s) => (
|
|
1843
|
+
<RechartsPrimitive.Radar
|
|
1844
|
+
key={s.key}
|
|
1845
|
+
name={s.label as string ?? s.key}
|
|
1846
|
+
dataKey={s.key}
|
|
1847
|
+
stroke={`var(--color-${s.key})`}
|
|
1848
|
+
fill={filled ? `var(--color-${s.key})` : "none"}
|
|
1849
|
+
fillOpacity={filled ? fillOpacity : 0}
|
|
1850
|
+
dot={showDots ? { r: 3, fill: `var(--color-${s.key})` } : false}
|
|
1851
|
+
isAnimationActive
|
|
1852
|
+
animationDuration={600}
|
|
1853
|
+
animationEasing="ease-out"
|
|
1854
|
+
/>
|
|
1855
|
+
))}
|
|
1856
|
+
</RechartsPrimitive.RadarChart>
|
|
1857
|
+
)}
|
|
1858
|
+
</ChartContainer>
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// ─── PieMetricChart ───────────────────────────────────────────────────────────
|
|
1863
|
+
|
|
1864
|
+
export interface PieMetricChartProps extends ChartStateProps {
|
|
1865
|
+
/** Data array — each item is one slice */
|
|
1866
|
+
data: DashboardChartDatum[];
|
|
1867
|
+
/** Key in each datum used as the slice name/label */
|
|
1868
|
+
nameKey: string;
|
|
1869
|
+
/** Key in each datum used as the slice value */
|
|
1870
|
+
valueKey: string;
|
|
1871
|
+
/** Override colors as an ordered array or per-name map */
|
|
1872
|
+
colors?: DashboardChartColors;
|
|
1873
|
+
/** Outer radius of the pie (default: "80%") */
|
|
1874
|
+
outerRadius?: number | string;
|
|
1875
|
+
/** Inner radius — set > 0 to make a donut (default: 0) */
|
|
1876
|
+
innerRadius?: number | string;
|
|
1877
|
+
/** Show percentage labels inside/outside each slice (default: false) */
|
|
1878
|
+
showLabels?: boolean;
|
|
1879
|
+
/** Show the legend (default: true) */
|
|
1880
|
+
showLegend?: boolean;
|
|
1881
|
+
/**
|
|
1882
|
+
* Index of the slice to "explode" (offset outward).
|
|
1883
|
+
* Pass -1 or undefined to disable.
|
|
1884
|
+
*/
|
|
1885
|
+
explodeIndex?: number;
|
|
1886
|
+
/** Offset distance for the exploded slice in px (default: 12) */
|
|
1887
|
+
explodeOffset?: number;
|
|
1888
|
+
/** Format tooltip values */
|
|
1889
|
+
valueFormatter?: ChartValueFormatter;
|
|
1890
|
+
className?: string;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function PieMetricChart({
|
|
1894
|
+
data,
|
|
1895
|
+
nameKey,
|
|
1896
|
+
valueKey,
|
|
1897
|
+
colors,
|
|
1898
|
+
outerRadius = "80%",
|
|
1899
|
+
innerRadius = 0,
|
|
1900
|
+
showLabels = false,
|
|
1901
|
+
showLegend = true,
|
|
1902
|
+
explodeIndex,
|
|
1903
|
+
explodeOffset = 12,
|
|
1904
|
+
valueFormatter,
|
|
1905
|
+
isLoading,
|
|
1906
|
+
error,
|
|
1907
|
+
onRetry,
|
|
1908
|
+
retryLabel,
|
|
1909
|
+
emptyTitle,
|
|
1910
|
+
emptyDescription,
|
|
1911
|
+
errorTitle,
|
|
1912
|
+
errorDescription,
|
|
1913
|
+
loadingLabel,
|
|
1914
|
+
stateClassName,
|
|
1915
|
+
className,
|
|
1916
|
+
}: PieMetricChartProps) {
|
|
1917
|
+
// Build config from unique name values
|
|
1918
|
+
const names = React.useMemo(
|
|
1919
|
+
() => data.map((d) => String(d[nameKey] ?? "")),
|
|
1920
|
+
[data, nameKey],
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1923
|
+
const chartConfig = React.useMemo(
|
|
1924
|
+
() => buildChartConfig(names, colors),
|
|
1925
|
+
[names, colors],
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
const hasData = hasPieData(data, nameKey, valueKey);
|
|
1929
|
+
const chartState = getChartState(
|
|
1930
|
+
{ isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
|
|
1931
|
+
hasData,
|
|
1932
|
+
stateClassName,
|
|
1933
|
+
);
|
|
1934
|
+
|
|
1935
|
+
return (
|
|
1936
|
+
<ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
|
|
1937
|
+
{chartState ?? (
|
|
1938
|
+
<RechartsPrimitive.PieChart accessibilityLayer>
|
|
1939
|
+
<ChartTooltip
|
|
1940
|
+
content={
|
|
1941
|
+
<ChartTooltipContent
|
|
1942
|
+
nameKey={nameKey}
|
|
1943
|
+
formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
|
|
1944
|
+
/>
|
|
1945
|
+
}
|
|
1946
|
+
/>
|
|
1947
|
+
{showLegend && <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} />}
|
|
1948
|
+
<RechartsPrimitive.Pie
|
|
1949
|
+
data={data}
|
|
1950
|
+
dataKey={valueKey}
|
|
1951
|
+
nameKey={nameKey}
|
|
1952
|
+
outerRadius={outerRadius}
|
|
1953
|
+
innerRadius={innerRadius}
|
|
1954
|
+
paddingAngle={2}
|
|
1955
|
+
label={
|
|
1956
|
+
showLabels
|
|
1957
|
+
? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
|
|
1958
|
+
const RADIAN = Math.PI / 180;
|
|
1959
|
+
const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
|
|
1960
|
+
const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
|
|
1961
|
+
const y = Number(cy) + radius * Math.sin(-midAngle * RADIAN);
|
|
1962
|
+
return percent > 0.04 ? (
|
|
1963
|
+
<text
|
|
1964
|
+
x={x}
|
|
1965
|
+
y={y}
|
|
1966
|
+
fill="var(--foreground)"
|
|
1967
|
+
textAnchor={x > Number(cx) ? "start" : "end"}
|
|
1968
|
+
dominantBaseline="central"
|
|
1969
|
+
fontSize={12}
|
|
1970
|
+
fontWeight={500}
|
|
1971
|
+
>
|
|
1972
|
+
{`${(percent * 100).toFixed(0)}%`}
|
|
1973
|
+
</text>
|
|
1974
|
+
) : null;
|
|
1975
|
+
}
|
|
1976
|
+
: false
|
|
1977
|
+
}
|
|
1978
|
+
labelLine={false}
|
|
1979
|
+
isAnimationActive
|
|
1980
|
+
animationDuration={600}
|
|
1981
|
+
animationEasing="ease-out"
|
|
1982
|
+
>
|
|
1983
|
+
{data.map((entry, index) => {
|
|
1984
|
+
const name = String(entry[nameKey] ?? "");
|
|
1985
|
+
const isExploded = index === explodeIndex;
|
|
1986
|
+
return (
|
|
1987
|
+
<RechartsPrimitive.Cell
|
|
1988
|
+
key={`cell-${index}`}
|
|
1989
|
+
fill={chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`}
|
|
1990
|
+
stroke="var(--background)"
|
|
1991
|
+
strokeWidth={2}
|
|
1992
|
+
style={
|
|
1993
|
+
isExploded
|
|
1994
|
+
? { filter: `drop-shadow(0 4px 8px color-mix(in srgb, ${chartConfig[name]?.color ?? "var(--chart-1)"} 40%, transparent))` }
|
|
1995
|
+
: undefined
|
|
1996
|
+
}
|
|
1997
|
+
/>
|
|
1998
|
+
);
|
|
1999
|
+
})}
|
|
2000
|
+
</RechartsPrimitive.Pie>
|
|
2001
|
+
</RechartsPrimitive.PieChart>
|
|
2002
|
+
)}
|
|
2003
|
+
</ChartContainer>
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// ─── RadialBarMetricChart ─────────────────────────────────────────────────────
|
|
2008
|
+
|
|
2009
|
+
export interface RadialBarMetricChartProps extends ChartStateProps {
|
|
2010
|
+
/**
|
|
2011
|
+
* Data array. Each item should have a `name` field (for labels) and
|
|
2012
|
+
* the `dataKey` field (for values, 0–100 for percentage-based display).
|
|
2013
|
+
*/
|
|
2014
|
+
data: DashboardChartDatum[];
|
|
2015
|
+
/** Key in each datum used as the bar value (default: "value") */
|
|
2016
|
+
dataKey?: string;
|
|
2017
|
+
/** Key in each datum used as the bar label (default: "name") */
|
|
2018
|
+
nameKey?: string;
|
|
2019
|
+
/** Override colors as an ordered array or per-name map */
|
|
2020
|
+
colors?: DashboardChartColors;
|
|
2021
|
+
/** Inner radius of the radial bar (default: "30%") */
|
|
2022
|
+
innerRadius?: number | string;
|
|
2023
|
+
/** Outer radius of the radial bar (default: "100%") */
|
|
2024
|
+
outerRadius?: number | string;
|
|
2025
|
+
/** Start angle in degrees (default: 90 — top) */
|
|
2026
|
+
startAngle?: number;
|
|
2027
|
+
/** End angle in degrees (default: -270 — full circle) */
|
|
2028
|
+
endAngle?: number;
|
|
2029
|
+
/** Show background track behind each bar (default: true) */
|
|
2030
|
+
showBackground?: boolean;
|
|
2031
|
+
/** Show the legend (default: true) */
|
|
2032
|
+
showLegend?: boolean;
|
|
2033
|
+
/** Format tooltip values */
|
|
2034
|
+
valueFormatter?: ChartValueFormatter;
|
|
2035
|
+
className?: string;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function RadialBarMetricChart({
|
|
2039
|
+
data,
|
|
2040
|
+
dataKey = "value",
|
|
2041
|
+
nameKey = "name",
|
|
2042
|
+
colors,
|
|
2043
|
+
innerRadius = "30%",
|
|
2044
|
+
outerRadius = "100%",
|
|
2045
|
+
startAngle = 90,
|
|
2046
|
+
endAngle = -270,
|
|
2047
|
+
showBackground = true,
|
|
2048
|
+
showLegend = true,
|
|
2049
|
+
valueFormatter,
|
|
2050
|
+
isLoading,
|
|
2051
|
+
error,
|
|
2052
|
+
onRetry,
|
|
2053
|
+
retryLabel,
|
|
2054
|
+
emptyTitle,
|
|
2055
|
+
emptyDescription,
|
|
2056
|
+
errorTitle,
|
|
2057
|
+
errorDescription,
|
|
2058
|
+
loadingLabel,
|
|
2059
|
+
stateClassName,
|
|
2060
|
+
className,
|
|
2061
|
+
}: RadialBarMetricChartProps) {
|
|
2062
|
+
const names = React.useMemo(
|
|
2063
|
+
() => data.map((d) => String(d[nameKey] ?? "")),
|
|
2064
|
+
[data, nameKey],
|
|
2065
|
+
);
|
|
2066
|
+
|
|
2067
|
+
const chartConfig = React.useMemo(
|
|
2068
|
+
() => buildChartConfig(names, colors),
|
|
2069
|
+
[names, colors],
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
const hasData = data.length > 0;
|
|
2073
|
+
const chartState = getChartState(
|
|
2074
|
+
{ isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
|
|
2075
|
+
hasData,
|
|
2076
|
+
stateClassName,
|
|
2077
|
+
);
|
|
2078
|
+
|
|
2079
|
+
// Inject per-item fill color
|
|
2080
|
+
const coloredData = React.useMemo(
|
|
2081
|
+
() =>
|
|
2082
|
+
data.map((item, index) => {
|
|
2083
|
+
const name = String(item[nameKey] ?? "");
|
|
2084
|
+
return {
|
|
2085
|
+
...item,
|
|
2086
|
+
fill: chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`,
|
|
2087
|
+
};
|
|
2088
|
+
}),
|
|
2089
|
+
[data, nameKey, chartConfig],
|
|
2090
|
+
);
|
|
2091
|
+
|
|
2092
|
+
return (
|
|
2093
|
+
<div className={cn("flex w-full flex-col gap-3", className)}>
|
|
2094
|
+
<ChartContainer config={chartConfig} className="h-[260px] w-full">
|
|
2095
|
+
{chartState ?? (
|
|
2096
|
+
<RechartsPrimitive.RadialBarChart
|
|
2097
|
+
data={coloredData}
|
|
2098
|
+
innerRadius={innerRadius}
|
|
2099
|
+
outerRadius={outerRadius}
|
|
2100
|
+
startAngle={startAngle}
|
|
2101
|
+
endAngle={endAngle}
|
|
2102
|
+
accessibilityLayer
|
|
2103
|
+
>
|
|
2104
|
+
<ChartTooltip
|
|
2105
|
+
cursor={false}
|
|
2106
|
+
content={
|
|
2107
|
+
<ChartTooltipContent
|
|
2108
|
+
nameKey={nameKey}
|
|
2109
|
+
formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
|
|
2110
|
+
/>
|
|
2111
|
+
}
|
|
2112
|
+
/>
|
|
2113
|
+
<RechartsPrimitive.RadialBar
|
|
2114
|
+
dataKey={dataKey}
|
|
2115
|
+
background={showBackground ? { fill: "var(--muted)" } : false}
|
|
2116
|
+
cornerRadius={6}
|
|
2117
|
+
isAnimationActive
|
|
2118
|
+
animationDuration={700}
|
|
2119
|
+
animationEasing="ease-out"
|
|
2120
|
+
/>
|
|
2121
|
+
</RechartsPrimitive.RadialBarChart>
|
|
2122
|
+
)}
|
|
2123
|
+
</ChartContainer>
|
|
2124
|
+
{showLegend && !chartState && (
|
|
2125
|
+
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1.5 px-2">
|
|
2126
|
+
{coloredData.map((item, index) => {
|
|
2127
|
+
const d = item as DashboardChartDatum & { fill: string };
|
|
2128
|
+
const name = String(d[nameKey] ?? "");
|
|
2129
|
+
const fillColor = d.fill ?? `var(--chart-${(index % 8) + 1})`;
|
|
2130
|
+
const val = d[dataKey];
|
|
2131
|
+
return (
|
|
2132
|
+
<div key={name} className="flex items-center gap-1.5">
|
|
2133
|
+
<span
|
|
2134
|
+
className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
|
|
2135
|
+
style={{ backgroundColor: fillColor }}
|
|
2136
|
+
/>
|
|
2137
|
+
<span className="text-xs text-muted-foreground">
|
|
2138
|
+
{name}
|
|
2139
|
+
{val !== undefined && val !== null && (
|
|
2140
|
+
<span className="ml-1 font-medium text-foreground">
|
|
2141
|
+
{valueFormatter ? valueFormatter(val as number) : String(val)}
|
|
2142
|
+
</span>
|
|
2143
|
+
)}
|
|
2144
|
+
</span>
|
|
2145
|
+
</div>
|
|
2146
|
+
);
|
|
2147
|
+
})}
|
|
2148
|
+
</div>
|
|
2149
|
+
)}
|
|
2150
|
+
</div>
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// ─── GaugeChart ───────────────────────────────────────────────────────────────
|
|
2155
|
+
|
|
2156
|
+
export interface GaugeChartThreshold {
|
|
2157
|
+
/** Upper bound of this zone (0–100) */
|
|
2158
|
+
value: number;
|
|
2159
|
+
/** Color for this zone */
|
|
2160
|
+
color: string;
|
|
2161
|
+
/** Optional label for this zone */
|
|
2162
|
+
label?: string;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
export interface GaugeChartProps {
|
|
2166
|
+
/** Current value (must be within [min, max]) */
|
|
2167
|
+
value: number;
|
|
2168
|
+
/** Minimum value (default: 0) */
|
|
2169
|
+
min?: number;
|
|
2170
|
+
/** Maximum value (default: 100) */
|
|
2171
|
+
max?: number;
|
|
2172
|
+
/**
|
|
2173
|
+
* Color zones. Each threshold defines the upper bound of a zone.
|
|
2174
|
+
* Zones are evaluated in order; the first zone whose `value >= current %`
|
|
2175
|
+
* determines the active color.
|
|
2176
|
+
* If omitted, uses `--chart-1`.
|
|
2177
|
+
*/
|
|
2178
|
+
thresholds?: GaugeChartThreshold[];
|
|
2179
|
+
/** Label shown below the value (e.g. "CPU Usage") */
|
|
2180
|
+
label?: React.ReactNode;
|
|
2181
|
+
/** Format the center value text (default: shows percentage) */
|
|
2182
|
+
valueFormatter?: (value: number, percent: number) => string;
|
|
2183
|
+
/** Show the needle indicator (default: true) */
|
|
2184
|
+
showNeedle?: boolean;
|
|
2185
|
+
className?: string;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function GaugeChart({
|
|
2189
|
+
value,
|
|
2190
|
+
min = 0,
|
|
2191
|
+
max = 100,
|
|
2192
|
+
thresholds,
|
|
2193
|
+
label,
|
|
2194
|
+
valueFormatter,
|
|
2195
|
+
showNeedle = true,
|
|
2196
|
+
className,
|
|
2197
|
+
}: GaugeChartProps) {
|
|
2198
|
+
const percent = Math.min(1, Math.max(0, (value - min) / (max - min)));
|
|
2199
|
+
const percentInt = Math.round(percent * 100);
|
|
2200
|
+
|
|
2201
|
+
// Determine active color from thresholds
|
|
2202
|
+
const activeColor = React.useMemo(() => {
|
|
2203
|
+
if (!thresholds || thresholds.length === 0) return "var(--chart-1)";
|
|
2204
|
+
for (const t of thresholds) {
|
|
2205
|
+
if (percentInt <= t.value) return t.color;
|
|
2206
|
+
}
|
|
2207
|
+
return thresholds[thresholds.length - 1].color;
|
|
2208
|
+
}, [thresholds, percentInt]);
|
|
2209
|
+
|
|
2210
|
+
const displayText = valueFormatter
|
|
2211
|
+
? valueFormatter(value, percentInt)
|
|
2212
|
+
: `${percentInt}%`;
|
|
2213
|
+
|
|
2214
|
+
// SVG gauge constants — viewBox is 200×110, arc center at (100, 100)
|
|
2215
|
+
const cx = 100;
|
|
2216
|
+
const cy = 100;
|
|
2217
|
+
const R = 80; // outer radius
|
|
2218
|
+
const r = 54; // inner radius (track width = 26)
|
|
2219
|
+
|
|
2220
|
+
// Helper: polar → cartesian (angle in degrees, 0° = right, CCW)
|
|
2221
|
+
const polar = (angleDeg: number, radius: number) => {
|
|
2222
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
2223
|
+
return {
|
|
2224
|
+
x: cx + radius * Math.cos(rad),
|
|
2225
|
+
y: cy - radius * Math.sin(rad),
|
|
2226
|
+
};
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// Semicircle: from 180° (left) to 0° (right)
|
|
2230
|
+
// Track background arc path
|
|
2231
|
+
const trackStart = polar(180, R);
|
|
2232
|
+
const trackEnd = polar(0, R);
|
|
2233
|
+
const trackStartI = polar(180, r);
|
|
2234
|
+
const trackEndI = polar(0, r);
|
|
2235
|
+
const trackPath = [
|
|
2236
|
+
`M ${trackStart.x} ${trackStart.y}`,
|
|
2237
|
+
`A ${R} ${R} 0 0 1 ${trackEnd.x} ${trackEnd.y}`,
|
|
2238
|
+
`L ${trackEndI.x} ${trackEndI.y}`,
|
|
2239
|
+
`A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
|
|
2240
|
+
"Z",
|
|
2241
|
+
].join(" ");
|
|
2242
|
+
|
|
2243
|
+
// Value arc: from 180° (left) clockwise to valueAngle
|
|
2244
|
+
// The gauge is a semicircle (max 180°), so largeArc is always 0.
|
|
2245
|
+
// largeArc=1 would sweep the reflex arc (>180°) which creates the filled-blob bug.
|
|
2246
|
+
const valueAngle = 180 - percent * 180;
|
|
2247
|
+
const valueEnd = polar(valueAngle, R);
|
|
2248
|
+
const valueEndI = polar(valueAngle, r);
|
|
2249
|
+
const valuePath =
|
|
2250
|
+
percent <= 0
|
|
2251
|
+
? ""
|
|
2252
|
+
: percent >= 1
|
|
2253
|
+
? trackPath // full arc = same as track
|
|
2254
|
+
: [
|
|
2255
|
+
`M ${trackStart.x} ${trackStart.y}`,
|
|
2256
|
+
`A ${R} ${R} 0 0 1 ${valueEnd.x} ${valueEnd.y}`,
|
|
2257
|
+
`L ${valueEndI.x} ${valueEndI.y}`,
|
|
2258
|
+
`A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
|
|
2259
|
+
"Z",
|
|
2260
|
+
].join(" ");
|
|
2261
|
+
|
|
2262
|
+
// Needle: rotates from -90° (left, 0%) to +90° (right, 100%)
|
|
2263
|
+
// In our coordinate system: 180° at 0%, 0° at 100%
|
|
2264
|
+
const needleAngle = 180 - percent * 180;
|
|
2265
|
+
const needleTip = polar(needleAngle, r - 6);
|
|
2266
|
+
const needleBase1 = polar(needleAngle + 90, 5);
|
|
2267
|
+
const needleBase2 = polar(needleAngle - 90, 5);
|
|
2268
|
+
|
|
2269
|
+
return (
|
|
2270
|
+
<div className={cn("flex flex-col items-center gap-2", className)}>
|
|
2271
|
+
<div className="relative w-full max-w-[260px]">
|
|
2272
|
+
<svg
|
|
2273
|
+
viewBox="0 0 200 130"
|
|
2274
|
+
className="w-full"
|
|
2275
|
+
aria-label={`Gauge: ${displayText}`}
|
|
2276
|
+
>
|
|
2277
|
+
{/* Background track */}
|
|
2278
|
+
<path d={trackPath} fill="var(--muted)" />
|
|
2279
|
+
|
|
2280
|
+
{/* Value arc */}
|
|
2281
|
+
{valuePath && (
|
|
2282
|
+
<path d={valuePath} fill={activeColor} />
|
|
2283
|
+
)}
|
|
2284
|
+
|
|
2285
|
+
{/* Needle */}
|
|
2286
|
+
{showNeedle && (
|
|
2287
|
+
<g>
|
|
2288
|
+
<polygon
|
|
2289
|
+
points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
|
|
2290
|
+
fill="var(--foreground)"
|
|
2291
|
+
opacity={0.85}
|
|
2292
|
+
/>
|
|
2293
|
+
<circle cx={cx} cy={cy} r={5} fill="var(--foreground)" />
|
|
2294
|
+
</g>
|
|
2295
|
+
)}
|
|
2296
|
+
|
|
2297
|
+
{/* Value text — placed below the arc baseline (cy=100) to avoid needle overlap */}
|
|
2298
|
+
<text
|
|
2299
|
+
x={cx}
|
|
2300
|
+
y={cy + 18}
|
|
2301
|
+
textAnchor="middle"
|
|
2302
|
+
dominantBaseline="middle"
|
|
2303
|
+
fontSize={22}
|
|
2304
|
+
fontWeight={700}
|
|
2305
|
+
fill="currentColor"
|
|
2306
|
+
className="fill-foreground"
|
|
2307
|
+
>
|
|
2308
|
+
{displayText}
|
|
2309
|
+
</text>
|
|
2310
|
+
|
|
2311
|
+
{/* Label below value */}
|
|
2312
|
+
{label && (
|
|
2313
|
+
<text
|
|
2314
|
+
x={cx}
|
|
2315
|
+
y={cy + 36}
|
|
2316
|
+
textAnchor="middle"
|
|
2317
|
+
dominantBaseline="middle"
|
|
2318
|
+
fontSize={10}
|
|
2319
|
+
fill="currentColor"
|
|
2320
|
+
className="fill-muted-foreground"
|
|
2321
|
+
>
|
|
2322
|
+
{label}
|
|
2323
|
+
</text>
|
|
2324
|
+
)}
|
|
2325
|
+
</svg>
|
|
2326
|
+
</div>
|
|
2327
|
+
|
|
2328
|
+
{/* Threshold legend */}
|
|
2329
|
+
{thresholds && thresholds.length > 0 && (
|
|
2330
|
+
<div className="flex flex-wrap justify-center gap-x-3 gap-y-1">
|
|
2331
|
+
{thresholds.map((t, i) => (
|
|
2332
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
2333
|
+
<span
|
|
2334
|
+
className="inline-block h-2 w-2 shrink-0 rounded-full"
|
|
2335
|
+
style={{ backgroundColor: t.color }}
|
|
2336
|
+
/>
|
|
2337
|
+
<span className="text-xs text-muted-foreground">{t.label}</span>
|
|
2338
|
+
</div>
|
|
2339
|
+
))}
|
|
2340
|
+
</div>
|
|
2341
|
+
)}
|
|
2342
|
+
</div>
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
2347
|
+
|
|
1480
2348
|
export {
|
|
1481
2349
|
ChartContainer,
|
|
1482
2350
|
ChartTooltip,
|
|
@@ -1491,4 +2359,9 @@ export {
|
|
|
1491
2359
|
InteractiveTimeSeriesChart,
|
|
1492
2360
|
ComboMetricChart,
|
|
1493
2361
|
DonutBreakdownChart,
|
|
2362
|
+
SparklineChart,
|
|
2363
|
+
RadarMetricChart,
|
|
2364
|
+
PieMetricChart,
|
|
2365
|
+
RadialBarMetricChart,
|
|
2366
|
+
GaugeChart,
|
|
1494
2367
|
};
|