mintwaterfall
Version:
A powerful, D3.js-compatible waterfall chart component with enterprise features including breakdown analysis, conditional formatting, stacking capabilities, animations, and extensive customization options
1,035 lines (864 loc) • 36.6 kB
text/typescript
// MintWaterfall Advanced Data Manipulation Utilities
// Enhanced D3.js data manipulation capabilities for comprehensive waterfall analysis
import * as d3 from 'd3';
import { group, rollup, flatRollup, cross, index } from 'd3-array';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export interface SequenceAnalysis {
from: string;
to: string;
change: number;
changePercent: number;
changeDirection: 'increase' | 'decrease' | 'neutral';
magnitude: 'small' | 'medium' | 'large';
}
export interface DataMergeOptions {
mergeStrategy: 'combine' | 'override' | 'average' | 'sum';
conflictResolution: 'first' | 'last' | 'max' | 'min';
keyField: string;
valueField: string;
}
export interface TickGenerationOptions {
count?: number;
step?: number;
nice?: boolean;
format?: string;
threshold?: number;
includeZero?: boolean;
}
export interface DataOrderingOptions {
field: string;
direction: 'ascending' | 'descending';
strategy: 'value' | 'cumulative' | 'magnitude' | 'alphabetical';
groupBy?: string;
}
export interface AdvancedDataProcessor {
// Sequence analysis using d3.pairs()
analyzeSequence(data: any[]): SequenceAnalysis[];
// Data reordering using d3.permute()
optimizeDataOrder(data: any[], options: DataOrderingOptions): any[];
// Complex dataset merging using d3.merge()
mergeDatasets(datasets: any[][], options: DataMergeOptions): any[];
// Custom axis tick generation using d3.ticks()
generateCustomTicks(domain: [number, number], options: TickGenerationOptions): number[];
// Advanced data transformation utilities
createDataPairs(data: any[], accessor?: (d: any) => any): any[];
permuteByIndices(data: any[], indices: number[]): any[];
mergeSimilarItems(data: any[], similarityThreshold: number): any[];
// Data quality and validation
validateSequentialData(data: any[]): { isValid: boolean; errors: string[] };
detectDataAnomalies(data: any[]): any[];
suggestDataOptimizations(data: any[]): string[];
}
// ============================================================================
// ADVANCED DATA PROCESSOR IMPLEMENTATION
// ============================================================================
function createAdvancedDataProcessorOLD(): AdvancedDataProcessor {
// ========================================================================
// SEQUENCE ANALYSIS (d3.pairs)
// ========================================================================
/**
* Analyze sequential relationships in waterfall data
* Uses d3.pairs() to understand flow between consecutive items
*/
function analyzeSequence(data: any[]): SequenceAnalysis[] {
if (!Array.isArray(data) || data.length < 2) {
return [];
}
// Extract values for analysis
const values = data.map(d => {
if (typeof d === 'number') return d;
if (d.value !== undefined) return d.value;
if (d.stacks && Array.isArray(d.stacks)) {
return d.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
return 0;
});
// Use d3.pairs() for sequential analysis
const sequences: SequenceAnalysis[] = d3.pairs(data, (a: any, b: any) => {
const aValue = extractValue(a);
const bValue = extractValue(b);
const change = bValue - aValue;
const changePercent = aValue !== 0 ? (change / Math.abs(aValue)) * 100 : 0;
// Determine change direction
let changeDirection: 'increase' | 'decrease' | 'neutral';
if (Math.abs(change) < 0.01) changeDirection = 'neutral';
else if (change > 0) changeDirection = 'increase';
else changeDirection = 'decrease';
// Determine magnitude
const absChangePercent = Math.abs(changePercent);
let magnitude: 'small' | 'medium' | 'large';
if (absChangePercent < 5) magnitude = 'small';
else if (absChangePercent < 20) magnitude = 'medium';
else magnitude = 'large';
return {
from: getLabel(a),
to: getLabel(b),
change,
changePercent,
changeDirection,
magnitude
};
});
return sequences;
}
// ========================================================================
// DATA REORDERING (d3.permute)
// ========================================================================
/**
* Optimize data ordering for better waterfall visualization
* Uses d3.permute() with intelligent sorting strategies
*/
function optimizeDataOrder(data: any[], options: DataOrderingOptions): any[] {
if (!Array.isArray(data) || data.length === 0) {
return data;
}
const { field, direction, strategy, groupBy } = options;
// Create sorting indices based on strategy
let indices: number[];
switch (strategy) {
case 'value':
indices = d3.range(data.length).sort((i, j) => {
const aValue = extractValue(data[i]);
const bValue = extractValue(data[j]);
return direction === 'ascending' ?
d3.ascending(aValue, bValue) :
d3.descending(aValue, bValue);
});
break;
case 'cumulative':
// Sort by cumulative impact on waterfall
const cumulativeValues = calculateCumulativeValues(data);
indices = d3.range(data.length).sort((i, j) => {
return direction === 'ascending' ?
d3.ascending(cumulativeValues[i], cumulativeValues[j]) :
d3.descending(cumulativeValues[i], cumulativeValues[j]);
});
break;
case 'magnitude':
indices = d3.range(data.length).sort((i, j) => {
const aMagnitude = Math.abs(extractValue(data[i]));
const bMagnitude = Math.abs(extractValue(data[j]));
return direction === 'ascending' ?
d3.ascending(aMagnitude, bMagnitude) :
d3.descending(aMagnitude, bMagnitude);
});
break;
case 'alphabetical':
indices = d3.range(data.length).sort((i, j) => {
const aLabel = getLabel(data[i]);
const bLabel = getLabel(data[j]);
return direction === 'ascending' ?
d3.ascending(aLabel, bLabel) :
d3.descending(aLabel, bLabel);
});
break;
default:
return data; // No reordering
}
// Use d3.permute() to reorder data
return d3.permute(data, indices);
}
// ========================================================================
// DATASET MERGING (d3.merge)
// ========================================================================
/**
* Merge multiple datasets with sophisticated conflict resolution
* Uses d3.merge() with custom merge strategies
*/
function mergeDatasets(datasets: any[][], options: DataMergeOptions): any[] {
if (!Array.isArray(datasets) || datasets.length === 0) {
return [];
}
const { mergeStrategy, conflictResolution, keyField, valueField } = options;
// Use d3.merge() to combine all datasets
const flatData = d3.merge(datasets);
// Group by key field for conflict resolution
const grouped = d3.group(flatData, (d: any) => d[keyField] || getLabel(d));
// Resolve conflicts and merge
const mergedData: any[] = [];
for (const [key, items] of grouped) {
if (items.length === 1) {
mergedData.push(items[0]);
continue;
}
// Handle conflicts with multiple items
let mergedItem: any;
switch (mergeStrategy) {
case 'combine':
mergedItem = combineItems(items, valueField);
break;
case 'override':
mergedItem = resolveConflict(items, conflictResolution);
break;
case 'average':
mergedItem = averageItems(items, valueField);
break;
case 'sum':
mergedItem = sumItems(items, valueField);
break;
default:
mergedItem = items[0];
}
mergedData.push(mergedItem);
}
return mergedData;
}
// ========================================================================
// CUSTOM TICK GENERATION (d3.ticks)
// ========================================================================
/**
* Generate custom axis ticks with advanced options
* Uses d3.ticks() with intelligent tick selection
*/
function generateCustomTicks(domain: [number, number], options: TickGenerationOptions): number[] {
const {
count = 10,
step,
nice = true,
threshold = 0,
includeZero = true
} = options;
let [min, max] = domain;
// Apply nice scaling if requested
if (nice) {
const scale = d3.scaleLinear().domain([min, max]).nice();
[min, max] = scale.domain() as [number, number];
}
// Generate base ticks using d3.ticks()
let ticks: number[];
if (step !== undefined) {
// Use custom step
ticks = d3.ticks(min, max, Math.abs(max - min) / step);
} else {
// Use count-based generation
ticks = d3.ticks(min, max, count);
}
// Apply threshold filtering
if (threshold > 0) {
ticks = ticks.filter(tick => Math.abs(tick) >= threshold);
}
// Ensure zero is included if requested
if (includeZero && !ticks.includes(0) && min <= 0 && max >= 0) {
ticks.push(0);
ticks.sort(d3.ascending);
}
return ticks;
}
// ========================================================================
// UTILITY FUNCTIONS
// ========================================================================
function createDataPairs(data: any[], accessor?: (d: any) => any): any[] {
if (accessor) {
return d3.pairs(data, (a, b) => ({ a: accessor(a), b: accessor(b) }));
}
return d3.pairs(data);
}
function permuteByIndices(data: any[], indices: number[]): any[] {
return d3.permute(data, indices);
}
function mergeSimilarItems(data: any[], similarityThreshold: number): any[] {
// Group similar items and merge them
const groups: any[][] = [];
const used = new Set<number>();
for (let i = 0; i < data.length; i++) {
if (used.has(i)) continue;
const group = [data[i]];
used.add(i);
for (let j = i + 1; j < data.length; j++) {
if (used.has(j)) continue;
const similarity = calculateSimilarity(data[i], data[j]);
if (similarity >= similarityThreshold) {
group.push(data[j]);
used.add(j);
}
}
groups.push(group);
}
// Merge groups using d3.merge()
return groups.map(group => {
if (group.length === 1) return group[0];
return mergeGroupItems(group);
});
}
function validateSequentialData(data: any[]): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!Array.isArray(data)) {
errors.push("Data must be an array");
return { isValid: false, errors };
}
if (data.length < 2) {
errors.push("Data must have at least 2 items for sequence analysis");
}
// Check for valid values
const invalidItems = data.filter((d, i) => {
const value = extractValue(d);
return isNaN(value) || !isFinite(value);
});
if (invalidItems.length > 0) {
errors.push(`Found ${invalidItems.length} items with invalid values`);
}
// Check for duplicate labels
const labels = data.map(getLabel);
const uniqueLabels = new Set(labels);
if (labels.length !== uniqueLabels.size) {
errors.push("Duplicate labels detected - may cause confusion in sequence analysis");
}
return { isValid: errors.length === 0, errors };
}
function detectDataAnomalies(data: any[]): any[] {
const values = data.map(extractValue);
const mean = d3.mean(values) || 0;
const deviation = d3.deviation(values) || 0;
const threshold = 2 * deviation; // 2-sigma rule
return data.filter((d, i) => {
const value = values[i];
return Math.abs(value - mean) > threshold;
});
}
function suggestDataOptimizations(data: any[]): string[] {
const suggestions: string[] = [];
// Analyze data characteristics
const values = data.map(extractValue);
const sequences = analyzeSequence(data);
// Check for optimization opportunities
if (values.some(v => v === 0)) {
suggestions.push("Consider removing or combining zero-value items");
}
const smallChanges = sequences.filter(s => s.magnitude === 'small').length;
if (smallChanges > data.length * 0.3) {
suggestions.push("Many small changes detected - consider grouping similar items");
}
const alternatingPattern = hasAlternatingPattern(values);
if (alternatingPattern) {
suggestions.push("Alternating positive/negative pattern detected - consider reordering by magnitude");
}
if (data.length > 20) {
suggestions.push("Large dataset - consider using hierarchical grouping or filtering");
}
return suggestions;
}
// ========================================================================
// HELPER FUNCTIONS
// ========================================================================
function extractValue(item: any): number {
if (typeof item === 'number') return item;
if (item.value !== undefined) return item.value;
if (item.stacks && Array.isArray(item.stacks)) {
return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
return 0;
}
function getLabel(item: any): string {
if (typeof item === 'string') return item;
if (item.label !== undefined) return item.label;
if (item.name !== undefined) return item.name;
return 'Unnamed';
}
function calculateCumulativeValues(data: any[]): number[] {
const values = data.map(extractValue);
const cumulative: number[] = [];
let running = 0;
for (const value of values) {
running += value;
cumulative.push(running);
}
return cumulative;
}
function combineItems(items: any[], valueField: string): any {
const combined = { ...items[0] };
const totalValue = items.reduce((sum, item) => sum + extractValue(item), 0);
if (combined.value !== undefined) combined.value = totalValue;
if (combined[valueField] !== undefined) combined[valueField] = totalValue;
// Combine stacks if present
if (combined.stacks) {
combined.stacks = items.flatMap(item => item.stacks || []);
}
return combined;
}
function resolveConflict(items: any[], strategy: string): any {
switch (strategy) {
case 'first': return items[0];
case 'last': return items[items.length - 1];
case 'max': return items.reduce((max, item) => extractValue(item) > extractValue(max) ? item : max);
case 'min': return items.reduce((min, item) => extractValue(item) < extractValue(min) ? item : min);
default: return items[0];
}
}
function averageItems(items: any[], valueField: string): any {
const averaged = { ...items[0] };
const avgValue = d3.mean(items, extractValue) || 0;
if (averaged.value !== undefined) averaged.value = avgValue;
if (averaged[valueField] !== undefined) averaged[valueField] = avgValue;
return averaged;
}
function sumItems(items: any[], valueField: string): any {
const summed = { ...items[0] };
const totalValue = d3.sum(items, extractValue);
if (summed.value !== undefined) summed.value = totalValue;
if (summed[valueField] !== undefined) summed[valueField] = totalValue;
return summed;
}
function calculateSimilarity(a: any, b: any): number {
const valueA = extractValue(a);
const valueB = extractValue(b);
const labelA = getLabel(a);
const labelB = getLabel(b);
// Simple similarity based on value proximity and label similarity
const valueSim = 1 - Math.abs(valueA - valueB) / (Math.abs(valueA) + Math.abs(valueB) + 1);
const labelSim = labelA === labelB ? 1 : 0;
return (valueSim + labelSim) / 2;
}
function mergeGroupItems(group: any[]): any {
return combineItems(group, 'value');
}
function hasAlternatingPattern(values: number[]): boolean {
if (values.length < 3) return false;
let alternating = 0;
for (let i = 1; i < values.length - 1; i++) {
const prev = values[i - 1];
const curr = values[i];
const next = values[i + 1];
if ((prev > 0 && curr < 0 && next > 0) || (prev < 0 && curr > 0 && next < 0)) {
alternating++;
}
}
return alternating > values.length * 0.3;
}
// ========================================================================
// RETURN PROCESSOR INTERFACE
// ========================================================================
return {
analyzeSequence,
optimizeDataOrder,
mergeDatasets,
generateCustomTicks,
createDataPairs,
permuteByIndices,
mergeSimilarItems,
validateSequentialData,
detectDataAnomalies,
suggestDataOptimizations
};
}
// ============================================================================
// SPECIALIZED WATERFALL UTILITIES
// ============================================================================
/**
* Create sequence analysis specifically for waterfall data
*/
export function createWaterfallSequenceAnalyzer(data: any[]): {
flowAnalysis: SequenceAnalysis[];
cumulativeFlow: Array<{step: number, cumulative: number, change: number}>;
criticalPaths: string[];
optimizationSuggestions: string[];
} {
const processor = createAdvancedDataProcessor();
const flowAnalysis = processor.analyzeSequence(data);
// Calculate cumulative flow
const cumulativeFlow: Array<{step: number, cumulative: number, change: number}> = [];
let cumulative = 0;
data.forEach((item, index) => {
const value = extractValue(item);
cumulative += value;
cumulativeFlow.push({
step: index,
cumulative,
change: value
});
});
// Identify critical paths (large impact changes)
const criticalPaths = flowAnalysis
.filter((seq: any) => seq.magnitude === 'large')
.map((seq: any) => `${seq.from} → ${seq.to}`);
// Generate optimization suggestions
const optimizationSuggestions = processor.suggestDataOptimizations(data);
return {
flowAnalysis,
cumulativeFlow,
criticalPaths,
optimizationSuggestions
};
function extractValue(item: any): number {
if (typeof item === 'number') return item;
if (item.value !== undefined) return item.value;
if (item.stacks && Array.isArray(item.stacks)) {
return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
return 0;
}
}
/**
* Create optimized tick generator for waterfall charts
*/
export function createWaterfallTickGenerator(domain: [number, number], dataPoints: any[]): {
ticks: number[];
labels: string[];
keyMarkers: number[];
} {
const processor = createAdvancedDataProcessor();
// Generate base ticks
const ticks = processor.generateCustomTicks(domain, {
count: 8,
nice: true,
includeZero: true,
threshold: Math.abs(domain[1] - domain[0]) / 100
});
// Generate labels
const labels = ticks.map((tick: number) => {
if (tick === 0) return '0';
if (Math.abs(tick) >= 1000000) return `${(tick / 1000000).toFixed(1)}M`;
if (Math.abs(tick) >= 1000) return `${(tick / 1000).toFixed(1)}K`;
return tick.toFixed(0);
});
// Identify key markers (data points that align with ticks)
const keyMarkers = ticks.filter((tick: number) => {
return dataPoints.some(d => Math.abs(extractValue(d) - tick) < Math.abs(domain[1] - domain[0]) / 50);
});
return { ticks, labels, keyMarkers };
function extractValue(item: any): number {
if (typeof item === 'number') return item;
if (item.value !== undefined) return item.value;
if (item.stacks && Array.isArray(item.stacks)) {
return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
return 0;
}
}
// ============================================================================
// MISSING ADVANCED DATA PROCESSOR FUNCTIONS
// ============================================================================
/**
* Creates an advanced data processor with D3.js data manipulation functions
*/
export function createAdvancedDataProcessor() {
// Group data by key using d3.group
function groupBy<T>(data: T[], accessor: (d: T) => string): Map<string, T[]> {
if (!data || !Array.isArray(data) || !accessor) {
return new Map();
}
return group(data, accessor);
}
// Rollup data with reducer using d3.rollup
function rollupBy<T, R>(data: T[], reducer: (values: T[]) => R, accessor: (d: T) => string): Map<string, R> {
if (!data || !Array.isArray(data) || !reducer || !accessor) {
return new Map();
}
return rollup(data, reducer, accessor);
}
// Flat rollup using d3.flatRollup
function flatRollupBy<T, R>(data: T[], reducer: (values: T[]) => R, accessor: (d: T) => string): [string, R][] {
if (!data || !Array.isArray(data) || !reducer || !accessor) {
return [];
}
return flatRollup(data, reducer, accessor);
}
// Cross tabulate two arrays using d3.cross
function crossTabulate<A, B, R>(a: A[], b: B[], reducer?: (a: A, b: B) => R): (R | [A, B])[] {
if (!Array.isArray(a) || !Array.isArray(b)) {
return [];
}
if (reducer) {
return cross(a, b, reducer);
} else {
return cross(a, b) as [A, B][];
}
}
// Index data by key using d3.index
function indexBy<T>(data: T[], accessor: (d: T) => string): Map<string, T> {
if (!data || !Array.isArray(data) || !accessor) {
return new Map();
}
try {
return index(data, accessor);
} catch (error) {
// Handle duplicate keys gracefully by creating a manual index
const result = new Map<string, T>();
data.forEach(item => {
const key = accessor(item);
if (!result.has(key)) {
result.set(key, item);
}
});
return result;
}
}
// Aggregate data by time periods
function aggregateByTime<T>(
data: T[],
timeAccessor: (d: T) => Date,
granularity: 'day' | 'week' | 'month' | 'year',
reducer: (values: T[]) => any
): any[] {
if (!data || !Array.isArray(data) || !timeAccessor || !reducer) {
return [];
}
const timeGroups = group(data, (d: T) => {
const date = timeAccessor(d);
if (!date || !(date instanceof Date)) return 'invalid';
switch (granularity) {
case 'day':
return date.toISOString().split('T')[0];
case 'week':
const week = new Date(date);
week.setDate(date.getDate() - date.getDay());
return week.toISOString().split('T')[0];
case 'month':
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
case 'year':
return String(date.getFullYear());
default:
return date.toISOString().split('T')[0];
}
});
return Array.from(timeGroups.entries()).map(([period, values]) => ({
period,
data: reducer(values),
count: values.length
}));
}
// Create multi-dimensional waterfall
function createMultiDimensionalWaterfall(
multiData: Record<string, any[]>,
options: {
aggregationMethod?: 'sum' | 'average' | 'count' | 'max' | 'min';
includeRegionalTotals?: boolean;
includeGrandTotal?: boolean;
}
): any[] {
const result: any[] = [];
const { aggregationMethod = 'sum' } = options;
if (!multiData || typeof multiData !== 'object') {
return result;
}
const regions = Object.keys(multiData);
let grandTotal = 0;
for (const region of regions) {
const data = multiData[region];
if (!Array.isArray(data)) continue;
let regionTotal = 0;
for (const item of data) {
let value = 0;
if (item.value !== undefined) {
value = item.value;
} else if (item.stacks && Array.isArray(item.stacks)) {
value = item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
result.push({
...item,
region,
value,
label: `${region}: ${item.label}`
});
switch (aggregationMethod) {
case 'sum':
regionTotal += value;
break;
case 'average':
regionTotal += value;
break;
case 'count':
regionTotal += 1;
break;
case 'max':
regionTotal = Math.max(regionTotal, value);
break;
case 'min':
regionTotal = regionTotal === 0 ? value : Math.min(regionTotal, value);
break;
}
}
if (options.includeRegionalTotals) {
result.push({
label: `${region} Total`,
value: aggregationMethod === 'average' ? regionTotal / data.length : regionTotal,
region,
isRegionalTotal: true
});
}
grandTotal += regionTotal;
}
if (options.includeGrandTotal) {
result.push({
label: 'Grand Total',
value: grandTotal,
isGrandTotal: true
});
}
return result;
}
// Aggregate waterfall by period with additional metrics
function aggregateWaterfallByPeriod(
data: any[],
periodField: string,
options: {
includeMovingAverage?: boolean;
movingAverageWindow?: number;
calculateGrowthRates?: boolean;
includeVariance?: boolean;
}
): any[] {
if (!data || !Array.isArray(data)) {
return [];
}
const periodGroups = group(data, (d: any) => d[periodField] || 'unknown');
const result = Array.from(periodGroups.entries()).map(([period, items]) => {
const total = items.reduce((sum, item) => {
if (item.value !== undefined) return sum + item.value;
if (item.stacks && Array.isArray(item.stacks)) {
return sum + item.stacks.reduce((s: number, stack: any) => s + (stack.value || 0), 0);
}
return sum;
}, 0);
return {
period,
items,
total,
count: items.length,
average: total / items.length,
movingAverage: 0, // Will be calculated if requested
growthRate: 0 // Will be calculated if requested
};
});
// Add moving average if requested
if (options.includeMovingAverage) {
const window = options.movingAverageWindow || 3;
result.forEach((item, index) => {
const start = Math.max(0, index - Math.floor(window / 2));
const end = Math.min(result.length, start + window);
const windowData = result.slice(start, end);
item.movingAverage = windowData.reduce((sum, w) => sum + w.total, 0) / windowData.length;
});
}
// Add growth rates if requested
if (options.calculateGrowthRates) {
result.forEach((item, index) => {
if (index > 0) {
const prev = result[index - 1];
item.growthRate = prev.total !== 0 ? (item.total - prev.total) / prev.total : 0;
}
});
}
return result;
}
// Create breakdown waterfall with sub-items
function createBreakdownWaterfall(
data: any[],
breakdownField: string,
options: {
maintainOriginalStructure?: boolean;
includeSubtotals?: boolean;
colorByBreakdown?: boolean;
}
): any[] {
if (!data || !Array.isArray(data)) {
return [];
}
const result: any[] = [];
for (const item of data) {
const breakdowns = item[breakdownField];
if (breakdowns && Array.isArray(breakdowns)) {
// Add main item
if (options.maintainOriginalStructure) {
result.push({ ...item, isMainItem: true });
}
// Add breakdown items
let subtotal = 0;
breakdowns.forEach((breakdown: any, index: number) => {
const breakdownItem = {
...breakdown,
parentLabel: item.label,
isBreakdown: true,
breakdownIndex: index,
color: options.colorByBreakdown ? `hsl(${index * 360 / breakdowns.length}, 70%, 60%)` : breakdown.color
};
result.push(breakdownItem);
subtotal += breakdown.value || 0;
});
// Add subtotal if requested
if (options.includeSubtotals && breakdowns.length > 1) {
result.push({
label: `${item.label} Subtotal`,
value: subtotal,
parentLabel: item.label,
isSubtotal: true
});
}
} else {
// No breakdown data, add as-is
result.push({ ...item, hasBreakdown: false });
}
}
return result;
}
// Additional methods needed by existing code
function analyzeSequence(data: any[]): any[] {
// Simplified implementation for compatibility
if (!Array.isArray(data) || data.length < 2) {
return [];
}
return data.slice(1).map((item, index) => {
const prev = data[index];
const current = item;
const prevValue = extractValue(prev);
const currentValue = extractValue(current);
const change = currentValue - prevValue;
return {
index,
from: prev.label || `Item ${index}`,
to: current.label || `Item ${index + 1}`,
fromValue: prevValue,
toValue: currentValue,
change,
percentChange: prevValue !== 0 ? (change / prevValue) * 100 : 0,
direction: change > 0 ? 'increase' : change < 0 ? 'decrease' : 'stable',
magnitude: Math.abs(change) > 1000 ? 'large' : Math.abs(change) > 100 ? 'medium' : 'small'
};
});
}
function suggestDataOptimizations(data: any[]): any[] {
// Simplified implementation for compatibility
const suggestions: any[] = [];
if (!Array.isArray(data) || data.length === 0) {
return suggestions;
}
if (data.length > 20) {
suggestions.push({
type: 'aggregation',
priority: 'medium',
description: 'Consider grouping similar items for better readability',
impact: 'Reduces visual clutter'
});
}
return suggestions;
}
function generateCustomTicks(domain: [number, number], options: any): number[] {
// Simplified implementation using d3.ticks
const tickCount = options.targetTickCount || 8;
return d3.ticks(domain[0], domain[1], tickCount);
}
function extractValue(item: any): number {
if (typeof item === 'number') return item;
if (item.value !== undefined) return item.value;
if (item.stacks && Array.isArray(item.stacks)) {
return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
}
return 0;
}
// Return the processor interface
return {
groupBy,
rollupBy,
flatRollupBy,
crossTabulate,
indexBy,
aggregateByTime,
createMultiDimensionalWaterfall,
aggregateWaterfallByPeriod,
createBreakdownWaterfall,
analyzeSequence,
suggestDataOptimizations,
generateCustomTicks
};
}