mintwaterfall 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/CONTRIBUTING.md +199 -0
  3. package/README.md +363 -0
  4. package/dist/index.d.ts +149 -0
  5. package/dist/mintwaterfall.cjs.js +7978 -0
  6. package/dist/mintwaterfall.esm.js +7907 -0
  7. package/dist/mintwaterfall.min.js +7 -0
  8. package/dist/mintwaterfall.umd.js +7978 -0
  9. package/index.d.ts +149 -0
  10. package/package.json +126 -0
  11. package/src/enterprise/enterprise-core.js +0 -0
  12. package/src/enterprise/enterprise-feature-template.js +0 -0
  13. package/src/enterprise/feature-registry.js +0 -0
  14. package/src/enterprise/features/breakdown.js +0 -0
  15. package/src/features/breakdown.js +0 -0
  16. package/src/features/conditional-formatting.js +0 -0
  17. package/src/index.js +111 -0
  18. package/src/mintwaterfall-accessibility.ts +680 -0
  19. package/src/mintwaterfall-advanced-data.ts +1034 -0
  20. package/src/mintwaterfall-advanced-interactions.ts +649 -0
  21. package/src/mintwaterfall-advanced-performance.ts +582 -0
  22. package/src/mintwaterfall-animations.ts +595 -0
  23. package/src/mintwaterfall-brush.ts +471 -0
  24. package/src/mintwaterfall-chart-core.ts +296 -0
  25. package/src/mintwaterfall-chart.ts +1915 -0
  26. package/src/mintwaterfall-data.ts +1100 -0
  27. package/src/mintwaterfall-export.ts +475 -0
  28. package/src/mintwaterfall-hierarchical-layouts.ts +724 -0
  29. package/src/mintwaterfall-layouts.ts +647 -0
  30. package/src/mintwaterfall-performance.ts +573 -0
  31. package/src/mintwaterfall-scales.ts +437 -0
  32. package/src/mintwaterfall-shapes.ts +385 -0
  33. package/src/mintwaterfall-statistics.ts +821 -0
  34. package/src/mintwaterfall-themes.ts +391 -0
  35. package/src/mintwaterfall-tooltip.ts +450 -0
  36. package/src/mintwaterfall-zoom.ts +399 -0
  37. package/src/types/js-modules.d.ts +25 -0
  38. package/src/utils/compatibility-layer.js +0 -0
@@ -0,0 +1,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
+ }