mintwaterfall 0.8.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/CONTRIBUTING.md +199 -0
  3. package/README.md +363 -0
  4. package/dist/index.d.ts +149 -0
  5. package/dist/mintwaterfall.cjs.js +7978 -0
  6. package/dist/mintwaterfall.esm.js +7907 -0
  7. package/dist/mintwaterfall.min.js +7 -0
  8. package/dist/mintwaterfall.umd.js +7978 -0
  9. package/index.d.ts +149 -0
  10. package/package.json +126 -0
  11. package/src/enterprise/enterprise-core.js +0 -0
  12. package/src/enterprise/enterprise-feature-template.js +0 -0
  13. package/src/enterprise/feature-registry.js +0 -0
  14. package/src/enterprise/features/breakdown.js +0 -0
  15. package/src/features/breakdown.js +0 -0
  16. package/src/features/conditional-formatting.js +0 -0
  17. package/src/index.js +111 -0
  18. package/src/mintwaterfall-accessibility.ts +680 -0
  19. package/src/mintwaterfall-advanced-data.ts +1034 -0
  20. package/src/mintwaterfall-advanced-interactions.ts +649 -0
  21. package/src/mintwaterfall-advanced-performance.ts +582 -0
  22. package/src/mintwaterfall-animations.ts +595 -0
  23. package/src/mintwaterfall-brush.ts +471 -0
  24. package/src/mintwaterfall-chart-core.ts +296 -0
  25. package/src/mintwaterfall-chart.ts +1915 -0
  26. package/src/mintwaterfall-data.ts +1100 -0
  27. package/src/mintwaterfall-export.ts +475 -0
  28. package/src/mintwaterfall-hierarchical-layouts.ts +724 -0
  29. package/src/mintwaterfall-layouts.ts +647 -0
  30. package/src/mintwaterfall-performance.ts +573 -0
  31. package/src/mintwaterfall-scales.ts +437 -0
  32. package/src/mintwaterfall-shapes.ts +385 -0
  33. package/src/mintwaterfall-statistics.ts +821 -0
  34. package/src/mintwaterfall-themes.ts +391 -0
  35. package/src/mintwaterfall-tooltip.ts +450 -0
  36. package/src/mintwaterfall-zoom.ts +399 -0
  37. package/src/types/js-modules.d.ts +25 -0
  38. package/src/utils/compatibility-layer.js +0 -0
@@ -0,0 +1,1034 @@
1
+ // MintWaterfall Advanced Data Manipulation Utilities
2
+ // Enhanced D3.js data manipulation capabilities for comprehensive waterfall analysis
3
+
4
+ import * as d3 from 'd3';
5
+ import { group, rollup, flatRollup, cross, index } from 'd3-array';
6
+
7
+ // ============================================================================
8
+ // TYPE DEFINITIONS
9
+ // ============================================================================
10
+
11
+ export interface SequenceAnalysis {
12
+ from: string;
13
+ to: string;
14
+ change: number;
15
+ changePercent: number;
16
+ changeDirection: 'increase' | 'decrease' | 'neutral';
17
+ magnitude: 'small' | 'medium' | 'large';
18
+ }
19
+
20
+ export interface DataMergeOptions {
21
+ mergeStrategy: 'combine' | 'override' | 'average' | 'sum';
22
+ conflictResolution: 'first' | 'last' | 'max' | 'min';
23
+ keyField: string;
24
+ valueField: string;
25
+ }
26
+
27
+ export interface TickGenerationOptions {
28
+ count?: number;
29
+ step?: number;
30
+ nice?: boolean;
31
+ format?: string;
32
+ threshold?: number;
33
+ includeZero?: boolean;
34
+ }
35
+
36
+ export interface DataOrderingOptions {
37
+ field: string;
38
+ direction: 'ascending' | 'descending';
39
+ strategy: 'value' | 'cumulative' | 'magnitude' | 'alphabetical';
40
+ groupBy?: string;
41
+ }
42
+
43
+ export interface AdvancedDataProcessor {
44
+ // Sequence analysis using d3.pairs()
45
+ analyzeSequence(data: any[]): SequenceAnalysis[];
46
+
47
+ // Data reordering using d3.permute()
48
+ optimizeDataOrder(data: any[], options: DataOrderingOptions): any[];
49
+
50
+ // Complex dataset merging using d3.merge()
51
+ mergeDatasets(datasets: any[][], options: DataMergeOptions): any[];
52
+
53
+ // Custom axis tick generation using d3.ticks()
54
+ generateCustomTicks(domain: [number, number], options: TickGenerationOptions): number[];
55
+
56
+ // Advanced data transformation utilities
57
+ createDataPairs(data: any[], accessor?: (d: any) => any): any[];
58
+ permuteByIndices(data: any[], indices: number[]): any[];
59
+ mergeSimilarItems(data: any[], similarityThreshold: number): any[];
60
+
61
+ // Data quality and validation
62
+ validateSequentialData(data: any[]): { isValid: boolean; errors: string[] };
63
+ detectDataAnomalies(data: any[]): any[];
64
+ suggestDataOptimizations(data: any[]): string[];
65
+ }
66
+
67
+ // ============================================================================
68
+ // ADVANCED DATA PROCESSOR IMPLEMENTATION
69
+ // ============================================================================
70
+
71
+ function createAdvancedDataProcessorOLD(): AdvancedDataProcessor {
72
+
73
+ // ========================================================================
74
+ // SEQUENCE ANALYSIS (d3.pairs)
75
+ // ========================================================================
76
+
77
+ /**
78
+ * Analyze sequential relationships in waterfall data
79
+ * Uses d3.pairs() to understand flow between consecutive items
80
+ */
81
+ function analyzeSequence(data: any[]): SequenceAnalysis[] {
82
+ if (!Array.isArray(data) || data.length < 2) {
83
+ return [];
84
+ }
85
+
86
+ // Extract values for analysis
87
+ const values = data.map(d => {
88
+ if (typeof d === 'number') return d;
89
+ if (d.value !== undefined) return d.value;
90
+ if (d.stacks && Array.isArray(d.stacks)) {
91
+ return d.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
92
+ }
93
+ return 0;
94
+ });
95
+
96
+ // Use d3.pairs() for sequential analysis
97
+ const sequences: SequenceAnalysis[] = d3.pairs(data, (a: any, b: any) => {
98
+ const aValue = extractValue(a);
99
+ const bValue = extractValue(b);
100
+ const change = bValue - aValue;
101
+ const changePercent = aValue !== 0 ? (change / Math.abs(aValue)) * 100 : 0;
102
+
103
+ // Determine change direction
104
+ let changeDirection: 'increase' | 'decrease' | 'neutral';
105
+ if (Math.abs(change) < 0.01) changeDirection = 'neutral';
106
+ else if (change > 0) changeDirection = 'increase';
107
+ else changeDirection = 'decrease';
108
+
109
+ // Determine magnitude
110
+ const absChangePercent = Math.abs(changePercent);
111
+ let magnitude: 'small' | 'medium' | 'large';
112
+ if (absChangePercent < 5) magnitude = 'small';
113
+ else if (absChangePercent < 20) magnitude = 'medium';
114
+ else magnitude = 'large';
115
+
116
+ return {
117
+ from: getLabel(a),
118
+ to: getLabel(b),
119
+ change,
120
+ changePercent,
121
+ changeDirection,
122
+ magnitude
123
+ };
124
+ });
125
+
126
+ return sequences;
127
+ }
128
+
129
+ // ========================================================================
130
+ // DATA REORDERING (d3.permute)
131
+ // ========================================================================
132
+
133
+ /**
134
+ * Optimize data ordering for better waterfall visualization
135
+ * Uses d3.permute() with intelligent sorting strategies
136
+ */
137
+ function optimizeDataOrder(data: any[], options: DataOrderingOptions): any[] {
138
+ if (!Array.isArray(data) || data.length === 0) {
139
+ return data;
140
+ }
141
+
142
+ const { field, direction, strategy, groupBy } = options;
143
+
144
+ // Create sorting indices based on strategy
145
+ let indices: number[];
146
+
147
+ switch (strategy) {
148
+ case 'value':
149
+ indices = d3.range(data.length).sort((i, j) => {
150
+ const aValue = extractValue(data[i]);
151
+ const bValue = extractValue(data[j]);
152
+ return direction === 'ascending' ?
153
+ d3.ascending(aValue, bValue) :
154
+ d3.descending(aValue, bValue);
155
+ });
156
+ break;
157
+
158
+ case 'cumulative':
159
+ // Sort by cumulative impact on waterfall
160
+ const cumulativeValues = calculateCumulativeValues(data);
161
+ indices = d3.range(data.length).sort((i, j) => {
162
+ return direction === 'ascending' ?
163
+ d3.ascending(cumulativeValues[i], cumulativeValues[j]) :
164
+ d3.descending(cumulativeValues[i], cumulativeValues[j]);
165
+ });
166
+ break;
167
+
168
+ case 'magnitude':
169
+ indices = d3.range(data.length).sort((i, j) => {
170
+ const aMagnitude = Math.abs(extractValue(data[i]));
171
+ const bMagnitude = Math.abs(extractValue(data[j]));
172
+ return direction === 'ascending' ?
173
+ d3.ascending(aMagnitude, bMagnitude) :
174
+ d3.descending(aMagnitude, bMagnitude);
175
+ });
176
+ break;
177
+
178
+ case 'alphabetical':
179
+ indices = d3.range(data.length).sort((i, j) => {
180
+ const aLabel = getLabel(data[i]);
181
+ const bLabel = getLabel(data[j]);
182
+ return direction === 'ascending' ?
183
+ d3.ascending(aLabel, bLabel) :
184
+ d3.descending(aLabel, bLabel);
185
+ });
186
+ break;
187
+
188
+ default:
189
+ return data; // No reordering
190
+ }
191
+
192
+ // Use d3.permute() to reorder data
193
+ return d3.permute(data, indices);
194
+ }
195
+
196
+ // ========================================================================
197
+ // DATASET MERGING (d3.merge)
198
+ // ========================================================================
199
+
200
+ /**
201
+ * Merge multiple datasets with sophisticated conflict resolution
202
+ * Uses d3.merge() with custom merge strategies
203
+ */
204
+ function mergeDatasets(datasets: any[][], options: DataMergeOptions): any[] {
205
+ if (!Array.isArray(datasets) || datasets.length === 0) {
206
+ return [];
207
+ }
208
+
209
+ const { mergeStrategy, conflictResolution, keyField, valueField } = options;
210
+
211
+ // Use d3.merge() to combine all datasets
212
+ const flatData = d3.merge(datasets);
213
+
214
+ // Group by key field for conflict resolution
215
+ const grouped = d3.group(flatData, (d: any) => d[keyField] || getLabel(d));
216
+
217
+ // Resolve conflicts and merge
218
+ const mergedData: any[] = [];
219
+
220
+ for (const [key, items] of grouped) {
221
+ if (items.length === 1) {
222
+ mergedData.push(items[0]);
223
+ continue;
224
+ }
225
+
226
+ // Handle conflicts with multiple items
227
+ let mergedItem: any;
228
+
229
+ switch (mergeStrategy) {
230
+ case 'combine':
231
+ mergedItem = combineItems(items, valueField);
232
+ break;
233
+
234
+ case 'override':
235
+ mergedItem = resolveConflict(items, conflictResolution);
236
+ break;
237
+
238
+ case 'average':
239
+ mergedItem = averageItems(items, valueField);
240
+ break;
241
+
242
+ case 'sum':
243
+ mergedItem = sumItems(items, valueField);
244
+ break;
245
+
246
+ default:
247
+ mergedItem = items[0];
248
+ }
249
+
250
+ mergedData.push(mergedItem);
251
+ }
252
+
253
+ return mergedData;
254
+ }
255
+
256
+ // ========================================================================
257
+ // CUSTOM TICK GENERATION (d3.ticks)
258
+ // ========================================================================
259
+
260
+ /**
261
+ * Generate custom axis ticks with advanced options
262
+ * Uses d3.ticks() with intelligent tick selection
263
+ */
264
+ function generateCustomTicks(domain: [number, number], options: TickGenerationOptions): number[] {
265
+ const {
266
+ count = 10,
267
+ step,
268
+ nice = true,
269
+ threshold = 0,
270
+ includeZero = true
271
+ } = options;
272
+
273
+ let [min, max] = domain;
274
+
275
+ // Apply nice scaling if requested
276
+ if (nice) {
277
+ const scale = d3.scaleLinear().domain([min, max]).nice();
278
+ [min, max] = scale.domain() as [number, number];
279
+ }
280
+
281
+ // Generate base ticks using d3.ticks()
282
+ let ticks: number[];
283
+
284
+ if (step !== undefined) {
285
+ // Use custom step
286
+ ticks = d3.ticks(min, max, Math.abs(max - min) / step);
287
+ } else {
288
+ // Use count-based generation
289
+ ticks = d3.ticks(min, max, count);
290
+ }
291
+
292
+ // Apply threshold filtering
293
+ if (threshold > 0) {
294
+ ticks = ticks.filter(tick => Math.abs(tick) >= threshold);
295
+ }
296
+
297
+ // Ensure zero is included if requested
298
+ if (includeZero && !ticks.includes(0) && min <= 0 && max >= 0) {
299
+ ticks.push(0);
300
+ ticks.sort(d3.ascending);
301
+ }
302
+
303
+ return ticks;
304
+ }
305
+
306
+ // ========================================================================
307
+ // UTILITY FUNCTIONS
308
+ // ========================================================================
309
+
310
+ function createDataPairs(data: any[], accessor?: (d: any) => any): any[] {
311
+ if (accessor) {
312
+ return d3.pairs(data, (a, b) => ({ a: accessor(a), b: accessor(b) }));
313
+ }
314
+ return d3.pairs(data);
315
+ }
316
+
317
+ function permuteByIndices(data: any[], indices: number[]): any[] {
318
+ return d3.permute(data, indices);
319
+ }
320
+
321
+ function mergeSimilarItems(data: any[], similarityThreshold: number): any[] {
322
+ // Group similar items and merge them
323
+ const groups: any[][] = [];
324
+ const used = new Set<number>();
325
+
326
+ for (let i = 0; i < data.length; i++) {
327
+ if (used.has(i)) continue;
328
+
329
+ const group = [data[i]];
330
+ used.add(i);
331
+
332
+ for (let j = i + 1; j < data.length; j++) {
333
+ if (used.has(j)) continue;
334
+
335
+ const similarity = calculateSimilarity(data[i], data[j]);
336
+ if (similarity >= similarityThreshold) {
337
+ group.push(data[j]);
338
+ used.add(j);
339
+ }
340
+ }
341
+
342
+ groups.push(group);
343
+ }
344
+
345
+ // Merge groups using d3.merge()
346
+ return groups.map(group => {
347
+ if (group.length === 1) return group[0];
348
+ return mergeGroupItems(group);
349
+ });
350
+ }
351
+
352
+ function validateSequentialData(data: any[]): { isValid: boolean; errors: string[] } {
353
+ const errors: string[] = [];
354
+
355
+ if (!Array.isArray(data)) {
356
+ errors.push("Data must be an array");
357
+ return { isValid: false, errors };
358
+ }
359
+
360
+ if (data.length < 2) {
361
+ errors.push("Data must have at least 2 items for sequence analysis");
362
+ }
363
+
364
+ // Check for valid values
365
+ const invalidItems = data.filter((d, i) => {
366
+ const value = extractValue(d);
367
+ return isNaN(value) || !isFinite(value);
368
+ });
369
+
370
+ if (invalidItems.length > 0) {
371
+ errors.push(`Found ${invalidItems.length} items with invalid values`);
372
+ }
373
+
374
+ // Check for duplicate labels
375
+ const labels = data.map(getLabel);
376
+ const uniqueLabels = new Set(labels);
377
+ if (labels.length !== uniqueLabels.size) {
378
+ errors.push("Duplicate labels detected - may cause confusion in sequence analysis");
379
+ }
380
+
381
+ return { isValid: errors.length === 0, errors };
382
+ }
383
+
384
+ function detectDataAnomalies(data: any[]): any[] {
385
+ const values = data.map(extractValue);
386
+ const mean = d3.mean(values) || 0;
387
+ const deviation = d3.deviation(values) || 0;
388
+ const threshold = 2 * deviation; // 2-sigma rule
389
+
390
+ return data.filter((d, i) => {
391
+ const value = values[i];
392
+ return Math.abs(value - mean) > threshold;
393
+ });
394
+ }
395
+
396
+ function suggestDataOptimizations(data: any[]): string[] {
397
+ const suggestions: string[] = [];
398
+
399
+ // Analyze data characteristics
400
+ const values = data.map(extractValue);
401
+ const sequences = analyzeSequence(data);
402
+
403
+ // Check for optimization opportunities
404
+ if (values.some(v => v === 0)) {
405
+ suggestions.push("Consider removing or combining zero-value items");
406
+ }
407
+
408
+ const smallChanges = sequences.filter(s => s.magnitude === 'small').length;
409
+ if (smallChanges > data.length * 0.3) {
410
+ suggestions.push("Many small changes detected - consider grouping similar items");
411
+ }
412
+
413
+ const alternatingPattern = hasAlternatingPattern(values);
414
+ if (alternatingPattern) {
415
+ suggestions.push("Alternating positive/negative pattern detected - consider reordering by magnitude");
416
+ }
417
+
418
+ if (data.length > 20) {
419
+ suggestions.push("Large dataset - consider using hierarchical grouping or filtering");
420
+ }
421
+
422
+ return suggestions;
423
+ }
424
+
425
+ // ========================================================================
426
+ // HELPER FUNCTIONS
427
+ // ========================================================================
428
+
429
+ function extractValue(item: any): number {
430
+ if (typeof item === 'number') return item;
431
+ if (item.value !== undefined) return item.value;
432
+ if (item.stacks && Array.isArray(item.stacks)) {
433
+ return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
434
+ }
435
+ return 0;
436
+ }
437
+
438
+ function getLabel(item: any): string {
439
+ if (typeof item === 'string') return item;
440
+ if (item.label !== undefined) return item.label;
441
+ if (item.name !== undefined) return item.name;
442
+ return 'Unnamed';
443
+ }
444
+
445
+ function calculateCumulativeValues(data: any[]): number[] {
446
+ const values = data.map(extractValue);
447
+ const cumulative: number[] = [];
448
+ let running = 0;
449
+
450
+ for (const value of values) {
451
+ running += value;
452
+ cumulative.push(running);
453
+ }
454
+
455
+ return cumulative;
456
+ }
457
+
458
+ function combineItems(items: any[], valueField: string): any {
459
+ const combined = { ...items[0] };
460
+ const totalValue = items.reduce((sum, item) => sum + extractValue(item), 0);
461
+
462
+ if (combined.value !== undefined) combined.value = totalValue;
463
+ if (combined[valueField] !== undefined) combined[valueField] = totalValue;
464
+
465
+ // Combine stacks if present
466
+ if (combined.stacks) {
467
+ combined.stacks = items.flatMap(item => item.stacks || []);
468
+ }
469
+
470
+ return combined;
471
+ }
472
+
473
+ function resolveConflict(items: any[], strategy: string): any {
474
+ switch (strategy) {
475
+ case 'first': return items[0];
476
+ case 'last': return items[items.length - 1];
477
+ case 'max': return items.reduce((max, item) => extractValue(item) > extractValue(max) ? item : max);
478
+ case 'min': return items.reduce((min, item) => extractValue(item) < extractValue(min) ? item : min);
479
+ default: return items[0];
480
+ }
481
+ }
482
+
483
+ function averageItems(items: any[], valueField: string): any {
484
+ const averaged = { ...items[0] };
485
+ const avgValue = d3.mean(items, extractValue) || 0;
486
+
487
+ if (averaged.value !== undefined) averaged.value = avgValue;
488
+ if (averaged[valueField] !== undefined) averaged[valueField] = avgValue;
489
+
490
+ return averaged;
491
+ }
492
+
493
+ function sumItems(items: any[], valueField: string): any {
494
+ const summed = { ...items[0] };
495
+ const totalValue = d3.sum(items, extractValue);
496
+
497
+ if (summed.value !== undefined) summed.value = totalValue;
498
+ if (summed[valueField] !== undefined) summed[valueField] = totalValue;
499
+
500
+ return summed;
501
+ }
502
+
503
+ function calculateSimilarity(a: any, b: any): number {
504
+ const valueA = extractValue(a);
505
+ const valueB = extractValue(b);
506
+ const labelA = getLabel(a);
507
+ const labelB = getLabel(b);
508
+
509
+ // Simple similarity based on value proximity and label similarity
510
+ const valueSim = 1 - Math.abs(valueA - valueB) / (Math.abs(valueA) + Math.abs(valueB) + 1);
511
+ const labelSim = labelA === labelB ? 1 : 0;
512
+
513
+ return (valueSim + labelSim) / 2;
514
+ }
515
+
516
+ function mergeGroupItems(group: any[]): any {
517
+ return combineItems(group, 'value');
518
+ }
519
+
520
+ function hasAlternatingPattern(values: number[]): boolean {
521
+ if (values.length < 3) return false;
522
+
523
+ let alternating = 0;
524
+ for (let i = 1; i < values.length - 1; i++) {
525
+ const prev = values[i - 1];
526
+ const curr = values[i];
527
+ const next = values[i + 1];
528
+
529
+ if ((prev > 0 && curr < 0 && next > 0) || (prev < 0 && curr > 0 && next < 0)) {
530
+ alternating++;
531
+ }
532
+ }
533
+
534
+ return alternating > values.length * 0.3;
535
+ }
536
+
537
+ // ========================================================================
538
+ // RETURN PROCESSOR INTERFACE
539
+ // ========================================================================
540
+
541
+ return {
542
+ analyzeSequence,
543
+ optimizeDataOrder,
544
+ mergeDatasets,
545
+ generateCustomTicks,
546
+ createDataPairs,
547
+ permuteByIndices,
548
+ mergeSimilarItems,
549
+ validateSequentialData,
550
+ detectDataAnomalies,
551
+ suggestDataOptimizations
552
+ };
553
+ }
554
+
555
+ // ============================================================================
556
+ // SPECIALIZED WATERFALL UTILITIES
557
+ // ============================================================================
558
+
559
+ /**
560
+ * Create sequence analysis specifically for waterfall data
561
+ */
562
+ export function createWaterfallSequenceAnalyzer(data: any[]): {
563
+ flowAnalysis: SequenceAnalysis[];
564
+ cumulativeFlow: Array<{step: number, cumulative: number, change: number}>;
565
+ criticalPaths: string[];
566
+ optimizationSuggestions: string[];
567
+ } {
568
+ const processor = createAdvancedDataProcessor();
569
+ const flowAnalysis = processor.analyzeSequence(data);
570
+
571
+ // Calculate cumulative flow
572
+ const cumulativeFlow: Array<{step: number, cumulative: number, change: number}> = [];
573
+ let cumulative = 0;
574
+
575
+ data.forEach((item, index) => {
576
+ const value = extractValue(item);
577
+ cumulative += value;
578
+ cumulativeFlow.push({
579
+ step: index,
580
+ cumulative,
581
+ change: value
582
+ });
583
+ });
584
+
585
+ // Identify critical paths (large impact changes)
586
+ const criticalPaths = flowAnalysis
587
+ .filter((seq: any) => seq.magnitude === 'large')
588
+ .map((seq: any) => `${seq.from} → ${seq.to}`);
589
+
590
+ // Generate optimization suggestions
591
+ const optimizationSuggestions = processor.suggestDataOptimizations(data);
592
+
593
+ return {
594
+ flowAnalysis,
595
+ cumulativeFlow,
596
+ criticalPaths,
597
+ optimizationSuggestions
598
+ };
599
+
600
+ function extractValue(item: any): number {
601
+ if (typeof item === 'number') return item;
602
+ if (item.value !== undefined) return item.value;
603
+ if (item.stacks && Array.isArray(item.stacks)) {
604
+ return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
605
+ }
606
+ return 0;
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Create optimized tick generator for waterfall charts
612
+ */
613
+ export function createWaterfallTickGenerator(domain: [number, number], dataPoints: any[]): {
614
+ ticks: number[];
615
+ labels: string[];
616
+ keyMarkers: number[];
617
+ } {
618
+ const processor = createAdvancedDataProcessor();
619
+
620
+ // Generate base ticks
621
+ const ticks = processor.generateCustomTicks(domain, {
622
+ count: 8,
623
+ nice: true,
624
+ includeZero: true,
625
+ threshold: Math.abs(domain[1] - domain[0]) / 100
626
+ });
627
+
628
+ // Generate labels
629
+ const labels = ticks.map((tick: number) => {
630
+ if (tick === 0) return '0';
631
+ if (Math.abs(tick) >= 1000000) return `${(tick / 1000000).toFixed(1)}M`;
632
+ if (Math.abs(tick) >= 1000) return `${(tick / 1000).toFixed(1)}K`;
633
+ return tick.toFixed(0);
634
+ });
635
+
636
+ // Identify key markers (data points that align with ticks)
637
+ const keyMarkers = ticks.filter((tick: number) => {
638
+ return dataPoints.some(d => Math.abs(extractValue(d) - tick) < Math.abs(domain[1] - domain[0]) / 50);
639
+ });
640
+
641
+ return { ticks, labels, keyMarkers };
642
+
643
+ function extractValue(item: any): number {
644
+ if (typeof item === 'number') return item;
645
+ if (item.value !== undefined) return item.value;
646
+ if (item.stacks && Array.isArray(item.stacks)) {
647
+ return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
648
+ }
649
+ return 0;
650
+ }
651
+ }
652
+
653
+ // ============================================================================
654
+ // MISSING ADVANCED DATA PROCESSOR FUNCTIONS
655
+ // ============================================================================
656
+
657
+ /**
658
+ * Creates an advanced data processor with D3.js data manipulation functions
659
+ */
660
+ export function createAdvancedDataProcessor() {
661
+
662
+ // Group data by key using d3.group
663
+ function groupBy<T>(data: T[], accessor: (d: T) => string): Map<string, T[]> {
664
+ if (!data || !Array.isArray(data) || !accessor) {
665
+ return new Map();
666
+ }
667
+ return group(data, accessor);
668
+ }
669
+
670
+ // Rollup data with reducer using d3.rollup
671
+ function rollupBy<T, R>(data: T[], reducer: (values: T[]) => R, accessor: (d: T) => string): Map<string, R> {
672
+ if (!data || !Array.isArray(data) || !reducer || !accessor) {
673
+ return new Map();
674
+ }
675
+ return rollup(data, reducer, accessor);
676
+ }
677
+
678
+ // Flat rollup using d3.flatRollup
679
+ function flatRollupBy<T, R>(data: T[], reducer: (values: T[]) => R, accessor: (d: T) => string): [string, R][] {
680
+ if (!data || !Array.isArray(data) || !reducer || !accessor) {
681
+ return [];
682
+ }
683
+ return flatRollup(data, reducer, accessor);
684
+ }
685
+
686
+ // Cross tabulate two arrays using d3.cross
687
+ function crossTabulate<A, B, R>(a: A[], b: B[], reducer?: (a: A, b: B) => R): (R | [A, B])[] {
688
+ if (!Array.isArray(a) || !Array.isArray(b)) {
689
+ return [];
690
+ }
691
+ if (reducer) {
692
+ return cross(a, b, reducer);
693
+ } else {
694
+ return cross(a, b) as [A, B][];
695
+ }
696
+ }
697
+
698
+ // Index data by key using d3.index
699
+ function indexBy<T>(data: T[], accessor: (d: T) => string): Map<string, T> {
700
+ if (!data || !Array.isArray(data) || !accessor) {
701
+ return new Map();
702
+ }
703
+
704
+ try {
705
+ return index(data, accessor);
706
+ } catch (error) {
707
+ // Handle duplicate keys gracefully by creating a manual index
708
+ const result = new Map<string, T>();
709
+ data.forEach(item => {
710
+ const key = accessor(item);
711
+ if (!result.has(key)) {
712
+ result.set(key, item);
713
+ }
714
+ });
715
+ return result;
716
+ }
717
+ }
718
+
719
+ // Aggregate data by time periods
720
+ function aggregateByTime<T>(
721
+ data: T[],
722
+ timeAccessor: (d: T) => Date,
723
+ granularity: 'day' | 'week' | 'month' | 'year',
724
+ reducer: (values: T[]) => any
725
+ ): any[] {
726
+ if (!data || !Array.isArray(data) || !timeAccessor || !reducer) {
727
+ return [];
728
+ }
729
+
730
+ const timeGroups = group(data, (d: T) => {
731
+ const date = timeAccessor(d);
732
+ if (!date || !(date instanceof Date)) return 'invalid';
733
+
734
+ switch (granularity) {
735
+ case 'day':
736
+ return date.toISOString().split('T')[0];
737
+ case 'week':
738
+ const week = new Date(date);
739
+ week.setDate(date.getDate() - date.getDay());
740
+ return week.toISOString().split('T')[0];
741
+ case 'month':
742
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
743
+ case 'year':
744
+ return String(date.getFullYear());
745
+ default:
746
+ return date.toISOString().split('T')[0];
747
+ }
748
+ });
749
+
750
+ return Array.from(timeGroups.entries()).map(([period, values]) => ({
751
+ period,
752
+ data: reducer(values),
753
+ count: values.length
754
+ }));
755
+ }
756
+
757
+ // Create multi-dimensional waterfall
758
+ function createMultiDimensionalWaterfall(
759
+ multiData: Record<string, any[]>,
760
+ options: {
761
+ aggregationMethod?: 'sum' | 'average' | 'count' | 'max' | 'min';
762
+ includeRegionalTotals?: boolean;
763
+ includeGrandTotal?: boolean;
764
+ }
765
+ ): any[] {
766
+ const result: any[] = [];
767
+ const { aggregationMethod = 'sum' } = options;
768
+
769
+ if (!multiData || typeof multiData !== 'object') {
770
+ return result;
771
+ }
772
+
773
+ const regions = Object.keys(multiData);
774
+ let grandTotal = 0;
775
+
776
+ for (const region of regions) {
777
+ const data = multiData[region];
778
+ if (!Array.isArray(data)) continue;
779
+
780
+ let regionTotal = 0;
781
+
782
+ for (const item of data) {
783
+ let value = 0;
784
+ if (item.value !== undefined) {
785
+ value = item.value;
786
+ } else if (item.stacks && Array.isArray(item.stacks)) {
787
+ value = item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
788
+ }
789
+
790
+ result.push({
791
+ ...item,
792
+ region,
793
+ value,
794
+ label: `${region}: ${item.label}`
795
+ });
796
+
797
+ switch (aggregationMethod) {
798
+ case 'sum':
799
+ regionTotal += value;
800
+ break;
801
+ case 'average':
802
+ regionTotal += value;
803
+ break;
804
+ case 'count':
805
+ regionTotal += 1;
806
+ break;
807
+ case 'max':
808
+ regionTotal = Math.max(regionTotal, value);
809
+ break;
810
+ case 'min':
811
+ regionTotal = regionTotal === 0 ? value : Math.min(regionTotal, value);
812
+ break;
813
+ }
814
+ }
815
+
816
+ if (options.includeRegionalTotals) {
817
+ result.push({
818
+ label: `${region} Total`,
819
+ value: aggregationMethod === 'average' ? regionTotal / data.length : regionTotal,
820
+ region,
821
+ isRegionalTotal: true
822
+ });
823
+ }
824
+
825
+ grandTotal += regionTotal;
826
+ }
827
+
828
+ if (options.includeGrandTotal) {
829
+ result.push({
830
+ label: 'Grand Total',
831
+ value: grandTotal,
832
+ isGrandTotal: true
833
+ });
834
+ }
835
+
836
+ return result;
837
+ }
838
+
839
+ // Aggregate waterfall by period with additional metrics
840
+ function aggregateWaterfallByPeriod(
841
+ data: any[],
842
+ periodField: string,
843
+ options: {
844
+ includeMovingAverage?: boolean;
845
+ movingAverageWindow?: number;
846
+ calculateGrowthRates?: boolean;
847
+ includeVariance?: boolean;
848
+ }
849
+ ): any[] {
850
+ if (!data || !Array.isArray(data)) {
851
+ return [];
852
+ }
853
+
854
+ const periodGroups = group(data, (d: any) => d[periodField] || 'unknown');
855
+ const result = Array.from(periodGroups.entries()).map(([period, items]) => {
856
+ const total = items.reduce((sum, item) => {
857
+ if (item.value !== undefined) return sum + item.value;
858
+ if (item.stacks && Array.isArray(item.stacks)) {
859
+ return sum + item.stacks.reduce((s: number, stack: any) => s + (stack.value || 0), 0);
860
+ }
861
+ return sum;
862
+ }, 0);
863
+
864
+ return {
865
+ period,
866
+ items,
867
+ total,
868
+ count: items.length,
869
+ average: total / items.length,
870
+ movingAverage: 0, // Will be calculated if requested
871
+ growthRate: 0 // Will be calculated if requested
872
+ };
873
+ });
874
+
875
+ // Add moving average if requested
876
+ if (options.includeMovingAverage) {
877
+ const window = options.movingAverageWindow || 3;
878
+ result.forEach((item, index) => {
879
+ const start = Math.max(0, index - Math.floor(window / 2));
880
+ const end = Math.min(result.length, start + window);
881
+ const windowData = result.slice(start, end);
882
+ item.movingAverage = windowData.reduce((sum, w) => sum + w.total, 0) / windowData.length;
883
+ });
884
+ }
885
+
886
+ // Add growth rates if requested
887
+ if (options.calculateGrowthRates) {
888
+ result.forEach((item, index) => {
889
+ if (index > 0) {
890
+ const prev = result[index - 1];
891
+ item.growthRate = prev.total !== 0 ? (item.total - prev.total) / prev.total : 0;
892
+ }
893
+ });
894
+ }
895
+
896
+ return result;
897
+ }
898
+
899
+ // Create breakdown waterfall with sub-items
900
+ function createBreakdownWaterfall(
901
+ data: any[],
902
+ breakdownField: string,
903
+ options: {
904
+ maintainOriginalStructure?: boolean;
905
+ includeSubtotals?: boolean;
906
+ colorByBreakdown?: boolean;
907
+ }
908
+ ): any[] {
909
+ if (!data || !Array.isArray(data)) {
910
+ return [];
911
+ }
912
+
913
+ const result: any[] = [];
914
+
915
+ for (const item of data) {
916
+ const breakdowns = item[breakdownField];
917
+
918
+ if (breakdowns && Array.isArray(breakdowns)) {
919
+ // Add main item
920
+ if (options.maintainOriginalStructure) {
921
+ result.push({ ...item, isMainItem: true });
922
+ }
923
+
924
+ // Add breakdown items
925
+ let subtotal = 0;
926
+ breakdowns.forEach((breakdown: any, index: number) => {
927
+ const breakdownItem = {
928
+ ...breakdown,
929
+ parentLabel: item.label,
930
+ isBreakdown: true,
931
+ breakdownIndex: index,
932
+ color: options.colorByBreakdown ? `hsl(${index * 360 / breakdowns.length}, 70%, 60%)` : breakdown.color
933
+ };
934
+ result.push(breakdownItem);
935
+ subtotal += breakdown.value || 0;
936
+ });
937
+
938
+ // Add subtotal if requested
939
+ if (options.includeSubtotals && breakdowns.length > 1) {
940
+ result.push({
941
+ label: `${item.label} Subtotal`,
942
+ value: subtotal,
943
+ parentLabel: item.label,
944
+ isSubtotal: true
945
+ });
946
+ }
947
+ } else {
948
+ // No breakdown data, add as-is
949
+ result.push({ ...item, hasBreakdown: false });
950
+ }
951
+ }
952
+
953
+ return result;
954
+ }
955
+
956
+ // Additional methods needed by existing code
957
+ function analyzeSequence(data: any[]): any[] {
958
+ // Simplified implementation for compatibility
959
+ if (!Array.isArray(data) || data.length < 2) {
960
+ return [];
961
+ }
962
+
963
+ return data.slice(1).map((item, index) => {
964
+ const prev = data[index];
965
+ const current = item;
966
+ const prevValue = extractValue(prev);
967
+ const currentValue = extractValue(current);
968
+ const change = currentValue - prevValue;
969
+
970
+ return {
971
+ index,
972
+ from: prev.label || `Item ${index}`,
973
+ to: current.label || `Item ${index + 1}`,
974
+ fromValue: prevValue,
975
+ toValue: currentValue,
976
+ change,
977
+ percentChange: prevValue !== 0 ? (change / prevValue) * 100 : 0,
978
+ direction: change > 0 ? 'increase' : change < 0 ? 'decrease' : 'stable',
979
+ magnitude: Math.abs(change) > 1000 ? 'large' : Math.abs(change) > 100 ? 'medium' : 'small'
980
+ };
981
+ });
982
+ }
983
+
984
+ function suggestDataOptimizations(data: any[]): any[] {
985
+ // Simplified implementation for compatibility
986
+ const suggestions: any[] = [];
987
+
988
+ if (!Array.isArray(data) || data.length === 0) {
989
+ return suggestions;
990
+ }
991
+
992
+ if (data.length > 20) {
993
+ suggestions.push({
994
+ type: 'aggregation',
995
+ priority: 'medium',
996
+ description: 'Consider grouping similar items for better readability',
997
+ impact: 'Reduces visual clutter'
998
+ });
999
+ }
1000
+
1001
+ return suggestions;
1002
+ }
1003
+
1004
+ function generateCustomTicks(domain: [number, number], options: any): number[] {
1005
+ // Simplified implementation using d3.ticks
1006
+ const tickCount = options.targetTickCount || 8;
1007
+ return d3.ticks(domain[0], domain[1], tickCount);
1008
+ }
1009
+
1010
+ function extractValue(item: any): number {
1011
+ if (typeof item === 'number') return item;
1012
+ if (item.value !== undefined) return item.value;
1013
+ if (item.stacks && Array.isArray(item.stacks)) {
1014
+ return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
1015
+ }
1016
+ return 0;
1017
+ }
1018
+
1019
+ // Return the processor interface
1020
+ return {
1021
+ groupBy,
1022
+ rollupBy,
1023
+ flatRollupBy,
1024
+ crossTabulate,
1025
+ indexBy,
1026
+ aggregateByTime,
1027
+ createMultiDimensionalWaterfall,
1028
+ aggregateWaterfallByPeriod,
1029
+ createBreakdownWaterfall,
1030
+ analyzeSequence,
1031
+ suggestDataOptimizations,
1032
+ generateCustomTicks
1033
+ };
1034
+ }