@acorex/charts 19.13.2
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/README.md +72 -0
- package/bar-chart/README.md +3 -0
- package/bar-chart/index.d.ts +3 -0
- package/bar-chart/lib/bar-chart.component.d.ts +123 -0
- package/bar-chart/lib/bar-chart.config.d.ts +6 -0
- package/bar-chart/lib/bar-chart.type.d.ts +44 -0
- package/chart-tooltip/README.md +3 -0
- package/chart-tooltip/index.d.ts +2 -0
- package/chart-tooltip/lib/chart-tooltip.component.d.ts +43 -0
- package/chart-tooltip/lib/chart-tooltip.type.d.ts +7 -0
- package/donut-chart/README.md +3 -0
- package/donut-chart/index.d.ts +3 -0
- package/donut-chart/lib/donut-chart.component.d.ts +143 -0
- package/donut-chart/lib/donut-chart.config.d.ts +6 -0
- package/donut-chart/lib/donut-chart.type.d.ts +25 -0
- package/fesm2022/acorex-charts-bar-chart.mjs +563 -0
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-chart-tooltip.mjs +75 -0
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -0
- package/fesm2022/acorex-charts-donut-chart.mjs +616 -0
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-gauge-chart.mjs +548 -0
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +652 -0
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-line-chart.mjs +738 -0
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts.mjs +8 -0
- package/fesm2022/acorex-charts.mjs.map +1 -0
- package/gauge-chart/README.md +3 -0
- package/gauge-chart/index.d.ts +3 -0
- package/gauge-chart/lib/gauge-chart.component.d.ts +110 -0
- package/gauge-chart/lib/gauge-chart.config.d.ts +6 -0
- package/gauge-chart/lib/gauge-chart.type.d.ts +37 -0
- package/hierarchy-chart/README.md +61 -0
- package/hierarchy-chart/index.d.ts +3 -0
- package/hierarchy-chart/lib/hierarchy-chart.component.d.ts +99 -0
- package/hierarchy-chart/lib/hierarchy-chart.config.d.ts +6 -0
- package/hierarchy-chart/lib/hierarchy-chart.type.d.ts +227 -0
- package/index.d.ts +1 -0
- package/line-chart/README.md +3 -0
- package/line-chart/index.d.ts +3 -0
- package/line-chart/lib/line-chart.component.d.ts +96 -0
- package/line-chart/lib/line-chart.config.d.ts +6 -0
- package/line-chart/lib/line-chart.type.d.ts +61 -0
- package/package.json +48 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { AXPanViewDirective } from '@acorex/cdk/pan-view';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { InjectionToken, inject, viewChild, contentChild, NgZone, ViewContainerRef, signal, input, output, computed, afterNextRender, effect, Component } from '@angular/core';
|
|
5
|
+
import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
|
|
6
|
+
import { set } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
const AXHierarchyChartDefaultConfig = {
|
|
9
|
+
direction: 'vertical',
|
|
10
|
+
margin: { top: 40, right: 120, bottom: 40, left: 120 },
|
|
11
|
+
nodeRadius: 30,
|
|
12
|
+
nodeColor: 'rgba(var(--ax-sys-color-primary-500))',
|
|
13
|
+
nodeStrokeColor: 'rgba(var(--ax-sys-color-primary-400))',
|
|
14
|
+
nodeStrokeWidth: 1.5,
|
|
15
|
+
linkColor: 'rgba(var(--ax-sys-color-primary-400))',
|
|
16
|
+
linkWidth: 1.5,
|
|
17
|
+
linkStyle: 'curved',
|
|
18
|
+
nodeWidth: 120,
|
|
19
|
+
nodeHeight: 60,
|
|
20
|
+
nodeSpacingX: 80,
|
|
21
|
+
nodeSpacingY: 120,
|
|
22
|
+
showTooltip: true,
|
|
23
|
+
collapsible: true,
|
|
24
|
+
expandAll: false,
|
|
25
|
+
animationDuration: 100,
|
|
26
|
+
animationEasing: 'cubic-out',
|
|
27
|
+
};
|
|
28
|
+
const AX_HIERARCHY_CHART_CONFIG = new InjectionToken('AX_HIERARCHY_CHART_CONFIG', {
|
|
29
|
+
providedIn: 'root',
|
|
30
|
+
factory: () => {
|
|
31
|
+
const global = inject(AX_GLOBAL_CONFIG);
|
|
32
|
+
set(global, 'chart.hierarchyChart', AXHierarchyChartDefaultConfig);
|
|
33
|
+
return AXHierarchyChartDefaultConfig;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
function hierarchyChartConfig(config = {}) {
|
|
37
|
+
const result = {
|
|
38
|
+
...AXHierarchyChartDefaultConfig,
|
|
39
|
+
...config,
|
|
40
|
+
};
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A highly customizable hierarchical visualization component that can be used for:
|
|
46
|
+
* - Organization charts
|
|
47
|
+
* - Tree diagrams
|
|
48
|
+
* - Dependency visualizations
|
|
49
|
+
* - Process flows
|
|
50
|
+
*
|
|
51
|
+
* Supports both default node styling and custom node templates.
|
|
52
|
+
*/
|
|
53
|
+
class AXHierarchyChartComponent {
|
|
54
|
+
// Chart container reference
|
|
55
|
+
chartContainer = viewChild.required('chartContainer');
|
|
56
|
+
// Custom node template provided by the user
|
|
57
|
+
customNodeTemplate = contentChild('nodeTemplate');
|
|
58
|
+
// Services
|
|
59
|
+
ngZone = inject(NgZone);
|
|
60
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
61
|
+
configToken = inject(AX_HIERARCHY_CHART_CONFIG);
|
|
62
|
+
// D3 instance
|
|
63
|
+
d3;
|
|
64
|
+
// Internal state signals
|
|
65
|
+
_expandedNodes = signal(new Map());
|
|
66
|
+
_initialized = signal(false);
|
|
67
|
+
_rendered = signal(false);
|
|
68
|
+
_dimensions = signal({ width: 0, height: 0 });
|
|
69
|
+
_nodeElements = signal(new Map()); // Store references to node elements
|
|
70
|
+
_chartData = signal(null); // Store the current chart data and layout
|
|
71
|
+
// Inputs
|
|
72
|
+
data = input([]);
|
|
73
|
+
options = input({});
|
|
74
|
+
nodeTemplate = input(null);
|
|
75
|
+
// Outputs
|
|
76
|
+
itemClick = output();
|
|
77
|
+
nodeToggle = output();
|
|
78
|
+
// Computed values
|
|
79
|
+
processedData = computed(() => {
|
|
80
|
+
const data = this.data();
|
|
81
|
+
// Handle both single node and array formats
|
|
82
|
+
return Array.isArray(data) ? { id: 'root', children: data } : data;
|
|
83
|
+
});
|
|
84
|
+
// Check if custom template is available
|
|
85
|
+
hasCustomTemplate = computed(() => {
|
|
86
|
+
return Boolean(this.customNodeTemplate() || this.nodeTemplate());
|
|
87
|
+
});
|
|
88
|
+
effectiveOptions = computed(() => {
|
|
89
|
+
return {
|
|
90
|
+
...this.configToken,
|
|
91
|
+
...this.options(),
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
constructor() {
|
|
95
|
+
// Dynamically load D3 and initialize the chart when the component is ready
|
|
96
|
+
afterNextRender(() => {
|
|
97
|
+
this._initialized.set(true);
|
|
98
|
+
this.loadD3();
|
|
99
|
+
this.initializeExpandedState();
|
|
100
|
+
});
|
|
101
|
+
// Watch for changes to redraw the chart
|
|
102
|
+
effect(() => {
|
|
103
|
+
// Access these to track them
|
|
104
|
+
this.processedData();
|
|
105
|
+
this.nodeTemplate();
|
|
106
|
+
this.customNodeTemplate();
|
|
107
|
+
this.effectiveOptions();
|
|
108
|
+
this._expandedNodes();
|
|
109
|
+
if (this._initialized() && this.d3 && this._rendered()) {
|
|
110
|
+
this.createChart();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Initialize the expanded state for all nodes
|
|
116
|
+
*/
|
|
117
|
+
initializeExpandedState() {
|
|
118
|
+
const newExpandedNodes = new Map();
|
|
119
|
+
const data = this.processedData();
|
|
120
|
+
const expandAll = this.effectiveOptions().expandAll;
|
|
121
|
+
// Helper function to recursively process nodes
|
|
122
|
+
const processNode = (node) => {
|
|
123
|
+
if (node.children && node.children.length > 0) {
|
|
124
|
+
// Set initial expansion state based on node property or global option
|
|
125
|
+
newExpandedNodes.set(node.id, node.isExpanded !== undefined ? node.isExpanded : expandAll);
|
|
126
|
+
// Process children
|
|
127
|
+
node.children.forEach((child) => processNode(child));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
// Start processing
|
|
131
|
+
if (data.children) {
|
|
132
|
+
data.children.forEach((node) => processNode(node));
|
|
133
|
+
}
|
|
134
|
+
else if (data.id) {
|
|
135
|
+
processNode(data);
|
|
136
|
+
}
|
|
137
|
+
this._expandedNodes.set(newExpandedNodes);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Dynamically load D3.js to reduce initial bundle size
|
|
141
|
+
*/
|
|
142
|
+
async loadD3() {
|
|
143
|
+
try {
|
|
144
|
+
this.d3 = await import('d3');
|
|
145
|
+
if (this._initialized() && this.chartContainer()) {
|
|
146
|
+
this.updateDimensions();
|
|
147
|
+
this.createChart();
|
|
148
|
+
this._rendered.set(true);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error('Failed to load D3.js:', error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Update dimensions based on container size
|
|
157
|
+
*/
|
|
158
|
+
updateDimensions() {
|
|
159
|
+
const container = this.chartContainer().nativeElement;
|
|
160
|
+
const options = this.effectiveOptions();
|
|
161
|
+
const containerRect = container.getBoundingClientRect();
|
|
162
|
+
const width = options.width || Math.max(containerRect.width, 300);
|
|
163
|
+
const height = options.height || Math.max(containerRect.height, 400);
|
|
164
|
+
this._dimensions.set({ width, height });
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create and render the hierarchy chart
|
|
168
|
+
*/
|
|
169
|
+
createChart() {
|
|
170
|
+
if (!this.d3 || !this.processedData()) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Clear any existing chart
|
|
174
|
+
this.clearChart();
|
|
175
|
+
const container = this.chartContainer().nativeElement;
|
|
176
|
+
const options = this.effectiveOptions();
|
|
177
|
+
const dimensions = this._dimensions();
|
|
178
|
+
const expandedNodes = this._expandedNodes();
|
|
179
|
+
this.ngZone.runOutsideAngular(() => {
|
|
180
|
+
// Get dimensions and options
|
|
181
|
+
const { width, height } = dimensions;
|
|
182
|
+
const margin = options.margin || { top: 40, right: 120, bottom: 40, left: 120 };
|
|
183
|
+
const nodeWidth = options.nodeWidth || 120;
|
|
184
|
+
const nodeHeight = options.nodeHeight || 60;
|
|
185
|
+
const nodeSpacingX = options.nodeSpacingX || 80;
|
|
186
|
+
const nodeSpacingY = options.nodeSpacingY || 120;
|
|
187
|
+
const isHorizontal = options.direction === 'horizontal';
|
|
188
|
+
// Create SVG container with viewBox for responsiveness
|
|
189
|
+
const svg = this.d3
|
|
190
|
+
.select(container)
|
|
191
|
+
.append('svg')
|
|
192
|
+
.attr('width', '100%')
|
|
193
|
+
.attr('height', '100%')
|
|
194
|
+
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
195
|
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
196
|
+
.style('overflow', 'visible')
|
|
197
|
+
.classed(options.className || '', !!options.className);
|
|
198
|
+
// Create hierarchy layout from data
|
|
199
|
+
const rootData = this.processedData();
|
|
200
|
+
const root = this.d3.hierarchy(rootData);
|
|
201
|
+
// Apply expansion state to nodes
|
|
202
|
+
root.descendants().forEach((node) => {
|
|
203
|
+
if (node.data.id !== 'root' && node.children) {
|
|
204
|
+
if (expandedNodes.has(node.data.id) && !expandedNodes.get(node.data.id)) {
|
|
205
|
+
node._children = node.children; // Store children
|
|
206
|
+
node.children = null; // Collapse node
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Set up the tree layout based on direction option
|
|
211
|
+
const treeLayout = this.d3
|
|
212
|
+
.tree()
|
|
213
|
+
.nodeSize([
|
|
214
|
+
isHorizontal ? nodeHeight + nodeSpacingY : nodeWidth + nodeSpacingX,
|
|
215
|
+
isHorizontal ? nodeWidth + nodeSpacingX : nodeHeight + nodeSpacingY,
|
|
216
|
+
])
|
|
217
|
+
.separation((a, b) => {
|
|
218
|
+
return a.parent === b.parent ? 1.2 : 1.5;
|
|
219
|
+
});
|
|
220
|
+
treeLayout(root);
|
|
221
|
+
// Store the current tree layout data for future updates
|
|
222
|
+
this._chartData.set({
|
|
223
|
+
root,
|
|
224
|
+
treeLayout,
|
|
225
|
+
isHorizontal,
|
|
226
|
+
nodeWidth,
|
|
227
|
+
nodeHeight,
|
|
228
|
+
});
|
|
229
|
+
// Adjust root position based on direction
|
|
230
|
+
const rootX = isHorizontal ? margin.top : width / 2;
|
|
231
|
+
const rootY = isHorizontal ? margin.left : margin.top;
|
|
232
|
+
// Calculate bounds of the tree after layout
|
|
233
|
+
let minX = Infinity;
|
|
234
|
+
let maxX = -Infinity;
|
|
235
|
+
let minY = Infinity;
|
|
236
|
+
let maxY = -Infinity;
|
|
237
|
+
root.descendants().forEach((d) => {
|
|
238
|
+
if (d.data.id === 'root')
|
|
239
|
+
return;
|
|
240
|
+
const x = isHorizontal ? d.x : d.x;
|
|
241
|
+
const y = isHorizontal ? d.y : d.y;
|
|
242
|
+
minX = Math.min(minX, x);
|
|
243
|
+
maxX = Math.max(maxX, x);
|
|
244
|
+
minY = Math.min(minY, y);
|
|
245
|
+
maxY = Math.max(maxY, y);
|
|
246
|
+
});
|
|
247
|
+
// Calculate the center of the tree
|
|
248
|
+
const centerX = (minX + maxX) / 2;
|
|
249
|
+
const centerY = (minY + maxY) / 2;
|
|
250
|
+
// Create a group for the chart content with proper centering
|
|
251
|
+
const g = svg
|
|
252
|
+
.append('g')
|
|
253
|
+
.attr('transform', isHorizontal
|
|
254
|
+
? `translate(${margin.left}, ${height / 2 - centerX})`
|
|
255
|
+
: `translate(${width / 2}, ${margin.top})`);
|
|
256
|
+
// Draw links between nodes
|
|
257
|
+
const links = g
|
|
258
|
+
.selectAll('.link')
|
|
259
|
+
.data(root.links().filter((d) => d.source.data.id !== 'root'))
|
|
260
|
+
.enter()
|
|
261
|
+
.append('path')
|
|
262
|
+
.attr('class', 'ax-hierarchy-chart-link')
|
|
263
|
+
.attr('d', (d) => {
|
|
264
|
+
// Get the link style from options
|
|
265
|
+
const linkStyle = options.linkStyle || 'curved';
|
|
266
|
+
// Source and target coordinates based on direction
|
|
267
|
+
const sourceX = isHorizontal ? d.source.y : d.source.x;
|
|
268
|
+
const sourceY = isHorizontal ? d.source.x : d.source.y;
|
|
269
|
+
const targetX = isHorizontal ? d.target.y : d.target.x;
|
|
270
|
+
const targetY = isHorizontal ? d.target.x : d.target.y;
|
|
271
|
+
// Variables for rounded corners
|
|
272
|
+
let xDistance, yDistance, cornerRadius, midX, midY;
|
|
273
|
+
switch (linkStyle) {
|
|
274
|
+
case 'straight':
|
|
275
|
+
// Direct straight line
|
|
276
|
+
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
|
|
277
|
+
case 'rounded':
|
|
278
|
+
// Curved line with rounded corners
|
|
279
|
+
xDistance = Math.abs(targetX - sourceX);
|
|
280
|
+
yDistance = Math.abs(targetY - sourceY);
|
|
281
|
+
cornerRadius = Math.min(xDistance, yDistance) * 0.2; // Reduced radius for better appearance
|
|
282
|
+
if (isHorizontal) {
|
|
283
|
+
// For horizontal layout
|
|
284
|
+
const halfDistance = (targetX - sourceX) / 2;
|
|
285
|
+
const xMid = sourceX + halfDistance;
|
|
286
|
+
return `
|
|
287
|
+
M${sourceX},${sourceY}
|
|
288
|
+
H${xMid - cornerRadius}
|
|
289
|
+
Q${xMid},${sourceY} ${xMid},${sourceY + Math.sign(targetY - sourceY) * cornerRadius}
|
|
290
|
+
V${targetY - Math.sign(targetY - sourceY) * cornerRadius}
|
|
291
|
+
Q${xMid},${targetY} ${xMid + cornerRadius},${targetY}
|
|
292
|
+
H${targetX}
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// For vertical layout
|
|
297
|
+
const halfDistance = (targetY - sourceY) / 2;
|
|
298
|
+
const yMid = sourceY + halfDistance;
|
|
299
|
+
return `
|
|
300
|
+
M${sourceX},${sourceY}
|
|
301
|
+
V${yMid - cornerRadius}
|
|
302
|
+
Q${sourceX},${yMid} ${sourceX + Math.sign(targetX - sourceX) * cornerRadius},${yMid}
|
|
303
|
+
H${targetX - Math.sign(targetX - sourceX) * cornerRadius}
|
|
304
|
+
Q${targetX},${yMid} ${targetX},${yMid + cornerRadius}
|
|
305
|
+
V${targetY}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
case 'step':
|
|
309
|
+
// L-shaped stepped line
|
|
310
|
+
if (isHorizontal) {
|
|
311
|
+
return `M${sourceX},${sourceY}H${(sourceX + targetX) / 2}V${targetY}H${targetX}`;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
return `M${sourceX},${sourceY}V${(sourceY + targetY) / 2}H${targetX}V${targetY}`;
|
|
315
|
+
}
|
|
316
|
+
case 'curved':
|
|
317
|
+
default:
|
|
318
|
+
// Default curved line using D3's built-in link generator
|
|
319
|
+
if (isHorizontal) {
|
|
320
|
+
return this.d3
|
|
321
|
+
.linkHorizontal()
|
|
322
|
+
.x((d) => d.y)
|
|
323
|
+
.y((d) => d.x)(d);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
return this.d3
|
|
327
|
+
.linkVertical()
|
|
328
|
+
.x((d) => d.x)
|
|
329
|
+
.y((d) => d.y)(d);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.attr('stroke', options.linkColor)
|
|
334
|
+
.attr('stroke-width', options.linkWidth)
|
|
335
|
+
.attr('fill', 'none')
|
|
336
|
+
.attr('opacity', 1);
|
|
337
|
+
// Determine which template to use for rendering nodes
|
|
338
|
+
const useCustomTemplate = this.hasCustomTemplate();
|
|
339
|
+
const templateToUse = this.nodeTemplate() || this.customNodeTemplate();
|
|
340
|
+
// Create a map to store node elements for future updates
|
|
341
|
+
const nodeElementsMap = new Map();
|
|
342
|
+
// Create node groups
|
|
343
|
+
const nodeGroups = g
|
|
344
|
+
.selectAll('.node-group')
|
|
345
|
+
.data(root.descendants().filter((d) => d.data.id !== 'root'))
|
|
346
|
+
.enter()
|
|
347
|
+
.append('g')
|
|
348
|
+
.attr('class', 'ax-hierarchy-chart-node-group')
|
|
349
|
+
.attr('data-node-id', (d) => d.data.id) // Add a data attribute for easier selection
|
|
350
|
+
.attr('transform', (d) => {
|
|
351
|
+
const x = isHorizontal ? d.y : d.x;
|
|
352
|
+
const y = isHorizontal ? d.x : d.y;
|
|
353
|
+
return `translate(${x},${y})`;
|
|
354
|
+
})
|
|
355
|
+
.attr('opacity', 1); // Display nodes immediately with full opacity
|
|
356
|
+
// Store node elements in the map
|
|
357
|
+
nodeGroups.each(function (d) {
|
|
358
|
+
nodeElementsMap.set(d.data.id, this);
|
|
359
|
+
});
|
|
360
|
+
// Update the node elements signal
|
|
361
|
+
this._nodeElements.set(nodeElementsMap);
|
|
362
|
+
if (useCustomTemplate && templateToUse) {
|
|
363
|
+
// Render custom node templates
|
|
364
|
+
nodeGroups.each((d, i, nodes) => {
|
|
365
|
+
const node = d.data;
|
|
366
|
+
const element = nodes[i];
|
|
367
|
+
const hasChildren = !!(node.children && node.children.length > 0);
|
|
368
|
+
const isExpanded = hasChildren && this._expandedNodes().has(node.id) && this._expandedNodes().get(node.id);
|
|
369
|
+
// Enhance the node with expanded state and toggle function
|
|
370
|
+
node.expanded = isExpanded || false;
|
|
371
|
+
node.toggleExpanded = () => this.toggleNode(node.id);
|
|
372
|
+
// Create a foreignObject for the template
|
|
373
|
+
const foreignObject = this.d3
|
|
374
|
+
.select(element)
|
|
375
|
+
.append('foreignObject')
|
|
376
|
+
.attr('x', -nodeWidth / 2)
|
|
377
|
+
.attr('y', -nodeHeight / 2)
|
|
378
|
+
.attr('width', nodeWidth)
|
|
379
|
+
.attr('height', nodeHeight)
|
|
380
|
+
.attr('class', 'ax-hierarchy-chart-node-container')
|
|
381
|
+
.on('click', (event) => {
|
|
382
|
+
// Don't propagate the event if it's coming from an interactive element
|
|
383
|
+
// like a button inside the template
|
|
384
|
+
const target = event.target;
|
|
385
|
+
const isInteractive = target.tagName === 'BUTTON' || target.tagName === 'A' || target.closest('button, a, [role="button"]');
|
|
386
|
+
if (!isInteractive) {
|
|
387
|
+
this.handleNodeClick(event, d);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// Create a div to hold the template
|
|
391
|
+
const div = foreignObject
|
|
392
|
+
.append('xhtml:div')
|
|
393
|
+
.attr('class', 'ax-hierarchy-chart-node-content')
|
|
394
|
+
.style('width', '100%')
|
|
395
|
+
.style('height', '100%');
|
|
396
|
+
// Render the template into the div
|
|
397
|
+
this.ngZone.run(() => {
|
|
398
|
+
// Create context with enhanced node as the implicit value
|
|
399
|
+
const context = {
|
|
400
|
+
$implicit: node,
|
|
401
|
+
};
|
|
402
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(templateToUse, context);
|
|
403
|
+
viewRef.detectChanges();
|
|
404
|
+
// Append template contents to the div
|
|
405
|
+
const nodes = viewRef.rootNodes;
|
|
406
|
+
for (const node of nodes) {
|
|
407
|
+
div.node().appendChild(node);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// Render enhanced default nodes (rectangles with text and icons)
|
|
414
|
+
nodeGroups.each((d, i, nodes) => {
|
|
415
|
+
const group = this.d3.select(nodes[i]);
|
|
416
|
+
const node = d.data;
|
|
417
|
+
const hasChildren = !!(node.children && node.children.length > 0);
|
|
418
|
+
// Node background rect with rounded corners
|
|
419
|
+
group
|
|
420
|
+
.append('rect')
|
|
421
|
+
.attr('x', -nodeWidth / 2)
|
|
422
|
+
.attr('y', -nodeHeight / 2)
|
|
423
|
+
.attr('width', nodeWidth)
|
|
424
|
+
.attr('height', nodeHeight)
|
|
425
|
+
.attr('rx', 6)
|
|
426
|
+
.attr('ry', 6)
|
|
427
|
+
.attr('fill', node.color || options.nodeColor)
|
|
428
|
+
.attr('stroke', options.nodeStrokeColor)
|
|
429
|
+
.attr('stroke-width', options.nodeStrokeWidth)
|
|
430
|
+
.attr('class', 'ax-hierarchy-chart-node');
|
|
431
|
+
// Main title/name
|
|
432
|
+
group
|
|
433
|
+
.append('text')
|
|
434
|
+
.attr('y', -10)
|
|
435
|
+
.attr('text-anchor', 'middle')
|
|
436
|
+
.attr('class', 'ax-hierarchy-chart-node-title')
|
|
437
|
+
.attr('fill', 'white')
|
|
438
|
+
.attr('font-weight', 'bold')
|
|
439
|
+
.text(node.name || node.label || '');
|
|
440
|
+
// Subtitle if provided
|
|
441
|
+
if (node.subtitle) {
|
|
442
|
+
group
|
|
443
|
+
.append('text')
|
|
444
|
+
.attr('y', 10)
|
|
445
|
+
.attr('text-anchor', 'middle')
|
|
446
|
+
.attr('class', 'ax-hierarchy-chart-node-subtitle')
|
|
447
|
+
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
|
448
|
+
.attr('font-size', '0.8em')
|
|
449
|
+
.text(node.subtitle);
|
|
450
|
+
}
|
|
451
|
+
// Type label if provided
|
|
452
|
+
if (node.type) {
|
|
453
|
+
group
|
|
454
|
+
.append('text')
|
|
455
|
+
.attr('y', 25)
|
|
456
|
+
.attr('text-anchor', 'middle')
|
|
457
|
+
.attr('class', 'ax-hierarchy-chart-node-type')
|
|
458
|
+
.attr('fill', 'rgba(255, 255, 255, 0.7)')
|
|
459
|
+
.attr('font-size', '0.7em')
|
|
460
|
+
.text(node.type);
|
|
461
|
+
}
|
|
462
|
+
// Add expand/collapse indicator if node has children
|
|
463
|
+
if (hasChildren && options.collapsible) {
|
|
464
|
+
const isExpanded = this._expandedNodes().has(node.id) && this._expandedNodes().get(node.id);
|
|
465
|
+
// Toggle button with better styling
|
|
466
|
+
group
|
|
467
|
+
.append('circle')
|
|
468
|
+
.attr('r', 12)
|
|
469
|
+
.attr('cx', nodeWidth / 2 - 10)
|
|
470
|
+
.attr('cy', -nodeHeight / 2 + 12)
|
|
471
|
+
.attr('fill', 'white')
|
|
472
|
+
.attr('stroke', options.nodeStrokeColor)
|
|
473
|
+
.attr('stroke-width', 1)
|
|
474
|
+
.attr('class', 'ax-hierarchy-chart-toggle-indicator')
|
|
475
|
+
.style('cursor', 'pointer')
|
|
476
|
+
.on('click', (event) => {
|
|
477
|
+
event.stopPropagation();
|
|
478
|
+
this.toggleNode(node.id);
|
|
479
|
+
});
|
|
480
|
+
// Toggle icon
|
|
481
|
+
group
|
|
482
|
+
.append('text')
|
|
483
|
+
.attr('x', nodeWidth / 2 - 10)
|
|
484
|
+
.attr('y', -nodeHeight / 2 + 12)
|
|
485
|
+
.attr('dy', '0.32em')
|
|
486
|
+
.attr('text-anchor', 'middle')
|
|
487
|
+
.attr('class', 'ax-hierarchy-chart-toggle-icon')
|
|
488
|
+
.attr('fill', node.color || options.nodeColor)
|
|
489
|
+
.attr('font-weight', 'bold')
|
|
490
|
+
.style('cursor', 'pointer')
|
|
491
|
+
.text(isExpanded ? '−' : '+')
|
|
492
|
+
.on('click', (event) => {
|
|
493
|
+
event.stopPropagation();
|
|
494
|
+
this.toggleNode(node.id);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
// Icon if provided
|
|
498
|
+
if (node.icon) {
|
|
499
|
+
group
|
|
500
|
+
.append('text')
|
|
501
|
+
.attr('x', -nodeWidth / 2 + 15)
|
|
502
|
+
.attr('y', 0)
|
|
503
|
+
.attr('dy', '0.32em')
|
|
504
|
+
.attr('class', 'ax-hierarchy-chart-node-icon')
|
|
505
|
+
.attr('fill', 'white')
|
|
506
|
+
.attr('class', node.icon);
|
|
507
|
+
}
|
|
508
|
+
// Add click handler to the whole node
|
|
509
|
+
group.on('click', (event) => {
|
|
510
|
+
this.handleNodeClick(event, d);
|
|
511
|
+
});
|
|
512
|
+
// Add tooltip if enabled
|
|
513
|
+
if (options.showTooltip && (node.tooltip || node.description)) {
|
|
514
|
+
group.append('title').text(node.tooltip || node.description || '');
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Toggle node expansion state
|
|
522
|
+
*/
|
|
523
|
+
toggleNode(nodeId) {
|
|
524
|
+
const expandedNodes = new Map(this._expandedNodes());
|
|
525
|
+
const currentState = expandedNodes.get(nodeId);
|
|
526
|
+
const newState = currentState === undefined ? false : !currentState;
|
|
527
|
+
expandedNodes.set(nodeId, newState);
|
|
528
|
+
this._expandedNodes.set(expandedNodes);
|
|
529
|
+
// Emit toggle event
|
|
530
|
+
this.findNodeById(this.processedData(), nodeId).then((node) => {
|
|
531
|
+
if (node) {
|
|
532
|
+
this.nodeToggle.emit({
|
|
533
|
+
node: node,
|
|
534
|
+
expanded: newState,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
// Update toggle indicator immediately for better UX
|
|
539
|
+
this.updateToggleIndicator(nodeId, newState);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Update just the toggle indicator without redrawing the chart
|
|
543
|
+
*/
|
|
544
|
+
updateToggleIndicator(nodeId, expanded) {
|
|
545
|
+
if (!this.d3 || !this.chartContainer())
|
|
546
|
+
return;
|
|
547
|
+
this.ngZone.runOutsideAngular(() => {
|
|
548
|
+
const container = this.chartContainer().nativeElement;
|
|
549
|
+
const svg = container.querySelector('svg');
|
|
550
|
+
if (!svg)
|
|
551
|
+
return;
|
|
552
|
+
// Find and update the toggle indicator for the specific node
|
|
553
|
+
const nodeGroup = this.d3.select(svg).select(`[data-node-id="${nodeId}"]`);
|
|
554
|
+
if (nodeGroup.empty())
|
|
555
|
+
return;
|
|
556
|
+
// Update the toggle icon (if it exists)
|
|
557
|
+
const toggleIcon = nodeGroup.select('.ax-hierarchy-chart-toggle-icon');
|
|
558
|
+
if (!toggleIcon.empty()) {
|
|
559
|
+
toggleIcon.text(expanded ? '−' : '+');
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
// Force effect to run that will update the chart
|
|
563
|
+
// This leverages Angular's reactivity rather than a direct method call
|
|
564
|
+
// which is more consistent with Angular's reactive approach
|
|
565
|
+
const expandedNodes = new Map(this._expandedNodes());
|
|
566
|
+
this._expandedNodes.set(expandedNodes);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Find a node by ID
|
|
570
|
+
*/
|
|
571
|
+
async findNodeById(data, id) {
|
|
572
|
+
if (data.id === id) {
|
|
573
|
+
return data;
|
|
574
|
+
}
|
|
575
|
+
if (data.children) {
|
|
576
|
+
for (const child of data.children) {
|
|
577
|
+
const found = await this.findNodeById(child, id);
|
|
578
|
+
if (found) {
|
|
579
|
+
return found;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Handle node click events
|
|
587
|
+
*/
|
|
588
|
+
handleNodeClick(event, d) {
|
|
589
|
+
const node = d.data;
|
|
590
|
+
// Emit click event
|
|
591
|
+
this.itemClick.emit({
|
|
592
|
+
event: event,
|
|
593
|
+
item: node,
|
|
594
|
+
element: event.currentTarget,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Clear existing chart
|
|
599
|
+
*/
|
|
600
|
+
clearChart() {
|
|
601
|
+
const container = this.chartContainer()?.nativeElement;
|
|
602
|
+
if (!container)
|
|
603
|
+
return;
|
|
604
|
+
// Remove existing SVG
|
|
605
|
+
const svg = container.querySelector('svg');
|
|
606
|
+
if (svg) {
|
|
607
|
+
svg.remove();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Get D3 easing function from string name
|
|
612
|
+
*/
|
|
613
|
+
getEasingFunction(easing) {
|
|
614
|
+
if (!easing)
|
|
615
|
+
return this.d3.easeCubicInOut;
|
|
616
|
+
switch (easing) {
|
|
617
|
+
case 'linear':
|
|
618
|
+
return this.d3.easeLinear;
|
|
619
|
+
case 'ease':
|
|
620
|
+
return this.d3.easePolyInOut;
|
|
621
|
+
case 'ease-in':
|
|
622
|
+
return this.d3.easePolyIn;
|
|
623
|
+
case 'ease-out':
|
|
624
|
+
return this.d3.easePolyOut;
|
|
625
|
+
case 'ease-in-out':
|
|
626
|
+
return this.d3.easePolyInOut;
|
|
627
|
+
case 'cubic':
|
|
628
|
+
return this.d3.easeCubic;
|
|
629
|
+
case 'cubic-in':
|
|
630
|
+
return this.d3.easeCubicIn;
|
|
631
|
+
case 'cubic-out':
|
|
632
|
+
return this.d3.easeCubicOut;
|
|
633
|
+
case 'cubic-in-out':
|
|
634
|
+
return this.d3.easeCubicInOut;
|
|
635
|
+
default:
|
|
636
|
+
return this.d3.easeCubicInOut;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: AXHierarchyChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
640
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.9", type: AXHierarchyChartComponent, isStandalone: true, selector: "ax-hierarchy-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", nodeToggle: "nodeToggle" }, queries: [{ propertyName: "customNodeTemplate", first: true, predicate: ["nodeTemplate"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "chartContainer", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div axPanView class=\"ax-hierarchy-chart\" #chartContainer></div>\n", styles: [".ax-hierarchy-chart{width:100%;height:100%;min-height:300px;position:relative;overflow:visible}.ax-hierarchy-chart svg{display:block;width:100%;height:100%;overflow:visible}.ax-hierarchy-chart .ax-hierarchy-chart-link{fill:none;stroke-width:1.5px;stroke-linecap:round}.ax-hierarchy-chart .ax-hierarchy-chart-link:hover{stroke-opacity:.8}.ax-hierarchy-chart .ax-hierarchy-chart-node-group{cursor:pointer}.ax-hierarchy-chart .ax-hierarchy-chart-node-group:hover .ax-hierarchy-chart-node{filter:brightness(1.05)}.ax-hierarchy-chart .ax-hierarchy-chart-node{fill:rgba(var(--ax-sys-color-primary-500));stroke:rgba(var(--ax-sys-color-primary-400));stroke-width:1.5px;transition:all .2s ease}.ax-hierarchy-chart .ax-hierarchy-chart-node-text{font-size:12px;font-weight:600;fill:rgba(var(--ax-sys-color-on-primary-text));-webkit-user-select:none;user-select:none;pointer-events:none}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-indicator{cursor:pointer;transition:all .2s ease}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-indicator:hover{fill:rgba(var(--ax-sys-color-surface-300))}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-icon{font-size:14px;font-weight:700;fill:rgba(var(--ax-sys-color-on-surface-text));-webkit-user-select:none;user-select:none;pointer-events:none}.ax-hierarchy-chart .ax-hierarchy-chart-node-container{overflow:visible}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: AXPanViewDirective, selector: "[axPanView]", inputs: ["zoomStep", "minZoom", "maxZoom", "freeMode", "fitContent", "disablePan", "disableZoom", "wrapperClasses", "panX", "panY", "zoom"], outputs: ["panXChange", "panYChange", "zoomChange", "positionChange"], exportAs: ["axPanView"] }] });
|
|
641
|
+
}
|
|
642
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: AXHierarchyChartComponent, decorators: [{
|
|
643
|
+
type: Component,
|
|
644
|
+
args: [{ selector: 'ax-hierarchy-chart', standalone: true, imports: [CommonModule, AXPanViewDirective], template: "<div axPanView class=\"ax-hierarchy-chart\" #chartContainer></div>\n", styles: [".ax-hierarchy-chart{width:100%;height:100%;min-height:300px;position:relative;overflow:visible}.ax-hierarchy-chart svg{display:block;width:100%;height:100%;overflow:visible}.ax-hierarchy-chart .ax-hierarchy-chart-link{fill:none;stroke-width:1.5px;stroke-linecap:round}.ax-hierarchy-chart .ax-hierarchy-chart-link:hover{stroke-opacity:.8}.ax-hierarchy-chart .ax-hierarchy-chart-node-group{cursor:pointer}.ax-hierarchy-chart .ax-hierarchy-chart-node-group:hover .ax-hierarchy-chart-node{filter:brightness(1.05)}.ax-hierarchy-chart .ax-hierarchy-chart-node{fill:rgba(var(--ax-sys-color-primary-500));stroke:rgba(var(--ax-sys-color-primary-400));stroke-width:1.5px;transition:all .2s ease}.ax-hierarchy-chart .ax-hierarchy-chart-node-text{font-size:12px;font-weight:600;fill:rgba(var(--ax-sys-color-on-primary-text));-webkit-user-select:none;user-select:none;pointer-events:none}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-indicator{cursor:pointer;transition:all .2s ease}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-indicator:hover{fill:rgba(var(--ax-sys-color-surface-300))}.ax-hierarchy-chart .ax-hierarchy-chart-toggle-icon{font-size:14px;font-weight:700;fill:rgba(var(--ax-sys-color-on-surface-text));-webkit-user-select:none;user-select:none;pointer-events:none}.ax-hierarchy-chart .ax-hierarchy-chart-node-container{overflow:visible}\n"] }]
|
|
645
|
+
}], ctorParameters: () => [] });
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Generated bundle index. Do not edit.
|
|
649
|
+
*/
|
|
650
|
+
|
|
651
|
+
export { AXHierarchyChartComponent, AXHierarchyChartDefaultConfig, AX_HIERARCHY_CHART_CONFIG, hierarchyChartConfig };
|
|
652
|
+
//# sourceMappingURL=acorex-charts-hierarchy-chart.mjs.map
|