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,1915 @@
|
|
|
1
|
+
// MintWaterfall - D3.js compatible waterfall chart component (TypeScript)
|
|
2
|
+
// Usage: d3.waterfallChart().width(800).height(400).showTotal(true)(selection)
|
|
3
|
+
|
|
4
|
+
import * as d3 from 'd3';
|
|
5
|
+
// Import TypeScript modules where available
|
|
6
|
+
import { DataItem, StackItem, ProcessedDataItem, dataProcessor, createDataProcessor } from './mintwaterfall-data.js';
|
|
7
|
+
import { createScaleSystem, createTimeScale, createOrdinalScale } from './mintwaterfall-scales.js';
|
|
8
|
+
// Import JavaScript modules for remaining components during gradual migration
|
|
9
|
+
import { createBrushSystem } from "./mintwaterfall-brush.js";
|
|
10
|
+
import { createAccessibilitySystem } from "./mintwaterfall-accessibility.js";
|
|
11
|
+
import { createTooltipSystem } from "./mintwaterfall-tooltip.js";
|
|
12
|
+
import { createExportSystem } from "./mintwaterfall-export.js";
|
|
13
|
+
import { createZoomSystem } from "./mintwaterfall-zoom.js";
|
|
14
|
+
import { createPerformanceManager } from "./mintwaterfall-performance.js";
|
|
15
|
+
|
|
16
|
+
// NEW: Import advanced features
|
|
17
|
+
import {
|
|
18
|
+
createSequentialScale,
|
|
19
|
+
createDivergingScale,
|
|
20
|
+
getConditionalColor,
|
|
21
|
+
createWaterfallColorScale,
|
|
22
|
+
interpolateThemeColor,
|
|
23
|
+
getAdvancedBarColor,
|
|
24
|
+
ThemeCollection
|
|
25
|
+
} from './mintwaterfall-themes.js';
|
|
26
|
+
import {
|
|
27
|
+
createShapeGenerators,
|
|
28
|
+
createWaterfallConfidenceBands,
|
|
29
|
+
createWaterfallMilestones
|
|
30
|
+
} from './mintwaterfall-shapes.js';
|
|
31
|
+
|
|
32
|
+
// NEW: Import MEDIUM PRIORITY analytical enhancement features
|
|
33
|
+
import {
|
|
34
|
+
createAdvancedDataProcessor,
|
|
35
|
+
createWaterfallSequenceAnalyzer,
|
|
36
|
+
createWaterfallTickGenerator
|
|
37
|
+
} from './mintwaterfall-advanced-data.js';
|
|
38
|
+
import {
|
|
39
|
+
createAdvancedInteractionSystem,
|
|
40
|
+
createWaterfallDragBehavior,
|
|
41
|
+
createWaterfallVoronoiConfig,
|
|
42
|
+
createWaterfallForceConfig
|
|
43
|
+
} from './mintwaterfall-advanced-interactions.js';
|
|
44
|
+
import {
|
|
45
|
+
createHierarchicalLayoutSystem,
|
|
46
|
+
createWaterfallTreemap,
|
|
47
|
+
createWaterfallSunburst,
|
|
48
|
+
createWaterfallBubbles
|
|
49
|
+
} from './mintwaterfall-hierarchical-layouts.js';
|
|
50
|
+
|
|
51
|
+
// Type definitions
|
|
52
|
+
export interface StackData {
|
|
53
|
+
value: number;
|
|
54
|
+
color: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ChartData {
|
|
59
|
+
label: string;
|
|
60
|
+
stacks: StackData[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ProcessedData extends ChartData {
|
|
64
|
+
barTotal: number;
|
|
65
|
+
cumulativeTotal: number;
|
|
66
|
+
prevCumulativeTotal?: number;
|
|
67
|
+
stackPositions?: Array<{ start: number; end: number; color: string; value: number; label?: string }>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MarginConfig {
|
|
71
|
+
top: number;
|
|
72
|
+
right: number;
|
|
73
|
+
bottom: number;
|
|
74
|
+
left: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BrushOptions {
|
|
78
|
+
extent?: [[number, number], [number, number]];
|
|
79
|
+
handleSize?: number;
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TooltipConfig {
|
|
84
|
+
enabled?: boolean;
|
|
85
|
+
className?: string;
|
|
86
|
+
offset?: { x: number; y: number };
|
|
87
|
+
[key: string]: any;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ExportConfig {
|
|
91
|
+
formats?: string[];
|
|
92
|
+
filename?: string;
|
|
93
|
+
[key: string]: any;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ZoomConfig {
|
|
97
|
+
scaleExtent?: [number, number];
|
|
98
|
+
translateExtent?: [[number, number], [number, number]];
|
|
99
|
+
[key: string]: any;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BreakdownConfig {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
levels: number;
|
|
105
|
+
field?: string;
|
|
106
|
+
minGroupSize?: number;
|
|
107
|
+
sortStrategy?: string;
|
|
108
|
+
showOthers?: boolean;
|
|
109
|
+
othersLabel?: string;
|
|
110
|
+
maxGroups?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// NEW: Advanced feature configurations
|
|
114
|
+
export interface AdvancedColorConfig {
|
|
115
|
+
enabled: boolean;
|
|
116
|
+
scaleType: 'auto' | 'sequential' | 'diverging' | 'conditional';
|
|
117
|
+
themeName?: string;
|
|
118
|
+
customColorScale?: (value: number) => string;
|
|
119
|
+
neutralThreshold?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ConfidenceBandConfig {
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
scenarios?: {
|
|
125
|
+
optimistic: Array<{label: string, value: number}>;
|
|
126
|
+
pessimistic: Array<{label: string, value: number}>;
|
|
127
|
+
};
|
|
128
|
+
opacity?: number;
|
|
129
|
+
showTrendLines?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface MilestoneConfig {
|
|
133
|
+
enabled: boolean;
|
|
134
|
+
milestones: Array<{
|
|
135
|
+
label: string;
|
|
136
|
+
value: number;
|
|
137
|
+
type: 'target' | 'threshold' | 'alert' | 'achievement';
|
|
138
|
+
description?: string;
|
|
139
|
+
}>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface BarEventHandler {
|
|
143
|
+
(event: Event, data: ProcessedData): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface WaterfallChart {
|
|
147
|
+
// Core configuration methods
|
|
148
|
+
width(): number;
|
|
149
|
+
width(value: number): WaterfallChart;
|
|
150
|
+
|
|
151
|
+
height(): number;
|
|
152
|
+
height(value: number): WaterfallChart;
|
|
153
|
+
|
|
154
|
+
margin(): MarginConfig;
|
|
155
|
+
margin(value: MarginConfig): WaterfallChart;
|
|
156
|
+
|
|
157
|
+
// Data and display options
|
|
158
|
+
stacked(): boolean;
|
|
159
|
+
stacked(value: boolean): WaterfallChart;
|
|
160
|
+
|
|
161
|
+
showTotal(): boolean;
|
|
162
|
+
showTotal(value: boolean): WaterfallChart;
|
|
163
|
+
|
|
164
|
+
totalLabel(): string;
|
|
165
|
+
totalLabel(value: string): WaterfallChart;
|
|
166
|
+
|
|
167
|
+
totalColor(): string;
|
|
168
|
+
totalColor(value: string): WaterfallChart;
|
|
169
|
+
|
|
170
|
+
barPadding(): number;
|
|
171
|
+
barPadding(value: number): WaterfallChart;
|
|
172
|
+
|
|
173
|
+
// Animation and transitions
|
|
174
|
+
duration(): number;
|
|
175
|
+
duration(value: number): WaterfallChart;
|
|
176
|
+
|
|
177
|
+
ease(): (t: number) => number;
|
|
178
|
+
ease(value: (t: number) => number): WaterfallChart;
|
|
179
|
+
|
|
180
|
+
// Formatting
|
|
181
|
+
formatNumber(): (n: number) => string;
|
|
182
|
+
formatNumber(value: (n: number) => string): WaterfallChart;
|
|
183
|
+
|
|
184
|
+
theme(): string | null;
|
|
185
|
+
theme(value: string | null): WaterfallChart;
|
|
186
|
+
|
|
187
|
+
// Advanced features
|
|
188
|
+
enableBrush(): boolean;
|
|
189
|
+
enableBrush(value: boolean): WaterfallChart;
|
|
190
|
+
|
|
191
|
+
brushOptions(): BrushOptions;
|
|
192
|
+
brushOptions(value: BrushOptions): WaterfallChart;
|
|
193
|
+
|
|
194
|
+
// NEW: Advanced color features
|
|
195
|
+
enableAdvancedColors(): boolean;
|
|
196
|
+
enableAdvancedColors(value: boolean): WaterfallChart;
|
|
197
|
+
|
|
198
|
+
colorMode(): 'default' | 'conditional' | 'sequential' | 'diverging';
|
|
199
|
+
colorMode(value: 'default' | 'conditional' | 'sequential' | 'diverging'): WaterfallChart;
|
|
200
|
+
|
|
201
|
+
colorTheme(): string;
|
|
202
|
+
colorTheme(value: string): WaterfallChart;
|
|
203
|
+
|
|
204
|
+
neutralThreshold(): number;
|
|
205
|
+
neutralThreshold(value: number): WaterfallChart;
|
|
206
|
+
|
|
207
|
+
staggeredAnimations(): boolean;
|
|
208
|
+
staggeredAnimations(value: boolean): WaterfallChart;
|
|
209
|
+
|
|
210
|
+
staggerDelay(): number;
|
|
211
|
+
staggerDelay(value: number): WaterfallChart;
|
|
212
|
+
|
|
213
|
+
scaleType(): string;
|
|
214
|
+
scaleType(value: string): WaterfallChart;
|
|
215
|
+
|
|
216
|
+
// Trend line features
|
|
217
|
+
showTrendLine(): boolean;
|
|
218
|
+
showTrendLine(value: boolean): WaterfallChart;
|
|
219
|
+
|
|
220
|
+
trendLineColor(): string;
|
|
221
|
+
trendLineColor(value: string): WaterfallChart;
|
|
222
|
+
|
|
223
|
+
trendLineWidth(): number;
|
|
224
|
+
trendLineWidth(value: number): WaterfallChart;
|
|
225
|
+
|
|
226
|
+
trendLineStyle(): string;
|
|
227
|
+
trendLineStyle(value: string): WaterfallChart;
|
|
228
|
+
|
|
229
|
+
trendLineOpacity(): number;
|
|
230
|
+
trendLineOpacity(value: number): WaterfallChart;
|
|
231
|
+
|
|
232
|
+
trendLineType(): string;
|
|
233
|
+
trendLineType(value: string): WaterfallChart;
|
|
234
|
+
|
|
235
|
+
trendLineWindow(): number;
|
|
236
|
+
trendLineWindow(value: number): WaterfallChart;
|
|
237
|
+
|
|
238
|
+
trendLineDegree(): number;
|
|
239
|
+
trendLineDegree(value: number): WaterfallChart;
|
|
240
|
+
|
|
241
|
+
// Accessibility and UX features
|
|
242
|
+
enableAccessibility(): boolean;
|
|
243
|
+
enableAccessibility(value: boolean): WaterfallChart;
|
|
244
|
+
|
|
245
|
+
enableTooltips(): boolean;
|
|
246
|
+
enableTooltips(value: boolean): WaterfallChart;
|
|
247
|
+
|
|
248
|
+
tooltipConfig(): TooltipConfig;
|
|
249
|
+
tooltipConfig(value: TooltipConfig): WaterfallChart;
|
|
250
|
+
|
|
251
|
+
enableExport(): boolean;
|
|
252
|
+
enableExport(value: boolean): WaterfallChart;
|
|
253
|
+
|
|
254
|
+
exportConfig(): ExportConfig;
|
|
255
|
+
exportConfig(value: ExportConfig): WaterfallChart;
|
|
256
|
+
|
|
257
|
+
enableZoom(): boolean;
|
|
258
|
+
enableZoom(value: boolean): WaterfallChart;
|
|
259
|
+
|
|
260
|
+
zoomConfig(): ZoomConfig;
|
|
261
|
+
zoomConfig(value: ZoomConfig): WaterfallChart;
|
|
262
|
+
|
|
263
|
+
// Enterprise features
|
|
264
|
+
breakdownConfig(): BreakdownConfig | null;
|
|
265
|
+
breakdownConfig(value: BreakdownConfig | null): WaterfallChart;
|
|
266
|
+
|
|
267
|
+
// Performance features
|
|
268
|
+
enablePerformanceOptimization(): boolean;
|
|
269
|
+
enablePerformanceOptimization(value: boolean): WaterfallChart;
|
|
270
|
+
|
|
271
|
+
performanceDashboard(): boolean;
|
|
272
|
+
performanceDashboard(value: boolean): WaterfallChart;
|
|
273
|
+
|
|
274
|
+
virtualizationThreshold(): number;
|
|
275
|
+
virtualizationThreshold(value: number): WaterfallChart;
|
|
276
|
+
|
|
277
|
+
// Event handling
|
|
278
|
+
on(event: string, handler: BarEventHandler | null): WaterfallChart;
|
|
279
|
+
|
|
280
|
+
// Note: MEDIUM PRIORITY analytical enhancement features are available
|
|
281
|
+
// via the exported utility functions but not integrated into the main chart API
|
|
282
|
+
|
|
283
|
+
// Internal system instances
|
|
284
|
+
zoomSystemInstance?: any;
|
|
285
|
+
|
|
286
|
+
// Rendering
|
|
287
|
+
(selection: d3.Selection<any, any, any, any>): void;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Utility function to get bar width from any scale type
|
|
291
|
+
function getBarWidth(scale: any, barCount: number, totalWidth: number): number {
|
|
292
|
+
if (scale.bandwidth) {
|
|
293
|
+
// Band scale has bandwidth method - use it directly
|
|
294
|
+
const bandwidth = scale.bandwidth();
|
|
295
|
+
// Using band scale bandwidth
|
|
296
|
+
return bandwidth;
|
|
297
|
+
} else {
|
|
298
|
+
// For continuous scales, calculate width based on bar count
|
|
299
|
+
const padding = 0.1;
|
|
300
|
+
const availableWidth = totalWidth * (1 - padding);
|
|
301
|
+
const calculatedWidth = availableWidth / barCount;
|
|
302
|
+
// Calculated width for continuous scale
|
|
303
|
+
return calculatedWidth;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Utility function to get bar position from any scale type
|
|
308
|
+
function getBarPosition(scale: any, value: any, barWidth: number): number {
|
|
309
|
+
if (scale.bandwidth) {
|
|
310
|
+
// Band scale - use scale directly
|
|
311
|
+
return scale(value);
|
|
312
|
+
} else {
|
|
313
|
+
// Continuous scale - center the bar around the scale value
|
|
314
|
+
return scale(value) - barWidth / 2;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function waterfallChart(): WaterfallChart {
|
|
319
|
+
let width: number = 800;
|
|
320
|
+
let height: number = 400;
|
|
321
|
+
let margin: MarginConfig = { top: 60, right: 80, bottom: 60, left: 80 };
|
|
322
|
+
let showTotal: boolean = false;
|
|
323
|
+
let totalLabel: string = "Total";
|
|
324
|
+
let totalColor: string = "#95A5A6";
|
|
325
|
+
let stacked: boolean = false;
|
|
326
|
+
let barPadding: number = 0.05;
|
|
327
|
+
let duration: number = 750;
|
|
328
|
+
let ease: (t: number) => number = d3.easeQuadInOut;
|
|
329
|
+
let formatNumber: (n: number) => string = d3.format(".0f");
|
|
330
|
+
let theme: string | null = null;
|
|
331
|
+
|
|
332
|
+
// Advanced features
|
|
333
|
+
let enableBrush: boolean = false;
|
|
334
|
+
let brushOptions: BrushOptions = {};
|
|
335
|
+
let staggeredAnimations: boolean = false;
|
|
336
|
+
let staggerDelay: number = 100;
|
|
337
|
+
let scaleType: string = "auto"; // 'auto', 'linear', 'time', 'ordinal'
|
|
338
|
+
|
|
339
|
+
// NEW: Advanced color and shape features
|
|
340
|
+
let advancedColorConfig: AdvancedColorConfig = {
|
|
341
|
+
enabled: false,
|
|
342
|
+
scaleType: 'auto',
|
|
343
|
+
themeName: 'default',
|
|
344
|
+
neutralThreshold: 0
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Advanced color mode for enhanced visual impact
|
|
348
|
+
let colorMode: 'default' | 'conditional' | 'sequential' | 'diverging' = 'conditional';
|
|
349
|
+
|
|
350
|
+
let confidenceBandConfig: ConfidenceBandConfig = {
|
|
351
|
+
enabled: false,
|
|
352
|
+
opacity: 0.3,
|
|
353
|
+
showTrendLines: true
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
let milestoneConfig: MilestoneConfig = {
|
|
357
|
+
enabled: false,
|
|
358
|
+
milestones: []
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Note: Advanced analytical enhancement feature variables removed
|
|
362
|
+
// Features are available via exported utility functions
|
|
363
|
+
|
|
364
|
+
// Note: Hierarchical layout variables removed
|
|
365
|
+
// Features are available via exported utility functions
|
|
366
|
+
|
|
367
|
+
// Trend line features
|
|
368
|
+
let showTrendLine: boolean = false;
|
|
369
|
+
let trendLineColor: string = "#e74c3c";
|
|
370
|
+
let trendLineWidth: number = 2;
|
|
371
|
+
let trendLineStyle: string = "solid"; // 'solid', 'dashed', 'dotted'
|
|
372
|
+
let trendLineOpacity: number = 0.8;
|
|
373
|
+
let trendLineType: string = "linear"; // 'linear', 'moving-average', 'polynomial'
|
|
374
|
+
let trendLineWindow: number = 3; // Moving average window size
|
|
375
|
+
let trendLineDegree: number = 2; // Polynomial degree
|
|
376
|
+
|
|
377
|
+
// Accessibility and UX features
|
|
378
|
+
let enableAccessibility: boolean = true;
|
|
379
|
+
let enableTooltips: boolean = false;
|
|
380
|
+
let tooltipConfig: TooltipConfig = {};
|
|
381
|
+
let enableExport: boolean = true;
|
|
382
|
+
let exportConfig: ExportConfig = {};
|
|
383
|
+
let enableZoom: boolean = false;
|
|
384
|
+
let zoomConfig: ZoomConfig = {};
|
|
385
|
+
|
|
386
|
+
// Enterprise features
|
|
387
|
+
let breakdownConfig: BreakdownConfig | null = null;
|
|
388
|
+
let formattingRules: Map<string, any> = new Map();
|
|
389
|
+
|
|
390
|
+
// Performance features
|
|
391
|
+
let lastDataHash: string | null = null;
|
|
392
|
+
let cachedProcessedData: ProcessedData[] | null = null;
|
|
393
|
+
|
|
394
|
+
// Initialize systems
|
|
395
|
+
const scaleSystem = createScaleSystem();
|
|
396
|
+
const brushSystem = createBrushSystem();
|
|
397
|
+
const accessibilitySystem = createAccessibilitySystem();
|
|
398
|
+
const tooltipSystem = createTooltipSystem();
|
|
399
|
+
const exportSystem = createExportSystem();
|
|
400
|
+
const zoomSystem = createZoomSystem();
|
|
401
|
+
|
|
402
|
+
// NEW: Initialize advanced feature systems
|
|
403
|
+
const shapeGeneratorSystem = createShapeGenerators();
|
|
404
|
+
const performanceManager = createPerformanceManager();
|
|
405
|
+
|
|
406
|
+
// Note: Advanced analytical enhancement system instances removed
|
|
407
|
+
// Systems are available via exported utility functions
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// Performance configuration
|
|
411
|
+
let enablePerformanceOptimization: boolean = false;
|
|
412
|
+
let performanceDashboard: boolean = false;
|
|
413
|
+
let virtualizationThreshold: number = 10000;
|
|
414
|
+
|
|
415
|
+
// Event listeners - enhanced with brush events
|
|
416
|
+
const listeners = d3.dispatch("barClick", "barMouseover", "barMouseout", "chartUpdate", "brushSelection");
|
|
417
|
+
|
|
418
|
+
function chart(selection: d3.Selection<any, any, any, any>): void {
|
|
419
|
+
selection.each(function(data: ChartData[]) {
|
|
420
|
+
// Data validation
|
|
421
|
+
if (!data || !Array.isArray(data)) {
|
|
422
|
+
console.warn("MintWaterfall: Invalid data provided. Expected an array.");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (data.length === 0) {
|
|
427
|
+
console.warn("MintWaterfall: Empty data array provided.");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Validate data structure
|
|
432
|
+
const isValidData = data.every(item =>
|
|
433
|
+
item &&
|
|
434
|
+
typeof item.label === "string" &&
|
|
435
|
+
Array.isArray(item.stacks) &&
|
|
436
|
+
item.stacks.every(stack =>
|
|
437
|
+
typeof stack.value === "number" &&
|
|
438
|
+
typeof stack.color === "string"
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (!isValidData) {
|
|
443
|
+
console.error("MintWaterfall: Invalid data structure. Each item must have a 'label' string and 'stacks' array with 'value' numbers and 'color' strings.");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Handle both div containers and existing SVG elements
|
|
448
|
+
const element = d3.select(this);
|
|
449
|
+
let svg: any;
|
|
450
|
+
|
|
451
|
+
if (this.tagName === 'svg') {
|
|
452
|
+
// Already an SVG element
|
|
453
|
+
svg = element;
|
|
454
|
+
} else {
|
|
455
|
+
// Container element (div) - create or select SVG
|
|
456
|
+
svg = element.selectAll('svg').data([0]);
|
|
457
|
+
const svgEnter = svg.enter().append('svg');
|
|
458
|
+
svg = svgEnter.merge(svg);
|
|
459
|
+
|
|
460
|
+
// Set SVG dimensions
|
|
461
|
+
svg.attr('width', width).attr('height', height);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Get actual SVG dimensions from attributes if available
|
|
465
|
+
const svgNode = svg.node() as SVGSVGElement;
|
|
466
|
+
if (svgNode) {
|
|
467
|
+
const svgWidth = svgNode.getAttribute('width');
|
|
468
|
+
const svgHeight = svgNode.getAttribute('height');
|
|
469
|
+
if (svgWidth) width = parseInt(svgWidth, 10);
|
|
470
|
+
if (svgHeight) height = parseInt(svgHeight, 10);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Chart dimensions set
|
|
474
|
+
|
|
475
|
+
const container = svg.selectAll(".waterfall-container").data([data]);
|
|
476
|
+
|
|
477
|
+
// Store reference for zoom system
|
|
478
|
+
const svgContainer = svg;
|
|
479
|
+
|
|
480
|
+
// Create main container group
|
|
481
|
+
const containerEnter = container.enter()
|
|
482
|
+
.append("g")
|
|
483
|
+
.attr("class", "waterfall-container");
|
|
484
|
+
|
|
485
|
+
const containerUpdate = containerEnter.merge(container);
|
|
486
|
+
|
|
487
|
+
// Create chart group for zoom transforms
|
|
488
|
+
let chartGroup: any = containerUpdate.select(".chart-group");
|
|
489
|
+
if (chartGroup.empty()) {
|
|
490
|
+
chartGroup = containerUpdate.append("g")
|
|
491
|
+
.attr("class", "chart-group");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Add clipping path to prevent overflow - this will be set after margins are calculated
|
|
495
|
+
const clipPathId = `chart-clip-${Date.now()}`;
|
|
496
|
+
svg.select(`#${clipPathId}`).remove(); // Remove existing if any
|
|
497
|
+
const clipPath = svg.append("defs")
|
|
498
|
+
.append("clipPath")
|
|
499
|
+
.attr("id", clipPathId)
|
|
500
|
+
.append("rect");
|
|
501
|
+
|
|
502
|
+
chartGroup.attr("clip-path", `url(#${clipPathId})`);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// Enable performance optimization for large datasets
|
|
506
|
+
if (data.length >= virtualizationThreshold && enablePerformanceOptimization) {
|
|
507
|
+
performanceManager.enableVirtualization({
|
|
508
|
+
chunkSize: Math.min(1000, Math.floor(data.length / 10)),
|
|
509
|
+
renderThreshold: virtualizationThreshold
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Check if we can use cached data (include showTotal in cache key)
|
|
514
|
+
const dataHash = JSON.stringify(data).slice(0, 100) + `_showTotal:${showTotal}`; // Quick hash with showTotal
|
|
515
|
+
let processedData: ProcessedData[];
|
|
516
|
+
|
|
517
|
+
if (dataHash === lastDataHash && cachedProcessedData) {
|
|
518
|
+
processedData = cachedProcessedData;
|
|
519
|
+
// Using cached processed data
|
|
520
|
+
} else {
|
|
521
|
+
// Prepare data with cumulative calculations
|
|
522
|
+
if (data.length > 50000) {
|
|
523
|
+
// For very large datasets, fall back to synchronous processing for now
|
|
524
|
+
// TODO: Implement proper async handling in future version
|
|
525
|
+
console.warn("MintWaterfall: Large dataset detected, using synchronous processing");
|
|
526
|
+
processedData = prepareData(data);
|
|
527
|
+
} else {
|
|
528
|
+
processedData = prepareData(data);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Cache the processed data
|
|
532
|
+
lastDataHash = dataHash;
|
|
533
|
+
cachedProcessedData = processedData;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Process data for chart rendering
|
|
537
|
+
|
|
538
|
+
// Calculate intelligent margins based on data
|
|
539
|
+
const intelligentMargins = calculateIntelligentMargins(processedData, margin);
|
|
540
|
+
|
|
541
|
+
// Set up scales using enhanced scale system
|
|
542
|
+
let xScale: any;
|
|
543
|
+
if (scaleType === "auto") {
|
|
544
|
+
xScale = scaleSystem.createAdaptiveScale(processedData, "x");
|
|
545
|
+
// If it's a band scale, apply padding
|
|
546
|
+
if (xScale.padding) {
|
|
547
|
+
xScale.padding(barPadding);
|
|
548
|
+
}
|
|
549
|
+
} else if (scaleType === "time") {
|
|
550
|
+
const timeValues = processedData.map(d => new Date(d.label));
|
|
551
|
+
xScale = scaleSystem.createTimeScale(timeValues);
|
|
552
|
+
} else if (scaleType === "ordinal") {
|
|
553
|
+
xScale = scaleSystem.createOrdinalScale(processedData.map(d => d.label));
|
|
554
|
+
} else {
|
|
555
|
+
// Default to band scale for categorical data
|
|
556
|
+
xScale = d3.scaleBand()
|
|
557
|
+
.domain(processedData.map(d => d.label))
|
|
558
|
+
.padding(barPadding);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// CRITICAL: Set range for x scale using intelligent margins - this must happen after scale creation
|
|
562
|
+
xScale.range([intelligentMargins.left, width - intelligentMargins.right]);
|
|
563
|
+
|
|
564
|
+
// Ensure the scale system uses the correct default range for future scales
|
|
565
|
+
scaleSystem.setDefaultRange([intelligentMargins.left, width - intelligentMargins.right]);
|
|
566
|
+
|
|
567
|
+
// Update clipping path with proper chart area dimensions
|
|
568
|
+
// IMPORTANT: Extend clipping area to include space for labels above bars
|
|
569
|
+
const labelSpace = 30; // Extra space for labels above the chart area
|
|
570
|
+
clipPath
|
|
571
|
+
.attr("x", intelligentMargins.left)
|
|
572
|
+
.attr("y", Math.max(0, intelligentMargins.top - labelSpace)) // Extend upward for labels
|
|
573
|
+
.attr("width", width - intelligentMargins.left - intelligentMargins.right)
|
|
574
|
+
.attr("height", height - intelligentMargins.top - intelligentMargins.bottom + labelSpace);
|
|
575
|
+
|
|
576
|
+
// Clipping path configured
|
|
577
|
+
|
|
578
|
+
// Scale configuration complete
|
|
579
|
+
|
|
580
|
+
// Enhanced Y scale using d3.extent and nice()
|
|
581
|
+
const yValues = processedData.map(d => d.cumulativeTotal);
|
|
582
|
+
|
|
583
|
+
// For waterfall charts, ensure proper baseline handling
|
|
584
|
+
const [min, max] = d3.extent(yValues) as [number, number];
|
|
585
|
+
const hasNegativeValues = min < 0;
|
|
586
|
+
|
|
587
|
+
let yScale: any;
|
|
588
|
+
if (hasNegativeValues) {
|
|
589
|
+
// When we have negative values, create scale that includes them but doesn't extend too far
|
|
590
|
+
const range = max - min;
|
|
591
|
+
const padding = range * 0.05; // 5% padding
|
|
592
|
+
yScale = d3.scaleLinear()
|
|
593
|
+
.domain([min - padding, max + padding])
|
|
594
|
+
.range([height - intelligentMargins.bottom, intelligentMargins.top]);
|
|
595
|
+
} else {
|
|
596
|
+
// For positive-only data, start at 0
|
|
597
|
+
yScale = scaleSystem.createLinearScale(yValues, {
|
|
598
|
+
range: [height - intelligentMargins.bottom, intelligentMargins.top],
|
|
599
|
+
nice: true
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Create/update grid
|
|
604
|
+
drawGrid(containerUpdate, yScale, intelligentMargins);
|
|
605
|
+
|
|
606
|
+
// Create/update axes (on container, not chart group)
|
|
607
|
+
drawAxes(containerUpdate, xScale, yScale, intelligentMargins);
|
|
608
|
+
|
|
609
|
+
// Create/update bars with enhanced animations (in chart group for zoom)
|
|
610
|
+
drawBars(chartGroup, processedData, xScale, yScale, intelligentMargins);
|
|
611
|
+
|
|
612
|
+
// Create/update connectors (in chart group for zoom)
|
|
613
|
+
drawConnectors(chartGroup, processedData, xScale, yScale);
|
|
614
|
+
|
|
615
|
+
// Create/update trend line (handles both show and hide cases)
|
|
616
|
+
drawTrendLine(chartGroup, processedData, xScale, yScale);
|
|
617
|
+
|
|
618
|
+
// NEW: Draw advanced features
|
|
619
|
+
drawConfidenceBands(chartGroup, processedData, xScale, yScale);
|
|
620
|
+
drawMilestones(chartGroup, processedData, xScale, yScale);
|
|
621
|
+
|
|
622
|
+
// Add brush functionality if enabled
|
|
623
|
+
if (enableBrush) {
|
|
624
|
+
addBrushSelection(containerUpdate, processedData, xScale, yScale);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Initialize features after rendering is complete
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
if (enableAccessibility) {
|
|
630
|
+
initializeAccessibility(svg, processedData);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (enableTooltips) {
|
|
634
|
+
initializeTooltips(svg);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (enableExport) {
|
|
638
|
+
initializeExport(svg, processedData);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (enableZoom) {
|
|
642
|
+
// Initialize zoom system if not already created
|
|
643
|
+
if (!(chart as any).zoomSystemInstance) {
|
|
644
|
+
(chart as any).zoomSystemInstance = createZoomSystem();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Attach zoom to the SVG container
|
|
648
|
+
(chart as any).zoomSystemInstance.attach(svgContainer);
|
|
649
|
+
(chart as any).zoomSystemInstance.setDimensions({ width, height, margin: intelligentMargins });
|
|
650
|
+
(chart as any).zoomSystemInstance.enable();
|
|
651
|
+
} else {
|
|
652
|
+
// Disable zoom if it was previously enabled
|
|
653
|
+
if ((chart as any).zoomSystemInstance) {
|
|
654
|
+
(chart as any).zoomSystemInstance.disable();
|
|
655
|
+
(chart as any).zoomSystemInstance.detach();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}, 50); // Small delay to ensure DOM is ready
|
|
659
|
+
|
|
660
|
+
} catch (error: any) {
|
|
661
|
+
console.error("MintWaterfall rendering error:", error);
|
|
662
|
+
console.error("Stack trace:", error.stack);
|
|
663
|
+
|
|
664
|
+
// Clear any partial rendering and show error
|
|
665
|
+
containerUpdate.selectAll("*").remove();
|
|
666
|
+
containerUpdate.append("text")
|
|
667
|
+
.attr("x", width / 2)
|
|
668
|
+
.attr("y", height / 2)
|
|
669
|
+
.attr("text-anchor", "middle")
|
|
670
|
+
.style("font-size", "14px")
|
|
671
|
+
.style("fill", "#ff6b6b")
|
|
672
|
+
.text(`Chart Error: ${error.message}`);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function calculateIntelligentMargins(processedData: ProcessedData[], baseMargin: MarginConfig): MarginConfig {
|
|
678
|
+
// Calculate required space for labels - handle all edge cases
|
|
679
|
+
const allValues = processedData.flatMap(d => [d.cumulativeTotal, d.prevCumulativeTotal || 0]);
|
|
680
|
+
const maxValue = d3.max(allValues) || 0;
|
|
681
|
+
const minValue = d3.min(allValues) || 0;
|
|
682
|
+
|
|
683
|
+
// Estimate label dimensions - be more generous with space
|
|
684
|
+
const labelHeight = 16; // Increased from 14 to account for font size
|
|
685
|
+
const labelPadding = 8; // Increased from 5 for better spacing
|
|
686
|
+
const requiredLabelSpace = labelHeight + labelPadding;
|
|
687
|
+
const safetyBuffer = 20; // Increased from 10 for more breathing room
|
|
688
|
+
|
|
689
|
+
// Handle edge cases for different data scenarios
|
|
690
|
+
const hasNegativeValues = minValue < 0;
|
|
691
|
+
|
|
692
|
+
// Start with a more generous top margin to ensure labels fit
|
|
693
|
+
const initialTopMargin = Math.max(baseMargin.top, 80); // Ensure minimum 80px for labels
|
|
694
|
+
|
|
695
|
+
// Create temporary scale that matches the actual rendering logic
|
|
696
|
+
let tempYScale: any;
|
|
697
|
+
const tempRange: [number, number] = [height - baseMargin.bottom, initialTopMargin];
|
|
698
|
+
|
|
699
|
+
if (hasNegativeValues) {
|
|
700
|
+
// Match the actual scale logic for negative values
|
|
701
|
+
const range = maxValue - minValue;
|
|
702
|
+
const padding = range * 0.05; // 5% padding (same as actual scale)
|
|
703
|
+
tempYScale = d3.scaleLinear()
|
|
704
|
+
.domain([minValue - padding, maxValue + padding])
|
|
705
|
+
.range(tempRange);
|
|
706
|
+
} else {
|
|
707
|
+
// For positive-only data, start at 0 with padding
|
|
708
|
+
const paddedMax = maxValue * 1.02; // 2% padding (same as actual scale)
|
|
709
|
+
tempYScale = d3.scaleLinear()
|
|
710
|
+
.domain([0, paddedMax])
|
|
711
|
+
.range(tempRange)
|
|
712
|
+
.nice(); // Apply nice() like the actual scale
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Find the highest point where any label will be positioned
|
|
716
|
+
const allLabelPositions = processedData.map(d => {
|
|
717
|
+
const barTop = tempYScale(d.cumulativeTotal);
|
|
718
|
+
return barTop - labelPadding;
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const highestLabelPosition = Math.min(...allLabelPositions);
|
|
722
|
+
|
|
723
|
+
// Calculate required top margin - ensure labels have enough space above them
|
|
724
|
+
const spaceNeededFromTop = Math.max(
|
|
725
|
+
initialTopMargin - highestLabelPosition + requiredLabelSpace,
|
|
726
|
+
requiredLabelSpace + safetyBuffer // Minimum space needed
|
|
727
|
+
);
|
|
728
|
+
const extraTopMarginNeeded = Math.max(0, spaceNeededFromTop - initialTopMargin);
|
|
729
|
+
|
|
730
|
+
// For negative values, we might also need bottom space
|
|
731
|
+
let extraBottomMargin = 0;
|
|
732
|
+
if (hasNegativeValues) {
|
|
733
|
+
const negativeData = processedData.filter(d => d.cumulativeTotal < 0);
|
|
734
|
+
if (negativeData.length > 0) {
|
|
735
|
+
const lowestLabelPosition = Math.max(
|
|
736
|
+
...negativeData.map(d => tempYScale(d.cumulativeTotal) + labelHeight + labelPadding)
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
if (lowestLabelPosition > height - baseMargin.bottom) {
|
|
740
|
+
extraBottomMargin = lowestLabelPosition - (height - baseMargin.bottom);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Calculate required right margin for labels
|
|
746
|
+
const maxLabelLength = Math.max(...processedData.map(d =>
|
|
747
|
+
formatNumber(d.cumulativeTotal).length
|
|
748
|
+
));
|
|
749
|
+
const estimatedLabelWidth = maxLabelLength * 9; // Increased from 8 to 9px per character
|
|
750
|
+
const minRightMargin = Math.max(baseMargin.right, estimatedLabelWidth / 2 + 15);
|
|
751
|
+
|
|
752
|
+
const intelligentMargin: MarginConfig = {
|
|
753
|
+
top: initialTopMargin + extraTopMarginNeeded + safetyBuffer,
|
|
754
|
+
right: minRightMargin,
|
|
755
|
+
bottom: baseMargin.bottom + extraBottomMargin + (hasNegativeValues ? safetyBuffer : 10),
|
|
756
|
+
left: baseMargin.left
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// Intelligent margins calculated
|
|
760
|
+
|
|
761
|
+
return intelligentMargin;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function prepareData(data: ChartData[]): ProcessedData[] {
|
|
765
|
+
let workingData = [...data];
|
|
766
|
+
|
|
767
|
+
// Apply breakdown analysis if enabled
|
|
768
|
+
if (breakdownConfig && breakdownConfig.enabled) {
|
|
769
|
+
workingData = applyBreakdownAnalysis(workingData, breakdownConfig);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let cumulativeTotal = 0;
|
|
773
|
+
let prevCumulativeTotal = 0;
|
|
774
|
+
|
|
775
|
+
// Process each bar with cumulative totals
|
|
776
|
+
const processedData: ProcessedData[] = workingData.map((bar, i) => {
|
|
777
|
+
const barTotal = bar.stacks.reduce((sum, stack) => sum + stack.value, 0);
|
|
778
|
+
prevCumulativeTotal = cumulativeTotal;
|
|
779
|
+
cumulativeTotal += barTotal;
|
|
780
|
+
|
|
781
|
+
// Apply conditional formatting if enabled
|
|
782
|
+
let processedStacks = bar.stacks;
|
|
783
|
+
if (formattingRules.size > 0) {
|
|
784
|
+
processedStacks = applyConditionalFormatting(bar.stacks, bar, formattingRules);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const result: ProcessedData = {
|
|
788
|
+
...bar,
|
|
789
|
+
stacks: processedStacks,
|
|
790
|
+
barTotal,
|
|
791
|
+
cumulativeTotal,
|
|
792
|
+
prevCumulativeTotal: i === 0 ? 0 : prevCumulativeTotal
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Add total bar if enabled
|
|
799
|
+
if (showTotal && processedData.length > 0) {
|
|
800
|
+
const totalValue = cumulativeTotal;
|
|
801
|
+
processedData.push({
|
|
802
|
+
label: totalLabel,
|
|
803
|
+
stacks: [{ value: totalValue, color: totalColor }],
|
|
804
|
+
barTotal: totalValue,
|
|
805
|
+
cumulativeTotal: totalValue,
|
|
806
|
+
prevCumulativeTotal: 0 // Total bar starts from zero
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return processedData;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Placeholder function implementations - these would be converted separately
|
|
814
|
+
function applyBreakdownAnalysis(data: ChartData[], config: BreakdownConfig): ChartData[] {
|
|
815
|
+
// Implementation would be migrated from JavaScript version
|
|
816
|
+
return data;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function applyConditionalFormatting(stacks: StackData[], barData: ChartData, rules: Map<string, any>): StackData[] {
|
|
820
|
+
// Implementation would be migrated from JavaScript version
|
|
821
|
+
return stacks;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function drawGrid(container: any, yScale: any, intelligentMargins: MarginConfig): void {
|
|
825
|
+
// Create horizontal grid lines
|
|
826
|
+
const gridGroup = container.selectAll(".grid-group").data([0]);
|
|
827
|
+
const gridGroupEnter = gridGroup.enter()
|
|
828
|
+
.append("g")
|
|
829
|
+
.attr("class", "grid-group");
|
|
830
|
+
const gridGroupUpdate = gridGroupEnter.merge(gridGroup);
|
|
831
|
+
|
|
832
|
+
// Get tick values from y scale
|
|
833
|
+
const tickValues = yScale.ticks();
|
|
834
|
+
|
|
835
|
+
// Create grid lines
|
|
836
|
+
const gridLines = gridGroupUpdate.selectAll(".grid-line").data(tickValues);
|
|
837
|
+
|
|
838
|
+
const gridLinesEnter = gridLines.enter()
|
|
839
|
+
.append("line")
|
|
840
|
+
.attr("class", "grid-line")
|
|
841
|
+
.attr("x1", intelligentMargins.left)
|
|
842
|
+
.attr("x2", width - intelligentMargins.right)
|
|
843
|
+
.attr("stroke", "rgba(224, 224, 224, 0.5)")
|
|
844
|
+
.attr("stroke-width", 1)
|
|
845
|
+
.style("opacity", 0);
|
|
846
|
+
|
|
847
|
+
gridLinesEnter.merge(gridLines)
|
|
848
|
+
.transition()
|
|
849
|
+
.duration(duration)
|
|
850
|
+
.ease(ease)
|
|
851
|
+
.attr("y1", (d: any) => yScale(d))
|
|
852
|
+
.attr("y2", (d: any) => yScale(d))
|
|
853
|
+
.attr("x1", intelligentMargins.left)
|
|
854
|
+
.attr("x2", width - intelligentMargins.right)
|
|
855
|
+
.style("opacity", 1);
|
|
856
|
+
|
|
857
|
+
gridLines.exit()
|
|
858
|
+
.transition()
|
|
859
|
+
.duration(duration)
|
|
860
|
+
.ease(ease)
|
|
861
|
+
.style("opacity", 0)
|
|
862
|
+
.remove();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function drawAxes(container: any, xScale: any, yScale: any, intelligentMargins: MarginConfig): void {
|
|
866
|
+
// Y-axis
|
|
867
|
+
const yAxisGroup = container.selectAll(".y-axis").data([0]);
|
|
868
|
+
const yAxisGroupEnter = yAxisGroup.enter()
|
|
869
|
+
.append("g")
|
|
870
|
+
.attr("class", "y-axis")
|
|
871
|
+
.attr("transform", `translate(${intelligentMargins.left},0)`);
|
|
872
|
+
|
|
873
|
+
yAxisGroupEnter.merge(yAxisGroup)
|
|
874
|
+
.transition()
|
|
875
|
+
.duration(duration)
|
|
876
|
+
.ease(ease)
|
|
877
|
+
.call(d3.axisLeft(yScale).tickFormat((d: any) => formatNumber(d as number)));
|
|
878
|
+
|
|
879
|
+
// X-axis
|
|
880
|
+
const xAxisGroup = container.selectAll(".x-axis").data([0]);
|
|
881
|
+
const xAxisGroupEnter = xAxisGroup.enter()
|
|
882
|
+
.append("g")
|
|
883
|
+
.attr("class", "x-axis")
|
|
884
|
+
.attr("transform", `translate(0,${height - intelligentMargins.bottom})`);
|
|
885
|
+
|
|
886
|
+
xAxisGroupEnter.merge(xAxisGroup)
|
|
887
|
+
.transition()
|
|
888
|
+
.duration(duration)
|
|
889
|
+
.ease(ease)
|
|
890
|
+
.call(d3.axisBottom(xScale));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function drawBars(container: any, processedData: ProcessedData[], xScale: any, yScale: any, intelligentMargins: MarginConfig): void {
|
|
894
|
+
const barsGroup = container.selectAll(".bars-group").data([0]);
|
|
895
|
+
const barsGroupEnter = barsGroup.enter()
|
|
896
|
+
.append("g")
|
|
897
|
+
.attr("class", "bars-group");
|
|
898
|
+
const barsGroupUpdate = barsGroupEnter.merge(barsGroup);
|
|
899
|
+
|
|
900
|
+
// Bar groups for each data point
|
|
901
|
+
const barGroups = barsGroupUpdate.selectAll(".bar-group").data(processedData, (d: any) => d.label);
|
|
902
|
+
|
|
903
|
+
// For band scales, we don't need manual positioning - the scale handles it
|
|
904
|
+
const barGroupsEnter = barGroups.enter()
|
|
905
|
+
.append("g")
|
|
906
|
+
.attr("class", "bar-group")
|
|
907
|
+
.attr("transform", (d: any) => {
|
|
908
|
+
if (xScale.bandwidth) {
|
|
909
|
+
// Band scale - use the scale directly
|
|
910
|
+
return `translate(${xScale(d.label)}, 0)`;
|
|
911
|
+
} else {
|
|
912
|
+
// Continuous scale - manual positioning using intelligent margins
|
|
913
|
+
const barWidth = getBarWidth(xScale, processedData.length, width - intelligentMargins.left - intelligentMargins.right);
|
|
914
|
+
const barX = getBarPosition(xScale, d.label, barWidth);
|
|
915
|
+
return `translate(${barX}, 0)`;
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const barGroupsUpdate = barGroupsEnter.merge(barGroups)
|
|
920
|
+
.transition()
|
|
921
|
+
.duration(duration)
|
|
922
|
+
.ease(ease)
|
|
923
|
+
.attr("transform", (d: any) => {
|
|
924
|
+
if (xScale.bandwidth) {
|
|
925
|
+
// Band scale - use the scale directly
|
|
926
|
+
return `translate(${xScale(d.label)}, 0)`;
|
|
927
|
+
} else {
|
|
928
|
+
// Continuous scale - manual positioning using intelligent margins
|
|
929
|
+
const barWidth = getBarWidth(xScale, processedData.length, width - intelligentMargins.left - intelligentMargins.right);
|
|
930
|
+
const barX = getBarPosition(xScale, d.label, barWidth);
|
|
931
|
+
return `translate(${barX}, 0)`;
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (stacked) {
|
|
936
|
+
drawStackedBars(barGroupsUpdate, xScale, yScale, intelligentMargins);
|
|
937
|
+
} else {
|
|
938
|
+
drawWaterfallBars(barGroupsUpdate, xScale, yScale, intelligentMargins, processedData);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Add value labels
|
|
942
|
+
drawValueLabels(barGroupsUpdate, xScale, yScale, intelligentMargins);
|
|
943
|
+
|
|
944
|
+
barGroups.exit()
|
|
945
|
+
.transition()
|
|
946
|
+
.duration(duration)
|
|
947
|
+
.ease(ease)
|
|
948
|
+
.style("opacity", 0)
|
|
949
|
+
.remove();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function drawStackedBars(barGroups: any, xScale: any, yScale: any, intelligentMargins: MarginConfig): void {
|
|
953
|
+
barGroups.each(function(this: SVGGElement, d: any) {
|
|
954
|
+
const group = d3.select(this);
|
|
955
|
+
const stackData = d.stacks.map((stack: any, i: number) => ({
|
|
956
|
+
...stack,
|
|
957
|
+
stackIndex: i,
|
|
958
|
+
parent: d
|
|
959
|
+
}));
|
|
960
|
+
|
|
961
|
+
// Calculate stack positions
|
|
962
|
+
let cumulativeHeight = d.prevCumulativeTotal || 0;
|
|
963
|
+
stackData.forEach((stack: any) => {
|
|
964
|
+
stack.startY = cumulativeHeight;
|
|
965
|
+
stack.endY = cumulativeHeight + stack.value;
|
|
966
|
+
stack.y = yScale(Math.max(stack.startY, stack.endY));
|
|
967
|
+
stack.height = Math.abs(yScale(stack.startY) - yScale(stack.endY));
|
|
968
|
+
cumulativeHeight += stack.value;
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
const stacks = group.selectAll(".stack").data(stackData);
|
|
972
|
+
|
|
973
|
+
// Get bar width - use scale bandwidth if available, otherwise calculate using intelligent margins
|
|
974
|
+
const barWidth = xScale.bandwidth ? xScale.bandwidth() : getBarWidth(xScale, barGroups.size(), width - intelligentMargins.left - intelligentMargins.right);
|
|
975
|
+
|
|
976
|
+
const stacksEnter = stacks.enter()
|
|
977
|
+
.append("rect")
|
|
978
|
+
.attr("class", "stack")
|
|
979
|
+
.attr("x", 0)
|
|
980
|
+
.attr("width", barWidth)
|
|
981
|
+
.attr("y", yScale(0))
|
|
982
|
+
.attr("height", 0)
|
|
983
|
+
.attr("fill", (stack: any) => stack.color);
|
|
984
|
+
|
|
985
|
+
(stacksEnter as any).merge(stacks)
|
|
986
|
+
.transition()
|
|
987
|
+
.duration(duration)
|
|
988
|
+
.ease(ease)
|
|
989
|
+
.attr("y", (stack: any) => stack.y)
|
|
990
|
+
.attr("height", (stack: any) => stack.height)
|
|
991
|
+
.attr("fill", (stack: any) => stack.color)
|
|
992
|
+
.attr("width", barWidth);
|
|
993
|
+
|
|
994
|
+
stacks.exit()
|
|
995
|
+
.transition()
|
|
996
|
+
.duration(duration)
|
|
997
|
+
.ease(ease)
|
|
998
|
+
.attr("height", 0)
|
|
999
|
+
.attr("y", yScale(0))
|
|
1000
|
+
.remove();
|
|
1001
|
+
|
|
1002
|
+
// Add stack labels if they exist
|
|
1003
|
+
const stackLabels = group.selectAll(".stack-label").data(stackData.filter((s: any) => s.label));
|
|
1004
|
+
|
|
1005
|
+
const stackLabelsEnter = stackLabels.enter()
|
|
1006
|
+
.append("text")
|
|
1007
|
+
.attr("class", "stack-label")
|
|
1008
|
+
.attr("text-anchor", "middle")
|
|
1009
|
+
.attr("x", barWidth / 2)
|
|
1010
|
+
.attr("y", yScale(0))
|
|
1011
|
+
.style("opacity", 0);
|
|
1012
|
+
|
|
1013
|
+
(stackLabelsEnter as any).merge(stackLabels)
|
|
1014
|
+
.transition()
|
|
1015
|
+
.duration(duration)
|
|
1016
|
+
.ease(ease)
|
|
1017
|
+
.attr("y", (stack: any) => stack.y + stack.height / 2 + 4)
|
|
1018
|
+
.attr("x", barWidth / 2)
|
|
1019
|
+
.style("opacity", 1)
|
|
1020
|
+
.text((stack: any) => stack.label);
|
|
1021
|
+
|
|
1022
|
+
stackLabels.exit()
|
|
1023
|
+
.transition()
|
|
1024
|
+
.duration(duration)
|
|
1025
|
+
.ease(ease)
|
|
1026
|
+
.style("opacity", 0)
|
|
1027
|
+
.remove();
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function drawWaterfallBars(barGroups: any, xScale: any, yScale: any, intelligentMargins: MarginConfig, allData: ProcessedData[] = []): void {
|
|
1032
|
+
barGroups.each(function(this: SVGGElement, d: any) {
|
|
1033
|
+
const group = d3.select(this);
|
|
1034
|
+
|
|
1035
|
+
// Get bar width - use scale bandwidth if available, otherwise calculate using intelligent margins
|
|
1036
|
+
const barWidth = xScale.bandwidth ? xScale.bandwidth() : getBarWidth(xScale, barGroups.size(), width - intelligentMargins.left - intelligentMargins.right);
|
|
1037
|
+
|
|
1038
|
+
// Determine bar color using advanced color features
|
|
1039
|
+
const defaultColor = d.stacks.length === 1 ? d.stacks[0].color : "#3498db";
|
|
1040
|
+
const advancedColor = advancedColorConfig.enabled ?
|
|
1041
|
+
getAdvancedBarColor(
|
|
1042
|
+
d.barTotal,
|
|
1043
|
+
defaultColor,
|
|
1044
|
+
allData,
|
|
1045
|
+
advancedColorConfig.themeName as keyof ThemeCollection || 'default',
|
|
1046
|
+
colorMode
|
|
1047
|
+
) : defaultColor;
|
|
1048
|
+
|
|
1049
|
+
const barData = [{
|
|
1050
|
+
value: d.barTotal,
|
|
1051
|
+
color: advancedColor,
|
|
1052
|
+
y: d.isTotal ?
|
|
1053
|
+
Math.min(yScale(0), yScale(d.cumulativeTotal)) : // Total bar: position correctly regardless of scale direction
|
|
1054
|
+
yScale(Math.max(d.prevCumulativeTotal, d.cumulativeTotal)),
|
|
1055
|
+
height: d.isTotal ?
|
|
1056
|
+
Math.abs(yScale(0) - yScale(d.cumulativeTotal)) : // Total bar: full height from zero to total
|
|
1057
|
+
Math.abs(yScale(d.prevCumulativeTotal || 0) - yScale(d.cumulativeTotal)),
|
|
1058
|
+
parent: d
|
|
1059
|
+
}];
|
|
1060
|
+
|
|
1061
|
+
const bars = group.selectAll(".waterfall-bar").data(barData);
|
|
1062
|
+
|
|
1063
|
+
const barsEnter = bars.enter()
|
|
1064
|
+
.append("rect")
|
|
1065
|
+
.attr("class", "waterfall-bar")
|
|
1066
|
+
.attr("x", 0)
|
|
1067
|
+
.attr("width", barWidth)
|
|
1068
|
+
.attr("y", yScale(0))
|
|
1069
|
+
.attr("height", 0)
|
|
1070
|
+
.attr("fill", (bar: any) => bar.color);
|
|
1071
|
+
|
|
1072
|
+
(barsEnter as any).merge(bars)
|
|
1073
|
+
.transition()
|
|
1074
|
+
.duration(duration)
|
|
1075
|
+
.ease(ease)
|
|
1076
|
+
.attr("y", (bar: any) => bar.y)
|
|
1077
|
+
.attr("height", (bar: any) => bar.height)
|
|
1078
|
+
.attr("fill", (bar: any) => bar.color)
|
|
1079
|
+
.attr("width", barWidth);
|
|
1080
|
+
|
|
1081
|
+
bars.exit()
|
|
1082
|
+
.transition()
|
|
1083
|
+
.duration(duration)
|
|
1084
|
+
.ease(ease)
|
|
1085
|
+
.attr("height", 0)
|
|
1086
|
+
.attr("y", yScale(0))
|
|
1087
|
+
.remove();
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function drawValueLabels(barGroups: any, xScale: any, yScale: any, intelligentMargins: MarginConfig): void {
|
|
1092
|
+
// Always show value labels on bars - this is independent of the total bar setting
|
|
1093
|
+
// Drawing value labels
|
|
1094
|
+
|
|
1095
|
+
barGroups.each(function(this: SVGGElement, d: any) {
|
|
1096
|
+
const group = d3.select(this);
|
|
1097
|
+
const barWidth = getBarWidth(xScale, barGroups.size(), width - intelligentMargins.left - intelligentMargins.right);
|
|
1098
|
+
|
|
1099
|
+
// Processing label for bar
|
|
1100
|
+
|
|
1101
|
+
const labelData = [{
|
|
1102
|
+
value: d.barTotal,
|
|
1103
|
+
formattedValue: formatNumber(d.barTotal),
|
|
1104
|
+
parent: d
|
|
1105
|
+
}];
|
|
1106
|
+
|
|
1107
|
+
const totalLabels = group.selectAll(".total-label").data(labelData);
|
|
1108
|
+
|
|
1109
|
+
const totalLabelsEnter = totalLabels.enter()
|
|
1110
|
+
.append("text")
|
|
1111
|
+
.attr("class", "total-label")
|
|
1112
|
+
.attr("text-anchor", "middle")
|
|
1113
|
+
.attr("x", barWidth / 2)
|
|
1114
|
+
.attr("y", yScale(0))
|
|
1115
|
+
.style("opacity", 0)
|
|
1116
|
+
.style("font-family", "Arial, sans-serif"); // Ensure font is set
|
|
1117
|
+
|
|
1118
|
+
const labelUpdate = (totalLabelsEnter as any).merge(totalLabels);
|
|
1119
|
+
|
|
1120
|
+
labelUpdate
|
|
1121
|
+
.transition()
|
|
1122
|
+
.duration(duration)
|
|
1123
|
+
.ease(ease)
|
|
1124
|
+
.attr("y", (labelD: any) => {
|
|
1125
|
+
const barTop = yScale(labelD.parent.cumulativeTotal);
|
|
1126
|
+
const padding = 8;
|
|
1127
|
+
const finalY = barTop - padding;
|
|
1128
|
+
|
|
1129
|
+
// Label positioning calculated
|
|
1130
|
+
|
|
1131
|
+
return finalY;
|
|
1132
|
+
})
|
|
1133
|
+
.attr("x", barWidth / 2)
|
|
1134
|
+
.style("opacity", 1)
|
|
1135
|
+
.style("fill", "#333")
|
|
1136
|
+
.style("font-weight", "bold")
|
|
1137
|
+
.style("font-size", "14px")
|
|
1138
|
+
.style("pointer-events", "none")
|
|
1139
|
+
.style("visibility", "visible") // Ensure visibility
|
|
1140
|
+
.style("display", "block") // Ensure display
|
|
1141
|
+
.attr("clip-path", "none") // Remove any clipping from labels themselves
|
|
1142
|
+
.text((labelD: any) => labelD.formattedValue)
|
|
1143
|
+
.each(function(this: SVGTextElement, labelD: any) {
|
|
1144
|
+
// Label element created
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
totalLabels.exit()
|
|
1148
|
+
.transition()
|
|
1149
|
+
.duration(duration)
|
|
1150
|
+
.ease(ease)
|
|
1151
|
+
.style("opacity", 0)
|
|
1152
|
+
.remove();
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function drawConnectors(container: any, processedData: ProcessedData[], xScale: any, yScale: any): void {
|
|
1157
|
+
if (stacked || processedData.length < 2) return; // Only show connectors for waterfall charts
|
|
1158
|
+
|
|
1159
|
+
const connectorsGroup = container.selectAll(".connectors-group").data([0]);
|
|
1160
|
+
const connectorsGroupEnter = connectorsGroup.enter()
|
|
1161
|
+
.append("g")
|
|
1162
|
+
.attr("class", "connectors-group");
|
|
1163
|
+
const connectorsGroupUpdate = connectorsGroupEnter.merge(connectorsGroup);
|
|
1164
|
+
|
|
1165
|
+
// Create connector data
|
|
1166
|
+
const connectorData: any[] = [];
|
|
1167
|
+
for (let i = 0; i < processedData.length - 1; i++) {
|
|
1168
|
+
const current = processedData[i];
|
|
1169
|
+
const next = processedData[i + 1];
|
|
1170
|
+
|
|
1171
|
+
const barWidth = getBarWidth(xScale, processedData.length, width - margin.left - margin.right);
|
|
1172
|
+
const currentX = getBarPosition(xScale, current.label, barWidth);
|
|
1173
|
+
const nextX = getBarPosition(xScale, next.label, barWidth);
|
|
1174
|
+
|
|
1175
|
+
connectorData.push({
|
|
1176
|
+
x1: currentX + barWidth,
|
|
1177
|
+
x2: nextX,
|
|
1178
|
+
y: yScale(current.cumulativeTotal),
|
|
1179
|
+
id: `${current.label}-${next.label}`
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Create/update connector lines
|
|
1184
|
+
const connectors = connectorsGroupUpdate.selectAll(".connector").data(connectorData, (d: any) => d.id);
|
|
1185
|
+
|
|
1186
|
+
const connectorsEnter = connectors.enter()
|
|
1187
|
+
.append("line")
|
|
1188
|
+
.attr("class", "connector")
|
|
1189
|
+
.attr("stroke", "#bdc3c7")
|
|
1190
|
+
.attr("stroke-width", 1)
|
|
1191
|
+
.attr("stroke-dasharray", "3,3")
|
|
1192
|
+
.style("opacity", 0)
|
|
1193
|
+
.attr("x1", (d: any) => d.x1)
|
|
1194
|
+
.attr("x2", (d: any) => d.x1)
|
|
1195
|
+
.attr("y1", (d: any) => d.y)
|
|
1196
|
+
.attr("y2", (d: any) => d.y);
|
|
1197
|
+
|
|
1198
|
+
connectorsEnter.merge(connectors)
|
|
1199
|
+
.transition()
|
|
1200
|
+
.duration(duration)
|
|
1201
|
+
.ease(ease)
|
|
1202
|
+
.delay((d: any, i: number) => staggeredAnimations ? i * staggerDelay : 0)
|
|
1203
|
+
.attr("x1", (d: any) => d.x1)
|
|
1204
|
+
.attr("x2", (d: any) => d.x2)
|
|
1205
|
+
.attr("y1", (d: any) => d.y)
|
|
1206
|
+
.attr("y2", (d: any) => d.y)
|
|
1207
|
+
.style("opacity", 0.6);
|
|
1208
|
+
|
|
1209
|
+
connectors.exit()
|
|
1210
|
+
.transition()
|
|
1211
|
+
.duration(duration)
|
|
1212
|
+
.ease(ease)
|
|
1213
|
+
.style("opacity", 0)
|
|
1214
|
+
.remove();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function drawTrendLine(container: any, processedData: ProcessedData[], xScale: any, yScale: any): void {
|
|
1218
|
+
// Remove trend line if disabled or insufficient data
|
|
1219
|
+
if (!showTrendLine || processedData.length < 2) {
|
|
1220
|
+
container.selectAll(".trend-group").remove();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const trendGroup = container.selectAll(".trend-group").data([0]);
|
|
1225
|
+
const trendGroupEnter = trendGroup.enter()
|
|
1226
|
+
.append("g")
|
|
1227
|
+
.attr("class", "trend-group");
|
|
1228
|
+
const trendGroupUpdate = trendGroupEnter.merge(trendGroup);
|
|
1229
|
+
|
|
1230
|
+
// Calculate trend line data points based on trend type
|
|
1231
|
+
const trendData: { x: number; y: number }[] = [];
|
|
1232
|
+
|
|
1233
|
+
// First, collect the actual data points
|
|
1234
|
+
const dataPoints: { x: number; y: number; value: number }[] = [];
|
|
1235
|
+
for (let i = 0; i < processedData.length; i++) {
|
|
1236
|
+
const item = processedData[i];
|
|
1237
|
+
const barWidth = getBarWidth(xScale, processedData.length, width - margin.left - margin.right);
|
|
1238
|
+
const x = getBarPosition(xScale, item.label, barWidth) + barWidth / 2;
|
|
1239
|
+
const actualY = yScale(item.cumulativeTotal);
|
|
1240
|
+
dataPoints.push({ x, y: actualY, value: item.cumulativeTotal });
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Calculate trend based on type
|
|
1244
|
+
if (trendLineType === "linear") {
|
|
1245
|
+
// Linear regression
|
|
1246
|
+
const n = dataPoints.length;
|
|
1247
|
+
const sumX = dataPoints.reduce((sum, p, i) => sum + i, 0);
|
|
1248
|
+
const sumY = dataPoints.reduce((sum, p) => sum + p.value, 0);
|
|
1249
|
+
const sumXY = dataPoints.reduce((sum, p, i) => sum + (i * p.value), 0);
|
|
1250
|
+
const sumXX = dataPoints.reduce((sum, p, i) => sum + (i * i), 0);
|
|
1251
|
+
|
|
1252
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
1253
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
1254
|
+
|
|
1255
|
+
dataPoints.forEach((point, i) => {
|
|
1256
|
+
const trendValue = slope * i + intercept;
|
|
1257
|
+
trendData.push({ x: point.x, y: yScale(trendValue) });
|
|
1258
|
+
});
|
|
1259
|
+
} else if (trendLineType === "moving-average") {
|
|
1260
|
+
// Moving average with configurable window
|
|
1261
|
+
const window = trendLineWindow;
|
|
1262
|
+
for (let i = 0; i < dataPoints.length; i++) {
|
|
1263
|
+
const start = Math.max(0, i - Math.floor(window / 2));
|
|
1264
|
+
const end = Math.min(dataPoints.length, start + window);
|
|
1265
|
+
const windowData = dataPoints.slice(start, end);
|
|
1266
|
+
const average = windowData.reduce((sum, p) => sum + p.value, 0) / windowData.length;
|
|
1267
|
+
trendData.push({ x: dataPoints[i].x, y: yScale(average) });
|
|
1268
|
+
}
|
|
1269
|
+
} else if (trendLineType === "polynomial") {
|
|
1270
|
+
// Simplified polynomial trend using D3's curve interpolation
|
|
1271
|
+
const n = dataPoints.length;
|
|
1272
|
+
|
|
1273
|
+
if (n >= 3) {
|
|
1274
|
+
// Use a simple approach: create a smooth curve using D3's cardinal interpolation
|
|
1275
|
+
// and add some curvature based on the degree setting
|
|
1276
|
+
const curvature = trendLineDegree / 10; // Convert degree to curvature factor
|
|
1277
|
+
|
|
1278
|
+
// Create control points for polynomial-like curve
|
|
1279
|
+
for (let i = 0; i < n; i++) {
|
|
1280
|
+
const point = dataPoints[i];
|
|
1281
|
+
let adjustedY = point.value;
|
|
1282
|
+
|
|
1283
|
+
// Add polynomial-like adjustment based on position
|
|
1284
|
+
if (n > 2) {
|
|
1285
|
+
const t = i / (n - 1); // Normalize position 0-1
|
|
1286
|
+
const mid = 0.5;
|
|
1287
|
+
|
|
1288
|
+
// Create a curved adjustment based on distance from middle
|
|
1289
|
+
const distFromMid = Math.abs(t - mid);
|
|
1290
|
+
const curveFactor = Math.sin(t * Math.PI) * curvature;
|
|
1291
|
+
|
|
1292
|
+
// Apply curve adjustment to create polynomial-like behavior
|
|
1293
|
+
const avgValue = dataPoints.reduce((sum, p) => sum + p.value, 0) / n;
|
|
1294
|
+
adjustedY = point.value + (point.value - avgValue) * curveFactor * 0.5;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
trendData.push({ x: point.x, y: yScale(adjustedY) });
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
// Not enough points for polynomial, use linear
|
|
1301
|
+
dataPoints.forEach(point => {
|
|
1302
|
+
trendData.push({ x: point.x, y: point.y });
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
// Default to connecting actual points
|
|
1307
|
+
dataPoints.forEach(point => {
|
|
1308
|
+
trendData.push({ x: point.x, y: point.y });
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Create line generator with appropriate curve type
|
|
1313
|
+
const line = d3.line<{ x: number; y: number }>()
|
|
1314
|
+
.x(d => d.x)
|
|
1315
|
+
.y(d => d.y)
|
|
1316
|
+
.curve(trendLineType === "polynomial" ? d3.curveCardinal :
|
|
1317
|
+
trendLineType === "moving-average" ? d3.curveMonotoneX :
|
|
1318
|
+
d3.curveLinear);
|
|
1319
|
+
|
|
1320
|
+
// Create/update trend line
|
|
1321
|
+
const trendLine = trendGroupUpdate.selectAll(".trend-line").data([trendData]);
|
|
1322
|
+
|
|
1323
|
+
const trendLineEnter = trendLine.enter()
|
|
1324
|
+
.append("path")
|
|
1325
|
+
.attr("class", "trend-line")
|
|
1326
|
+
.attr("fill", "none")
|
|
1327
|
+
.attr("stroke", trendLineColor)
|
|
1328
|
+
.attr("stroke-width", trendLineWidth)
|
|
1329
|
+
.attr("stroke-opacity", trendLineOpacity)
|
|
1330
|
+
.style("opacity", 0);
|
|
1331
|
+
|
|
1332
|
+
// Apply stroke-dasharray based on style
|
|
1333
|
+
function applyStrokeStyle(selection: any) {
|
|
1334
|
+
if (trendLineStyle === "dashed") {
|
|
1335
|
+
selection.attr("stroke-dasharray", "5,5");
|
|
1336
|
+
} else if (trendLineStyle === "dotted") {
|
|
1337
|
+
selection.attr("stroke-dasharray", "2,3");
|
|
1338
|
+
} else {
|
|
1339
|
+
selection.attr("stroke-dasharray", null);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
applyStrokeStyle(trendLineEnter);
|
|
1344
|
+
|
|
1345
|
+
const updatedTrendLine = trendLineEnter.merge(trendLine);
|
|
1346
|
+
applyStrokeStyle(updatedTrendLine);
|
|
1347
|
+
|
|
1348
|
+
updatedTrendLine
|
|
1349
|
+
.transition()
|
|
1350
|
+
.duration(duration)
|
|
1351
|
+
.ease(ease)
|
|
1352
|
+
.attr("d", line)
|
|
1353
|
+
.attr("stroke", trendLineColor)
|
|
1354
|
+
.attr("stroke-width", trendLineWidth)
|
|
1355
|
+
.attr("stroke-opacity", trendLineOpacity)
|
|
1356
|
+
.style("opacity", 1);
|
|
1357
|
+
|
|
1358
|
+
trendLine.exit()
|
|
1359
|
+
.transition()
|
|
1360
|
+
.duration(duration)
|
|
1361
|
+
.ease(ease)
|
|
1362
|
+
.style("opacity", 0)
|
|
1363
|
+
.remove();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function addBrushSelection(container: any, processedData: ProcessedData[], xScale: any, yScale: any): void {
|
|
1367
|
+
// Stub: would add brush interaction if enabled
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function initializeAccessibility(svg: any, processedData: ProcessedData[]): void {
|
|
1371
|
+
if (!enableAccessibility) return;
|
|
1372
|
+
|
|
1373
|
+
// Add ARIA attributes to the SVG
|
|
1374
|
+
svg.attr("role", "img")
|
|
1375
|
+
.attr("aria-label", `Waterfall chart with ${processedData.length} data points`);
|
|
1376
|
+
|
|
1377
|
+
// Add title and description for screen readers
|
|
1378
|
+
const title = svg.selectAll("title").data([0]);
|
|
1379
|
+
title.enter()
|
|
1380
|
+
.append("title")
|
|
1381
|
+
.merge(title)
|
|
1382
|
+
.text(`Waterfall chart showing ${stacked ? 'stacked' : 'sequential'} data visualization`);
|
|
1383
|
+
|
|
1384
|
+
const desc = svg.selectAll("desc").data([0]);
|
|
1385
|
+
desc.enter()
|
|
1386
|
+
.append("desc")
|
|
1387
|
+
.merge(desc)
|
|
1388
|
+
.text(() => {
|
|
1389
|
+
const totalValue = processedData[processedData.length - 1]?.cumulativeTotal || 0;
|
|
1390
|
+
return `Chart contains ${processedData.length} data points. ` +
|
|
1391
|
+
`Final cumulative value: ${formatNumber(totalValue)}. ` +
|
|
1392
|
+
`Data ranges from ${processedData[0]?.label} to ${processedData[processedData.length - 1]?.label}.`;
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// Add keyboard navigation
|
|
1396
|
+
svg.attr("tabindex", "0")
|
|
1397
|
+
.on("keydown", function(event: KeyboardEvent) {
|
|
1398
|
+
const focusedElement = svg.select(".focused");
|
|
1399
|
+
const allBars = svg.selectAll(".waterfall-bar, .stack");
|
|
1400
|
+
const currentIndex = allBars.nodes().indexOf(focusedElement.node());
|
|
1401
|
+
|
|
1402
|
+
switch(event.key) {
|
|
1403
|
+
case "ArrowRight":
|
|
1404
|
+
case "ArrowDown":
|
|
1405
|
+
event.preventDefault();
|
|
1406
|
+
const nextIndex = Math.min(currentIndex + 1, allBars.size() - 1);
|
|
1407
|
+
focusBar(allBars, nextIndex);
|
|
1408
|
+
break;
|
|
1409
|
+
case "ArrowLeft":
|
|
1410
|
+
case "ArrowUp":
|
|
1411
|
+
event.preventDefault();
|
|
1412
|
+
const prevIndex = Math.max(currentIndex - 1, 0);
|
|
1413
|
+
focusBar(allBars, prevIndex);
|
|
1414
|
+
break;
|
|
1415
|
+
case "Home":
|
|
1416
|
+
event.preventDefault();
|
|
1417
|
+
focusBar(allBars, 0);
|
|
1418
|
+
break;
|
|
1419
|
+
case "End":
|
|
1420
|
+
event.preventDefault();
|
|
1421
|
+
focusBar(allBars, allBars.size() - 1);
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Helper function to focus a bar
|
|
1427
|
+
function focusBar(allBars: any, index: number) {
|
|
1428
|
+
allBars.classed("focused", false);
|
|
1429
|
+
const targetBar = d3.select(allBars.nodes()[index]);
|
|
1430
|
+
targetBar.classed("focused", true);
|
|
1431
|
+
|
|
1432
|
+
// Announce the focused element
|
|
1433
|
+
const data = targetBar.datum() as any;
|
|
1434
|
+
const announcement = `${data.parent?.label || data.label}: ${formatNumber(data.value || data.barTotal)}`;
|
|
1435
|
+
announceToScreenReader(announcement);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Screen reader announcements
|
|
1439
|
+
function announceToScreenReader(message: string) {
|
|
1440
|
+
const announcement = d3.select("body").selectAll(".sr-announcement").data([0]);
|
|
1441
|
+
const announcementEnter = announcement.enter()
|
|
1442
|
+
.append("div")
|
|
1443
|
+
.attr("class", "sr-announcement")
|
|
1444
|
+
.attr("aria-live", "polite")
|
|
1445
|
+
.attr("aria-atomic", "true")
|
|
1446
|
+
.style("position", "absolute")
|
|
1447
|
+
.style("left", "-10000px")
|
|
1448
|
+
.style("width", "1px")
|
|
1449
|
+
.style("height", "1px")
|
|
1450
|
+
.style("overflow", "hidden");
|
|
1451
|
+
|
|
1452
|
+
(announcementEnter as any).merge(announcement)
|
|
1453
|
+
.text(message);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Add focus styles
|
|
1457
|
+
const style = svg.selectAll("style.accessibility-styles").data([0]);
|
|
1458
|
+
style.enter()
|
|
1459
|
+
.append("style")
|
|
1460
|
+
.attr("class", "accessibility-styles")
|
|
1461
|
+
.merge(style)
|
|
1462
|
+
.text(`
|
|
1463
|
+
.focused {
|
|
1464
|
+
stroke: #0066cc !important;
|
|
1465
|
+
stroke-width: 3px !important;
|
|
1466
|
+
filter: brightness(1.1);
|
|
1467
|
+
}
|
|
1468
|
+
.waterfall-bar:focus,
|
|
1469
|
+
.stack:focus {
|
|
1470
|
+
outline: 2px solid #0066cc;
|
|
1471
|
+
outline-offset: 2px;
|
|
1472
|
+
}
|
|
1473
|
+
`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function initializeTooltips(svg: any): void {
|
|
1477
|
+
if (!enableTooltips) return;
|
|
1478
|
+
|
|
1479
|
+
// Initialize the tooltip system
|
|
1480
|
+
const tooltip = tooltipSystem;
|
|
1481
|
+
|
|
1482
|
+
// Configure tooltip theme
|
|
1483
|
+
tooltip.configure(tooltipConfig);
|
|
1484
|
+
|
|
1485
|
+
// Add tooltip events to all chart elements
|
|
1486
|
+
svg.selectAll(".waterfall-bar, .stack")
|
|
1487
|
+
.on("mouseover", function(this: SVGElement, event: MouseEvent, d: any) {
|
|
1488
|
+
const element = d3.select(this);
|
|
1489
|
+
const data = d.parent || d; // Handle both stacked and waterfall bars
|
|
1490
|
+
|
|
1491
|
+
// Create tooltip content
|
|
1492
|
+
const content = `
|
|
1493
|
+
<div style="font-weight: bold; margin-bottom: 8px;">${data.label}</div>
|
|
1494
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
|
1495
|
+
<span>Value:</span>
|
|
1496
|
+
<span style="font-weight: bold;">${formatNumber(d.value || data.barTotal)}</span>
|
|
1497
|
+
</div>
|
|
1498
|
+
${data.cumulativeTotal !== undefined ? `
|
|
1499
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
|
1500
|
+
<span>Cumulative:</span>
|
|
1501
|
+
<span style="font-weight: bold;">${formatNumber(data.cumulativeTotal)}</span>
|
|
1502
|
+
</div>
|
|
1503
|
+
` : ''}
|
|
1504
|
+
${d.label && d.label !== data.label ? `
|
|
1505
|
+
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.3);">
|
|
1506
|
+
<div style="font-size: 11px; opacity: 0.8;">${d.label}</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
` : ''}
|
|
1509
|
+
`;
|
|
1510
|
+
|
|
1511
|
+
// Show tooltip
|
|
1512
|
+
tooltip.show(content, event, {
|
|
1513
|
+
label: data.label,
|
|
1514
|
+
value: d.value || data.barTotal,
|
|
1515
|
+
cumulative: data.cumulativeTotal,
|
|
1516
|
+
color: d.color || (data.stacks && data.stacks[0] ? data.stacks[0].color : '#3498db'),
|
|
1517
|
+
x: parseFloat(element.attr("x") || "0"),
|
|
1518
|
+
y: parseFloat(element.attr("y") || "0"),
|
|
1519
|
+
quadrant: 1
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
// Highlight element
|
|
1523
|
+
element.style("opacity", 0.8);
|
|
1524
|
+
})
|
|
1525
|
+
.on("mousemove", function(this: SVGElement, event: MouseEvent) {
|
|
1526
|
+
tooltip.move(event);
|
|
1527
|
+
})
|
|
1528
|
+
.on("mouseout", function(this: SVGElement) {
|
|
1529
|
+
// Hide tooltip
|
|
1530
|
+
tooltip.hide();
|
|
1531
|
+
|
|
1532
|
+
// Remove highlight
|
|
1533
|
+
d3.select(this).style("opacity", null);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
// Also add tooltips to value labels
|
|
1537
|
+
svg.selectAll(".total-label")
|
|
1538
|
+
.on("mouseover", function(this: SVGElement, event: MouseEvent, d: any) {
|
|
1539
|
+
const data = d.parent;
|
|
1540
|
+
|
|
1541
|
+
const content = `
|
|
1542
|
+
<div style="font-weight: bold; margin-bottom: 8px;">${data.label}</div>
|
|
1543
|
+
<div style="display: flex; justify-content: space-between;">
|
|
1544
|
+
<span>Total Value:</span>
|
|
1545
|
+
<span style="font-weight: bold;">${formatNumber(data.barTotal)}</span>
|
|
1546
|
+
</div>
|
|
1547
|
+
`;
|
|
1548
|
+
|
|
1549
|
+
tooltip.show(content, event, {
|
|
1550
|
+
label: data.label,
|
|
1551
|
+
value: data.barTotal,
|
|
1552
|
+
cumulative: data.cumulativeTotal,
|
|
1553
|
+
color: '#333',
|
|
1554
|
+
x: 0,
|
|
1555
|
+
y: 0,
|
|
1556
|
+
quadrant: 1
|
|
1557
|
+
});
|
|
1558
|
+
})
|
|
1559
|
+
.on("mousemove", function(this: SVGElement, event: MouseEvent) {
|
|
1560
|
+
tooltip.move(event);
|
|
1561
|
+
})
|
|
1562
|
+
.on("mouseout", function(this: SVGElement) {
|
|
1563
|
+
tooltip.hide();
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function initializeExport(svg: any, processedData: ProcessedData[]): void {
|
|
1568
|
+
// Stub: would initialize export functionality
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function initializeZoom(svgContainer: any, config: { width: number; height: number; margin: MarginConfig }): void {
|
|
1572
|
+
// Stub: would initialize zoom functionality
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Getter/setter methods using TypeScript
|
|
1576
|
+
chart.width = function(_?: number): number | WaterfallChart {
|
|
1577
|
+
return arguments.length ? (width = _!, chart) : width;
|
|
1578
|
+
} as any;
|
|
1579
|
+
|
|
1580
|
+
chart.height = function(_?: number): number | WaterfallChart {
|
|
1581
|
+
return arguments.length ? (height = _!, chart) : height;
|
|
1582
|
+
} as any;
|
|
1583
|
+
|
|
1584
|
+
chart.margin = function(_?: MarginConfig): MarginConfig | WaterfallChart {
|
|
1585
|
+
return arguments.length ? (margin = _!, chart) : margin;
|
|
1586
|
+
} as any;
|
|
1587
|
+
|
|
1588
|
+
chart.stacked = function(_?: boolean): boolean | WaterfallChart {
|
|
1589
|
+
return arguments.length ? (stacked = _!, chart) : stacked;
|
|
1590
|
+
} as any;
|
|
1591
|
+
|
|
1592
|
+
chart.showTotal = function(_?: boolean): boolean | WaterfallChart {
|
|
1593
|
+
return arguments.length ? (showTotal = _!, chart) : showTotal;
|
|
1594
|
+
} as any;
|
|
1595
|
+
|
|
1596
|
+
chart.totalLabel = function(_?: string): string | WaterfallChart {
|
|
1597
|
+
return arguments.length ? (totalLabel = _!, chart) : totalLabel;
|
|
1598
|
+
} as any;
|
|
1599
|
+
|
|
1600
|
+
chart.totalColor = function(_?: string): string | WaterfallChart {
|
|
1601
|
+
return arguments.length ? (totalColor = _!, chart) : totalColor;
|
|
1602
|
+
} as any;
|
|
1603
|
+
|
|
1604
|
+
chart.barPadding = function(_?: number): number | WaterfallChart {
|
|
1605
|
+
return arguments.length ? (barPadding = _!, chart) : barPadding;
|
|
1606
|
+
} as any;
|
|
1607
|
+
|
|
1608
|
+
chart.duration = function(_?: number): number | WaterfallChart {
|
|
1609
|
+
return arguments.length ? (duration = _!, chart) : duration;
|
|
1610
|
+
} as any;
|
|
1611
|
+
|
|
1612
|
+
chart.ease = function(_?: (t: number) => number): ((t: number) => number) | WaterfallChart {
|
|
1613
|
+
return arguments.length ? (ease = _!, chart) : ease;
|
|
1614
|
+
} as any;
|
|
1615
|
+
|
|
1616
|
+
chart.formatNumber = function(_?: (n: number) => string): ((n: number) => string) | WaterfallChart {
|
|
1617
|
+
return arguments.length ? (formatNumber = _!, chart) : formatNumber;
|
|
1618
|
+
} as any;
|
|
1619
|
+
|
|
1620
|
+
chart.theme = function(_?: string | null): (string | null) | WaterfallChart {
|
|
1621
|
+
return arguments.length ? (theme = _!, chart) : theme;
|
|
1622
|
+
} as any;
|
|
1623
|
+
|
|
1624
|
+
chart.enableBrush = function(_?: boolean): boolean | WaterfallChart {
|
|
1625
|
+
return arguments.length ? (enableBrush = _!, chart) : enableBrush;
|
|
1626
|
+
} as any;
|
|
1627
|
+
|
|
1628
|
+
chart.brushOptions = function(_?: BrushOptions): BrushOptions | WaterfallChart {
|
|
1629
|
+
return arguments.length ? (brushOptions = { ...brushOptions, ..._! }, chart) : brushOptions;
|
|
1630
|
+
} as any;
|
|
1631
|
+
|
|
1632
|
+
chart.staggeredAnimations = function(_?: boolean): boolean | WaterfallChart {
|
|
1633
|
+
return arguments.length ? (staggeredAnimations = _!, chart) : staggeredAnimations;
|
|
1634
|
+
} as any;
|
|
1635
|
+
|
|
1636
|
+
chart.staggerDelay = function(_?: number): number | WaterfallChart {
|
|
1637
|
+
return arguments.length ? (staggerDelay = _!, chart) : staggerDelay;
|
|
1638
|
+
} as any;
|
|
1639
|
+
|
|
1640
|
+
chart.scaleType = function(_?: string): string | WaterfallChart {
|
|
1641
|
+
return arguments.length ? (scaleType = _!, chart) : scaleType;
|
|
1642
|
+
} as any;
|
|
1643
|
+
|
|
1644
|
+
chart.showTrendLine = function(_?: boolean): boolean | WaterfallChart {
|
|
1645
|
+
return arguments.length ? (showTrendLine = _!, chart) : showTrendLine;
|
|
1646
|
+
} as any;
|
|
1647
|
+
|
|
1648
|
+
chart.trendLineColor = function(_?: string): string | WaterfallChart {
|
|
1649
|
+
return arguments.length ? (trendLineColor = _!, chart) : trendLineColor;
|
|
1650
|
+
} as any;
|
|
1651
|
+
|
|
1652
|
+
chart.trendLineWidth = function(_?: number): number | WaterfallChart {
|
|
1653
|
+
return arguments.length ? (trendLineWidth = _!, chart) : trendLineWidth;
|
|
1654
|
+
} as any;
|
|
1655
|
+
|
|
1656
|
+
chart.trendLineStyle = function(_?: string): string | WaterfallChart {
|
|
1657
|
+
return arguments.length ? (trendLineStyle = _!, chart) : trendLineStyle;
|
|
1658
|
+
} as any;
|
|
1659
|
+
|
|
1660
|
+
chart.trendLineOpacity = function(_?: number): number | WaterfallChart {
|
|
1661
|
+
return arguments.length ? (trendLineOpacity = _!, chart) : trendLineOpacity;
|
|
1662
|
+
} as any;
|
|
1663
|
+
|
|
1664
|
+
chart.trendLineType = function(_?: string): string | WaterfallChart {
|
|
1665
|
+
return arguments.length ? (trendLineType = _!, chart) : trendLineType;
|
|
1666
|
+
} as any;
|
|
1667
|
+
|
|
1668
|
+
chart.trendLineWindow = function(_?: number): number | WaterfallChart {
|
|
1669
|
+
return arguments.length ? (trendLineWindow = _!, chart) : trendLineWindow;
|
|
1670
|
+
} as any;
|
|
1671
|
+
|
|
1672
|
+
chart.trendLineDegree = function(_?: number): number | WaterfallChart {
|
|
1673
|
+
return arguments.length ? (trendLineDegree = _!, chart) : trendLineDegree;
|
|
1674
|
+
} as any;
|
|
1675
|
+
|
|
1676
|
+
chart.enableAccessibility = function(_?: boolean): boolean | WaterfallChart {
|
|
1677
|
+
return arguments.length ? (enableAccessibility = _!, chart) : enableAccessibility;
|
|
1678
|
+
} as any;
|
|
1679
|
+
|
|
1680
|
+
chart.enableTooltips = function(_?: boolean): boolean | WaterfallChart {
|
|
1681
|
+
return arguments.length ? (enableTooltips = _!, chart) : enableTooltips;
|
|
1682
|
+
} as any;
|
|
1683
|
+
|
|
1684
|
+
chart.tooltipConfig = function(_?: TooltipConfig): TooltipConfig | WaterfallChart {
|
|
1685
|
+
return arguments.length ? (tooltipConfig = { ...tooltipConfig, ..._ }, chart) : tooltipConfig;
|
|
1686
|
+
} as any;
|
|
1687
|
+
|
|
1688
|
+
chart.enableExport = function(_?: boolean): boolean | WaterfallChart {
|
|
1689
|
+
return arguments.length ? (enableExport = _!, chart) : enableExport;
|
|
1690
|
+
} as any;
|
|
1691
|
+
|
|
1692
|
+
chart.exportConfig = function(_?: ExportConfig): ExportConfig | WaterfallChart {
|
|
1693
|
+
return arguments.length ? (exportConfig = { ...exportConfig, ..._ }, chart) : exportConfig;
|
|
1694
|
+
} as any;
|
|
1695
|
+
|
|
1696
|
+
chart.enableZoom = function(_?: boolean): boolean | WaterfallChart {
|
|
1697
|
+
return arguments.length ? (enableZoom = _!, chart) : enableZoom;
|
|
1698
|
+
} as any;
|
|
1699
|
+
|
|
1700
|
+
chart.zoomConfig = function(_?: ZoomConfig): ZoomConfig | WaterfallChart {
|
|
1701
|
+
return arguments.length ? (zoomConfig = { ...zoomConfig, ..._ }, chart) : zoomConfig;
|
|
1702
|
+
} as any;
|
|
1703
|
+
|
|
1704
|
+
chart.breakdownConfig = function(_?: BreakdownConfig | null): (BreakdownConfig | null) | WaterfallChart {
|
|
1705
|
+
return arguments.length ? (breakdownConfig = _!, chart) : breakdownConfig;
|
|
1706
|
+
} as any;
|
|
1707
|
+
|
|
1708
|
+
chart.enablePerformanceOptimization = function(_?: boolean): boolean | WaterfallChart {
|
|
1709
|
+
return arguments.length ? (enablePerformanceOptimization = _!, chart) : enablePerformanceOptimization;
|
|
1710
|
+
} as any;
|
|
1711
|
+
|
|
1712
|
+
chart.performanceDashboard = function(_?: boolean): boolean | WaterfallChart {
|
|
1713
|
+
return arguments.length ? (performanceDashboard = _!, chart) : performanceDashboard;
|
|
1714
|
+
} as any;
|
|
1715
|
+
|
|
1716
|
+
chart.virtualizationThreshold = function(_?: number): number | WaterfallChart {
|
|
1717
|
+
return arguments.length ? (virtualizationThreshold = _!, chart) : virtualizationThreshold;
|
|
1718
|
+
} as any;
|
|
1719
|
+
|
|
1720
|
+
// Data method for API completeness
|
|
1721
|
+
chart.data = function(_?: ChartData[]): ChartData[] | WaterfallChart {
|
|
1722
|
+
// This method is for API completeness - actual data is passed to the chart function
|
|
1723
|
+
// Always return the chart instance for method chaining
|
|
1724
|
+
return chart;
|
|
1725
|
+
} as any;
|
|
1726
|
+
|
|
1727
|
+
// NEW: Advanced color and shape feature methods
|
|
1728
|
+
chart.advancedColors = function(_?: AdvancedColorConfig): AdvancedColorConfig | WaterfallChart {
|
|
1729
|
+
return arguments.length ? (advancedColorConfig = { ...advancedColorConfig, ..._! }, chart) : advancedColorConfig;
|
|
1730
|
+
} as any;
|
|
1731
|
+
|
|
1732
|
+
chart.enableAdvancedColors = function(_?: boolean): boolean | WaterfallChart {
|
|
1733
|
+
return arguments.length ? (advancedColorConfig.enabled = _!, chart) : advancedColorConfig.enabled;
|
|
1734
|
+
} as any;
|
|
1735
|
+
|
|
1736
|
+
chart.colorScaleType = function(_?: 'auto' | 'sequential' | 'diverging' | 'conditional'): 'auto' | 'sequential' | 'diverging' | 'conditional' | WaterfallChart {
|
|
1737
|
+
return arguments.length ? (advancedColorConfig.scaleType = _!, chart) : advancedColorConfig.scaleType;
|
|
1738
|
+
} as any;
|
|
1739
|
+
|
|
1740
|
+
// NEW: Additional advanced color methods
|
|
1741
|
+
chart.colorMode = function(_?: 'default' | 'conditional' | 'sequential' | 'diverging'): 'default' | 'conditional' | 'sequential' | 'diverging' | WaterfallChart {
|
|
1742
|
+
return arguments.length ? (colorMode = _!, chart) : colorMode;
|
|
1743
|
+
} as any;
|
|
1744
|
+
|
|
1745
|
+
chart.colorTheme = function(_?: string): string | WaterfallChart {
|
|
1746
|
+
return arguments.length ? (advancedColorConfig.themeName = _!, chart) : (advancedColorConfig.themeName || 'default');
|
|
1747
|
+
} as any;
|
|
1748
|
+
|
|
1749
|
+
chart.neutralThreshold = function(_?: number): number | WaterfallChart {
|
|
1750
|
+
return arguments.length ? (advancedColorConfig.neutralThreshold = _!, chart) : (advancedColorConfig.neutralThreshold || 0);
|
|
1751
|
+
} as any;
|
|
1752
|
+
|
|
1753
|
+
chart.confidenceBands = function(_?: ConfidenceBandConfig): ConfidenceBandConfig | WaterfallChart {
|
|
1754
|
+
return arguments.length ? (confidenceBandConfig = { ...confidenceBandConfig, ..._! }, chart) : confidenceBandConfig;
|
|
1755
|
+
} as any;
|
|
1756
|
+
|
|
1757
|
+
chart.enableConfidenceBands = function(_?: boolean): boolean | WaterfallChart {
|
|
1758
|
+
return arguments.length ? (confidenceBandConfig.enabled = _!, chart) : confidenceBandConfig.enabled;
|
|
1759
|
+
} as any;
|
|
1760
|
+
|
|
1761
|
+
chart.milestones = function(_?: MilestoneConfig): MilestoneConfig | WaterfallChart {
|
|
1762
|
+
return arguments.length ? (milestoneConfig = { ...milestoneConfig, ..._! }, chart) : milestoneConfig;
|
|
1763
|
+
} as any;
|
|
1764
|
+
|
|
1765
|
+
chart.enableMilestones = function(_?: boolean): boolean | WaterfallChart {
|
|
1766
|
+
return arguments.length ? (milestoneConfig.enabled = _!, chart) : milestoneConfig.enabled;
|
|
1767
|
+
} as any;
|
|
1768
|
+
|
|
1769
|
+
chart.addMilestone = function(milestone: {label: string, value: number, type: 'target' | 'threshold' | 'alert' | 'achievement', description?: string}): WaterfallChart {
|
|
1770
|
+
milestoneConfig.milestones.push(milestone);
|
|
1771
|
+
return chart;
|
|
1772
|
+
} as any;
|
|
1773
|
+
|
|
1774
|
+
// Event handling methods
|
|
1775
|
+
chart.on = function(): any {
|
|
1776
|
+
const value = (listeners.on as any).apply(listeners, Array.from(arguments));
|
|
1777
|
+
return value === listeners ? chart : value;
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
// NEW: Advanced feature rendering functions
|
|
1781
|
+
function drawConfidenceBands(container: any, processedData: ProcessedData[], xScale: any, yScale: any): void {
|
|
1782
|
+
if (!confidenceBandConfig.enabled || !confidenceBandConfig.scenarios) return;
|
|
1783
|
+
|
|
1784
|
+
// Create confidence bands group
|
|
1785
|
+
const confidenceGroup = container.selectAll(".confidence-bands-group").data([0]);
|
|
1786
|
+
const confidenceGroupEnter = confidenceGroup.enter()
|
|
1787
|
+
.append("g")
|
|
1788
|
+
.attr("class", "confidence-bands-group");
|
|
1789
|
+
|
|
1790
|
+
const confidenceGroupUpdate = confidenceGroupEnter.merge(confidenceGroup);
|
|
1791
|
+
|
|
1792
|
+
// Generate confidence band data using the waterfall-specific utility
|
|
1793
|
+
const confidenceBandData = createWaterfallConfidenceBands(
|
|
1794
|
+
processedData.map(d => ({ label: d.label, value: d.barTotal })),
|
|
1795
|
+
confidenceBandConfig.scenarios,
|
|
1796
|
+
xScale,
|
|
1797
|
+
yScale
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
// Render confidence band
|
|
1801
|
+
const confidencePath = confidenceGroupUpdate.selectAll(".confidence-band").data([confidenceBandData.confidencePath]);
|
|
1802
|
+
|
|
1803
|
+
const confidencePathEnter = confidencePath.enter()
|
|
1804
|
+
.append("path")
|
|
1805
|
+
.attr("class", "confidence-band")
|
|
1806
|
+
.attr("fill", `rgba(52, 152, 219, ${confidenceBandConfig.opacity || 0.3})`)
|
|
1807
|
+
.attr("stroke", "none")
|
|
1808
|
+
.style("opacity", 0);
|
|
1809
|
+
|
|
1810
|
+
confidencePathEnter.merge(confidencePath)
|
|
1811
|
+
.transition()
|
|
1812
|
+
.duration(duration)
|
|
1813
|
+
.ease(ease)
|
|
1814
|
+
.attr("d", confidenceBandData.confidencePath)
|
|
1815
|
+
.style("opacity", 1);
|
|
1816
|
+
|
|
1817
|
+
// Render trend lines if enabled
|
|
1818
|
+
if (confidenceBandConfig.showTrendLines) {
|
|
1819
|
+
// Optimistic trend line
|
|
1820
|
+
const optimisticPath = confidenceGroupUpdate.selectAll(".optimistic-trend").data([confidenceBandData.optimisticPath]);
|
|
1821
|
+
|
|
1822
|
+
const optimisticPathEnter = optimisticPath.enter()
|
|
1823
|
+
.append("path")
|
|
1824
|
+
.attr("class", "optimistic-trend")
|
|
1825
|
+
.attr("fill", "none")
|
|
1826
|
+
.attr("stroke", "#27ae60")
|
|
1827
|
+
.attr("stroke-width", 2)
|
|
1828
|
+
.attr("stroke-dasharray", "5,5")
|
|
1829
|
+
.style("opacity", 0);
|
|
1830
|
+
|
|
1831
|
+
optimisticPathEnter.merge(optimisticPath)
|
|
1832
|
+
.transition()
|
|
1833
|
+
.duration(duration)
|
|
1834
|
+
.ease(ease)
|
|
1835
|
+
.attr("d", confidenceBandData.optimisticPath)
|
|
1836
|
+
.style("opacity", 0.8);
|
|
1837
|
+
|
|
1838
|
+
// Pessimistic trend line
|
|
1839
|
+
const pessimisticPath = confidenceGroupUpdate.selectAll(".pessimistic-trend").data([confidenceBandData.pessimisticPath]);
|
|
1840
|
+
|
|
1841
|
+
const pessimisticPathEnter = pessimisticPath.enter()
|
|
1842
|
+
.append("path")
|
|
1843
|
+
.attr("class", "pessimistic-trend")
|
|
1844
|
+
.attr("fill", "none")
|
|
1845
|
+
.attr("stroke", "#e74c3c")
|
|
1846
|
+
.attr("stroke-width", 2)
|
|
1847
|
+
.attr("stroke-dasharray", "5,5")
|
|
1848
|
+
.style("opacity", 0);
|
|
1849
|
+
|
|
1850
|
+
pessimisticPathEnter.merge(pessimisticPath)
|
|
1851
|
+
.transition()
|
|
1852
|
+
.duration(duration)
|
|
1853
|
+
.ease(ease)
|
|
1854
|
+
.attr("d", confidenceBandData.pessimisticPath)
|
|
1855
|
+
.style("opacity", 0.8);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Remove old elements
|
|
1859
|
+
confidencePath.exit()
|
|
1860
|
+
.transition()
|
|
1861
|
+
.duration(duration)
|
|
1862
|
+
.style("opacity", 0)
|
|
1863
|
+
.remove();
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function drawMilestones(container: any, processedData: ProcessedData[], xScale: any, yScale: any): void {
|
|
1867
|
+
if (!milestoneConfig.enabled || milestoneConfig.milestones.length === 0) return;
|
|
1868
|
+
|
|
1869
|
+
// Create milestones group
|
|
1870
|
+
const milestonesGroup = container.selectAll(".milestones-group").data([0]);
|
|
1871
|
+
const milestonesGroupEnter = milestonesGroup.enter()
|
|
1872
|
+
.append("g")
|
|
1873
|
+
.attr("class", "milestones-group");
|
|
1874
|
+
|
|
1875
|
+
const milestonesGroupUpdate = milestonesGroupEnter.merge(milestonesGroup);
|
|
1876
|
+
|
|
1877
|
+
// Generate milestone markers using the waterfall-specific utility
|
|
1878
|
+
const milestoneMarkers = createWaterfallMilestones(
|
|
1879
|
+
milestoneConfig.milestones,
|
|
1880
|
+
xScale,
|
|
1881
|
+
yScale
|
|
1882
|
+
);
|
|
1883
|
+
|
|
1884
|
+
// Render milestone markers
|
|
1885
|
+
const markers = milestonesGroupUpdate.selectAll(".milestone-marker").data(milestoneMarkers);
|
|
1886
|
+
|
|
1887
|
+
const markersEnter = markers.enter()
|
|
1888
|
+
.append("path")
|
|
1889
|
+
.attr("class", "milestone-marker")
|
|
1890
|
+
.attr("transform", (d: any) => d.transform)
|
|
1891
|
+
.attr("d", (d: any) => d.path)
|
|
1892
|
+
.attr("fill", (d: any) => d.config.fillColor || "#f39c12")
|
|
1893
|
+
.attr("stroke", (d: any) => d.config.strokeColor || "#ffffff")
|
|
1894
|
+
.attr("stroke-width", (d: any) => d.config.strokeWidth || 2)
|
|
1895
|
+
.style("opacity", 0);
|
|
1896
|
+
|
|
1897
|
+
markersEnter.merge(markers)
|
|
1898
|
+
.transition()
|
|
1899
|
+
.duration(duration)
|
|
1900
|
+
.ease(ease)
|
|
1901
|
+
.attr("transform", (d: any) => d.transform)
|
|
1902
|
+
.attr("d", (d: any) => d.path)
|
|
1903
|
+
.attr("fill", (d: any) => d.config.fillColor || "#f39c12")
|
|
1904
|
+
.style("opacity", 1);
|
|
1905
|
+
|
|
1906
|
+
// Remove old markers
|
|
1907
|
+
markers.exit()
|
|
1908
|
+
.transition()
|
|
1909
|
+
.duration(duration)
|
|
1910
|
+
.style("opacity", 0)
|
|
1911
|
+
.remove();
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
return chart as WaterfallChart;
|
|
1915
|
+
}
|