@moontra/moonui-pro 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +22 -4
- package/dist/index.mjs +1025 -206
- package/package.json +1 -2
- package/src/components/advanced-chart/index.tsx +962 -142
- package/src/components/data-table/data-table-column-toggle.tsx +7 -4
- package/src/components/data-table/data-table-filter-drawer.tsx +48 -42
- package/src/components/data-table/index.tsx +98 -24
- package/src/styles/advanced-chart.css +239 -0
- package/src/styles/index.css +2 -1
- package/src/utils/chart-helpers.ts +100 -0
|
@@ -22,6 +22,19 @@ import {
|
|
|
22
22
|
ReferenceLine,
|
|
23
23
|
ReferenceArea,
|
|
24
24
|
Brush,
|
|
25
|
+
ComposedChart,
|
|
26
|
+
RadarChart,
|
|
27
|
+
PolarGrid,
|
|
28
|
+
PolarAngleAxis,
|
|
29
|
+
PolarRadiusAxis,
|
|
30
|
+
Radar,
|
|
31
|
+
Treemap,
|
|
32
|
+
Funnel,
|
|
33
|
+
FunnelChart,
|
|
34
|
+
Sankey,
|
|
35
|
+
RadialBarChart,
|
|
36
|
+
RadialBar,
|
|
37
|
+
BarChart as RechartsBarChart,
|
|
25
38
|
} from 'recharts'
|
|
26
39
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
|
27
40
|
import { Button } from '../ui/button'
|
|
@@ -34,11 +47,42 @@ import {
|
|
|
34
47
|
TrendingDown,
|
|
35
48
|
Minus,
|
|
36
49
|
Lock,
|
|
37
|
-
Sparkles
|
|
50
|
+
Sparkles,
|
|
51
|
+
Eye,
|
|
52
|
+
EyeOff,
|
|
53
|
+
FileJson,
|
|
54
|
+
FileSpreadsheet,
|
|
55
|
+
Image,
|
|
56
|
+
ZoomIn,
|
|
57
|
+
ZoomOut,
|
|
58
|
+
Move,
|
|
59
|
+
Crosshair,
|
|
60
|
+
Palette,
|
|
61
|
+
Activity,
|
|
62
|
+
MoreVertical,
|
|
63
|
+
X,
|
|
64
|
+
Check
|
|
38
65
|
} from 'lucide-react'
|
|
39
66
|
import { cn } from '../../lib/utils'
|
|
67
|
+
import { motion, AnimatePresence, useMotionValue, useTransform } from 'framer-motion'
|
|
68
|
+
import {
|
|
69
|
+
DropdownMenu,
|
|
70
|
+
DropdownMenuContent,
|
|
71
|
+
DropdownMenuItem,
|
|
72
|
+
DropdownMenuTrigger,
|
|
73
|
+
DropdownMenuSeparator,
|
|
74
|
+
} from '../ui/dropdown-menu'
|
|
75
|
+
import { Skeleton } from '../ui/skeleton'
|
|
76
|
+
import { Switch } from '../ui/switch'
|
|
77
|
+
import { Label } from '../ui/label'
|
|
78
|
+
import { Slider } from '../ui/slider'
|
|
79
|
+
import {
|
|
80
|
+
Popover,
|
|
81
|
+
PopoverContent,
|
|
82
|
+
PopoverTrigger,
|
|
83
|
+
} from '../ui/popover'
|
|
40
84
|
|
|
41
|
-
export type ChartType = 'line' | 'bar' | 'area' | 'pie' | 'scatter'
|
|
85
|
+
export type ChartType = 'line' | 'bar' | 'area' | 'pie' | 'scatter' | 'composed' | 'radar' | 'radialBar' | 'treemap' | 'funnel'
|
|
42
86
|
|
|
43
87
|
interface ChartDataPoint {
|
|
44
88
|
[key: string]: string | number | null
|
|
@@ -48,10 +92,17 @@ interface ChartSeries {
|
|
|
48
92
|
dataKey: string
|
|
49
93
|
name: string
|
|
50
94
|
color: string
|
|
51
|
-
type?: 'monotone' | 'linear' | 'step'
|
|
95
|
+
type?: 'monotone' | 'linear' | 'step' | 'basis' | 'basisClosed' | 'basisOpen' | 'natural'
|
|
52
96
|
strokeWidth?: number
|
|
53
97
|
fillOpacity?: number
|
|
54
98
|
hide?: boolean
|
|
99
|
+
gradient?: boolean
|
|
100
|
+
strokeDasharray?: string
|
|
101
|
+
dot?: boolean | object
|
|
102
|
+
activeDot?: boolean | object
|
|
103
|
+
label?: boolean | object
|
|
104
|
+
stackId?: string
|
|
105
|
+
yAxisId?: string
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
interface AdvancedChartProps {
|
|
@@ -83,7 +134,7 @@ interface AdvancedChartProps {
|
|
|
83
134
|
colors?: string[]
|
|
84
135
|
className?: string
|
|
85
136
|
onDataPointClick?: (data: ChartDataPoint) => void
|
|
86
|
-
onExport?: (format: 'png' | 'svg' | 'pdf') => void
|
|
137
|
+
onExport?: (format: 'png' | 'svg' | 'pdf' | 'json' | 'csv') => void
|
|
87
138
|
onRefresh?: () => void
|
|
88
139
|
customTooltip?: React.ComponentType<any>
|
|
89
140
|
customLegend?: React.ComponentType<any>
|
|
@@ -91,12 +142,144 @@ interface AdvancedChartProps {
|
|
|
91
142
|
error?: string | null
|
|
92
143
|
animated?: boolean
|
|
93
144
|
responsive?: boolean
|
|
145
|
+
showCrosshair?: boolean
|
|
146
|
+
enableZoom?: boolean
|
|
147
|
+
enablePan?: boolean
|
|
148
|
+
showMiniMap?: boolean
|
|
149
|
+
darkMode?: boolean
|
|
150
|
+
gradientColors?: boolean
|
|
151
|
+
interactiveLegend?: boolean
|
|
152
|
+
showDataLabels?: boolean
|
|
153
|
+
sparklineMode?: boolean
|
|
154
|
+
animationDuration?: number
|
|
155
|
+
theme?: 'default' | 'vibrant' | 'pastel' | 'dark' | 'neon'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Tema renk paletleri
|
|
159
|
+
const COLOR_THEMES = {
|
|
160
|
+
default: [
|
|
161
|
+
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
|
162
|
+
'#06b6d4', '#f97316', '#84cc16', '#ec4899', '#6366f1'
|
|
163
|
+
],
|
|
164
|
+
vibrant: [
|
|
165
|
+
'#FF006E', '#FB5607', '#FFBE0B', '#8338EC', '#3A86FF',
|
|
166
|
+
'#06FFB4', '#FF4365', '#00F5FF', '#FF124F', '#7209B7'
|
|
167
|
+
],
|
|
168
|
+
pastel: [
|
|
169
|
+
'#B5E2FA', '#F7AEF8', '#FDC5F5', '#F8B7D3', '#FAC9B8',
|
|
170
|
+
'#C8E9A0', '#A7D2CB', '#F2D98D', '#E5B3BB', '#D6A2E8'
|
|
171
|
+
],
|
|
172
|
+
dark: [
|
|
173
|
+
'#1e3a5f', '#4b0e0e', '#0e4b2b', '#4b3c0e', '#2e0e4b',
|
|
174
|
+
'#0e3c4b', '#4b2e0e', '#2b4b0e', '#4b0e3c', '#1e0e4b'
|
|
175
|
+
],
|
|
176
|
+
neon: [
|
|
177
|
+
'#39FF14', '#FF10F0', '#00FFFF', '#FE019A', '#FFFF00',
|
|
178
|
+
'#FF073A', '#00FF00', '#FF0099', '#0FF0FC', '#FFE400'
|
|
179
|
+
]
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
const DEFAULT_COLORS =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
182
|
+
const DEFAULT_COLORS = COLOR_THEMES.default
|
|
183
|
+
|
|
184
|
+
// Özel Tooltip bileşeni
|
|
185
|
+
const CustomTooltip: React.FC<any> = ({ active, payload, label }) => {
|
|
186
|
+
if (!active || !payload || !payload.length) return null
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<motion.div
|
|
190
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
191
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
192
|
+
exit={{ opacity: 0, scale: 0.9 }}
|
|
193
|
+
className="backdrop-blur-xl bg-background/80 dark:bg-gray-900/90 p-3 rounded-xl shadow-2xl border border-border/50 dark:border-gray-700/50"
|
|
194
|
+
>
|
|
195
|
+
<p className="text-sm font-medium mb-2">{label}</p>
|
|
196
|
+
{payload.map((entry: any, index: number) => (
|
|
197
|
+
<div key={index} className="flex items-center gap-2 text-sm">
|
|
198
|
+
<div
|
|
199
|
+
className="w-3 h-3 rounded-full"
|
|
200
|
+
style={{ backgroundColor: entry.color }}
|
|
201
|
+
/>
|
|
202
|
+
<span className="text-muted-foreground">{entry.name}:</span>
|
|
203
|
+
<span className="font-semibold">{entry.value}</span>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</motion.div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Özel Legend bileşeni
|
|
211
|
+
const CustomLegend: React.FC<any> = ({ payload, onItemClick }) => {
|
|
212
|
+
return (
|
|
213
|
+
<div className="flex flex-wrap items-center justify-center gap-4 mt-4">
|
|
214
|
+
{payload.map((entry: any, index: number) => (
|
|
215
|
+
<motion.button
|
|
216
|
+
key={`item-${index}`}
|
|
217
|
+
whileHover={{ scale: 1.05 }}
|
|
218
|
+
whileTap={{ scale: 0.95 }}
|
|
219
|
+
onClick={() => onItemClick && onItemClick(entry)}
|
|
220
|
+
className={cn(
|
|
221
|
+
"flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all",
|
|
222
|
+
"hover:bg-accent/10 dark:hover:bg-gray-800/50",
|
|
223
|
+
entry.inactive && "opacity-50"
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
<motion.div
|
|
227
|
+
animate={{
|
|
228
|
+
scale: entry.inactive ? 0.8 : 1,
|
|
229
|
+
opacity: entry.inactive ? 0.5 : 1
|
|
230
|
+
}}
|
|
231
|
+
className="w-3 h-3 rounded-full"
|
|
232
|
+
style={{ backgroundColor: entry.color }}
|
|
233
|
+
/>
|
|
234
|
+
<span className="text-sm font-medium">{entry.value}</span>
|
|
235
|
+
</motion.button>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Mini sparkline bileşeni
|
|
242
|
+
const SparklinePreview = ({ data, dataKey, color }: any) => {
|
|
243
|
+
const values = data.map((d: any) => d[dataKey])
|
|
244
|
+
const min = Math.min(...values)
|
|
245
|
+
const max = Math.max(...values)
|
|
246
|
+
const range = max - min
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<svg width="60" height="20" className="ml-2">
|
|
250
|
+
<polyline
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke={color}
|
|
253
|
+
strokeWidth="2"
|
|
254
|
+
points={values.map((v: number, i: number) =>
|
|
255
|
+
`${(i / (values.length - 1)) * 60},${20 - ((v - min) / range) * 20}`
|
|
256
|
+
).join(' ')}
|
|
257
|
+
/>
|
|
258
|
+
</svg>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Loading Skeleton bileşeni
|
|
263
|
+
const ChartSkeleton = ({ height }: { height: number }) => (
|
|
264
|
+
<div className="w-full" style={{ height }}>
|
|
265
|
+
<div className="flex items-end justify-center h-full gap-2 px-8">
|
|
266
|
+
{[...Array(8)].map((_, i) => (
|
|
267
|
+
<motion.div
|
|
268
|
+
key={i}
|
|
269
|
+
initial={{ height: 0 }}
|
|
270
|
+
animate={{ height: `${Math.random() * 80 + 20}%` }}
|
|
271
|
+
transition={{
|
|
272
|
+
duration: 1.5,
|
|
273
|
+
delay: i * 0.1,
|
|
274
|
+
repeat: Infinity,
|
|
275
|
+
repeatType: "reverse"
|
|
276
|
+
}}
|
|
277
|
+
className="w-full bg-gradient-to-t from-primary/20 to-primary/5 rounded-t-lg"
|
|
278
|
+
/>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
100
283
|
|
|
101
284
|
export function AdvancedChart({
|
|
102
285
|
data,
|
|
@@ -126,9 +309,34 @@ export function AdvancedChart({
|
|
|
126
309
|
error = null,
|
|
127
310
|
animated = true,
|
|
128
311
|
responsive = true,
|
|
312
|
+
showCrosshair = false,
|
|
313
|
+
enableZoom = false,
|
|
314
|
+
enablePan = false,
|
|
315
|
+
showMiniMap = false,
|
|
316
|
+
darkMode = false,
|
|
317
|
+
gradientColors = true,
|
|
318
|
+
interactiveLegend = true,
|
|
319
|
+
showDataLabels = false,
|
|
320
|
+
sparklineMode = false,
|
|
321
|
+
animationDuration = 1500,
|
|
322
|
+
theme = 'default',
|
|
129
323
|
}: AdvancedChartProps) {
|
|
130
324
|
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
|
325
|
+
const [showSettings, setShowSettings] = React.useState(false)
|
|
326
|
+
const [zoomLevel, setZoomLevel] = React.useState(100)
|
|
327
|
+
const [selectedTheme, setSelectedTheme] = React.useState(theme)
|
|
328
|
+
const [hoveredDataPoint, setHoveredDataPoint] = React.useState<any>(null)
|
|
329
|
+
const [hiddenSeries, setHiddenSeries] = React.useState<Set<string>>(new Set())
|
|
131
330
|
const chartRef = React.useRef<HTMLDivElement>(null)
|
|
331
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
332
|
+
|
|
333
|
+
// Tema renklerini al
|
|
334
|
+
const themeColors = React.useMemo(() => {
|
|
335
|
+
return COLOR_THEMES[selectedTheme] || DEFAULT_COLORS
|
|
336
|
+
}, [selectedTheme])
|
|
337
|
+
|
|
338
|
+
// Gradient tanımlamaları için benzersiz ID oluştur
|
|
339
|
+
const gradientId = React.useId()
|
|
132
340
|
|
|
133
341
|
// Calculate trend for the first series
|
|
134
342
|
const trend = React.useMemo(() => {
|
|
@@ -149,48 +357,159 @@ export function AdvancedChart({
|
|
|
149
357
|
}
|
|
150
358
|
}, [data, series])
|
|
151
359
|
|
|
152
|
-
|
|
360
|
+
// Seri görünürlüğünü toggle et
|
|
361
|
+
const toggleSeriesVisibility = (dataKey: string) => {
|
|
362
|
+
setHiddenSeries(prev => {
|
|
363
|
+
const newSet = new Set(prev)
|
|
364
|
+
if (newSet.has(dataKey)) {
|
|
365
|
+
newSet.delete(dataKey)
|
|
366
|
+
} else {
|
|
367
|
+
newSet.add(dataKey)
|
|
368
|
+
}
|
|
369
|
+
return newSet
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Legend item click handler
|
|
374
|
+
const handleLegendItemClick = (entry: any) => {
|
|
375
|
+
if (interactiveLegend) {
|
|
376
|
+
toggleSeriesVisibility(entry.dataKey)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const handleExport = (format: 'png' | 'svg' | 'pdf' | 'json' | 'csv') => {
|
|
153
381
|
if (onExport) {
|
|
154
382
|
onExport(format)
|
|
155
383
|
}
|
|
156
384
|
}
|
|
385
|
+
|
|
386
|
+
// Zoom kontrolü
|
|
387
|
+
const handleZoom = (direction: 'in' | 'out') => {
|
|
388
|
+
setZoomLevel(prev => {
|
|
389
|
+
if (direction === 'in') return Math.min(prev + 10, 200)
|
|
390
|
+
return Math.max(prev - 10, 50)
|
|
391
|
+
})
|
|
392
|
+
}
|
|
157
393
|
|
|
394
|
+
// Gradient tanımlamaları oluştur
|
|
395
|
+
const renderGradientDefs = () => (
|
|
396
|
+
<defs>
|
|
397
|
+
{series.map((s, index) => {
|
|
398
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
399
|
+
return (
|
|
400
|
+
<linearGradient
|
|
401
|
+
key={`gradient-${s.dataKey}`}
|
|
402
|
+
id={`gradient-${gradientId}-${s.dataKey}`}
|
|
403
|
+
x1="0" y1="0" x2="0" y2="1"
|
|
404
|
+
>
|
|
405
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
|
|
406
|
+
<stop offset="100%" stopColor={color} stopOpacity={0.1} />
|
|
407
|
+
</linearGradient>
|
|
408
|
+
)
|
|
409
|
+
})}
|
|
410
|
+
</defs>
|
|
411
|
+
)
|
|
412
|
+
|
|
158
413
|
const renderChart = () => {
|
|
159
414
|
const commonProps = {
|
|
160
415
|
data,
|
|
161
416
|
width: typeof width === 'string' ? undefined : width,
|
|
162
|
-
height,
|
|
163
|
-
margin:
|
|
164
|
-
|
|
417
|
+
height: sparklineMode ? 60 : height,
|
|
418
|
+
margin: sparklineMode
|
|
419
|
+
? { top: 5, right: 5, left: 5, bottom: 5 }
|
|
420
|
+
: { top: 20, right: 30, left: 20, bottom: showBrush ? 60 : 20 },
|
|
165
421
|
}
|
|
166
422
|
|
|
167
|
-
const visibleSeries = series.filter(s => !s.hide)
|
|
423
|
+
const visibleSeries = series.filter(s => !s.hide && !hiddenSeries.has(s.dataKey))
|
|
424
|
+
|
|
425
|
+
// Axis stil özellikleri
|
|
426
|
+
const axisStyle = {
|
|
427
|
+
fontSize: 12,
|
|
428
|
+
fill: darkMode ? '#9ca3af' : '#6b7280',
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Grid stil özellikleri
|
|
432
|
+
const gridStyle = {
|
|
433
|
+
stroke: darkMode ? '#374151' : '#e5e7eb',
|
|
434
|
+
strokeDasharray: '3 3',
|
|
435
|
+
opacity: 0.5
|
|
436
|
+
}
|
|
168
437
|
|
|
169
438
|
switch (type) {
|
|
170
439
|
case 'line':
|
|
171
440
|
return (
|
|
172
441
|
<LineChart {...commonProps}>
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
442
|
+
{gradientColors && renderGradientDefs()}
|
|
443
|
+
{showGrid && !sparklineMode && (
|
|
444
|
+
<CartesianGrid {...gridStyle} />
|
|
445
|
+
)}
|
|
446
|
+
{!sparklineMode && (
|
|
447
|
+
<>
|
|
448
|
+
<XAxis
|
|
449
|
+
dataKey={xAxisKey}
|
|
450
|
+
{...axisStyle}
|
|
451
|
+
tick={{ fontSize: 11 }}
|
|
452
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
453
|
+
/>
|
|
454
|
+
<YAxis
|
|
455
|
+
{...axisStyle}
|
|
456
|
+
tick={{ fontSize: 11 }}
|
|
457
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
458
|
+
/>
|
|
459
|
+
</>
|
|
460
|
+
)}
|
|
461
|
+
{showTooltip && !sparklineMode && (
|
|
462
|
+
<Tooltip
|
|
463
|
+
cursor={showCrosshair ? {
|
|
464
|
+
stroke: darkMode ? '#6b7280' : '#9ca3af',
|
|
465
|
+
strokeWidth: 1,
|
|
466
|
+
strokeDasharray: '5 5'
|
|
467
|
+
} : false}
|
|
187
468
|
/>
|
|
188
|
-
)
|
|
469
|
+
)}
|
|
470
|
+
{showLegend && !sparklineMode && (
|
|
471
|
+
<Legend
|
|
472
|
+
content={(props) => {
|
|
473
|
+
const CustomLegendComponent = customLegend
|
|
474
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
475
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
476
|
+
}}
|
|
477
|
+
/>
|
|
478
|
+
)}
|
|
479
|
+
{visibleSeries.map((s, index) => {
|
|
480
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
481
|
+
return (
|
|
482
|
+
<Line
|
|
483
|
+
key={s.dataKey}
|
|
484
|
+
type={s.type || 'monotone'}
|
|
485
|
+
dataKey={s.dataKey}
|
|
486
|
+
stroke={color}
|
|
487
|
+
strokeWidth={sparklineMode ? 1.5 : (s.strokeWidth || 2)}
|
|
488
|
+
strokeDasharray={s.strokeDasharray}
|
|
489
|
+
name={s.name}
|
|
490
|
+
dot={sparklineMode ? false : (s.dot !== undefined ? s.dot : {
|
|
491
|
+
r: 3,
|
|
492
|
+
strokeWidth: 2,
|
|
493
|
+
fill: darkMode ? '#1f2937' : '#ffffff'
|
|
494
|
+
})}
|
|
495
|
+
activeDot={sparklineMode ? false : (s.activeDot !== undefined ? s.activeDot : {
|
|
496
|
+
r: 5,
|
|
497
|
+
strokeWidth: 2,
|
|
498
|
+
fill: color,
|
|
499
|
+
stroke: darkMode ? '#1f2937' : '#ffffff',
|
|
500
|
+
className: 'animate-pulse'
|
|
501
|
+
})}
|
|
502
|
+
animationDuration={animated ? animationDuration : 0}
|
|
503
|
+
animationBegin={index * 100}
|
|
504
|
+
label={showDataLabels ? s.label : false}
|
|
505
|
+
/>
|
|
506
|
+
)
|
|
507
|
+
})}
|
|
189
508
|
{showReference && referenceLines.map((ref, index) => (
|
|
190
509
|
<ReferenceLine
|
|
191
510
|
key={index}
|
|
192
511
|
y={ref.value}
|
|
193
|
-
stroke={ref.color || '#
|
|
512
|
+
stroke={ref.color || (darkMode ? '#6b7280' : '#9ca3af')}
|
|
194
513
|
strokeDasharray="5 5"
|
|
195
514
|
label={ref.label}
|
|
196
515
|
/>
|
|
@@ -200,95 +519,210 @@ export function AdvancedChart({
|
|
|
200
519
|
key={index}
|
|
201
520
|
x1={ref.x1}
|
|
202
521
|
x2={ref.x2}
|
|
203
|
-
fill={ref.color || '#
|
|
522
|
+
fill={ref.color || (darkMode ? '#374151' : '#f3f4f6')}
|
|
204
523
|
fillOpacity={0.3}
|
|
205
524
|
label={ref.label}
|
|
206
525
|
/>
|
|
207
526
|
))}
|
|
208
|
-
{showBrush &&
|
|
527
|
+
{showBrush && !sparklineMode && (
|
|
528
|
+
<Brush
|
|
529
|
+
height={30}
|
|
530
|
+
fill={darkMode ? '#1f2937' : '#f9fafb'}
|
|
531
|
+
stroke={darkMode ? '#374151' : '#e5e7eb'}
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
209
534
|
</LineChart>
|
|
210
535
|
)
|
|
211
536
|
|
|
212
537
|
case 'bar':
|
|
213
538
|
return (
|
|
214
539
|
<BarChart {...commonProps}>
|
|
215
|
-
{
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
540
|
+
{gradientColors && renderGradientDefs()}
|
|
541
|
+
{showGrid && <CartesianGrid {...gridStyle} />}
|
|
542
|
+
<XAxis
|
|
543
|
+
dataKey={xAxisKey}
|
|
544
|
+
{...axisStyle}
|
|
545
|
+
tick={{ fontSize: 11 }}
|
|
546
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
547
|
+
/>
|
|
548
|
+
<YAxis
|
|
549
|
+
{...axisStyle}
|
|
550
|
+
tick={{ fontSize: 11 }}
|
|
551
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
552
|
+
/>
|
|
553
|
+
{showTooltip && (
|
|
554
|
+
<Tooltip
|
|
555
|
+
cursor={{ fill: darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' }}
|
|
227
556
|
/>
|
|
228
|
-
)
|
|
557
|
+
)}
|
|
558
|
+
{showLegend && (
|
|
559
|
+
<Legend
|
|
560
|
+
content={(props) => {
|
|
561
|
+
const CustomLegendComponent = customLegend
|
|
562
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
563
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
564
|
+
}}
|
|
565
|
+
/>
|
|
566
|
+
)}
|
|
567
|
+
{visibleSeries.map((s, index) => {
|
|
568
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
569
|
+
return (
|
|
570
|
+
<Bar
|
|
571
|
+
key={s.dataKey}
|
|
572
|
+
dataKey={s.dataKey}
|
|
573
|
+
fill={gradientColors ? `url(#gradient-${gradientId}-${s.dataKey})` : color}
|
|
574
|
+
name={s.name}
|
|
575
|
+
animationDuration={animated ? animationDuration : 0}
|
|
576
|
+
animationBegin={index * 100}
|
|
577
|
+
radius={[4, 4, 0, 0]}
|
|
578
|
+
label={showDataLabels ? s.label : false}
|
|
579
|
+
stackId={s.stackId}
|
|
580
|
+
onMouseEnter={() => setHoveredDataPoint(s.dataKey)}
|
|
581
|
+
onMouseLeave={() => setHoveredDataPoint(null)}
|
|
582
|
+
className="hover:opacity-80 transition-opacity cursor-pointer"
|
|
583
|
+
/>
|
|
584
|
+
)
|
|
585
|
+
})}
|
|
229
586
|
{showReference && referenceLines.map((ref, index) => (
|
|
230
587
|
<ReferenceLine
|
|
231
588
|
key={index}
|
|
232
589
|
y={ref.value}
|
|
233
|
-
stroke={ref.color || '#
|
|
590
|
+
stroke={ref.color || (darkMode ? '#6b7280' : '#9ca3af')}
|
|
234
591
|
strokeDasharray="5 5"
|
|
235
592
|
label={ref.label}
|
|
236
593
|
/>
|
|
237
594
|
))}
|
|
238
|
-
{showBrush &&
|
|
595
|
+
{showBrush && (
|
|
596
|
+
<Brush
|
|
597
|
+
height={30}
|
|
598
|
+
fill={darkMode ? '#1f2937' : '#f9fafb'}
|
|
599
|
+
stroke={darkMode ? '#374151' : '#e5e7eb'}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
239
602
|
</BarChart>
|
|
240
603
|
)
|
|
241
604
|
|
|
242
605
|
case 'area':
|
|
243
606
|
return (
|
|
244
607
|
<AreaChart {...commonProps}>
|
|
245
|
-
{
|
|
246
|
-
<
|
|
247
|
-
<
|
|
608
|
+
{gradientColors && renderGradientDefs()}
|
|
609
|
+
{showGrid && <CartesianGrid {...gridStyle} />}
|
|
610
|
+
<XAxis
|
|
611
|
+
dataKey={xAxisKey}
|
|
612
|
+
{...axisStyle}
|
|
613
|
+
tick={{ fontSize: 11 }}
|
|
614
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
615
|
+
/>
|
|
616
|
+
<YAxis
|
|
617
|
+
{...axisStyle}
|
|
618
|
+
tick={{ fontSize: 11 }}
|
|
619
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
620
|
+
/>
|
|
248
621
|
{showTooltip && <Tooltip />}
|
|
249
|
-
{showLegend &&
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
fill={s.color || colors[index % colors.length]}
|
|
257
|
-
fillOpacity={s.fillOpacity || 0.3}
|
|
258
|
-
name={s.name}
|
|
259
|
-
animationDuration={animated ? 1000 : 0}
|
|
622
|
+
{showLegend && (
|
|
623
|
+
<Legend
|
|
624
|
+
content={(props) => {
|
|
625
|
+
const CustomLegendComponent = customLegend
|
|
626
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
627
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
628
|
+
}}
|
|
260
629
|
/>
|
|
261
|
-
)
|
|
630
|
+
)}
|
|
631
|
+
{visibleSeries.map((s, index) => {
|
|
632
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
633
|
+
return (
|
|
634
|
+
<Area
|
|
635
|
+
key={s.dataKey}
|
|
636
|
+
type={s.type || 'monotone'}
|
|
637
|
+
dataKey={s.dataKey}
|
|
638
|
+
stroke={color}
|
|
639
|
+
strokeWidth={2}
|
|
640
|
+
fill={gradientColors ? `url(#gradient-${gradientId}-${s.dataKey})` : color}
|
|
641
|
+
fillOpacity={s.fillOpacity || 0.6}
|
|
642
|
+
name={s.name}
|
|
643
|
+
animationDuration={animated ? animationDuration : 0}
|
|
644
|
+
animationBegin={index * 100}
|
|
645
|
+
dot={s.dot !== undefined ? s.dot : false}
|
|
646
|
+
activeDot={s.activeDot !== undefined ? s.activeDot : {
|
|
647
|
+
r: 4,
|
|
648
|
+
strokeWidth: 2,
|
|
649
|
+
fill: color,
|
|
650
|
+
stroke: darkMode ? '#1f2937' : '#ffffff',
|
|
651
|
+
className: 'animate-pulse'
|
|
652
|
+
}}
|
|
653
|
+
label={showDataLabels ? s.label : false}
|
|
654
|
+
stackId={s.stackId}
|
|
655
|
+
/>
|
|
656
|
+
)
|
|
657
|
+
})}
|
|
262
658
|
{showReference && referenceLines.map((ref, index) => (
|
|
263
659
|
<ReferenceLine
|
|
264
660
|
key={index}
|
|
265
661
|
y={ref.value}
|
|
266
|
-
stroke={ref.color || '#
|
|
662
|
+
stroke={ref.color || (darkMode ? '#6b7280' : '#9ca3af')}
|
|
267
663
|
strokeDasharray="5 5"
|
|
268
664
|
label={ref.label}
|
|
269
665
|
/>
|
|
270
666
|
))}
|
|
271
|
-
{showBrush &&
|
|
667
|
+
{showBrush && (
|
|
668
|
+
<Brush
|
|
669
|
+
height={30}
|
|
670
|
+
fill={darkMode ? '#1f2937' : '#f9fafb'}
|
|
671
|
+
stroke={darkMode ? '#374151' : '#e5e7eb'}
|
|
672
|
+
/>
|
|
673
|
+
)}
|
|
272
674
|
</AreaChart>
|
|
273
675
|
)
|
|
274
676
|
|
|
275
677
|
case 'pie':
|
|
276
678
|
return (
|
|
277
679
|
<PieChart {...commonProps}>
|
|
278
|
-
{showTooltip &&
|
|
279
|
-
|
|
680
|
+
{showTooltip && (
|
|
681
|
+
<Tooltip
|
|
682
|
+
/* @ts-ignore */
|
|
683
|
+
content={customTooltip ? customTooltip : CustomTooltip}
|
|
684
|
+
/>
|
|
685
|
+
)}
|
|
686
|
+
{showLegend && (
|
|
687
|
+
<Legend
|
|
688
|
+
content={(props) => {
|
|
689
|
+
const CustomLegendComponent = customLegend
|
|
690
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
691
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
692
|
+
}}
|
|
693
|
+
/>
|
|
694
|
+
)}
|
|
280
695
|
<Pie
|
|
281
696
|
data={data}
|
|
282
697
|
dataKey={series[0]?.dataKey || 'value'}
|
|
283
698
|
nameKey={xAxisKey}
|
|
284
699
|
cx="50%"
|
|
285
700
|
cy="50%"
|
|
701
|
+
innerRadius={type === 'pie' ? 0 : '40%'}
|
|
286
702
|
outerRadius={Math.min(height, typeof width === 'number' ? width : 400) / 3}
|
|
287
|
-
animationDuration={animated ?
|
|
703
|
+
animationDuration={animated ? animationDuration : 0}
|
|
704
|
+
animationBegin={0}
|
|
705
|
+
label={showDataLabels ? {
|
|
706
|
+
fill: darkMode ? '#e5e7eb' : '#374151',
|
|
707
|
+
fontSize: 12
|
|
708
|
+
} : false}
|
|
709
|
+
labelLine={showDataLabels ? {
|
|
710
|
+
stroke: darkMode ? '#6b7280' : '#9ca3af',
|
|
711
|
+
strokeWidth: 1
|
|
712
|
+
} : false}
|
|
288
713
|
>
|
|
289
|
-
{data.map((entry, index) =>
|
|
290
|
-
|
|
291
|
-
|
|
714
|
+
{data.map((entry, index) => {
|
|
715
|
+
const color = themeColors[index % themeColors.length]
|
|
716
|
+
return (
|
|
717
|
+
<Cell
|
|
718
|
+
key={`cell-${index}`}
|
|
719
|
+
fill={color}
|
|
720
|
+
className="hover:opacity-80 transition-opacity cursor-pointer"
|
|
721
|
+
onMouseEnter={() => setHoveredDataPoint(entry)}
|
|
722
|
+
onMouseLeave={() => setHoveredDataPoint(null)}
|
|
723
|
+
/>
|
|
724
|
+
)
|
|
725
|
+
})}
|
|
292
726
|
</Pie>
|
|
293
727
|
</PieChart>
|
|
294
728
|
)
|
|
@@ -296,23 +730,142 @@ export function AdvancedChart({
|
|
|
296
730
|
case 'scatter':
|
|
297
731
|
return (
|
|
298
732
|
<ScatterChart {...commonProps}>
|
|
299
|
-
{showGrid && <CartesianGrid
|
|
300
|
-
<XAxis
|
|
301
|
-
|
|
733
|
+
{showGrid && <CartesianGrid {...gridStyle} />}
|
|
734
|
+
<XAxis
|
|
735
|
+
dataKey={xAxisKey}
|
|
736
|
+
{...axisStyle}
|
|
737
|
+
tick={{ fontSize: 11 }}
|
|
738
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
739
|
+
/>
|
|
740
|
+
<YAxis
|
|
741
|
+
dataKey={yAxisKey}
|
|
742
|
+
{...axisStyle}
|
|
743
|
+
tick={{ fontSize: 11 }}
|
|
744
|
+
axisLine={{ stroke: darkMode ? '#4b5563' : '#d1d5db' }}
|
|
745
|
+
/>
|
|
302
746
|
{showTooltip && <Tooltip />}
|
|
303
|
-
{showLegend &&
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
animationDuration={animated ? 1000 : 0}
|
|
747
|
+
{showLegend && (
|
|
748
|
+
<Legend
|
|
749
|
+
content={(props) => {
|
|
750
|
+
const CustomLegendComponent = customLegend
|
|
751
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
752
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
753
|
+
}}
|
|
311
754
|
/>
|
|
312
|
-
)
|
|
755
|
+
)}
|
|
756
|
+
{visibleSeries.map((s, index) => {
|
|
757
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
758
|
+
return (
|
|
759
|
+
<Scatter
|
|
760
|
+
key={s.dataKey}
|
|
761
|
+
data={data}
|
|
762
|
+
fill={color}
|
|
763
|
+
name={s.name}
|
|
764
|
+
animationDuration={animated ? animationDuration : 0}
|
|
765
|
+
animationBegin={index * 100}
|
|
766
|
+
shape={(props: any) => {
|
|
767
|
+
const isHovered = hoveredDataPoint === props.payload
|
|
768
|
+
return (
|
|
769
|
+
<circle
|
|
770
|
+
{...props}
|
|
771
|
+
r={isHovered ? 6 : 4}
|
|
772
|
+
className="transition-all duration-200 hover:r-6"
|
|
773
|
+
onMouseEnter={() => setHoveredDataPoint(props.payload)}
|
|
774
|
+
onMouseLeave={() => setHoveredDataPoint(null)}
|
|
775
|
+
/>
|
|
776
|
+
)
|
|
777
|
+
}}
|
|
778
|
+
/>
|
|
779
|
+
)
|
|
780
|
+
})}
|
|
313
781
|
</ScatterChart>
|
|
314
782
|
)
|
|
315
783
|
|
|
784
|
+
case 'radar':
|
|
785
|
+
return (
|
|
786
|
+
<RadarChart {...commonProps}>
|
|
787
|
+
<PolarGrid
|
|
788
|
+
stroke={darkMode ? '#374151' : '#e5e7eb'}
|
|
789
|
+
strokeDasharray="3 3"
|
|
790
|
+
/>
|
|
791
|
+
<PolarAngleAxis
|
|
792
|
+
dataKey={xAxisKey}
|
|
793
|
+
{...axisStyle}
|
|
794
|
+
tick={{ fontSize: 10 }}
|
|
795
|
+
/>
|
|
796
|
+
<PolarRadiusAxis
|
|
797
|
+
{...axisStyle}
|
|
798
|
+
tick={{ fontSize: 10 }}
|
|
799
|
+
axisLine={false}
|
|
800
|
+
/>
|
|
801
|
+
{showTooltip && (
|
|
802
|
+
<Tooltip
|
|
803
|
+
/* @ts-ignore */
|
|
804
|
+
content={customTooltip ? customTooltip : CustomTooltip}
|
|
805
|
+
/>
|
|
806
|
+
)}
|
|
807
|
+
{showLegend && (
|
|
808
|
+
<Legend
|
|
809
|
+
content={(props) => {
|
|
810
|
+
const CustomLegendComponent = customLegend
|
|
811
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
812
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
813
|
+
}}
|
|
814
|
+
/>
|
|
815
|
+
)}
|
|
816
|
+
{visibleSeries.map((s, index) => {
|
|
817
|
+
const color = s.color || themeColors[index % themeColors.length]
|
|
818
|
+
return (
|
|
819
|
+
<Radar
|
|
820
|
+
key={s.dataKey}
|
|
821
|
+
dataKey={s.dataKey}
|
|
822
|
+
stroke={color}
|
|
823
|
+
fill={color}
|
|
824
|
+
fillOpacity={0.3}
|
|
825
|
+
name={s.name}
|
|
826
|
+
animationDuration={animated ? animationDuration : 0}
|
|
827
|
+
animationBegin={index * 100}
|
|
828
|
+
/>
|
|
829
|
+
)
|
|
830
|
+
})}
|
|
831
|
+
</RadarChart>
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
case 'radialBar':
|
|
835
|
+
return (
|
|
836
|
+
<RadialBarChart
|
|
837
|
+
{...commonProps}
|
|
838
|
+
innerRadius="10%"
|
|
839
|
+
outerRadius="90%"
|
|
840
|
+
>
|
|
841
|
+
{showTooltip && (
|
|
842
|
+
<Tooltip
|
|
843
|
+
/* @ts-ignore */
|
|
844
|
+
content={customTooltip ? customTooltip : CustomTooltip}
|
|
845
|
+
/>
|
|
846
|
+
)}
|
|
847
|
+
{showLegend && (
|
|
848
|
+
<Legend
|
|
849
|
+
content={(props) => {
|
|
850
|
+
const CustomLegendComponent = customLegend
|
|
851
|
+
return CustomLegendComponent ? <CustomLegendComponent {...props} /> :
|
|
852
|
+
<CustomLegend {...props} onItemClick={handleLegendItemClick} />
|
|
853
|
+
}}
|
|
854
|
+
/>
|
|
855
|
+
)}
|
|
856
|
+
<RadialBar
|
|
857
|
+
dataKey={series[0]?.dataKey || 'value'}
|
|
858
|
+
cornerRadius={10}
|
|
859
|
+
fill={themeColors[0]}
|
|
860
|
+
animationDuration={animated ? animationDuration : 0}
|
|
861
|
+
label={showDataLabels ? {
|
|
862
|
+
fill: darkMode ? '#e5e7eb' : '#374151',
|
|
863
|
+
position: 'center'
|
|
864
|
+
} : false}
|
|
865
|
+
/>
|
|
866
|
+
</RadialBarChart>
|
|
867
|
+
)
|
|
868
|
+
|
|
316
869
|
default:
|
|
317
870
|
return <div>Unsupported chart type</div>
|
|
318
871
|
}
|
|
@@ -320,12 +873,20 @@ export function AdvancedChart({
|
|
|
320
873
|
|
|
321
874
|
if (loading) {
|
|
322
875
|
return (
|
|
323
|
-
<Card className={cn("w-full", className)}>
|
|
324
|
-
<
|
|
325
|
-
<div className="
|
|
326
|
-
<
|
|
327
|
-
<
|
|
876
|
+
<Card className={cn("w-full overflow-hidden", className)}>
|
|
877
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
878
|
+
<div className="space-y-1">
|
|
879
|
+
<Skeleton className="h-4 w-32" />
|
|
880
|
+
<Skeleton className="h-3 w-48" />
|
|
881
|
+
</div>
|
|
882
|
+
<div className="flex items-center space-x-1">
|
|
883
|
+
<Skeleton className="h-8 w-8 rounded" />
|
|
884
|
+
<Skeleton className="h-8 w-8 rounded" />
|
|
885
|
+
<Skeleton className="h-8 w-8 rounded" />
|
|
328
886
|
</div>
|
|
887
|
+
</CardHeader>
|
|
888
|
+
<CardContent>
|
|
889
|
+
<ChartSkeleton height={height} />
|
|
329
890
|
</CardContent>
|
|
330
891
|
</Card>
|
|
331
892
|
)
|
|
@@ -335,77 +896,336 @@ export function AdvancedChart({
|
|
|
335
896
|
return (
|
|
336
897
|
<Card className={cn("w-full", className)}>
|
|
337
898
|
<CardContent className="flex items-center justify-center" style={{ height }}>
|
|
338
|
-
<div
|
|
339
|
-
|
|
899
|
+
<motion.div
|
|
900
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
901
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
902
|
+
className="text-center"
|
|
903
|
+
>
|
|
904
|
+
<div className="w-12 h-12 rounded-full bg-destructive/10 dark:bg-destructive/20 flex items-center justify-center mx-auto mb-4">
|
|
905
|
+
<X className="w-6 h-6 text-destructive" />
|
|
906
|
+
</div>
|
|
907
|
+
<p className="text-destructive mb-4 font-medium">{error}</p>
|
|
340
908
|
{onRefresh && (
|
|
341
|
-
<Button
|
|
342
|
-
|
|
909
|
+
<Button
|
|
910
|
+
variant="outline"
|
|
911
|
+
onClick={onRefresh}
|
|
912
|
+
className="group"
|
|
913
|
+
>
|
|
914
|
+
<RefreshCw className="mr-2 h-4 w-4 group-hover:rotate-180 transition-transform duration-500" />
|
|
343
915
|
Retry
|
|
344
916
|
</Button>
|
|
345
917
|
)}
|
|
346
|
-
</div>
|
|
918
|
+
</motion.div>
|
|
347
919
|
</CardContent>
|
|
348
920
|
</Card>
|
|
349
921
|
)
|
|
350
922
|
}
|
|
351
923
|
|
|
352
924
|
return (
|
|
353
|
-
<
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
925
|
+
<motion.div
|
|
926
|
+
ref={containerRef}
|
|
927
|
+
className={cn(
|
|
928
|
+
"relative",
|
|
929
|
+
isFullscreen && "fixed inset-0 z-50 bg-background p-4"
|
|
930
|
+
)}
|
|
931
|
+
initial={{ opacity: 0, y: 20 }}
|
|
932
|
+
animate={{ opacity: 1, y: 0 }}
|
|
933
|
+
transition={{ duration: 0.5 }}
|
|
934
|
+
>
|
|
935
|
+
<Card className={cn(
|
|
936
|
+
"w-full overflow-hidden transition-all duration-300",
|
|
937
|
+
darkMode && "dark",
|
|
938
|
+
isFullscreen && "h-full",
|
|
939
|
+
className
|
|
940
|
+
)} ref={chartRef}>
|
|
941
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
942
|
+
<motion.div
|
|
943
|
+
className="space-y-1"
|
|
944
|
+
initial={{ opacity: 0, x: -20 }}
|
|
945
|
+
animate={{ opacity: 1, x: 0 }}
|
|
946
|
+
transition={{ delay: 0.1 }}
|
|
947
|
+
>
|
|
948
|
+
<CardTitle className="text-base font-medium flex items-center gap-2">
|
|
949
|
+
{title}
|
|
950
|
+
{trend && (
|
|
951
|
+
<motion.span
|
|
952
|
+
className="inline-flex items-center"
|
|
953
|
+
initial={{ scale: 0 }}
|
|
954
|
+
animate={{ scale: 1 }}
|
|
955
|
+
transition={{ type: "spring", delay: 0.3 }}
|
|
956
|
+
>
|
|
957
|
+
{trend.direction === 'up' && (
|
|
958
|
+
<TrendingUp className="h-4 w-4 text-green-500 dark:text-green-400" />
|
|
959
|
+
)}
|
|
960
|
+
{trend.direction === 'down' && (
|
|
961
|
+
<TrendingDown className="h-4 w-4 text-red-500 dark:text-red-400" />
|
|
962
|
+
)}
|
|
963
|
+
{trend.direction === 'neutral' && (
|
|
964
|
+
<Minus className="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
965
|
+
)}
|
|
966
|
+
<span className={cn(
|
|
967
|
+
"ml-1 text-sm font-semibold",
|
|
968
|
+
trend.direction === 'up' && "text-green-500 dark:text-green-400",
|
|
969
|
+
trend.direction === 'down' && "text-red-500 dark:text-red-400",
|
|
970
|
+
trend.direction === 'neutral' && "text-gray-500 dark:text-gray-400"
|
|
971
|
+
)}>
|
|
972
|
+
{trend.percentage}%
|
|
973
|
+
</span>
|
|
974
|
+
</motion.span>
|
|
975
|
+
)}
|
|
976
|
+
{sparklineMode && series[0] && (
|
|
977
|
+
<SparklinePreview
|
|
978
|
+
data={data}
|
|
979
|
+
dataKey={series[0].dataKey}
|
|
980
|
+
color={series[0].color || themeColors[0]}
|
|
981
|
+
/>
|
|
982
|
+
)}
|
|
983
|
+
</CardTitle>
|
|
984
|
+
{subtitle && (
|
|
985
|
+
<p className="text-xs text-muted-foreground">{subtitle}</p>
|
|
986
|
+
)}
|
|
987
|
+
</motion.div>
|
|
988
|
+
|
|
989
|
+
<motion.div
|
|
990
|
+
className="flex items-center space-x-1"
|
|
991
|
+
initial={{ opacity: 0, x: 20 }}
|
|
992
|
+
animate={{ opacity: 1, x: 0 }}
|
|
993
|
+
transition={{ delay: 0.2 }}
|
|
994
|
+
>
|
|
995
|
+
{/* Zoom controls */}
|
|
996
|
+
{enableZoom && !sparklineMode && (
|
|
997
|
+
<div className="flex items-center border rounded-lg mr-2">
|
|
998
|
+
<Button
|
|
999
|
+
variant="ghost"
|
|
1000
|
+
size="sm"
|
|
1001
|
+
onClick={() => handleZoom('out')}
|
|
1002
|
+
disabled={zoomLevel <= 50}
|
|
1003
|
+
className="h-7 w-7 p-0"
|
|
1004
|
+
>
|
|
1005
|
+
<ZoomOut className="h-3 w-3" />
|
|
1006
|
+
</Button>
|
|
1007
|
+
<span className="text-xs px-2 text-muted-foreground">
|
|
1008
|
+
{zoomLevel}%
|
|
370
1009
|
</span>
|
|
371
|
-
|
|
1010
|
+
<Button
|
|
1011
|
+
variant="ghost"
|
|
1012
|
+
size="sm"
|
|
1013
|
+
onClick={() => handleZoom('in')}
|
|
1014
|
+
disabled={zoomLevel >= 200}
|
|
1015
|
+
className="h-7 w-7 p-0"
|
|
1016
|
+
>
|
|
1017
|
+
<ZoomIn className="h-3 w-3" />
|
|
1018
|
+
</Button>
|
|
1019
|
+
</div>
|
|
372
1020
|
)}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1021
|
+
|
|
1022
|
+
{/* Other controls */}
|
|
1023
|
+
{onRefresh && (
|
|
1024
|
+
<Button
|
|
1025
|
+
variant="ghost"
|
|
1026
|
+
size="sm"
|
|
1027
|
+
onClick={onRefresh}
|
|
1028
|
+
className="group"
|
|
1029
|
+
>
|
|
1030
|
+
<RefreshCw className="h-4 w-4 group-hover:rotate-180 transition-transform duration-500" />
|
|
1031
|
+
</Button>
|
|
1032
|
+
)}
|
|
1033
|
+
|
|
1034
|
+
{/* Settings popover */}
|
|
1035
|
+
<Popover open={showSettings} onOpenChange={setShowSettings}>
|
|
1036
|
+
<PopoverTrigger asChild>
|
|
1037
|
+
<Button variant="ghost" size="sm">
|
|
1038
|
+
<Settings className="h-4 w-4" />
|
|
1039
|
+
</Button>
|
|
1040
|
+
</PopoverTrigger>
|
|
1041
|
+
<PopoverContent className="w-80" align="end">
|
|
1042
|
+
<div className="space-y-4">
|
|
1043
|
+
<div className="space-y-2">
|
|
1044
|
+
<h4 className="font-medium text-sm">Chart Settings</h4>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
{/* Theme selector */}
|
|
1048
|
+
<div className="space-y-2">
|
|
1049
|
+
<Label className="text-xs">Color Theme</Label>
|
|
1050
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1051
|
+
{Object.entries(COLOR_THEMES).map(([themeName, colors]) => (
|
|
1052
|
+
<button
|
|
1053
|
+
key={themeName}
|
|
1054
|
+
onClick={() => setSelectedTheme(themeName as any)}
|
|
1055
|
+
className={cn(
|
|
1056
|
+
"p-2 rounded-lg border-2 transition-all",
|
|
1057
|
+
selectedTheme === themeName
|
|
1058
|
+
? "border-primary"
|
|
1059
|
+
: "border-transparent hover:border-muted-foreground/20"
|
|
1060
|
+
)}
|
|
1061
|
+
>
|
|
1062
|
+
<div className="flex gap-1">
|
|
1063
|
+
{colors.slice(0, 3).map((color, i) => (
|
|
1064
|
+
<div
|
|
1065
|
+
key={i}
|
|
1066
|
+
className="w-3 h-3 rounded-full"
|
|
1067
|
+
style={{ backgroundColor: color }}
|
|
1068
|
+
/>
|
|
1069
|
+
))}
|
|
1070
|
+
</div>
|
|
1071
|
+
<span className="text-xs mt-1 block capitalize">
|
|
1072
|
+
{themeName}
|
|
1073
|
+
</span>
|
|
1074
|
+
</button>
|
|
1075
|
+
))}
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
|
|
1079
|
+
{/* Animation toggle */}
|
|
1080
|
+
<div className="flex items-center justify-between">
|
|
1081
|
+
<Label htmlFor="animation" className="text-xs">
|
|
1082
|
+
Animations
|
|
1083
|
+
</Label>
|
|
1084
|
+
<Switch
|
|
1085
|
+
id="animation"
|
|
1086
|
+
checked={animated}
|
|
1087
|
+
onCheckedChange={(checked) => {
|
|
1088
|
+
// Update animated prop
|
|
1089
|
+
}}
|
|
1090
|
+
/>
|
|
1091
|
+
</div>
|
|
1092
|
+
|
|
1093
|
+
{/* Grid toggle */}
|
|
1094
|
+
<div className="flex items-center justify-between">
|
|
1095
|
+
<Label htmlFor="grid" className="text-xs">
|
|
1096
|
+
Show Grid
|
|
1097
|
+
</Label>
|
|
1098
|
+
<Switch
|
|
1099
|
+
id="grid"
|
|
1100
|
+
checked={showGrid}
|
|
1101
|
+
onCheckedChange={(checked) => {
|
|
1102
|
+
// Update showGrid prop
|
|
1103
|
+
}}
|
|
1104
|
+
/>
|
|
1105
|
+
</div>
|
|
1106
|
+
|
|
1107
|
+
{/* Crosshair toggle */}
|
|
1108
|
+
<div className="flex items-center justify-between">
|
|
1109
|
+
<Label htmlFor="crosshair" className="text-xs">
|
|
1110
|
+
Crosshair Cursor
|
|
1111
|
+
</Label>
|
|
1112
|
+
<Switch
|
|
1113
|
+
id="crosshair"
|
|
1114
|
+
checked={showCrosshair}
|
|
1115
|
+
onCheckedChange={(checked) => {
|
|
1116
|
+
// Update showCrosshair prop
|
|
1117
|
+
}}
|
|
1118
|
+
/>
|
|
1119
|
+
</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
</PopoverContent>
|
|
1122
|
+
</Popover>
|
|
1123
|
+
|
|
1124
|
+
{/* Export menu */}
|
|
1125
|
+
{onExport && (
|
|
1126
|
+
<DropdownMenu>
|
|
1127
|
+
<DropdownMenuTrigger asChild>
|
|
1128
|
+
<Button variant="ghost" size="sm">
|
|
1129
|
+
<Download className="h-4 w-4" />
|
|
1130
|
+
</Button>
|
|
1131
|
+
</DropdownMenuTrigger>
|
|
1132
|
+
<DropdownMenuContent align="end">
|
|
1133
|
+
<DropdownMenuItem onClick={() => handleExport('png')}>
|
|
1134
|
+
<Image className="mr-2 h-4 w-4" />
|
|
1135
|
+
Export as PNG
|
|
1136
|
+
</DropdownMenuItem>
|
|
1137
|
+
<DropdownMenuItem onClick={() => handleExport('svg')}>
|
|
1138
|
+
<FileSpreadsheet className="mr-2 h-4 w-4" />
|
|
1139
|
+
Export as SVG
|
|
1140
|
+
</DropdownMenuItem>
|
|
1141
|
+
<DropdownMenuItem onClick={() => handleExport('pdf')}>
|
|
1142
|
+
<FileSpreadsheet className="mr-2 h-4 w-4" />
|
|
1143
|
+
Export as PDF
|
|
1144
|
+
</DropdownMenuItem>
|
|
1145
|
+
<DropdownMenuSeparator />
|
|
1146
|
+
<DropdownMenuItem onClick={() => handleExport('csv')}>
|
|
1147
|
+
<FileSpreadsheet className="mr-2 h-4 w-4" />
|
|
1148
|
+
Export as CSV
|
|
1149
|
+
</DropdownMenuItem>
|
|
1150
|
+
<DropdownMenuItem onClick={() => handleExport('json')}>
|
|
1151
|
+
<FileJson className="mr-2 h-4 w-4" />
|
|
1152
|
+
Export as JSON
|
|
1153
|
+
</DropdownMenuItem>
|
|
1154
|
+
</DropdownMenuContent>
|
|
1155
|
+
</DropdownMenu>
|
|
1156
|
+
)}
|
|
1157
|
+
|
|
1158
|
+
{/* Fullscreen toggle */}
|
|
1159
|
+
<Button
|
|
1160
|
+
variant="ghost"
|
|
1161
|
+
size="sm"
|
|
1162
|
+
onClick={() => setIsFullscreen(!isFullscreen)}
|
|
1163
|
+
>
|
|
1164
|
+
{isFullscreen ? (
|
|
1165
|
+
<X className="h-4 w-4" />
|
|
1166
|
+
) : (
|
|
1167
|
+
<Maximize2 className="h-4 w-4" />
|
|
1168
|
+
)}
|
|
382
1169
|
</Button>
|
|
1170
|
+
</motion.div>
|
|
1171
|
+
</CardHeader>
|
|
1172
|
+
|
|
1173
|
+
<CardContent className="relative">
|
|
1174
|
+
{/* Pan indicator */}
|
|
1175
|
+
{enablePan && (
|
|
1176
|
+
<motion.div
|
|
1177
|
+
initial={{ opacity: 0 }}
|
|
1178
|
+
animate={{ opacity: 1 }}
|
|
1179
|
+
className="absolute top-2 left-2 z-10 flex items-center gap-1 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded-md"
|
|
1180
|
+
>
|
|
1181
|
+
<Move className="h-3 w-3" />
|
|
1182
|
+
<span>Drag to pan</span>
|
|
1183
|
+
</motion.div>
|
|
383
1184
|
)}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1185
|
+
|
|
1186
|
+
{/* Chart container */}
|
|
1187
|
+
<motion.div
|
|
1188
|
+
animate={{ scale: zoomLevel / 100 }}
|
|
1189
|
+
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
1190
|
+
style={{ transformOrigin: "center" }}
|
|
1191
|
+
>
|
|
1192
|
+
{responsive ? (
|
|
1193
|
+
<ResponsiveContainer width="100%" height={sparklineMode ? 60 : height}>
|
|
1194
|
+
{renderChart()}
|
|
1195
|
+
</ResponsiveContainer>
|
|
1196
|
+
) : (
|
|
1197
|
+
<div style={{ width: '100%', height: sparklineMode ? '60px' : `${height}px` }}>
|
|
1198
|
+
{renderChart()}
|
|
1199
|
+
</div>
|
|
1200
|
+
)}
|
|
1201
|
+
</motion.div>
|
|
1202
|
+
|
|
1203
|
+
{/* Mini map */}
|
|
1204
|
+
{showMiniMap && !sparklineMode && (
|
|
1205
|
+
<motion.div
|
|
1206
|
+
initial={{ opacity: 0, y: 20 }}
|
|
1207
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1208
|
+
className="absolute bottom-2 right-2 w-32 h-20 bg-background/80 backdrop-blur-sm border rounded-lg p-1"
|
|
1209
|
+
>
|
|
1210
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
1211
|
+
<LineChart data={data} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
|
1212
|
+
{series.filter(s => !s.hide && !hiddenSeries.has(s.dataKey)).map((s, index) => (
|
|
1213
|
+
<Line
|
|
1214
|
+
key={s.dataKey}
|
|
1215
|
+
type="monotone"
|
|
1216
|
+
dataKey={s.dataKey}
|
|
1217
|
+
stroke={s.color || themeColors[index % themeColors.length]}
|
|
1218
|
+
strokeWidth={1}
|
|
1219
|
+
dot={false}
|
|
1220
|
+
/>
|
|
1221
|
+
))}
|
|
1222
|
+
</LineChart>
|
|
1223
|
+
</ResponsiveContainer>
|
|
1224
|
+
</motion.div>
|
|
391
1225
|
)}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
</div>
|
|
396
|
-
</CardHeader>
|
|
397
|
-
<CardContent>
|
|
398
|
-
{responsive ? (
|
|
399
|
-
<ResponsiveContainer width="100%" height={height}>
|
|
400
|
-
{renderChart()}
|
|
401
|
-
</ResponsiveContainer>
|
|
402
|
-
) : (
|
|
403
|
-
<div style={{ width: '100%', height: `${height}px` }}>
|
|
404
|
-
{renderChart()}
|
|
405
|
-
</div>
|
|
406
|
-
)}
|
|
407
|
-
</CardContent>
|
|
408
|
-
</Card>
|
|
1226
|
+
</CardContent>
|
|
1227
|
+
</Card>
|
|
1228
|
+
</motion.div>
|
|
409
1229
|
)
|
|
410
1230
|
}
|
|
411
1231
|
|