@acorex/charts 0.0.1

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