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,724 @@
|
|
|
1
|
+
// MintWaterfall Hierarchical Layout Extensions
|
|
2
|
+
// Advanced D3.js hierarchical layouts for multi-dimensional waterfall analysis
|
|
3
|
+
|
|
4
|
+
import * as d3 from 'd3';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// TYPE DEFINITIONS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface HierarchicalData {
|
|
11
|
+
name: string;
|
|
12
|
+
value?: number;
|
|
13
|
+
children?: HierarchicalData[];
|
|
14
|
+
category?: string;
|
|
15
|
+
level?: number;
|
|
16
|
+
parent?: string;
|
|
17
|
+
metadata?: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TreemapConfig {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
padding: number;
|
|
24
|
+
tile: any; // Simplified type for compatibility
|
|
25
|
+
round: boolean;
|
|
26
|
+
colorScale?: d3.ScaleOrdinal<string, string>;
|
|
27
|
+
onNodeClick?: (node: d3.HierarchyRectangularNode<HierarchicalData>) => void;
|
|
28
|
+
onNodeHover?: (node: d3.HierarchyRectangularNode<HierarchicalData>) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PartitionConfig {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
innerRadius?: number;
|
|
35
|
+
outerRadius?: number;
|
|
36
|
+
type: 'sunburst' | 'icicle';
|
|
37
|
+
colorScale?: d3.ScaleOrdinal<string, string>;
|
|
38
|
+
onNodeClick?: (node: d3.HierarchyRectangularNode<HierarchicalData>) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ClusterConfig {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
nodeSize: [number, number];
|
|
45
|
+
separation?: (a: d3.HierarchyPointNode<HierarchicalData>, b: d3.HierarchyPointNode<HierarchicalData>) => number;
|
|
46
|
+
linkColor?: string;
|
|
47
|
+
nodeColor?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PackConfig {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
padding: number;
|
|
54
|
+
colorScale?: d3.ScaleOrdinal<string, string>;
|
|
55
|
+
sizeAccessor?: (d: HierarchicalData) => number;
|
|
56
|
+
onNodeClick?: (node: d3.HierarchyCircularNode<HierarchicalData>) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface HierarchicalLayoutSystem {
|
|
60
|
+
// Treemap layout for nested waterfall breakdowns
|
|
61
|
+
createTreemapLayout(data: HierarchicalData, config: TreemapConfig): d3.HierarchyRectangularNode<HierarchicalData>;
|
|
62
|
+
renderTreemap(container: d3.Selection<any, any, any, any>, layout: d3.HierarchyRectangularNode<HierarchicalData>, config: TreemapConfig): void;
|
|
63
|
+
|
|
64
|
+
// Partition layout for circular/radial waterfall views
|
|
65
|
+
createPartitionLayout(data: HierarchicalData, config: PartitionConfig): d3.HierarchyRectangularNode<HierarchicalData>;
|
|
66
|
+
renderPartition(container: d3.Selection<any, any, any, any>, layout: d3.HierarchyRectangularNode<HierarchicalData>, config: PartitionConfig): void;
|
|
67
|
+
|
|
68
|
+
// Cluster layout for dendogram-style categorization
|
|
69
|
+
createClusterLayout(data: HierarchicalData, config: ClusterConfig): d3.HierarchyPointNode<HierarchicalData>;
|
|
70
|
+
renderCluster(container: d3.Selection<any, any, any, any>, layout: d3.HierarchyPointNode<HierarchicalData>, config: ClusterConfig): void;
|
|
71
|
+
|
|
72
|
+
// Pack layout for bubble-based grouped waterfalls
|
|
73
|
+
createPackLayout(data: HierarchicalData, config: PackConfig): d3.HierarchyCircularNode<HierarchicalData>;
|
|
74
|
+
renderPack(container: d3.Selection<any, any, any, any>, layout: d3.HierarchyCircularNode<HierarchicalData>, config: PackConfig): void;
|
|
75
|
+
|
|
76
|
+
// Utility functions
|
|
77
|
+
transformWaterfallToHierarchy(waterfallData: any[]): HierarchicalData;
|
|
78
|
+
createDrillDownNavigation(data: HierarchicalData): any[];
|
|
79
|
+
calculateHierarchicalMetrics(node: d3.HierarchyNode<HierarchicalData>): any;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// HIERARCHICAL LAYOUT SYSTEM IMPLEMENTATION
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export function createHierarchicalLayoutSystem(): HierarchicalLayoutSystem {
|
|
87
|
+
|
|
88
|
+
// ========================================================================
|
|
89
|
+
// TREEMAP LAYOUT (d3.treemap)
|
|
90
|
+
// ========================================================================
|
|
91
|
+
|
|
92
|
+
function createTreemapLayout(data: HierarchicalData, config: TreemapConfig): d3.HierarchyRectangularNode<HierarchicalData> {
|
|
93
|
+
// Create hierarchy
|
|
94
|
+
const root = d3.hierarchy(data)
|
|
95
|
+
.sum(d => d.value || 0)
|
|
96
|
+
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
97
|
+
|
|
98
|
+
// Create treemap layout
|
|
99
|
+
const treemap = d3.treemap<HierarchicalData>()
|
|
100
|
+
.size([config.width, config.height])
|
|
101
|
+
.padding(config.padding)
|
|
102
|
+
.tile(config.tile)
|
|
103
|
+
.round(config.round);
|
|
104
|
+
|
|
105
|
+
return treemap(root);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderTreemap(
|
|
109
|
+
container: d3.Selection<any, any, any, any>,
|
|
110
|
+
layout: d3.HierarchyRectangularNode<HierarchicalData>,
|
|
111
|
+
config: TreemapConfig
|
|
112
|
+
): void {
|
|
113
|
+
const colorScale = config.colorScale || d3.scaleOrdinal(d3.schemeCategory10);
|
|
114
|
+
|
|
115
|
+
// Create treemap group
|
|
116
|
+
const treemapGroup = container.selectAll('.treemap-group')
|
|
117
|
+
.data([0]);
|
|
118
|
+
|
|
119
|
+
const treemapGroupEnter = treemapGroup.enter()
|
|
120
|
+
.append('g')
|
|
121
|
+
.attr('class', 'treemap-group');
|
|
122
|
+
|
|
123
|
+
const treemapGroupMerged = treemapGroupEnter.merge(treemapGroup as any);
|
|
124
|
+
|
|
125
|
+
// Get leaf nodes for rendering
|
|
126
|
+
const leaves = layout.leaves();
|
|
127
|
+
|
|
128
|
+
// Create rectangles for leaf nodes
|
|
129
|
+
const cells = treemapGroupMerged.selectAll('.treemap-cell')
|
|
130
|
+
.data(leaves, (d: any) => d.data.name);
|
|
131
|
+
|
|
132
|
+
const cellsEnter = cells.enter()
|
|
133
|
+
.append('g')
|
|
134
|
+
.attr('class', 'treemap-cell');
|
|
135
|
+
|
|
136
|
+
// Add rectangles
|
|
137
|
+
cellsEnter.append('rect')
|
|
138
|
+
.attr('class', 'treemap-rect');
|
|
139
|
+
|
|
140
|
+
// Add labels
|
|
141
|
+
cellsEnter.append('text')
|
|
142
|
+
.attr('class', 'treemap-label');
|
|
143
|
+
|
|
144
|
+
// Add value labels
|
|
145
|
+
cellsEnter.append('text')
|
|
146
|
+
.attr('class', 'treemap-value');
|
|
147
|
+
|
|
148
|
+
const cellsMerged = cellsEnter.merge(cells as any);
|
|
149
|
+
|
|
150
|
+
// Update rectangles
|
|
151
|
+
cellsMerged.select('.treemap-rect')
|
|
152
|
+
.transition()
|
|
153
|
+
.duration(750)
|
|
154
|
+
.attr('x', d => d.x0)
|
|
155
|
+
.attr('y', d => d.y0)
|
|
156
|
+
.attr('width', d => d.x1 - d.x0)
|
|
157
|
+
.attr('height', d => d.y1 - d.y0)
|
|
158
|
+
.attr('fill', d => colorScale(d.data.category || d.data.name))
|
|
159
|
+
.attr('stroke', '#fff')
|
|
160
|
+
.attr('stroke-width', 1)
|
|
161
|
+
.attr('opacity', 0.8);
|
|
162
|
+
|
|
163
|
+
// Update labels
|
|
164
|
+
cellsMerged.select('.treemap-label')
|
|
165
|
+
.attr('x', d => (d.x0 + d.x1) / 2)
|
|
166
|
+
.attr('y', d => (d.y0 + d.y1) / 2 - 8)
|
|
167
|
+
.attr('text-anchor', 'middle')
|
|
168
|
+
.attr('font-size', d => Math.min(12, (d.x1 - d.x0) / 8))
|
|
169
|
+
.attr('fill', '#333')
|
|
170
|
+
.text(d => d.data.name);
|
|
171
|
+
|
|
172
|
+
// Update value labels
|
|
173
|
+
cellsMerged.select('.treemap-value')
|
|
174
|
+
.attr('x', d => (d.x0 + d.x1) / 2)
|
|
175
|
+
.attr('y', d => (d.y0 + d.y1) / 2 + 8)
|
|
176
|
+
.attr('text-anchor', 'middle')
|
|
177
|
+
.attr('font-size', d => Math.min(10, (d.x1 - d.x0) / 10))
|
|
178
|
+
.attr('fill', '#666')
|
|
179
|
+
.text(d => formatValue(d.value || 0));
|
|
180
|
+
|
|
181
|
+
// Add interaction
|
|
182
|
+
cellsMerged
|
|
183
|
+
.style('cursor', 'pointer')
|
|
184
|
+
.on('click', (event, d) => {
|
|
185
|
+
if (config.onNodeClick) {
|
|
186
|
+
config.onNodeClick(d);
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.on('mouseenter', (event, d) => {
|
|
190
|
+
d3.select(event.currentTarget).select('.treemap-rect')
|
|
191
|
+
.attr('opacity', 1)
|
|
192
|
+
.attr('stroke-width', 2);
|
|
193
|
+
|
|
194
|
+
if (config.onNodeHover) {
|
|
195
|
+
config.onNodeHover(d);
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.on('mouseleave', (event, d) => {
|
|
199
|
+
d3.select(event.currentTarget).select('.treemap-rect')
|
|
200
|
+
.attr('opacity', 0.8)
|
|
201
|
+
.attr('stroke-width', 1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
cells.exit()
|
|
205
|
+
.transition()
|
|
206
|
+
.duration(300)
|
|
207
|
+
.attr('opacity', 0)
|
|
208
|
+
.remove();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ========================================================================
|
|
212
|
+
// PARTITION LAYOUT (d3.partition)
|
|
213
|
+
// ========================================================================
|
|
214
|
+
|
|
215
|
+
function createPartitionLayout(data: HierarchicalData, config: PartitionConfig): d3.HierarchyRectangularNode<HierarchicalData> {
|
|
216
|
+
// Create hierarchy
|
|
217
|
+
const root = d3.hierarchy(data)
|
|
218
|
+
.sum(d => d.value || 0)
|
|
219
|
+
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
220
|
+
|
|
221
|
+
// Create partition layout
|
|
222
|
+
const partition = d3.partition<HierarchicalData>()
|
|
223
|
+
.size([2 * Math.PI, Math.min(config.width, config.height) / 2]);
|
|
224
|
+
|
|
225
|
+
return partition(root);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderPartition(
|
|
229
|
+
container: d3.Selection<any, any, any, any>,
|
|
230
|
+
layout: d3.HierarchyRectangularNode<HierarchicalData>,
|
|
231
|
+
config: PartitionConfig
|
|
232
|
+
): void {
|
|
233
|
+
const colorScale = config.colorScale || d3.scaleOrdinal(d3.schemeCategory10);
|
|
234
|
+
const radius = Math.min(config.width, config.height) / 2;
|
|
235
|
+
const innerRadius = config.innerRadius || 0;
|
|
236
|
+
|
|
237
|
+
// Create partition group
|
|
238
|
+
const partitionGroup = container.selectAll('.partition-group')
|
|
239
|
+
.data([0]);
|
|
240
|
+
|
|
241
|
+
const partitionGroupEnter = partitionGroup.enter()
|
|
242
|
+
.append('g')
|
|
243
|
+
.attr('class', 'partition-group')
|
|
244
|
+
.attr('transform', `translate(${config.width / 2}, ${config.height / 2})`);
|
|
245
|
+
|
|
246
|
+
const partitionGroupMerged = partitionGroupEnter.merge(partitionGroup as any);
|
|
247
|
+
|
|
248
|
+
if (config.type === 'sunburst') {
|
|
249
|
+
renderSunburst(partitionGroupMerged, layout, colorScale, radius, innerRadius, config);
|
|
250
|
+
} else {
|
|
251
|
+
renderIcicle(partitionGroupMerged, layout, colorScale, config);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderSunburst(
|
|
256
|
+
container: d3.Selection<any, any, any, any>,
|
|
257
|
+
layout: d3.HierarchyRectangularNode<HierarchicalData>,
|
|
258
|
+
colorScale: d3.ScaleOrdinal<string, string>,
|
|
259
|
+
radius: number,
|
|
260
|
+
innerRadius: number,
|
|
261
|
+
config: PartitionConfig
|
|
262
|
+
): void {
|
|
263
|
+
const arc = d3.arc<d3.HierarchyRectangularNode<HierarchicalData>>()
|
|
264
|
+
.startAngle(d => d.x0)
|
|
265
|
+
.endAngle(d => d.x1)
|
|
266
|
+
.innerRadius(d => Math.sqrt(d.y0) * radius / Math.sqrt(layout.y1))
|
|
267
|
+
.outerRadius(d => Math.sqrt(d.y1) * radius / Math.sqrt(layout.y1));
|
|
268
|
+
|
|
269
|
+
const descendants = layout.descendants().filter(d => d.depth > 0);
|
|
270
|
+
|
|
271
|
+
const paths = container.selectAll('.partition-arc')
|
|
272
|
+
.data(descendants, (d: any) => d.data.name);
|
|
273
|
+
|
|
274
|
+
const pathsEnter = paths.enter()
|
|
275
|
+
.append('path')
|
|
276
|
+
.attr('class', 'partition-arc')
|
|
277
|
+
.attr('fill', d => colorScale(d.data.category || d.data.name))
|
|
278
|
+
.attr('stroke', '#fff')
|
|
279
|
+
.attr('stroke-width', 1)
|
|
280
|
+
.style('cursor', 'pointer');
|
|
281
|
+
|
|
282
|
+
pathsEnter.merge(paths as any)
|
|
283
|
+
.transition()
|
|
284
|
+
.duration(750)
|
|
285
|
+
.attr('d', arc);
|
|
286
|
+
|
|
287
|
+
// Add interaction
|
|
288
|
+
pathsEnter.merge(paths as any)
|
|
289
|
+
.on('click', (event, d) => {
|
|
290
|
+
if (config.onNodeClick) {
|
|
291
|
+
config.onNodeClick(d);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
paths.exit()
|
|
296
|
+
.transition()
|
|
297
|
+
.duration(300)
|
|
298
|
+
.attr('opacity', 0)
|
|
299
|
+
.remove();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderIcicle(
|
|
303
|
+
container: d3.Selection<any, any, any, any>,
|
|
304
|
+
layout: d3.HierarchyRectangularNode<HierarchicalData>,
|
|
305
|
+
colorScale: d3.ScaleOrdinal<string, string>,
|
|
306
|
+
config: PartitionConfig
|
|
307
|
+
): void {
|
|
308
|
+
const descendants = layout.descendants();
|
|
309
|
+
|
|
310
|
+
const rects = container.selectAll('.partition-rect')
|
|
311
|
+
.data(descendants, (d: any) => d.data.name);
|
|
312
|
+
|
|
313
|
+
const rectsEnter = rects.enter()
|
|
314
|
+
.append('rect')
|
|
315
|
+
.attr('class', 'partition-rect')
|
|
316
|
+
.attr('fill', d => colorScale(d.data.category || d.data.name))
|
|
317
|
+
.attr('stroke', '#fff')
|
|
318
|
+
.attr('stroke-width', 1)
|
|
319
|
+
.style('cursor', 'pointer');
|
|
320
|
+
|
|
321
|
+
rectsEnter.merge(rects as any)
|
|
322
|
+
.transition()
|
|
323
|
+
.duration(750)
|
|
324
|
+
.attr('x', d => d.y0)
|
|
325
|
+
.attr('y', d => d.x0)
|
|
326
|
+
.attr('width', d => d.y1 - d.y0)
|
|
327
|
+
.attr('height', d => d.x1 - d.x0);
|
|
328
|
+
|
|
329
|
+
// Add interaction
|
|
330
|
+
rectsEnter.merge(rects as any)
|
|
331
|
+
.on('click', (event, d) => {
|
|
332
|
+
if (config.onNodeClick) {
|
|
333
|
+
config.onNodeClick(d);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
rects.exit()
|
|
338
|
+
.transition()
|
|
339
|
+
.duration(300)
|
|
340
|
+
.attr('opacity', 0)
|
|
341
|
+
.remove();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ========================================================================
|
|
345
|
+
// CLUSTER LAYOUT (d3.cluster)
|
|
346
|
+
// ========================================================================
|
|
347
|
+
|
|
348
|
+
function createClusterLayout(data: HierarchicalData, config: ClusterConfig): d3.HierarchyPointNode<HierarchicalData> {
|
|
349
|
+
// Create hierarchy
|
|
350
|
+
const root = d3.hierarchy(data);
|
|
351
|
+
|
|
352
|
+
// Create cluster layout
|
|
353
|
+
const cluster = d3.cluster<HierarchicalData>()
|
|
354
|
+
.size([config.width, config.height])
|
|
355
|
+
.nodeSize(config.nodeSize);
|
|
356
|
+
|
|
357
|
+
if (config.separation) {
|
|
358
|
+
cluster.separation(config.separation);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return cluster(root);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderCluster(
|
|
365
|
+
container: d3.Selection<any, any, any, any>,
|
|
366
|
+
layout: d3.HierarchyPointNode<HierarchicalData>,
|
|
367
|
+
config: ClusterConfig
|
|
368
|
+
): void {
|
|
369
|
+
// Create cluster group
|
|
370
|
+
const clusterGroup = container.selectAll('.cluster-group')
|
|
371
|
+
.data([0]);
|
|
372
|
+
|
|
373
|
+
const clusterGroupEnter = clusterGroup.enter()
|
|
374
|
+
.append('g')
|
|
375
|
+
.attr('class', 'cluster-group');
|
|
376
|
+
|
|
377
|
+
const clusterGroupMerged = clusterGroupEnter.merge(clusterGroup as any);
|
|
378
|
+
|
|
379
|
+
const descendants = layout.descendants();
|
|
380
|
+
const links = layout.links();
|
|
381
|
+
|
|
382
|
+
// Render links
|
|
383
|
+
const linkSelection = clusterGroupMerged.selectAll('.cluster-link')
|
|
384
|
+
.data(links, (d: any) => `${d.source.data.name}-${d.target.data.name}`);
|
|
385
|
+
|
|
386
|
+
linkSelection.enter()
|
|
387
|
+
.append('path')
|
|
388
|
+
.attr('class', 'cluster-link')
|
|
389
|
+
.attr('fill', 'none')
|
|
390
|
+
.attr('stroke', config.linkColor || '#999')
|
|
391
|
+
.attr('stroke-width', 1)
|
|
392
|
+
.merge(linkSelection as any)
|
|
393
|
+
.transition()
|
|
394
|
+
.duration(750)
|
|
395
|
+
.attr('d', d3.linkHorizontal<d3.HierarchyPointLink<HierarchicalData>, d3.HierarchyPointNode<HierarchicalData>>()
|
|
396
|
+
.x(d => d.y || 0)
|
|
397
|
+
.y(d => d.x || 0));
|
|
398
|
+
|
|
399
|
+
linkSelection.exit().remove();
|
|
400
|
+
|
|
401
|
+
// Render nodes
|
|
402
|
+
const nodeSelection = clusterGroupMerged.selectAll('.cluster-node')
|
|
403
|
+
.data(descendants, (d: any) => d.data.name);
|
|
404
|
+
|
|
405
|
+
const nodeEnter = nodeSelection.enter()
|
|
406
|
+
.append('g')
|
|
407
|
+
.attr('class', 'cluster-node');
|
|
408
|
+
|
|
409
|
+
nodeEnter.append('circle')
|
|
410
|
+
.attr('r', 5)
|
|
411
|
+
.attr('fill', config.nodeColor || '#69b3a2');
|
|
412
|
+
|
|
413
|
+
nodeEnter.append('text')
|
|
414
|
+
.attr('dy', 3)
|
|
415
|
+
.attr('x', 8)
|
|
416
|
+
.style('font-size', '12px')
|
|
417
|
+
.text(d => d.data.name);
|
|
418
|
+
|
|
419
|
+
const nodeMerged = nodeEnter.merge(nodeSelection as any);
|
|
420
|
+
|
|
421
|
+
nodeMerged
|
|
422
|
+
.transition()
|
|
423
|
+
.duration(750)
|
|
424
|
+
.attr('transform', d => `translate(${d.y},${d.x})`);
|
|
425
|
+
|
|
426
|
+
nodeSelection.exit()
|
|
427
|
+
.transition()
|
|
428
|
+
.duration(300)
|
|
429
|
+
.attr('opacity', 0)
|
|
430
|
+
.remove();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ========================================================================
|
|
434
|
+
// PACK LAYOUT (d3.pack)
|
|
435
|
+
// ========================================================================
|
|
436
|
+
|
|
437
|
+
function createPackLayout(data: HierarchicalData, config: PackConfig): d3.HierarchyCircularNode<HierarchicalData> {
|
|
438
|
+
// Create hierarchy
|
|
439
|
+
const root = d3.hierarchy(data)
|
|
440
|
+
.sum(config.sizeAccessor || (d => d.value || 0))
|
|
441
|
+
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
442
|
+
|
|
443
|
+
// Create pack layout
|
|
444
|
+
const pack = d3.pack<HierarchicalData>()
|
|
445
|
+
.size([config.width, config.height])
|
|
446
|
+
.padding(config.padding);
|
|
447
|
+
|
|
448
|
+
return pack(root);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function renderPack(
|
|
452
|
+
container: d3.Selection<any, any, any, any>,
|
|
453
|
+
layout: d3.HierarchyCircularNode<HierarchicalData>,
|
|
454
|
+
config: PackConfig
|
|
455
|
+
): void {
|
|
456
|
+
const colorScale = config.colorScale || d3.scaleOrdinal(d3.schemeCategory10);
|
|
457
|
+
|
|
458
|
+
// Create pack group
|
|
459
|
+
const packGroup = container.selectAll('.pack-group')
|
|
460
|
+
.data([0]);
|
|
461
|
+
|
|
462
|
+
const packGroupEnter = packGroup.enter()
|
|
463
|
+
.append('g')
|
|
464
|
+
.attr('class', 'pack-group');
|
|
465
|
+
|
|
466
|
+
const packGroupMerged = packGroupEnter.merge(packGroup as any);
|
|
467
|
+
|
|
468
|
+
const descendants = layout.descendants().filter(d => d.depth > 0);
|
|
469
|
+
|
|
470
|
+
// Create circles for nodes
|
|
471
|
+
const nodes = packGroupMerged.selectAll('.pack-node')
|
|
472
|
+
.data(descendants, (d: any) => d.data.name);
|
|
473
|
+
|
|
474
|
+
const nodesEnter = nodes.enter()
|
|
475
|
+
.append('g')
|
|
476
|
+
.attr('class', 'pack-node');
|
|
477
|
+
|
|
478
|
+
// Add circles
|
|
479
|
+
nodesEnter.append('circle')
|
|
480
|
+
.attr('class', 'pack-circle')
|
|
481
|
+
.style('cursor', 'pointer');
|
|
482
|
+
|
|
483
|
+
// Add labels
|
|
484
|
+
nodesEnter.append('text')
|
|
485
|
+
.attr('class', 'pack-label')
|
|
486
|
+
.attr('text-anchor', 'middle')
|
|
487
|
+
.attr('dy', '0.3em')
|
|
488
|
+
.style('font-size', '10px')
|
|
489
|
+
.style('fill', '#333')
|
|
490
|
+
.style('pointer-events', 'none');
|
|
491
|
+
|
|
492
|
+
const nodesMerged = nodesEnter.merge(nodes as any);
|
|
493
|
+
|
|
494
|
+
// Update positions and attributes
|
|
495
|
+
nodesMerged
|
|
496
|
+
.transition()
|
|
497
|
+
.duration(750)
|
|
498
|
+
.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
499
|
+
|
|
500
|
+
nodesMerged.select('.pack-circle')
|
|
501
|
+
.transition()
|
|
502
|
+
.duration(750)
|
|
503
|
+
.attr('r', d => d.r)
|
|
504
|
+
.attr('fill', d => colorScale(d.data.category || d.data.name))
|
|
505
|
+
.attr('stroke', '#fff')
|
|
506
|
+
.attr('stroke-width', 1)
|
|
507
|
+
.attr('opacity', 0.7);
|
|
508
|
+
|
|
509
|
+
nodesMerged.select('.pack-label')
|
|
510
|
+
.text(d => d.r > 15 ? d.data.name : '')
|
|
511
|
+
.attr('font-size', d => Math.min(d.r / 3, 12));
|
|
512
|
+
|
|
513
|
+
// Add interaction
|
|
514
|
+
nodesMerged
|
|
515
|
+
.on('click', (event, d) => {
|
|
516
|
+
if (config.onNodeClick) {
|
|
517
|
+
config.onNodeClick(d);
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
.on('mouseenter', (event, d) => {
|
|
521
|
+
d3.select(event.currentTarget).select('.pack-circle')
|
|
522
|
+
.attr('opacity', 1)
|
|
523
|
+
.attr('stroke-width', 2);
|
|
524
|
+
})
|
|
525
|
+
.on('mouseleave', (event, d) => {
|
|
526
|
+
d3.select(event.currentTarget).select('.pack-circle')
|
|
527
|
+
.attr('opacity', 0.7)
|
|
528
|
+
.attr('stroke-width', 1);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
nodes.exit()
|
|
532
|
+
.transition()
|
|
533
|
+
.duration(300)
|
|
534
|
+
.attr('opacity', 0)
|
|
535
|
+
.remove();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ========================================================================
|
|
539
|
+
// UTILITY FUNCTIONS
|
|
540
|
+
// ========================================================================
|
|
541
|
+
|
|
542
|
+
function transformWaterfallToHierarchy(waterfallData: any[]): HierarchicalData {
|
|
543
|
+
// Group data by categories if available
|
|
544
|
+
const grouped = d3.group(waterfallData, d => d.category || 'Default');
|
|
545
|
+
|
|
546
|
+
const children: HierarchicalData[] = [];
|
|
547
|
+
|
|
548
|
+
for (const [category, items] of grouped) {
|
|
549
|
+
const categoryNode: HierarchicalData = {
|
|
550
|
+
name: category,
|
|
551
|
+
value: d3.sum(items, d => Math.abs(extractValue(d))),
|
|
552
|
+
category,
|
|
553
|
+
children: items.map(item => ({
|
|
554
|
+
name: getLabel(item),
|
|
555
|
+
value: Math.abs(extractValue(item)),
|
|
556
|
+
category,
|
|
557
|
+
metadata: item
|
|
558
|
+
}))
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
children.push(categoryNode);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
name: 'Root',
|
|
566
|
+
children,
|
|
567
|
+
value: d3.sum(children, d => d.value || 0)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function createDrillDownNavigation(data: HierarchicalData): any[] {
|
|
572
|
+
const navigation: any[] = [];
|
|
573
|
+
|
|
574
|
+
function traverse(node: HierarchicalData, path: string[] = []): void {
|
|
575
|
+
const currentPath = [...path, node.name];
|
|
576
|
+
|
|
577
|
+
navigation.push({
|
|
578
|
+
path: currentPath,
|
|
579
|
+
name: node.name,
|
|
580
|
+
value: node.value,
|
|
581
|
+
level: currentPath.length - 1,
|
|
582
|
+
hasChildren: !!(node.children && node.children.length > 0)
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
if (node.children) {
|
|
586
|
+
node.children.forEach(child => traverse(child, currentPath));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
traverse(data);
|
|
591
|
+
return navigation;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function calculateHierarchicalMetrics(node: d3.HierarchyNode<HierarchicalData>): any {
|
|
595
|
+
return {
|
|
596
|
+
depth: node.depth,
|
|
597
|
+
height: node.height,
|
|
598
|
+
value: node.value,
|
|
599
|
+
leafCount: node.leaves().length,
|
|
600
|
+
descendants: node.descendants().length,
|
|
601
|
+
ancestors: node.ancestors().length,
|
|
602
|
+
totalValue: node.descendants().reduce((sum, d) => sum + (d.value || 0), 0),
|
|
603
|
+
averageValue: node.value && node.leaves().length ? node.value / node.leaves().length : 0
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Helper functions
|
|
608
|
+
function extractValue(item: any): number {
|
|
609
|
+
if (typeof item === 'number') return item;
|
|
610
|
+
if (item.value !== undefined) return item.value;
|
|
611
|
+
if (item.stacks && Array.isArray(item.stacks)) {
|
|
612
|
+
return item.stacks.reduce((sum: number, stack: any) => sum + (stack.value || 0), 0);
|
|
613
|
+
}
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function getLabel(item: any): string {
|
|
618
|
+
if (typeof item === 'string') return item;
|
|
619
|
+
if (item.label !== undefined) return item.label;
|
|
620
|
+
if (item.name !== undefined) return item.name;
|
|
621
|
+
return 'Unnamed';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function formatValue(value: number): string {
|
|
625
|
+
if (Math.abs(value) >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
|
|
626
|
+
if (Math.abs(value) >= 1000) return `${(value / 1000).toFixed(1)}K`;
|
|
627
|
+
return value.toFixed(0);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ========================================================================
|
|
631
|
+
// RETURN LAYOUT SYSTEM INTERFACE
|
|
632
|
+
// ========================================================================
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
createTreemapLayout,
|
|
636
|
+
renderTreemap,
|
|
637
|
+
createPartitionLayout,
|
|
638
|
+
renderPartition,
|
|
639
|
+
createClusterLayout,
|
|
640
|
+
renderCluster,
|
|
641
|
+
createPackLayout,
|
|
642
|
+
renderPack,
|
|
643
|
+
transformWaterfallToHierarchy,
|
|
644
|
+
createDrillDownNavigation,
|
|
645
|
+
calculateHierarchicalMetrics
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ============================================================================
|
|
650
|
+
// SPECIALIZED WATERFALL HIERARCHICAL UTILITIES
|
|
651
|
+
// ============================================================================
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Create hierarchical waterfall breakdown using treemap
|
|
655
|
+
*/
|
|
656
|
+
export function createWaterfallTreemap(
|
|
657
|
+
data: any[],
|
|
658
|
+
container: d3.Selection<any, any, any, any>,
|
|
659
|
+
width: number,
|
|
660
|
+
height: number
|
|
661
|
+
): void {
|
|
662
|
+
const layoutSystem = createHierarchicalLayoutSystem();
|
|
663
|
+
const hierarchicalData = layoutSystem.transformWaterfallToHierarchy(data);
|
|
664
|
+
|
|
665
|
+
const config: TreemapConfig = {
|
|
666
|
+
width,
|
|
667
|
+
height,
|
|
668
|
+
padding: 2,
|
|
669
|
+
tile: d3.treemapBinary,
|
|
670
|
+
round: true,
|
|
671
|
+
colorScale: d3.scaleOrdinal(d3.schemeSet3)
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const layout = layoutSystem.createTreemapLayout(hierarchicalData, config);
|
|
675
|
+
layoutSystem.renderTreemap(container, layout, config);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Create circular waterfall visualization using partition layout
|
|
680
|
+
*/
|
|
681
|
+
export function createWaterfallSunburst(
|
|
682
|
+
data: any[],
|
|
683
|
+
container: d3.Selection<any, any, any, any>,
|
|
684
|
+
width: number,
|
|
685
|
+
height: number
|
|
686
|
+
): void {
|
|
687
|
+
const layoutSystem = createHierarchicalLayoutSystem();
|
|
688
|
+
const hierarchicalData = layoutSystem.transformWaterfallToHierarchy(data);
|
|
689
|
+
|
|
690
|
+
const config: PartitionConfig = {
|
|
691
|
+
width,
|
|
692
|
+
height,
|
|
693
|
+
innerRadius: 40,
|
|
694
|
+
type: 'sunburst',
|
|
695
|
+
colorScale: d3.scaleOrdinal(d3.schemeCategory10)
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const layout = layoutSystem.createPartitionLayout(hierarchicalData, config);
|
|
699
|
+
layoutSystem.renderPartition(container, layout, config);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Create bubble chart waterfall using pack layout
|
|
704
|
+
*/
|
|
705
|
+
export function createWaterfallBubbles(
|
|
706
|
+
data: any[],
|
|
707
|
+
container: d3.Selection<any, any, any, any>,
|
|
708
|
+
width: number,
|
|
709
|
+
height: number
|
|
710
|
+
): void {
|
|
711
|
+
const layoutSystem = createHierarchicalLayoutSystem();
|
|
712
|
+
const hierarchicalData = layoutSystem.transformWaterfallToHierarchy(data);
|
|
713
|
+
|
|
714
|
+
const config: PackConfig = {
|
|
715
|
+
width,
|
|
716
|
+
height,
|
|
717
|
+
padding: 3,
|
|
718
|
+
colorScale: d3.scaleOrdinal(d3.schemePastel1),
|
|
719
|
+
sizeAccessor: d => Math.abs(d.value || 0)
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const layout = layoutSystem.createPackLayout(hierarchicalData, config);
|
|
723
|
+
layoutSystem.renderPack(container, layout, config);
|
|
724
|
+
}
|