@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.
@@ -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) => column.getCanHide())
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
- columns.forEach((column: any) => {
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 [matchAll, setMatchAll] = useState(true)
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
- // Reset all column filters first
139
- table.resetColumnFilters()
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
- // Apply each filter
142
- filters.forEach(filter => {
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
- // Apply filter based on operator
147
- switch (filter.operator) {
153
+ switch (filterCondition.operator) {
148
154
  case 'equals':
149
- column.setFilterValue(filter.value)
150
- break
155
+ return cellValue === filterVal
151
156
  case 'notEquals':
152
- column.setFilterValue((value: any) => value !== filter.value)
153
- break
157
+ return cellValue !== filterVal
154
158
  case 'contains':
155
- column.setFilterValue((value: any) =>
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
- column.setFilterValue((value: any) =>
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
- column.setFilterValue((value: any) =>
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
- column.setFilterValue((value: any) =>
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
- column.setFilterValue((value: any) => Number(value) > Number(filter.value))
176
- break
167
+ return Number(cellValue) > Number(filterVal)
177
168
  case 'lessThan':
178
- column.setFilterValue((value: any) => Number(value) < Number(filter.value))
179
- break
169
+ return Number(cellValue) < Number(filterVal)
180
170
  case 'greaterThanOrEqual':
181
- column.setFilterValue((value: any) => Number(value) >= Number(filter.value))
182
- break
171
+ return Number(cellValue) >= Number(filterVal)
183
172
  case 'lessThanOrEqual':
184
- column.setFilterValue((value: any) => Number(value) <= Number(filter.value))
185
- break
173
+ return Number(cellValue) <= Number(filterVal)
186
174
  case 'isNull':
187
- column.setFilterValue((value: any) => value == null || value === '')
188
- break
175
+ return cellValue == null || cellValue === ''
189
176
  case 'isNotNull':
190
- column.setFilterValue((value: any) => value != null && value !== '')
191
- break
192
- // Add more operators as needed
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
- ...col,
160
- enableHiding: col.enableHiding !== false,
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
- enableColumnFilters: true,
232
- enableSorting: true,
233
- enableHiding: true,
234
- state: externalState || {
235
- sorting,
236
- columnFilters,
237
- columnVisibility,
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.getVisibleCells().map((cell) => (
730
- <td key={cell.id} className="moonui-data-table-td p-4 align-middle">
731
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
732
- </td>
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.getVisibleCells().length} className="p-0 overflow-hidden">
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 or expanded state changed
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
+ }
@@ -1,4 +1,5 @@
1
1
  /* MoonUI Pro - Complete CSS System */
2
2
  @import "./tailwind.css";
3
3
  @import "./tokens.css";
4
- @import "./design-system.css";
4
+ @import "./design-system.css";
5
+ @import "./advanced-chart.css";
@@ -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
+ }