@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,310 @@
1
+ import * as React from "react";
2
+ import { cn } from "@optilogic/core";
3
+ import { ChartRenderer, type ChartConfig } from "./chart-renderer";
4
+
5
+ const CHART_TYPE_OPTIONS = [
6
+ { value: "line", label: "Line" },
7
+ { value: "bar", label: "Bar" },
8
+ { value: "area", label: "Area" },
9
+ { value: "pie", label: "Pie" },
10
+ { value: "donut", label: "Donut" },
11
+ { value: "scatter", label: "Scatter" },
12
+ { value: "radar", label: "Radar" },
13
+ { value: "composed", label: "Composed" },
14
+ { value: "waterfall", label: "Waterfall" },
15
+ { value: "funnel", label: "Funnel" },
16
+ { value: "treemap", label: "Treemap" },
17
+ { value: "radialBar", label: "Radial Bar" },
18
+ { value: "heatmap", label: "Heatmap" },
19
+ ] as const;
20
+
21
+ const LEGEND_POSITIONS = [
22
+ { value: "none", label: "None" },
23
+ { value: "top-left", label: "Top Left" },
24
+ { value: "top-center", label: "Top Center" },
25
+ { value: "top-right", label: "Top Right" },
26
+ { value: "bottom-center", label: "Bottom Center" },
27
+ ] as const;
28
+
29
+ export interface ChartBuilderProps {
30
+ /** Available data columns the user can map to chart dimensions */
31
+ columns: string[];
32
+ /** The raw data rows */
33
+ data: Record<string, unknown>[];
34
+ /** Called when the config changes */
35
+ onChange?: (config: ChartConfig) => void;
36
+ /** Called when user explicitly saves */
37
+ onSave?: (config: ChartConfig) => void;
38
+ /** Initial config to start editing */
39
+ initialConfig?: Partial<ChartConfig>;
40
+ className?: string;
41
+ }
42
+
43
+ function useControlledConfig(
44
+ initial: Partial<ChartConfig> | undefined,
45
+ columns: string[],
46
+ ): [ChartConfig, React.Dispatch<React.SetStateAction<ChartConfig>>] {
47
+ const defaultConfig: ChartConfig = {
48
+ type: "bar",
49
+ categoryKey: columns[0] ?? "",
50
+ series: columns.length > 1 ? [{ dataKey: columns[1], name: columns[1] }] : [],
51
+ legend: "none",
52
+ grid: true,
53
+ animate: true,
54
+ };
55
+ return React.useState<ChartConfig>({ ...defaultConfig, ...initial });
56
+ }
57
+
58
+ const selectStyle: React.CSSProperties = {
59
+ width: "100%",
60
+ padding: "5px 8px",
61
+ fontSize: 12,
62
+ borderRadius: 4,
63
+ border: "1px solid hsl(var(--border))",
64
+ background: "hsl(var(--card))",
65
+ color: "hsl(var(--foreground))",
66
+ };
67
+
68
+ const labelStyle: React.CSSProperties = {
69
+ fontSize: 11,
70
+ fontWeight: 500,
71
+ color: "hsl(var(--muted-foreground))",
72
+ marginBottom: 3,
73
+ display: "block",
74
+ };
75
+
76
+ const ChartBuilder = React.memo(function ChartBuilder({
77
+ columns,
78
+ data,
79
+ onChange,
80
+ onSave,
81
+ initialConfig,
82
+ className,
83
+ }: ChartBuilderProps) {
84
+ const [config, setConfig] = useControlledConfig(initialConfig, columns);
85
+
86
+ React.useEffect(() => {
87
+ onChange?.(config);
88
+ }, [config, onChange]);
89
+
90
+ const addSeries = () => {
91
+ const unused = columns.find(
92
+ (c) =>
93
+ c !== config.categoryKey &&
94
+ !config.series.some((s) => s.dataKey === c),
95
+ );
96
+ if (!unused) return;
97
+ setConfig((prev) => ({
98
+ ...prev,
99
+ series: [...prev.series, { dataKey: unused, name: unused }],
100
+ }));
101
+ };
102
+
103
+ const removeSeries = (index: number) => {
104
+ setConfig((prev) => ({
105
+ ...prev,
106
+ series: prev.series.filter((_, i) => i !== index),
107
+ }));
108
+ };
109
+
110
+ const updateSeries = (index: number, field: string, value: string) => {
111
+ setConfig((prev) => ({
112
+ ...prev,
113
+ series: prev.series.map((s, i) =>
114
+ i === index ? { ...s, [field]: value } : s,
115
+ ),
116
+ }));
117
+ };
118
+
119
+ return (
120
+ <div
121
+ className={cn("flex gap-4", className)}
122
+ style={{ minHeight: 400 }}
123
+ >
124
+ {/* Config panel */}
125
+ <div
126
+ className="flex flex-col gap-3 shrink-0 overflow-y-auto"
127
+ style={{
128
+ width: 240,
129
+ padding: 12,
130
+ background: "hsl(var(--card))",
131
+ border: "1px solid hsl(var(--border))",
132
+ borderRadius: 8,
133
+ }}
134
+ >
135
+ <div style={{ fontSize: 13, fontWeight: 600, color: "hsl(var(--foreground))" }}>
136
+ Chart Builder
137
+ </div>
138
+
139
+ {/* Chart type */}
140
+ <div>
141
+ <label style={labelStyle}>Chart Type</label>
142
+ <select
143
+ style={selectStyle}
144
+ value={config.type}
145
+ onChange={(e) =>
146
+ setConfig((prev) => ({ ...prev, type: e.target.value as ChartConfig["type"] }))
147
+ }
148
+ >
149
+ {CHART_TYPE_OPTIONS.map((opt) => (
150
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
151
+ ))}
152
+ </select>
153
+ </div>
154
+
155
+ {/* Category axis */}
156
+ <div>
157
+ <label style={labelStyle}>Category Axis</label>
158
+ <select
159
+ style={selectStyle}
160
+ value={config.categoryKey}
161
+ onChange={(e) =>
162
+ setConfig((prev) => ({ ...prev, categoryKey: e.target.value }))
163
+ }
164
+ >
165
+ {columns.map((c) => (
166
+ <option key={c} value={c}>{c}</option>
167
+ ))}
168
+ </select>
169
+ </div>
170
+
171
+ {/* Series */}
172
+ <div>
173
+ <label style={labelStyle}>Series</label>
174
+ {config.series.map((s, i) => (
175
+ <div
176
+ key={i}
177
+ className="flex gap-1 items-center"
178
+ style={{ marginBottom: 4 }}
179
+ >
180
+ <select
181
+ style={{ ...selectStyle, flex: 1 }}
182
+ value={s.dataKey}
183
+ onChange={(e) => updateSeries(i, "dataKey", e.target.value)}
184
+ >
185
+ {columns
186
+ .filter((c) => c !== config.categoryKey)
187
+ .map((c) => (
188
+ <option key={c} value={c}>{c}</option>
189
+ ))}
190
+ </select>
191
+ <button
192
+ onClick={() => removeSeries(i)}
193
+ style={{
194
+ padding: "2px 6px",
195
+ fontSize: 14,
196
+ lineHeight: 1,
197
+ border: "1px solid hsl(var(--border))",
198
+ borderRadius: 4,
199
+ background: "hsl(var(--card))",
200
+ color: "hsl(var(--muted-foreground))",
201
+ cursor: "pointer",
202
+ }}
203
+ >
204
+ ×
205
+ </button>
206
+ </div>
207
+ ))}
208
+ <button
209
+ onClick={addSeries}
210
+ style={{
211
+ width: "100%",
212
+ padding: "4px 8px",
213
+ fontSize: 11,
214
+ border: "1px dashed hsl(var(--border))",
215
+ borderRadius: 4,
216
+ background: "transparent",
217
+ color: "hsl(var(--muted-foreground))",
218
+ cursor: "pointer",
219
+ marginTop: 2,
220
+ }}
221
+ >
222
+ + Add Series
223
+ </button>
224
+ </div>
225
+
226
+ {/* Legend */}
227
+ <div>
228
+ <label style={labelStyle}>Legend</label>
229
+ <select
230
+ style={selectStyle}
231
+ value={config.legend}
232
+ onChange={(e) =>
233
+ setConfig((prev) => ({ ...prev, legend: e.target.value }))
234
+ }
235
+ >
236
+ {LEGEND_POSITIONS.map((opt) => (
237
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
238
+ ))}
239
+ </select>
240
+ </div>
241
+
242
+ {/* Grid toggle */}
243
+ <div className="flex items-center gap-2">
244
+ <input
245
+ type="checkbox"
246
+ id="grid-toggle"
247
+ checked={config.grid}
248
+ onChange={(e) =>
249
+ setConfig((prev) => ({ ...prev, grid: e.target.checked }))
250
+ }
251
+ />
252
+ <label htmlFor="grid-toggle" style={{ ...labelStyle, margin: 0 }}>
253
+ Show Grid
254
+ </label>
255
+ </div>
256
+
257
+ {/* Animate toggle */}
258
+ <div className="flex items-center gap-2">
259
+ <input
260
+ type="checkbox"
261
+ id="animate-toggle"
262
+ checked={config.animate}
263
+ onChange={(e) =>
264
+ setConfig((prev) => ({ ...prev, animate: e.target.checked }))
265
+ }
266
+ />
267
+ <label htmlFor="animate-toggle" style={{ ...labelStyle, margin: 0 }}>
268
+ Animate
269
+ </label>
270
+ </div>
271
+
272
+ {onSave && (
273
+ <button
274
+ onClick={() => onSave(config)}
275
+ style={{
276
+ marginTop: 8,
277
+ padding: "6px 12px",
278
+ fontSize: 12,
279
+ fontWeight: 600,
280
+ borderRadius: 6,
281
+ border: "none",
282
+ background: "hsl(var(--primary))",
283
+ color: "hsl(var(--primary-foreground))",
284
+ cursor: "pointer",
285
+ }}
286
+ >
287
+ Save Chart
288
+ </button>
289
+ )}
290
+ </div>
291
+
292
+ {/* Preview */}
293
+ <div
294
+ className="flex-1"
295
+ style={{
296
+ border: "1px solid hsl(var(--border))",
297
+ borderRadius: 8,
298
+ padding: 12,
299
+ background: "hsl(var(--background))",
300
+ minHeight: 300,
301
+ }}
302
+ >
303
+ <ChartRenderer config={config} data={data} height="100%" />
304
+ </div>
305
+ </div>
306
+ );
307
+ });
308
+
309
+ ChartBuilder.displayName = "ChartBuilder";
310
+ export { ChartBuilder };
@@ -0,0 +1,250 @@
1
+ import * as React from "react";
2
+ import { LineChart } from "../cartesian/line-chart";
3
+ import { BarChart } from "../cartesian/bar-chart";
4
+ import { AreaChart } from "../cartesian/area-chart";
5
+ import { ScatterChart } from "../cartesian/scatter-chart";
6
+ import { ComposedChart } from "../cartesian/composed-chart";
7
+ import { WaterfallChart, type WaterfallItem } from "../cartesian/waterfall-chart";
8
+ import { PieChart } from "../radial/pie-chart";
9
+ import { DonutChart } from "../radial/donut-chart";
10
+ import { RadarChart } from "../radial/radar-chart";
11
+ import { RadialBarChart } from "../radial/radial-bar-chart";
12
+ import { FunnelChart } from "../specialized/funnel-chart";
13
+ import { TreemapChart } from "../specialized/treemap-chart";
14
+ import { HeatmapChart } from "../specialized/heatmap-chart";
15
+ import type { ChartLegend, ChartTooltipProp } from "../shared/types";
16
+
17
+ export interface ChartConfigSeries {
18
+ dataKey: string;
19
+ name: string;
20
+ color?: string;
21
+ /** Only used for composed chart */
22
+ composedType?: "line" | "bar" | "area";
23
+ }
24
+
25
+ export interface ChartConfig {
26
+ type:
27
+ | "line"
28
+ | "bar"
29
+ | "area"
30
+ | "pie"
31
+ | "donut"
32
+ | "scatter"
33
+ | "radar"
34
+ | "composed"
35
+ | "waterfall"
36
+ | "funnel"
37
+ | "treemap"
38
+ | "radialBar"
39
+ | "heatmap";
40
+ categoryKey: string;
41
+ series: ChartConfigSeries[];
42
+ legend?: string;
43
+ grid?: boolean;
44
+ animate?: boolean;
45
+ tooltip?: ChartTooltipProp;
46
+ }
47
+
48
+ export interface ChartRendererProps {
49
+ config: ChartConfig;
50
+ data: Record<string, unknown>[];
51
+ height?: number | string;
52
+ className?: string;
53
+ }
54
+
55
+ function resolveLegendFromString(
56
+ val: string | undefined,
57
+ ): boolean | ChartLegend {
58
+ if (!val || val === "none") return false;
59
+ const parts = val.split("-");
60
+ return {
61
+ position: (parts[0] as "top" | "bottom") ?? "top",
62
+ align: (parts[1] as "left" | "center" | "right") ?? "right",
63
+ };
64
+ }
65
+
66
+ const ChartRenderer = React.memo(function ChartRenderer({
67
+ config,
68
+ data,
69
+ height = "100%",
70
+ className,
71
+ }: ChartRendererProps) {
72
+ if (!data?.length || !config.series.length) {
73
+ return (
74
+ <div
75
+ className={className}
76
+ style={{
77
+ height,
78
+ display: "flex",
79
+ alignItems: "center",
80
+ justifyContent: "center",
81
+ color: "hsl(var(--muted-foreground))",
82
+ fontSize: 12,
83
+ }}
84
+ >
85
+ Configure a chart to preview
86
+ </div>
87
+ );
88
+ }
89
+
90
+ const legend = resolveLegendFromString(config.legend);
91
+ const common = {
92
+ className,
93
+ height,
94
+ grid: config.grid ?? true,
95
+ animate: config.animate ?? true,
96
+ tooltip: config.tooltip ?? true,
97
+ legend,
98
+ };
99
+ const xAxis = { dataKey: config.categoryKey };
100
+ const series = config.series;
101
+
102
+ switch (config.type) {
103
+ case "line":
104
+ return <LineChart data={data} series={series} xAxis={xAxis} {...common} />;
105
+ case "bar":
106
+ return <BarChart data={data} series={series} xAxis={xAxis} {...common} />;
107
+ case "area":
108
+ return <AreaChart data={data} series={series} xAxis={xAxis} {...common} />;
109
+ case "pie":
110
+ return (
111
+ <PieChart
112
+ data={data.map((d) => ({
113
+ name: String(d[config.categoryKey]),
114
+ value: Number(d[series[0].dataKey] ?? 0),
115
+ }))}
116
+ label
117
+ {...common}
118
+ />
119
+ );
120
+ case "donut":
121
+ return (
122
+ <DonutChart
123
+ data={data.map((d) => ({
124
+ name: String(d[config.categoryKey]),
125
+ value: Number(d[series[0].dataKey] ?? 0),
126
+ }))}
127
+ {...common}
128
+ />
129
+ );
130
+ case "scatter":
131
+ return (
132
+ <ScatterChart
133
+ series={[
134
+ {
135
+ name: series[0]?.name ?? "Data",
136
+ data: data as Record<string, unknown>[],
137
+ },
138
+ ]}
139
+ xAxis={{ ...xAxis, name: config.categoryKey }}
140
+ {...common}
141
+ />
142
+ );
143
+ case "radar":
144
+ return (
145
+ <RadarChart
146
+ data={data}
147
+ series={series}
148
+ categoryKey={config.categoryKey}
149
+ {...common}
150
+ />
151
+ );
152
+ case "composed":
153
+ return (
154
+ <ComposedChart
155
+ data={data}
156
+ series={series.map((s) => ({
157
+ type: s.composedType ?? "bar",
158
+ dataKey: s.dataKey,
159
+ name: s.name,
160
+ color: s.color,
161
+ }))}
162
+ xAxis={xAxis}
163
+ {...common}
164
+ />
165
+ );
166
+ case "waterfall":
167
+ return (
168
+ <WaterfallChart
169
+ data={data.map((d) => ({
170
+ name: String(d[config.categoryKey]),
171
+ value: Number(d[series[0]?.dataKey] ?? 0),
172
+ }))}
173
+ {...common}
174
+ />
175
+ );
176
+ case "funnel":
177
+ return (
178
+ <FunnelChart
179
+ data={data.map((d) => ({
180
+ name: String(d[config.categoryKey]),
181
+ value: Number(d[series[0]?.dataKey] ?? 0),
182
+ }))}
183
+ {...common}
184
+ />
185
+ );
186
+ case "treemap":
187
+ return (
188
+ <TreemapChart
189
+ data={data.map((d) => ({
190
+ name: String(d[config.categoryKey]),
191
+ value: Number(d[series[0]?.dataKey] ?? 0),
192
+ }))}
193
+ {...common}
194
+ />
195
+ );
196
+ case "radialBar":
197
+ return (
198
+ <RadialBarChart
199
+ data={data.map((d) => ({
200
+ name: String(d[config.categoryKey]),
201
+ value: Number(d[series[0]?.dataKey] ?? 0),
202
+ }))}
203
+ {...common}
204
+ />
205
+ );
206
+ case "heatmap": {
207
+ if (series.length < 2) {
208
+ return (
209
+ <div
210
+ style={{
211
+ height,
212
+ display: "flex",
213
+ alignItems: "center",
214
+ justifyContent: "center",
215
+ color: "hsl(var(--muted-foreground))",
216
+ fontSize: 12,
217
+ }}
218
+ >
219
+ Heatmap requires at least 2 series (x, value)
220
+ </div>
221
+ );
222
+ }
223
+ const xLabels = Array.from(
224
+ new Set(data.map((d) => String(d[config.categoryKey]))),
225
+ );
226
+ const yLabels = Array.from(
227
+ new Set(data.map((d) => String(d[series[0].dataKey]))),
228
+ );
229
+ const cells = data.map((d) => ({
230
+ x: String(d[config.categoryKey]),
231
+ y: String(d[series[0].dataKey]),
232
+ value: Number(d[series[1].dataKey] ?? 0),
233
+ }));
234
+ return (
235
+ <HeatmapChart
236
+ data={cells}
237
+ xLabels={xLabels}
238
+ yLabels={yLabels}
239
+ height={height}
240
+ className={className}
241
+ />
242
+ );
243
+ }
244
+ default:
245
+ return null;
246
+ }
247
+ });
248
+
249
+ ChartRenderer.displayName = "ChartRenderer";
250
+ export { ChartRenderer };
@@ -0,0 +1,121 @@
1
+ import * as React from "react";
2
+ import { cn } from "@optilogic/core";
3
+ import { Sparkline, type SparklineVariant } from "./sparkline";
4
+
5
+ export interface KPICardProps {
6
+ label: string;
7
+ value: string | number;
8
+ /** Previous period value for comparison */
9
+ previousValue?: number;
10
+ /** Change descriptor (e.g. "vs last month") */
11
+ changeLabel?: string;
12
+ sparklineData?: number[];
13
+ sparklineVariant?: SparklineVariant;
14
+ sparklineColor?: string;
15
+ icon?: React.ReactNode;
16
+ className?: string;
17
+ }
18
+
19
+ const KPICard = React.memo(function KPICard({
20
+ label,
21
+ value,
22
+ previousValue,
23
+ changeLabel,
24
+ sparklineData,
25
+ sparklineVariant = "area",
26
+ sparklineColor,
27
+ icon,
28
+ className,
29
+ }: KPICardProps) {
30
+ const currentNum = typeof value === "number" ? value : parseFloat(String(value));
31
+ const hasDelta =
32
+ previousValue != null && !isNaN(currentNum) && previousValue !== 0;
33
+ const delta = hasDelta ? ((currentNum - previousValue!) / previousValue!) * 100 : null;
34
+
35
+ return (
36
+ <div
37
+ className={cn(
38
+ "rounded-lg border p-4 flex flex-col gap-2",
39
+ className,
40
+ )}
41
+ style={{
42
+ background: "hsl(var(--card))",
43
+ borderColor: "hsl(var(--border))",
44
+ }}
45
+ >
46
+ <div className="flex items-center justify-between">
47
+ <span
48
+ style={{
49
+ fontSize: 12,
50
+ color: "hsl(var(--muted-foreground))",
51
+ fontWeight: 500,
52
+ }}
53
+ >
54
+ {label}
55
+ </span>
56
+ {icon && (
57
+ <span style={{ color: "hsl(var(--muted-foreground))" }}>{icon}</span>
58
+ )}
59
+ </div>
60
+ <div className="flex items-end justify-between gap-3">
61
+ <div className="flex flex-col gap-1">
62
+ <span
63
+ style={{
64
+ fontSize: 22,
65
+ fontWeight: 700,
66
+ color: "hsl(var(--foreground))",
67
+ lineHeight: 1,
68
+ fontVariantNumeric: "tabular-nums",
69
+ }}
70
+ >
71
+ {value}
72
+ </span>
73
+ {delta != null && (
74
+ <span
75
+ style={{
76
+ fontSize: 11,
77
+ fontWeight: 500,
78
+ color:
79
+ delta > 0
80
+ ? "hsl(var(--success))"
81
+ : delta < 0
82
+ ? "hsl(var(--destructive))"
83
+ : "hsl(var(--muted-foreground))",
84
+ display: "flex",
85
+ alignItems: "center",
86
+ gap: 2,
87
+ }}
88
+ >
89
+ {delta >= 0 ? "▲" : "▼"} {delta >= 0 ? "+" : ""}
90
+ {delta.toFixed(1)}%
91
+ {changeLabel && (
92
+ <span style={{ color: "hsl(var(--muted-foreground))", marginLeft: 4 }}>
93
+ {changeLabel}
94
+ </span>
95
+ )}
96
+ </span>
97
+ )}
98
+ </div>
99
+ {sparklineData && sparklineData.length > 1 && (
100
+ <Sparkline
101
+ data={sparklineData}
102
+ variant={sparklineVariant}
103
+ color={
104
+ sparklineColor ??
105
+ (delta != null
106
+ ? delta >= 0
107
+ ? "hsl(var(--success))"
108
+ : "hsl(var(--destructive))"
109
+ : undefined)
110
+ }
111
+ width={80}
112
+ height={32}
113
+ />
114
+ )}
115
+ </div>
116
+ </div>
117
+ );
118
+ });
119
+
120
+ KPICard.displayName = "KPICard";
121
+ export { KPICard };