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.
- package/CHANGELOG.md +223 -0
- package/CONTRIBUTING.md +199 -0
- package/README.md +363 -0
- package/dist/index.d.ts +149 -0
- package/dist/mintwaterfall.cjs.js +7978 -0
- package/dist/mintwaterfall.esm.js +7907 -0
- package/dist/mintwaterfall.min.js +7 -0
- package/dist/mintwaterfall.umd.js +7978 -0
- package/index.d.ts +149 -0
- package/package.json +126 -0
- package/src/enterprise/enterprise-core.js +0 -0
- package/src/enterprise/enterprise-feature-template.js +0 -0
- package/src/enterprise/feature-registry.js +0 -0
- package/src/enterprise/features/breakdown.js +0 -0
- package/src/features/breakdown.js +0 -0
- package/src/features/conditional-formatting.js +0 -0
- package/src/index.js +111 -0
- package/src/mintwaterfall-accessibility.ts +680 -0
- package/src/mintwaterfall-advanced-data.ts +1034 -0
- package/src/mintwaterfall-advanced-interactions.ts +649 -0
- package/src/mintwaterfall-advanced-performance.ts +582 -0
- package/src/mintwaterfall-animations.ts +595 -0
- package/src/mintwaterfall-brush.ts +471 -0
- package/src/mintwaterfall-chart-core.ts +296 -0
- package/src/mintwaterfall-chart.ts +1915 -0
- package/src/mintwaterfall-data.ts +1100 -0
- package/src/mintwaterfall-export.ts +475 -0
- package/src/mintwaterfall-hierarchical-layouts.ts +724 -0
- package/src/mintwaterfall-layouts.ts +647 -0
- package/src/mintwaterfall-performance.ts +573 -0
- package/src/mintwaterfall-scales.ts +437 -0
- package/src/mintwaterfall-shapes.ts +385 -0
- package/src/mintwaterfall-statistics.ts +821 -0
- package/src/mintwaterfall-themes.ts +391 -0
- package/src/mintwaterfall-tooltip.ts +450 -0
- package/src/mintwaterfall-zoom.ts +399 -0
- package/src/types/js-modules.d.ts +25 -0
- 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
|
+
}
|