@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
|
@@ -25,7 +25,12 @@ export function DataTableColumnToggle({ table, trigger }: DataTableColumnToggleP
|
|
|
25
25
|
// Get all columns that can be hidden
|
|
26
26
|
const columns = table
|
|
27
27
|
.getAllColumns()
|
|
28
|
-
.filter((column: any) =>
|
|
28
|
+
.filter((column: any) =>
|
|
29
|
+
column.getCanHide() &&
|
|
30
|
+
column.id !== 'select' &&
|
|
31
|
+
column.id !== 'actions' &&
|
|
32
|
+
column.id !== 'expander'
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
// Filter columns based on search
|
|
31
36
|
const filteredColumns = React.useMemo(() => {
|
|
@@ -45,9 +50,7 @@ export function DataTableColumnToggle({ table, trigger }: DataTableColumnToggleP
|
|
|
45
50
|
const visibleCount = columns.filter((col: any) => col.getIsVisible()).length
|
|
46
51
|
|
|
47
52
|
const handleToggleAll = (visible: boolean) => {
|
|
48
|
-
|
|
49
|
-
column.toggleVisibility(visible)
|
|
50
|
-
})
|
|
53
|
+
table.toggleAllColumnsVisible(visible)
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
return (
|
|
@@ -44,6 +44,8 @@ export interface DataTableFilterDrawerProps<TData> {
|
|
|
44
44
|
filters?: FilterCondition[]
|
|
45
45
|
onFiltersChange?: (filters: FilterCondition[]) => void
|
|
46
46
|
customFilters?: React.ReactNode
|
|
47
|
+
matchAll?: boolean
|
|
48
|
+
onMatchAllChange?: (matchAll: boolean) => void
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const operatorLabels: Record<FilterOperator, string> = {
|
|
@@ -88,12 +90,16 @@ export function DataTableFilterDrawer<TData>({
|
|
|
88
90
|
filters: externalFilters,
|
|
89
91
|
onFiltersChange,
|
|
90
92
|
customFilters,
|
|
93
|
+
matchAll: externalMatchAll,
|
|
94
|
+
onMatchAllChange,
|
|
91
95
|
}: DataTableFilterDrawerProps<TData>) {
|
|
92
96
|
const [internalFilters, setInternalFilters] = useState<FilterCondition[]>([])
|
|
93
|
-
const [
|
|
97
|
+
const [internalMatchAll, setInternalMatchAll] = useState(true)
|
|
94
98
|
|
|
95
99
|
const filters = externalFilters || internalFilters
|
|
96
100
|
const setFilters = onFiltersChange || setInternalFilters
|
|
101
|
+
const matchAll = externalMatchAll !== undefined ? externalMatchAll : internalMatchAll
|
|
102
|
+
const setMatchAll = onMatchAllChange || setInternalMatchAll
|
|
97
103
|
|
|
98
104
|
// Get filterable columns
|
|
99
105
|
const filterableColumns = useMemo(() => {
|
|
@@ -135,64 +141,64 @@ export function DataTableFilterDrawer<TData>({
|
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
const applyFilters = () => {
|
|
138
|
-
//
|
|
139
|
-
|
|
144
|
+
// Create a custom filter function that handles all our conditions
|
|
145
|
+
const customFilterFn = (row: any, columnId: string, filterValue: any) => {
|
|
146
|
+
// Find the filter condition for this column
|
|
147
|
+
const filterCondition = filters.find(f => f.column === columnId)
|
|
148
|
+
if (!filterCondition) return true
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const column = table.getColumn(filter.column)
|
|
144
|
-
if (!column) return
|
|
150
|
+
const cellValue = row.getValue(columnId)
|
|
151
|
+
const filterVal = filterCondition.value
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
switch (filter.operator) {
|
|
153
|
+
switch (filterCondition.operator) {
|
|
148
154
|
case 'equals':
|
|
149
|
-
|
|
150
|
-
break
|
|
155
|
+
return cellValue === filterVal
|
|
151
156
|
case 'notEquals':
|
|
152
|
-
|
|
153
|
-
break
|
|
157
|
+
return cellValue !== filterVal
|
|
154
158
|
case 'contains':
|
|
155
|
-
|
|
156
|
-
String(value).toLowerCase().includes(String(filter.value).toLowerCase())
|
|
157
|
-
)
|
|
158
|
-
break
|
|
159
|
+
return String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
159
160
|
case 'notContains':
|
|
160
|
-
|
|
161
|
-
!String(value).toLowerCase().includes(String(filter.value).toLowerCase())
|
|
162
|
-
)
|
|
163
|
-
break
|
|
161
|
+
return !String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
164
162
|
case 'startsWith':
|
|
165
|
-
|
|
166
|
-
String(value).toLowerCase().startsWith(String(filter.value).toLowerCase())
|
|
167
|
-
)
|
|
168
|
-
break
|
|
163
|
+
return String(cellValue).toLowerCase().startsWith(String(filterVal).toLowerCase())
|
|
169
164
|
case 'endsWith':
|
|
170
|
-
|
|
171
|
-
String(value).toLowerCase().endsWith(String(filter.value).toLowerCase())
|
|
172
|
-
)
|
|
173
|
-
break
|
|
165
|
+
return String(cellValue).toLowerCase().endsWith(String(filterVal).toLowerCase())
|
|
174
166
|
case 'greaterThan':
|
|
175
|
-
|
|
176
|
-
break
|
|
167
|
+
return Number(cellValue) > Number(filterVal)
|
|
177
168
|
case 'lessThan':
|
|
178
|
-
|
|
179
|
-
break
|
|
169
|
+
return Number(cellValue) < Number(filterVal)
|
|
180
170
|
case 'greaterThanOrEqual':
|
|
181
|
-
|
|
182
|
-
break
|
|
171
|
+
return Number(cellValue) >= Number(filterVal)
|
|
183
172
|
case 'lessThanOrEqual':
|
|
184
|
-
|
|
185
|
-
break
|
|
173
|
+
return Number(cellValue) <= Number(filterVal)
|
|
186
174
|
case 'isNull':
|
|
187
|
-
|
|
188
|
-
break
|
|
175
|
+
return cellValue == null || cellValue === ''
|
|
189
176
|
case 'isNotNull':
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
177
|
+
return cellValue != null && cellValue !== ''
|
|
178
|
+
default:
|
|
179
|
+
return true
|
|
193
180
|
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Reset all column filters first
|
|
184
|
+
table.resetColumnFilters()
|
|
185
|
+
|
|
186
|
+
// Apply filter for each column that has a filter condition
|
|
187
|
+
const columnsWithFilters = [...new Set(filters.map(f => f.column))]
|
|
188
|
+
|
|
189
|
+
columnsWithFilters.forEach(columnId => {
|
|
190
|
+
const column = table.getColumn(columnId)
|
|
191
|
+
if (!column) return
|
|
192
|
+
|
|
193
|
+
// Set filter with custom function
|
|
194
|
+
column.setFilterValue({ custom: true, filters, matchAll })
|
|
194
195
|
})
|
|
195
196
|
|
|
197
|
+
// If no filters, ensure all filters are cleared
|
|
198
|
+
if (filters.length === 0) {
|
|
199
|
+
table.resetColumnFilters()
|
|
200
|
+
}
|
|
201
|
+
|
|
196
202
|
onOpenChange(false)
|
|
197
203
|
}
|
|
198
204
|
|
|
@@ -44,7 +44,7 @@ import { motion, AnimatePresence } from 'framer-motion'
|
|
|
44
44
|
import { DataTableColumnToggle } from './data-table-column-toggle'
|
|
45
45
|
import { DataTableBulkActions, type BulkAction } from './data-table-bulk-actions'
|
|
46
46
|
import { exportData, type ExportFormat, getVisibleColumns } from './data-table-export'
|
|
47
|
-
import { DataTableFilterDrawer, type FilterCondition } from './data-table-filter-drawer'
|
|
47
|
+
import { DataTableFilterDrawer, type FilterCondition, type FilterOperator } from './data-table-filter-drawer'
|
|
48
48
|
import {
|
|
49
49
|
DropdownMenu,
|
|
50
50
|
DropdownMenuContent,
|
|
@@ -153,12 +153,17 @@ export function DataTable<TData, TValue>({
|
|
|
153
153
|
onColumnFiltersChange,
|
|
154
154
|
state: externalState,
|
|
155
155
|
}: DataTableProps<TData, TValue>) {
|
|
156
|
-
// Process columns to ensure they can be hidden
|
|
156
|
+
// Process columns to ensure they can be hidden and use custom filter
|
|
157
157
|
const columns = React.useMemo(() => {
|
|
158
|
-
return originalColumns.map(col =>
|
|
159
|
-
|
|
160
|
-
enableHiding
|
|
161
|
-
|
|
158
|
+
return originalColumns.map(col => {
|
|
159
|
+
// Remove any enableHiding: false to avoid conflicts
|
|
160
|
+
const { enableHiding, ...restCol } = col as any;
|
|
161
|
+
return {
|
|
162
|
+
...restCol,
|
|
163
|
+
enableHiding: true, // Force all columns to be hideable
|
|
164
|
+
filterFn: 'custom', // Use our custom filter function
|
|
165
|
+
}
|
|
166
|
+
})
|
|
162
167
|
}, [originalColumns])
|
|
163
168
|
// Check if we're in docs mode or have pro access
|
|
164
169
|
const { hasProAccess, isLoading } = useSubscription()
|
|
@@ -226,17 +231,65 @@ export function DataTable<TData, TValue>({
|
|
|
226
231
|
onRowSelectionChange: setRowSelection,
|
|
227
232
|
onGlobalFilterChange: setGlobalFilter,
|
|
228
233
|
globalFilterFn: 'includesString',
|
|
234
|
+
filterFns: {
|
|
235
|
+
custom: (row, columnId, filterValue) => {
|
|
236
|
+
if (!filterValue?.custom || !filterValue?.filters) return true
|
|
237
|
+
|
|
238
|
+
const filters = filterValue.filters as FilterCondition[]
|
|
239
|
+
const matchAll = filterValue.matchAll !== undefined ? filterValue.matchAll : true
|
|
240
|
+
|
|
241
|
+
// Get all filter conditions (not just for this column)
|
|
242
|
+
const allFilterResults = filters.map(filterCondition => {
|
|
243
|
+
const cellValue = row.getValue(filterCondition.column)
|
|
244
|
+
const filterVal = filterCondition.value
|
|
245
|
+
|
|
246
|
+
switch (filterCondition.operator) {
|
|
247
|
+
case 'equals':
|
|
248
|
+
return cellValue === filterVal
|
|
249
|
+
case 'notEquals':
|
|
250
|
+
return cellValue !== filterVal
|
|
251
|
+
case 'contains':
|
|
252
|
+
return String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
253
|
+
case 'notContains':
|
|
254
|
+
return !String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
255
|
+
case 'startsWith':
|
|
256
|
+
return String(cellValue).toLowerCase().startsWith(String(filterVal).toLowerCase())
|
|
257
|
+
case 'endsWith':
|
|
258
|
+
return String(cellValue).toLowerCase().endsWith(String(filterVal).toLowerCase())
|
|
259
|
+
case 'greaterThan':
|
|
260
|
+
return Number(cellValue) > Number(filterVal)
|
|
261
|
+
case 'lessThan':
|
|
262
|
+
return Number(cellValue) < Number(filterVal)
|
|
263
|
+
case 'greaterThanOrEqual':
|
|
264
|
+
return Number(cellValue) >= Number(filterVal)
|
|
265
|
+
case 'lessThanOrEqual':
|
|
266
|
+
return Number(cellValue) <= Number(filterVal)
|
|
267
|
+
case 'isNull':
|
|
268
|
+
return cellValue == null || cellValue === ''
|
|
269
|
+
case 'isNotNull':
|
|
270
|
+
return cellValue != null && cellValue !== ''
|
|
271
|
+
default:
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Apply match logic
|
|
277
|
+
if (matchAll) {
|
|
278
|
+
return allFilterResults.every(result => result)
|
|
279
|
+
} else {
|
|
280
|
+
return allFilterResults.some(result => result)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
229
284
|
manualPagination,
|
|
230
285
|
pageCount,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
rowSelection,
|
|
239
|
-
globalFilter,
|
|
286
|
+
state: {
|
|
287
|
+
sorting: externalState?.sorting ?? sorting,
|
|
288
|
+
columnFilters: externalState?.columnFilters ?? columnFilters,
|
|
289
|
+
columnVisibility: externalState?.columnVisibility ?? columnVisibility,
|
|
290
|
+
rowSelection: externalState?.rowSelection ?? rowSelection,
|
|
291
|
+
globalFilter: externalState?.globalFilter ?? globalFilter,
|
|
292
|
+
...(externalState || {}),
|
|
240
293
|
},
|
|
241
294
|
initialState: {
|
|
242
295
|
pagination: {
|
|
@@ -485,6 +538,7 @@ export function DataTable<TData, TValue>({
|
|
|
485
538
|
isExpanded={isExpanded}
|
|
486
539
|
enableExpandable={enableExpandable}
|
|
487
540
|
renderSubComponent={renderSubComponent}
|
|
541
|
+
visibilityState={table.getState().columnVisibility}
|
|
488
542
|
/>
|
|
489
543
|
);
|
|
490
544
|
})}
|
|
@@ -706,6 +760,7 @@ interface TableRowProps {
|
|
|
706
760
|
isExpanded: boolean
|
|
707
761
|
enableExpandable: boolean
|
|
708
762
|
renderSubComponent?: (props: { row: { original: any; id: string } }) => React.ReactNode
|
|
763
|
+
visibilityState: Record<string, boolean>
|
|
709
764
|
}
|
|
710
765
|
|
|
711
766
|
const TableRow = React.memo(({
|
|
@@ -713,7 +768,8 @@ const TableRow = React.memo(({
|
|
|
713
768
|
columns,
|
|
714
769
|
isExpanded,
|
|
715
770
|
enableExpandable,
|
|
716
|
-
renderSubComponent
|
|
771
|
+
renderSubComponent,
|
|
772
|
+
visibilityState
|
|
717
773
|
}: TableRowProps) => {
|
|
718
774
|
const rowId = (row.original as any).id || row.id
|
|
719
775
|
|
|
@@ -726,16 +782,26 @@ const TableRow = React.memo(({
|
|
|
726
782
|
isExpanded && "border-b-0"
|
|
727
783
|
)}
|
|
728
784
|
>
|
|
729
|
-
{row.
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
785
|
+
{row.getAllCells()
|
|
786
|
+
.filter((cell) => {
|
|
787
|
+
// Manual visibility check
|
|
788
|
+
const isVisible = visibilityState[cell.column.id] !== false;
|
|
789
|
+
return isVisible;
|
|
790
|
+
})
|
|
791
|
+
.map((cell) => {
|
|
792
|
+
return (
|
|
793
|
+
<td key={cell.id} className="moonui-data-table-td p-4 align-middle">
|
|
794
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
795
|
+
</td>
|
|
796
|
+
);
|
|
797
|
+
})}
|
|
734
798
|
</tr>
|
|
735
799
|
|
|
736
800
|
{isExpanded && renderSubComponent && (
|
|
737
801
|
<tr className="border-b">
|
|
738
|
-
<td colSpan={row.
|
|
802
|
+
<td colSpan={row.getAllCells().filter(cell =>
|
|
803
|
+
visibilityState[cell.column.id] !== false
|
|
804
|
+
).length || 1} className="p-0 overflow-hidden">
|
|
739
805
|
<div
|
|
740
806
|
className="transition-all duration-300 ease-out"
|
|
741
807
|
style={{
|
|
@@ -753,13 +819,21 @@ const TableRow = React.memo(({
|
|
|
753
819
|
</>
|
|
754
820
|
)
|
|
755
821
|
}, (prevProps, nextProps) => {
|
|
756
|
-
// Custom comparison - only re-render if row data
|
|
822
|
+
// Custom comparison - only re-render if row data, expanded state, or visibility changed
|
|
757
823
|
const prevRowId = (prevProps.row.original as any).id || prevProps.row.id
|
|
758
824
|
const nextRowId = (nextProps.row.original as any).id || nextProps.row.id
|
|
759
825
|
|
|
826
|
+
// Include visibility state in comparison
|
|
827
|
+
const prevVisibilityKeys = Object.keys(prevProps.visibilityState).sort().join(',')
|
|
828
|
+
const nextVisibilityKeys = Object.keys(nextProps.visibilityState).sort().join(',')
|
|
829
|
+
const prevVisibilityValues = Object.values(prevProps.visibilityState).join(',')
|
|
830
|
+
const nextVisibilityValues = Object.values(nextProps.visibilityState).join(',')
|
|
831
|
+
|
|
760
832
|
return prevRowId === nextRowId &&
|
|
761
833
|
prevProps.isExpanded === nextProps.isExpanded &&
|
|
762
|
-
prevProps.row.getIsSelected() === nextProps.row.getIsSelected()
|
|
834
|
+
prevProps.row.getIsSelected() === nextProps.row.getIsSelected() &&
|
|
835
|
+
prevVisibilityKeys === nextVisibilityKeys &&
|
|
836
|
+
prevVisibilityValues === nextVisibilityValues
|
|
763
837
|
})
|
|
764
838
|
|
|
765
839
|
TableRow.displayName = 'TableRow'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/* Advanced Chart Styles */
|
|
2
|
+
|
|
3
|
+
/* Chart animation classes */
|
|
4
|
+
.chart-animate-in {
|
|
5
|
+
animation: chartFadeIn 0.5s ease-out;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.chart-point-pulse {
|
|
9
|
+
animation: pointPulse 2s ease-in-out infinite;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.chart-gradient-shift {
|
|
13
|
+
animation: gradientShift 3s ease-in-out infinite;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Custom chart tooltips */
|
|
17
|
+
.recharts-tooltip-wrapper {
|
|
18
|
+
outline: none !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.recharts-tooltip-cursor {
|
|
22
|
+
fill: hsl(var(--muted) / 0.1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Legend hover effects */
|
|
26
|
+
.recharts-legend-item {
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 0.2s ease;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.recharts-legend-item:hover {
|
|
32
|
+
transform: translateY(-1px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Smooth chart transitions */
|
|
36
|
+
.recharts-line-curve,
|
|
37
|
+
.recharts-area-curve {
|
|
38
|
+
transition: all 0.3s ease;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.recharts-bar-rectangle {
|
|
42
|
+
transition: all 0.2s ease;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.recharts-bar-rectangle:hover {
|
|
46
|
+
filter: brightness(1.1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Pie chart enhancements */
|
|
50
|
+
.recharts-pie-sector {
|
|
51
|
+
transition: all 0.2s ease;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.recharts-pie-sector:hover {
|
|
56
|
+
filter: brightness(1.1);
|
|
57
|
+
transform: scale(1.02);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Brush styling */
|
|
61
|
+
.recharts-brush {
|
|
62
|
+
fill: hsl(var(--primary) / 0.1);
|
|
63
|
+
stroke: hsl(var(--primary) / 0.3);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.recharts-brush-slide {
|
|
67
|
+
fill: hsl(var(--primary) / 0.2);
|
|
68
|
+
fill-opacity: 0.5;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Grid styling */
|
|
72
|
+
.recharts-cartesian-grid-horizontal line,
|
|
73
|
+
.recharts-cartesian-grid-vertical line {
|
|
74
|
+
stroke-dasharray: 3 3;
|
|
75
|
+
opacity: 0.3;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Axis styling */
|
|
79
|
+
.recharts-xAxis .recharts-cartesian-axis-tick-value,
|
|
80
|
+
.recharts-yAxis .recharts-cartesian-axis-tick-value {
|
|
81
|
+
fill: hsl(var(--muted-foreground));
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Dark mode enhancements */
|
|
86
|
+
.dark .recharts-tooltip-wrapper {
|
|
87
|
+
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dark .recharts-cartesian-grid-horizontal line,
|
|
91
|
+
.dark .recharts-cartesian-grid-vertical line {
|
|
92
|
+
opacity: 0.2;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Sparkline mode */
|
|
96
|
+
.sparkline-chart .recharts-surface {
|
|
97
|
+
overflow: visible !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Loading skeleton animation */
|
|
101
|
+
@keyframes chartSkeletonPulse {
|
|
102
|
+
0%, 100% {
|
|
103
|
+
opacity: 0.5;
|
|
104
|
+
transform: scaleY(0.8);
|
|
105
|
+
}
|
|
106
|
+
50% {
|
|
107
|
+
opacity: 1;
|
|
108
|
+
transform: scaleY(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Chart animations */
|
|
113
|
+
@keyframes chartFadeIn {
|
|
114
|
+
from {
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transform: translateY(10px);
|
|
117
|
+
}
|
|
118
|
+
to {
|
|
119
|
+
opacity: 1;
|
|
120
|
+
transform: translateY(0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes pointPulse {
|
|
125
|
+
0%, 100% {
|
|
126
|
+
transform: scale(1);
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
50% {
|
|
130
|
+
transform: scale(1.2);
|
|
131
|
+
opacity: 0.8;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@keyframes gradientShift {
|
|
136
|
+
0%, 100% {
|
|
137
|
+
stop-opacity: 0.8;
|
|
138
|
+
}
|
|
139
|
+
50% {
|
|
140
|
+
stop-opacity: 0.4;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Interactive legend styles */
|
|
145
|
+
.chart-legend-item {
|
|
146
|
+
position: relative;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.chart-legend-item::before {
|
|
151
|
+
content: '';
|
|
152
|
+
position: absolute;
|
|
153
|
+
top: 0;
|
|
154
|
+
left: -100%;
|
|
155
|
+
width: 100%;
|
|
156
|
+
height: 100%;
|
|
157
|
+
background: linear-gradient(90deg, transparent, hsl(var(--primary) / 0.1), transparent);
|
|
158
|
+
transition: left 0.5s ease;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.chart-legend-item:hover::before {
|
|
162
|
+
left: 100%;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Crosshair cursor */
|
|
166
|
+
.chart-crosshair {
|
|
167
|
+
stroke: hsl(var(--muted-foreground));
|
|
168
|
+
stroke-width: 1;
|
|
169
|
+
stroke-dasharray: 5 5;
|
|
170
|
+
opacity: 0.5;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Mini map styles */
|
|
174
|
+
.chart-minimap {
|
|
175
|
+
border: 1px solid hsl(var(--border));
|
|
176
|
+
border-radius: var(--radius-md);
|
|
177
|
+
background: hsl(var(--background) / 0.8);
|
|
178
|
+
backdrop-filter: blur(8px);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Export menu animation */
|
|
182
|
+
.chart-export-menu {
|
|
183
|
+
animation: slideDown 0.2s ease-out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@keyframes slideDown {
|
|
187
|
+
from {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transform: translateY(-10px);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
transform: translateY(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Zoom indicator */
|
|
198
|
+
.chart-zoom-indicator {
|
|
199
|
+
background: hsl(var(--background) / 0.9);
|
|
200
|
+
backdrop-filter: blur(8px);
|
|
201
|
+
border: 1px solid hsl(var(--border));
|
|
202
|
+
border-radius: var(--radius-full);
|
|
203
|
+
padding: 0.25rem 0.5rem;
|
|
204
|
+
font-size: 0.75rem;
|
|
205
|
+
font-weight: 500;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Performance optimizations */
|
|
209
|
+
.recharts-surface {
|
|
210
|
+
will-change: transform;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.recharts-line,
|
|
214
|
+
.recharts-area,
|
|
215
|
+
.recharts-bar {
|
|
216
|
+
will-change: opacity, transform;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Responsive adjustments */
|
|
220
|
+
@media (max-width: 640px) {
|
|
221
|
+
.recharts-wrapper {
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.recharts-legend-wrapper {
|
|
226
|
+
margin-top: 1rem !important;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Print styles */
|
|
231
|
+
@media print {
|
|
232
|
+
.chart-controls {
|
|
233
|
+
display: none !important;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.recharts-tooltip-wrapper {
|
|
237
|
+
display: none !important;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -255,3 +255,103 @@ export function exportChartAsImage(
|
|
|
255
255
|
// Implementation would go here
|
|
256
256
|
// This is a placeholder for the actual export functionality
|
|
257
257
|
}
|
|
258
|
+
|
|
259
|
+
export function exportChartData(
|
|
260
|
+
data: ChartDataPoint[],
|
|
261
|
+
filename: string = 'chart-data',
|
|
262
|
+
format: 'json' | 'csv' = 'json'
|
|
263
|
+
): void {
|
|
264
|
+
let content: string
|
|
265
|
+
let mimeType: string
|
|
266
|
+
|
|
267
|
+
if (format === 'json') {
|
|
268
|
+
content = JSON.stringify(data, null, 2)
|
|
269
|
+
mimeType = 'application/json'
|
|
270
|
+
} else {
|
|
271
|
+
// CSV export
|
|
272
|
+
const headers = Object.keys(data[0] || {})
|
|
273
|
+
const csvRows = [
|
|
274
|
+
headers.join(','),
|
|
275
|
+
...data.map(row =>
|
|
276
|
+
headers.map(header => {
|
|
277
|
+
const value = row[header]
|
|
278
|
+
return typeof value === 'string' && value.includes(',')
|
|
279
|
+
? `"${value}"`
|
|
280
|
+
: value
|
|
281
|
+
}).join(',')
|
|
282
|
+
)
|
|
283
|
+
]
|
|
284
|
+
content = csvRows.join('\n')
|
|
285
|
+
mimeType = 'text/csv'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const blob = new Blob([content], { type: mimeType })
|
|
289
|
+
const url = window.URL.createObjectURL(blob)
|
|
290
|
+
const link = document.createElement('a')
|
|
291
|
+
link.href = url
|
|
292
|
+
link.download = `${filename}.${format}`
|
|
293
|
+
document.body.appendChild(link)
|
|
294
|
+
link.click()
|
|
295
|
+
document.body.removeChild(link)
|
|
296
|
+
window.URL.revokeObjectURL(url)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Gradient color generator
|
|
300
|
+
export function generateGradientColors(baseColor: string, count: number = 5): string[] {
|
|
301
|
+
const colors: string[] = []
|
|
302
|
+
const rgb = hexToRgb(baseColor)
|
|
303
|
+
|
|
304
|
+
if (!rgb) return [baseColor]
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < count; i++) {
|
|
307
|
+
const factor = i / (count - 1)
|
|
308
|
+
const r = Math.round(rgb.r + (255 - rgb.r) * factor * 0.5)
|
|
309
|
+
const g = Math.round(rgb.g + (255 - rgb.g) * factor * 0.5)
|
|
310
|
+
const b = Math.round(rgb.b + (255 - rgb.b) * factor * 0.5)
|
|
311
|
+
colors.push(`rgb(${r}, ${g}, ${b})`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return colors
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
318
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
319
|
+
return result ? {
|
|
320
|
+
r: parseInt(result[1], 16),
|
|
321
|
+
g: parseInt(result[2], 16),
|
|
322
|
+
b: parseInt(result[3], 16)
|
|
323
|
+
} : null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Performance optimization helpers
|
|
327
|
+
export function throttleChartData(data: ChartDataPoint[], maxPoints: number = 1000): ChartDataPoint[] {
|
|
328
|
+
if (data.length <= maxPoints) return data
|
|
329
|
+
|
|
330
|
+
const step = Math.ceil(data.length / maxPoints)
|
|
331
|
+
return data.filter((_, index) => index % step === 0)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Format large numbers
|
|
335
|
+
export function formatChartValue(value: number): string {
|
|
336
|
+
if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`
|
|
337
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`
|
|
338
|
+
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`
|
|
339
|
+
return value.toString()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Calculate chart statistics
|
|
343
|
+
export function calculateChartStats(data: ChartDataPoint[], keys: string[]): Record<string, { min: number; max: number; avg: number; total: number }> {
|
|
344
|
+
const stats: Record<string, { min: number; max: number; avg: number; total: number }> = {}
|
|
345
|
+
|
|
346
|
+
keys.forEach(key => {
|
|
347
|
+
const values = data.map(point => Number(point[key]) || 0)
|
|
348
|
+
stats[key] = {
|
|
349
|
+
min: Math.min(...values),
|
|
350
|
+
max: Math.max(...values),
|
|
351
|
+
avg: values.reduce((sum, val) => sum + val, 0) / values.length,
|
|
352
|
+
total: values.reduce((sum, val) => sum + val, 0)
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
return stats
|
|
357
|
+
}
|