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,649 @@
|
|
|
1
|
+
// MintWaterfall Advanced Interactions
|
|
2
|
+
// Sophisticated D3.js interaction capabilities for enhanced waterfall analysis
|
|
3
|
+
|
|
4
|
+
import * as d3 from 'd3';
|
|
5
|
+
import { drag } from 'd3-drag';
|
|
6
|
+
import { forceSimulation, forceCenter, forceCollide, forceManyBody } from 'd3-force';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// TYPE DEFINITIONS
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface DragConfig {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
axis: 'both' | 'horizontal' | 'vertical';
|
|
15
|
+
constraints?: {
|
|
16
|
+
minValue?: number;
|
|
17
|
+
maxValue?: number;
|
|
18
|
+
snapToGrid?: boolean;
|
|
19
|
+
gridSize?: number;
|
|
20
|
+
};
|
|
21
|
+
onDragStart?: (event: d3.D3DragEvent<any, any, any>, data: any) => void;
|
|
22
|
+
onDrag?: (event: d3.D3DragEvent<any, any, any>, data: any) => void;
|
|
23
|
+
onDragEnd?: (event: d3.D3DragEvent<any, any, any>, data: any) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface VoronoiConfig {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
extent: [[number, number], [number, number]];
|
|
29
|
+
showCells?: boolean;
|
|
30
|
+
cellOpacity?: number;
|
|
31
|
+
onCellEnter?: (event: MouseEvent, data: any) => void;
|
|
32
|
+
onCellLeave?: (event: MouseEvent, data: any) => void;
|
|
33
|
+
onCellClick?: (event: MouseEvent, data: any) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ForceSimulationConfig {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
forces: {
|
|
39
|
+
collision?: boolean;
|
|
40
|
+
centering?: boolean;
|
|
41
|
+
positioning?: boolean;
|
|
42
|
+
links?: boolean;
|
|
43
|
+
};
|
|
44
|
+
strength: {
|
|
45
|
+
collision?: number;
|
|
46
|
+
centering?: number;
|
|
47
|
+
positioning?: number;
|
|
48
|
+
links?: number;
|
|
49
|
+
};
|
|
50
|
+
duration?: number;
|
|
51
|
+
onTick?: (simulation: d3.Simulation<any, any>) => void;
|
|
52
|
+
onEnd?: (simulation: d3.Simulation<any, any>) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface InteractionSystem {
|
|
56
|
+
// Drag functionality
|
|
57
|
+
enableDrag(config: DragConfig): void;
|
|
58
|
+
disableDrag(): void;
|
|
59
|
+
updateDragConstraints(constraints: DragConfig['constraints']): void;
|
|
60
|
+
|
|
61
|
+
// Enhanced hover detection (simplified approach)
|
|
62
|
+
enableEnhancedHover(config: VoronoiConfig): void;
|
|
63
|
+
disableEnhancedHover(): void;
|
|
64
|
+
updateHoverExtent(extent: [[number, number], [number, number]]): void;
|
|
65
|
+
|
|
66
|
+
// Force simulation for dynamic layouts
|
|
67
|
+
startForceSimulation(config: ForceSimulationConfig): d3.Simulation<any, any>;
|
|
68
|
+
stopForceSimulation(): void;
|
|
69
|
+
updateForces(forces: ForceSimulationConfig['forces']): void;
|
|
70
|
+
|
|
71
|
+
// Combined interaction management
|
|
72
|
+
setInteractionMode(mode: 'drag' | 'voronoi' | 'force' | 'combined' | 'none'): void;
|
|
73
|
+
getActiveInteractions(): string[];
|
|
74
|
+
|
|
75
|
+
// Data management
|
|
76
|
+
updateData(data: any[]): void;
|
|
77
|
+
|
|
78
|
+
// Event management
|
|
79
|
+
on(event: string, callback: Function): void;
|
|
80
|
+
off(event: string): void;
|
|
81
|
+
trigger(event: string, data?: any): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// ADVANCED INTERACTION SYSTEM IMPLEMENTATION
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export function createAdvancedInteractionSystem(
|
|
89
|
+
container: d3.Selection<any, any, any, any>,
|
|
90
|
+
xScale: d3.ScaleLinear<number, number> | d3.ScaleBand<string>,
|
|
91
|
+
yScale: d3.ScaleLinear<number, number>
|
|
92
|
+
): InteractionSystem {
|
|
93
|
+
|
|
94
|
+
// Internal state
|
|
95
|
+
let dragBehavior: d3.DragBehavior<any, any, any> | null = null;
|
|
96
|
+
let enhancedHoverEnabled: boolean = false;
|
|
97
|
+
let currentSimulation: d3.Simulation<any, any> | null = null;
|
|
98
|
+
let currentData: any[] = [];
|
|
99
|
+
let eventListeners: Map<string, Function[]> = new Map();
|
|
100
|
+
|
|
101
|
+
// ========================================================================
|
|
102
|
+
// DRAG FUNCTIONALITY (d3.drag)
|
|
103
|
+
// ========================================================================
|
|
104
|
+
|
|
105
|
+
function enableDrag(config: DragConfig): void {
|
|
106
|
+
if (!config || !config.enabled) {
|
|
107
|
+
disableDrag();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create drag behavior
|
|
112
|
+
dragBehavior = drag<any, any>()
|
|
113
|
+
.on('start', (event, d) => {
|
|
114
|
+
// Visual feedback on drag start
|
|
115
|
+
d3.select(event.sourceEvent.target)
|
|
116
|
+
.raise()
|
|
117
|
+
.attr('stroke', '#ff6b6b')
|
|
118
|
+
.attr('stroke-width', 2);
|
|
119
|
+
|
|
120
|
+
if (config.onDragStart) {
|
|
121
|
+
config.onDragStart(event, d);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
trigger('dragStart', { event, data: d });
|
|
125
|
+
})
|
|
126
|
+
.on('drag', (event, d) => {
|
|
127
|
+
const bar = d3.select(event.sourceEvent.target);
|
|
128
|
+
let newValue = d.value || 0;
|
|
129
|
+
|
|
130
|
+
// Handle axis constraints
|
|
131
|
+
if (config.axis === 'vertical' || config.axis === 'both') {
|
|
132
|
+
const newY = event.y;
|
|
133
|
+
newValue = yScale.invert(newY);
|
|
134
|
+
|
|
135
|
+
// Apply constraints
|
|
136
|
+
if (config.constraints) {
|
|
137
|
+
const { minValue, maxValue, snapToGrid, gridSize } = config.constraints;
|
|
138
|
+
|
|
139
|
+
if (minValue !== undefined) newValue = Math.max(minValue, newValue);
|
|
140
|
+
if (maxValue !== undefined) newValue = Math.min(maxValue, newValue);
|
|
141
|
+
|
|
142
|
+
if (snapToGrid && gridSize) {
|
|
143
|
+
newValue = Math.round(newValue / gridSize) * gridSize;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update visual position
|
|
148
|
+
const barHeight = Math.abs(yScale(0) - yScale(newValue));
|
|
149
|
+
const barY = newValue >= 0 ? yScale(newValue) : yScale(0);
|
|
150
|
+
|
|
151
|
+
bar.attr('y', barY)
|
|
152
|
+
.attr('height', barHeight);
|
|
153
|
+
|
|
154
|
+
// Update data
|
|
155
|
+
d.value = newValue;
|
|
156
|
+
if (d.stacks && d.stacks.length > 0) {
|
|
157
|
+
d.stacks[0].value = newValue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle horizontal movement (for reordering)
|
|
162
|
+
if (config.axis === 'horizontal' || config.axis === 'both') {
|
|
163
|
+
const newX = event.x;
|
|
164
|
+
// Implementation for horizontal dragging/reordering
|
|
165
|
+
const barWidth = parseFloat(bar.attr('width') || '0');
|
|
166
|
+
bar.attr('transform', `translate(${newX - barWidth / 2}, 0)`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.onDrag) {
|
|
170
|
+
config.onDrag(event, d);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
trigger('drag', { event, data: d, newValue });
|
|
174
|
+
})
|
|
175
|
+
.on('end', (event, d) => {
|
|
176
|
+
// Remove visual feedback
|
|
177
|
+
d3.select(event.sourceEvent.target)
|
|
178
|
+
.attr('stroke', null)
|
|
179
|
+
.attr('stroke-width', null);
|
|
180
|
+
|
|
181
|
+
if (config.onDragEnd) {
|
|
182
|
+
config.onDragEnd(event, d);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
trigger('dragEnd', { event, data: d });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Apply drag behavior to all bars
|
|
189
|
+
container.selectAll('.bar')
|
|
190
|
+
.call(dragBehavior);
|
|
191
|
+
|
|
192
|
+
trigger('dragEnabled', config);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function disableDrag(): void {
|
|
196
|
+
if (dragBehavior) {
|
|
197
|
+
container.selectAll('.bar')
|
|
198
|
+
.on('.drag', null);
|
|
199
|
+
dragBehavior = null;
|
|
200
|
+
trigger('dragDisabled');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateDragConstraints(constraints: DragConfig['constraints']): void {
|
|
205
|
+
// Constraints are checked during drag events
|
|
206
|
+
trigger('dragConstraintsUpdated', constraints);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ========================================================================
|
|
210
|
+
// ENHANCED HOVER DETECTION (Simplified approach)
|
|
211
|
+
// ========================================================================
|
|
212
|
+
|
|
213
|
+
function enableEnhancedHover(config: VoronoiConfig): void {
|
|
214
|
+
if (!config || !config.enabled || currentData.length === 0) {
|
|
215
|
+
disableEnhancedHover();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
enhancedHoverEnabled = true;
|
|
220
|
+
|
|
221
|
+
// Create enhanced hover zones around bars
|
|
222
|
+
const hoverGroup = container.selectAll('.enhanced-hover-group')
|
|
223
|
+
.data([0]);
|
|
224
|
+
|
|
225
|
+
const hoverGroupEnter = hoverGroup.enter()
|
|
226
|
+
.append('g')
|
|
227
|
+
.attr('class', 'enhanced-hover-group');
|
|
228
|
+
|
|
229
|
+
const hoverGroupMerged = hoverGroupEnter.merge(hoverGroup as any);
|
|
230
|
+
|
|
231
|
+
// Add enhanced hover zones
|
|
232
|
+
const zones = hoverGroupMerged.selectAll('.hover-zone')
|
|
233
|
+
.data(currentData);
|
|
234
|
+
|
|
235
|
+
zones.enter()
|
|
236
|
+
.append('rect')
|
|
237
|
+
.attr('class', 'hover-zone')
|
|
238
|
+
.merge(zones as any)
|
|
239
|
+
.attr('x', d => getBarCenterX(d) - getBarWidth(d) * 0.75)
|
|
240
|
+
.attr('y', d => Math.min(getBarCenterY(d), yScale(0)) - 10)
|
|
241
|
+
.attr('width', d => getBarWidth(d) * 1.5)
|
|
242
|
+
.attr('height', d => Math.abs(yScale(0) - getBarCenterY(d)) + 20)
|
|
243
|
+
.style('fill', 'transparent')
|
|
244
|
+
.style('pointer-events', 'all')
|
|
245
|
+
.on('mouseenter', function(event, d) {
|
|
246
|
+
highlightBar(d);
|
|
247
|
+
if (config.onCellEnter) {
|
|
248
|
+
config.onCellEnter(event, d);
|
|
249
|
+
}
|
|
250
|
+
trigger('enhancedHoverEnter', { event, data: d });
|
|
251
|
+
})
|
|
252
|
+
.on('mouseleave', function(event, d) {
|
|
253
|
+
unhighlightBar(d);
|
|
254
|
+
if (config.onCellLeave) {
|
|
255
|
+
config.onCellLeave(event, d);
|
|
256
|
+
}
|
|
257
|
+
trigger('enhancedHoverLeave', { event, data: d });
|
|
258
|
+
})
|
|
259
|
+
.on('click', function(event, d) {
|
|
260
|
+
if (config.onCellClick) {
|
|
261
|
+
config.onCellClick(event, d);
|
|
262
|
+
}
|
|
263
|
+
trigger('enhancedHoverClick', { event, data: d });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
zones.exit().remove();
|
|
267
|
+
|
|
268
|
+
trigger('enhancedHoverEnabled', config);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function disableEnhancedHover(): void {
|
|
272
|
+
try {
|
|
273
|
+
// Only attempt to remove elements if container has proper D3 methods
|
|
274
|
+
if (container && container.selectAll && typeof container.selectAll === 'function') {
|
|
275
|
+
const selection = container.selectAll('.enhanced-hover-group');
|
|
276
|
+
if (selection && selection.remove && typeof selection.remove === 'function') {
|
|
277
|
+
selection.remove();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Silently handle any DOM manipulation errors in test environment
|
|
282
|
+
}
|
|
283
|
+
enhancedHoverEnabled = false;
|
|
284
|
+
trigger('enhancedHoverDisabled');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function updateHoverExtent(extent: [[number, number], [number, number]]): void {
|
|
288
|
+
if (enhancedHoverEnabled) {
|
|
289
|
+
// Re-enable with current data
|
|
290
|
+
const currentConfig = { enabled: true, extent };
|
|
291
|
+
enableEnhancedHover(currentConfig);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ========================================================================
|
|
296
|
+
// FORCE SIMULATION FOR DYNAMIC LAYOUTS (d3.forceSimulation)
|
|
297
|
+
// ========================================================================
|
|
298
|
+
|
|
299
|
+
function startForceSimulation(config: ForceSimulationConfig): d3.Simulation<any, any> {
|
|
300
|
+
if (!config || !config.enabled || currentData.length === 0) {
|
|
301
|
+
return forceSimulation<any, any>([]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Stop any existing simulation
|
|
305
|
+
stopForceSimulation();
|
|
306
|
+
|
|
307
|
+
// Create new simulation
|
|
308
|
+
currentSimulation = forceSimulation(currentData);
|
|
309
|
+
|
|
310
|
+
// Add forces based on configuration
|
|
311
|
+
if (config.forces.collision) {
|
|
312
|
+
currentSimulation.force('collision', forceCollide()
|
|
313
|
+
.radius(d => getBarWidth(d) / 2 + 5)
|
|
314
|
+
.strength(config.strength.collision || 0.7));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (config.forces.centering) {
|
|
318
|
+
const centerX = (xScale.range()[0] + xScale.range()[1]) / 2;
|
|
319
|
+
const centerY = (yScale.range()[0] + yScale.range()[1]) / 2;
|
|
320
|
+
currentSimulation.force('center', forceCenter(centerX, centerY)
|
|
321
|
+
.strength(config.strength.centering || 0.1));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (config.forces.positioning) {
|
|
325
|
+
currentSimulation.force('x', d3.forceX(d => getBarCenterX(d))
|
|
326
|
+
.strength(config.strength.positioning || 0.5));
|
|
327
|
+
currentSimulation.force('y', d3.forceY(d => getBarCenterY(d))
|
|
328
|
+
.strength(config.strength.positioning || 0.5));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (config.forces.links && currentData.length > 1) {
|
|
332
|
+
// Create links between consecutive bars
|
|
333
|
+
const links = currentData.slice(1).map((d, i) => ({
|
|
334
|
+
source: currentData[i],
|
|
335
|
+
target: d
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
currentSimulation.force('link', d3.forceLink(links)
|
|
339
|
+
.distance(50)
|
|
340
|
+
.strength(config.strength.links || 0.3));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Set up tick handler
|
|
344
|
+
currentSimulation.on('tick', () => {
|
|
345
|
+
updateBarPositions();
|
|
346
|
+
if (config.onTick && currentSimulation) {
|
|
347
|
+
config.onTick(currentSimulation);
|
|
348
|
+
}
|
|
349
|
+
trigger('forceTick', currentSimulation);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Set up end handler
|
|
353
|
+
currentSimulation.on('end', () => {
|
|
354
|
+
if (config.onEnd && currentSimulation) {
|
|
355
|
+
config.onEnd(currentSimulation);
|
|
356
|
+
}
|
|
357
|
+
trigger('forceEnd', currentSimulation);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Set alpha decay for animation duration
|
|
361
|
+
if (config.duration) {
|
|
362
|
+
const targetAlpha = 0.001;
|
|
363
|
+
const decay = 1 - Math.pow(targetAlpha, 1 / config.duration);
|
|
364
|
+
currentSimulation.alphaDecay(decay);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
trigger('forceSimulationStarted', config);
|
|
368
|
+
return currentSimulation;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function stopForceSimulation(): void {
|
|
372
|
+
if (currentSimulation) {
|
|
373
|
+
currentSimulation.stop();
|
|
374
|
+
currentSimulation = null;
|
|
375
|
+
trigger('forceSimulationStopped');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function updateForces(forces: ForceSimulationConfig['forces']): void {
|
|
380
|
+
if (currentSimulation) {
|
|
381
|
+
// Update or remove forces based on configuration
|
|
382
|
+
if (!forces.collision) currentSimulation.force('collision', null);
|
|
383
|
+
if (!forces.centering) currentSimulation.force('center', null);
|
|
384
|
+
if (!forces.positioning) {
|
|
385
|
+
currentSimulation.force('x', null);
|
|
386
|
+
currentSimulation.force('y', null);
|
|
387
|
+
}
|
|
388
|
+
if (!forces.links) currentSimulation.force('link', null);
|
|
389
|
+
|
|
390
|
+
currentSimulation.alpha(1).restart();
|
|
391
|
+
trigger('forcesUpdated', forces);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ========================================================================
|
|
396
|
+
// INTERACTION MODE MANAGEMENT
|
|
397
|
+
// ========================================================================
|
|
398
|
+
|
|
399
|
+
function setInteractionMode(mode: 'drag' | 'voronoi' | 'force' | 'combined' | 'none'): void {
|
|
400
|
+
// Disable all interactions first
|
|
401
|
+
disableDrag();
|
|
402
|
+
disableEnhancedHover();
|
|
403
|
+
stopForceSimulation();
|
|
404
|
+
|
|
405
|
+
const xRange = (xScale as any).range() || [0, 800];
|
|
406
|
+
const yRange = (yScale as any).range() || [400, 0];
|
|
407
|
+
|
|
408
|
+
switch (mode) {
|
|
409
|
+
case 'drag':
|
|
410
|
+
enableDrag({
|
|
411
|
+
enabled: true,
|
|
412
|
+
axis: 'vertical',
|
|
413
|
+
constraints: { snapToGrid: true, gridSize: 10 }
|
|
414
|
+
});
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
case 'voronoi':
|
|
418
|
+
enableEnhancedHover({
|
|
419
|
+
enabled: true,
|
|
420
|
+
extent: [[0, 0], [xRange[1], yRange[0]]]
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
|
|
424
|
+
case 'force':
|
|
425
|
+
startForceSimulation({
|
|
426
|
+
enabled: true,
|
|
427
|
+
forces: { collision: true, positioning: true },
|
|
428
|
+
strength: { collision: 0.7, positioning: 0.5 },
|
|
429
|
+
duration: 1000
|
|
430
|
+
});
|
|
431
|
+
break;
|
|
432
|
+
|
|
433
|
+
case 'combined':
|
|
434
|
+
enableEnhancedHover({
|
|
435
|
+
enabled: true,
|
|
436
|
+
extent: [[0, 0], [xRange[1], yRange[0]]]
|
|
437
|
+
});
|
|
438
|
+
enableDrag({
|
|
439
|
+
enabled: true,
|
|
440
|
+
axis: 'vertical'
|
|
441
|
+
});
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case 'none':
|
|
445
|
+
default:
|
|
446
|
+
// All interactions disabled
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
trigger('interactionModeChanged', mode);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getActiveInteractions(): string[] {
|
|
454
|
+
const active: string[] = [];
|
|
455
|
+
if (dragBehavior) active.push('drag');
|
|
456
|
+
if (enhancedHoverEnabled) active.push('hover');
|
|
457
|
+
if (currentSimulation) active.push('force');
|
|
458
|
+
return active;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ========================================================================
|
|
462
|
+
// EVENT MANAGEMENT
|
|
463
|
+
// ========================================================================
|
|
464
|
+
|
|
465
|
+
function on(event: string, callback: Function): void {
|
|
466
|
+
if (!eventListeners.has(event)) {
|
|
467
|
+
eventListeners.set(event, []);
|
|
468
|
+
}
|
|
469
|
+
eventListeners.get(event)!.push(callback);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function off(event: string): void {
|
|
473
|
+
eventListeners.delete(event);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function trigger(event: string, data?: any): void {
|
|
477
|
+
const callbacks = eventListeners.get(event);
|
|
478
|
+
if (callbacks) {
|
|
479
|
+
callbacks.forEach(callback => callback(data));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ========================================================================
|
|
484
|
+
// UTILITY FUNCTIONS
|
|
485
|
+
// ========================================================================
|
|
486
|
+
|
|
487
|
+
function getBarCenterX(d: any): number {
|
|
488
|
+
const scale = xScale as any; // Type assertion for compatibility
|
|
489
|
+
if (scale.bandwidth) {
|
|
490
|
+
// Band scale
|
|
491
|
+
return (scale(d.label) || 0) + scale.bandwidth() / 2;
|
|
492
|
+
} else {
|
|
493
|
+
// Linear scale - assume equal spacing
|
|
494
|
+
return scale(parseFloat(d.label) || 0);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getBarCenterY(d: any): number {
|
|
499
|
+
const value = d.value || (d.stacks && d.stacks[0] ? d.stacks[0].value : 0);
|
|
500
|
+
return yScale(value / 2);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function getBarWidth(d: any): number {
|
|
504
|
+
const scale = xScale as any; // Type assertion for compatibility
|
|
505
|
+
if (scale.bandwidth) {
|
|
506
|
+
return scale.bandwidth();
|
|
507
|
+
}
|
|
508
|
+
return 40; // Default width for linear scales
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function highlightBar(data: any): void {
|
|
512
|
+
container.selectAll('.bar')
|
|
513
|
+
.filter((d: any) => d === data)
|
|
514
|
+
.transition()
|
|
515
|
+
.duration(150)
|
|
516
|
+
.attr('opacity', 0.8)
|
|
517
|
+
.attr('stroke', '#ff6b6b')
|
|
518
|
+
.attr('stroke-width', 2);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function unhighlightBar(data: any): void {
|
|
522
|
+
container.selectAll('.bar')
|
|
523
|
+
.filter((d: any) => d === data)
|
|
524
|
+
.transition()
|
|
525
|
+
.duration(150)
|
|
526
|
+
.attr('opacity', 1)
|
|
527
|
+
.attr('stroke', null)
|
|
528
|
+
.attr('stroke-width', null);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function updateBarPositions(): void {
|
|
532
|
+
if (!forceSimulation) return;
|
|
533
|
+
|
|
534
|
+
container.selectAll('.bar')
|
|
535
|
+
.data(currentData)
|
|
536
|
+
.attr('transform', (d: any) => {
|
|
537
|
+
const x = (d as any).x || getBarCenterX(d);
|
|
538
|
+
const y = (d as any).y || getBarCenterY(d);
|
|
539
|
+
return `translate(${x - getBarWidth(d) / 2}, ${y})`;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ========================================================================
|
|
544
|
+
// PUBLIC API
|
|
545
|
+
// ========================================================================
|
|
546
|
+
|
|
547
|
+
// Method to update data for interactions
|
|
548
|
+
function updateData(data: any[]): void {
|
|
549
|
+
currentData = data;
|
|
550
|
+
|
|
551
|
+
// Update active interactions with new data
|
|
552
|
+
if (enhancedHoverEnabled) {
|
|
553
|
+
const config = { enabled: true, extent: [[0, 0], [800, 600]] as [[number, number], [number, number]] };
|
|
554
|
+
enableEnhancedHover(config);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (currentSimulation) {
|
|
558
|
+
currentSimulation.nodes(data);
|
|
559
|
+
currentSimulation.alpha(1).restart();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
enableDrag,
|
|
565
|
+
disableDrag,
|
|
566
|
+
updateDragConstraints,
|
|
567
|
+
enableEnhancedHover,
|
|
568
|
+
disableEnhancedHover,
|
|
569
|
+
updateHoverExtent,
|
|
570
|
+
startForceSimulation,
|
|
571
|
+
stopForceSimulation,
|
|
572
|
+
updateForces,
|
|
573
|
+
setInteractionMode,
|
|
574
|
+
getActiveInteractions,
|
|
575
|
+
updateData,
|
|
576
|
+
on,
|
|
577
|
+
off,
|
|
578
|
+
trigger
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ============================================================================
|
|
583
|
+
// SPECIALIZED WATERFALL INTERACTION UTILITIES
|
|
584
|
+
// ============================================================================
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Create drag behavior specifically optimized for waterfall charts
|
|
588
|
+
*/
|
|
589
|
+
export function createWaterfallDragBehavior(
|
|
590
|
+
onValueChange: (data: any, newValue: number) => void,
|
|
591
|
+
constraints?: { min?: number; max?: number }
|
|
592
|
+
): DragConfig {
|
|
593
|
+
return {
|
|
594
|
+
enabled: true,
|
|
595
|
+
axis: 'vertical',
|
|
596
|
+
constraints: {
|
|
597
|
+
minValue: constraints?.min,
|
|
598
|
+
maxValue: constraints?.max,
|
|
599
|
+
snapToGrid: true,
|
|
600
|
+
gridSize: 100 // Snap to hundreds
|
|
601
|
+
},
|
|
602
|
+
onDrag: (event, data) => {
|
|
603
|
+
if (onValueChange) {
|
|
604
|
+
onValueChange(data, data.value);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Create voronoi configuration optimized for waterfall hover detection
|
|
612
|
+
*/
|
|
613
|
+
export function createWaterfallVoronoiConfig(
|
|
614
|
+
chartWidth: number,
|
|
615
|
+
chartHeight: number,
|
|
616
|
+
margin: { top: number; right: number; bottom: number; left: number }
|
|
617
|
+
): VoronoiConfig {
|
|
618
|
+
return {
|
|
619
|
+
enabled: true,
|
|
620
|
+
extent: [
|
|
621
|
+
[margin.left, margin.top],
|
|
622
|
+
[chartWidth - margin.right, chartHeight - margin.bottom]
|
|
623
|
+
],
|
|
624
|
+
showCells: false,
|
|
625
|
+
cellOpacity: 0.1
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Create force simulation for animated waterfall reordering
|
|
631
|
+
*/
|
|
632
|
+
export function createWaterfallForceConfig(
|
|
633
|
+
animationDuration: number = 1000
|
|
634
|
+
): ForceSimulationConfig {
|
|
635
|
+
return {
|
|
636
|
+
enabled: true,
|
|
637
|
+
forces: {
|
|
638
|
+
collision: true,
|
|
639
|
+
positioning: true,
|
|
640
|
+
centering: false,
|
|
641
|
+
links: false
|
|
642
|
+
},
|
|
643
|
+
strength: {
|
|
644
|
+
collision: 0.8,
|
|
645
|
+
positioning: 0.6
|
|
646
|
+
},
|
|
647
|
+
duration: animationDuration
|
|
648
|
+
};
|
|
649
|
+
}
|