@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,772 @@
1
+ import { NXComponent } from '@acorex/cdk/common';
2
+ import { AX_CHART_COLOR_PALETTE, getChartColor } from '@acorex/charts';
3
+ import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
4
+ import { AXPlatform } from '@acorex/core/platform';
5
+ import * as i0 from '@angular/core';
6
+ import { InjectionToken, inject, input, output, viewChild, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
7
+ import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
8
+ import { set } from 'lodash-es';
9
+
10
+ const AXBarChartDefaultConfig = {
11
+ showXAxis: true,
12
+ showYAxis: true,
13
+ showGrid: true,
14
+ showDataLabels: true,
15
+ showTooltip: true,
16
+ barWidth: 80,
17
+ cornerRadius: 4,
18
+ animationDuration: 800,
19
+ animationEasing: 'cubic-out',
20
+ };
21
+ const AX_BAR_CHART_CONFIG = new InjectionToken('AX_BAR_CHART_CONFIG', {
22
+ providedIn: 'root',
23
+ factory: () => {
24
+ const global = inject(AX_GLOBAL_CONFIG);
25
+ set(global, 'chart.barChart', AXBarChartDefaultConfig);
26
+ return AXBarChartDefaultConfig;
27
+ },
28
+ });
29
+ function barChartConfig(config = {}) {
30
+ const result = {
31
+ ...AXBarChartDefaultConfig,
32
+ ...config,
33
+ };
34
+ return result;
35
+ }
36
+
37
+ /**
38
+ * Bar Chart Component
39
+ * Renders data as vertical bars with interactive hover effects and animations
40
+ */
41
+ class AXBarChartComponent extends NXComponent {
42
+ // Inputs
43
+ /** Chart data input */
44
+ data = input([]);
45
+ /** Chart options input */
46
+ options = input({});
47
+ // Outputs
48
+ /** Emitted when a bar is clicked */
49
+ barClick = output();
50
+ // Chart container reference
51
+ chartContainerEl = viewChild.required('chartContainer');
52
+ // D3 reference - loaded asynchronously
53
+ d3;
54
+ // Chart elements
55
+ svg;
56
+ chart;
57
+ xScale;
58
+ yScale;
59
+ xAxis;
60
+ yAxis;
61
+ // Chart dimensions
62
+ width;
63
+ height;
64
+ margin = { top: 20, right: 20, bottom: 30, left: 40 };
65
+ // Animation state
66
+ _initialAnimationComplete = signal(false);
67
+ // Tooltip state
68
+ _tooltipVisible = signal(false);
69
+ _tooltipPosition = signal({ x: 0, y: 0 });
70
+ _tooltipData = signal({
71
+ title: '',
72
+ value: '0',
73
+ percentage: '0%',
74
+ color: '',
75
+ });
76
+ // Signals for component state
77
+ _initialized = signal(false);
78
+ _rendered = signal(false);
79
+ // Tooltip accessors
80
+ tooltipVisible = this._tooltipVisible.asReadonly();
81
+ tooltipPosition = this._tooltipPosition.asReadonly();
82
+ tooltipData = this._tooltipData.asReadonly();
83
+ // Inject configuration
84
+ configToken = inject(AX_BAR_CHART_CONFIG);
85
+ // Inject the chart colors
86
+ chartColors = inject(AX_CHART_COLOR_PALETTE);
87
+ // Inject AXPlatform
88
+ platform = inject(AXPlatform);
89
+ // Configuration with defaults
90
+ effectiveOptions = computed(() => {
91
+ return {
92
+ ...this.configToken,
93
+ ...this.options(),
94
+ };
95
+ });
96
+ // Track hidden bars
97
+ hiddenBars = new Set();
98
+ constructor() {
99
+ super();
100
+ // Dynamically load D3 and initialize the chart when the component is ready
101
+ afterNextRender(() => {
102
+ this._initialized.set(true);
103
+ this.loadD3();
104
+ // Create chart after D3 is loaded and container is available
105
+ if (this.d3 && this.chartContainerEl()) {
106
+ this.createChart();
107
+ this._rendered.set(true);
108
+ }
109
+ });
110
+ // Watch for changes to redraw the chart
111
+ effect(() => {
112
+ // Access inputs to track them
113
+ this.data();
114
+ this.effectiveOptions();
115
+ // Only update if already rendered
116
+ if (this._rendered()) {
117
+ this.updateChart();
118
+ }
119
+ });
120
+ }
121
+ ngOnDestroy() {
122
+ this.cleanupChart();
123
+ }
124
+ /**
125
+ * Loads D3.js dynamically
126
+ */
127
+ async loadD3() {
128
+ try {
129
+ this.d3 = await import('d3');
130
+ // If container is ready, create chart
131
+ if (this._initialized() && this.chartContainerEl()) {
132
+ this.createChart();
133
+ this._rendered.set(true);
134
+ }
135
+ }
136
+ catch (error) {
137
+ console.error('Failed to load D3.js:', error);
138
+ }
139
+ }
140
+ /**
141
+ * Creates the bar chart SVG and renders all elements
142
+ */
143
+ createChart() {
144
+ if (!this.d3 || !this.chartContainerEl()?.nativeElement)
145
+ return;
146
+ const containerElement = this.chartContainerEl().nativeElement;
147
+ const data = this.data() || [];
148
+ // Filter out hidden bars for visible data
149
+ const visibleData = data.filter((d) => !this.isBarHidden(d.id));
150
+ // Clear existing chart
151
+ this.d3.select(containerElement).selectAll('svg').remove();
152
+ // Early return if no data
153
+ if (!data.length) {
154
+ this.showNoDataMessage(containerElement);
155
+ return;
156
+ }
157
+ // Early return if all bars are hidden
158
+ if (visibleData.length === 0) {
159
+ this.showAllBarsHiddenMessage(containerElement);
160
+ return;
161
+ }
162
+ // Get options and setup dimensions
163
+ const chartOptions = this.effectiveOptions();
164
+ this.setupDimensions(containerElement, chartOptions);
165
+ // Create scales and axes using visible data
166
+ this.setupScales(visibleData);
167
+ this.createAxes(chartOptions);
168
+ // Render the bars
169
+ this.renderBars(data);
170
+ }
171
+ /**
172
+ * Updates the chart when inputs change
173
+ */
174
+ updateChart() {
175
+ this.createChart();
176
+ }
177
+ /**
178
+ * Cleans up chart resources
179
+ */
180
+ cleanupChart() {
181
+ if (this.svg) {
182
+ this.d3?.select(this.chartContainerEl()?.nativeElement).selectAll('svg').remove();
183
+ this.svg = null;
184
+ this.chart = null;
185
+ }
186
+ this._tooltipVisible.set(false);
187
+ }
188
+ /**
189
+ * Sets up chart dimensions and creates SVG with responsive attributes
190
+ */
191
+ setupDimensions(containerElement, options) {
192
+ // Calculate margins based on options
193
+ this.calculateMargins(options);
194
+ // Get container dimensions
195
+ const containerWidth = containerElement.clientWidth;
196
+ const containerHeight = containerElement.clientHeight;
197
+ // If options specify width and height, use those, otherwise default to container size
198
+ const minDim = Math.min(200, containerWidth, containerHeight); // Ensure reasonable minimum
199
+ if (options.width && options.height) {
200
+ // Explicit dimensions provided
201
+ this.width = options.width - this.margin.left - this.margin.right;
202
+ this.height = options.height - this.margin.top - this.margin.bottom;
203
+ }
204
+ else {
205
+ // Responsive dimensions
206
+ this.width = Math.max(containerWidth, minDim) - this.margin.left - this.margin.right;
207
+ this.height = Math.max(containerHeight, minDim) - this.margin.top - this.margin.bottom;
208
+ }
209
+ // Create responsive SVG that scales with its container
210
+ const svg = this.d3
211
+ .select(containerElement)
212
+ .append('svg')
213
+ .attr('width', '100%')
214
+ .attr('height', '100%')
215
+ .attr('viewBox', `0 0 ${this.width + this.margin.left + this.margin.right} ${this.height + this.margin.top + this.margin.bottom}`)
216
+ .attr('preserveAspectRatio', 'xMidYMid meet');
217
+ this.svg = svg;
218
+ // Create chart group with margins
219
+ this.chart = this.svg.append('g').attr('transform', `translate(${this.margin.left},${this.margin.top})`);
220
+ }
221
+ /**
222
+ * Calculates chart margins based on options
223
+ */
224
+ calculateMargins(options) {
225
+ // Start with default margins
226
+ this.margin = {
227
+ top: 20,
228
+ right: 20,
229
+ bottom: 30,
230
+ left: 40,
231
+ };
232
+ // Adjust margins if axis labels are present
233
+ if (options.xAxisLabel) {
234
+ const xLabelLength = options.xAxisLabel.length;
235
+ const extraBottomMargin = Math.min(20, Math.max(10, xLabelLength * 0.8));
236
+ this.margin.bottom = Math.max(this.margin.bottom, 30 + extraBottomMargin);
237
+ }
238
+ if (options.yAxisLabel) {
239
+ const yLabelLength = options.yAxisLabel.length;
240
+ const extraLeftMargin = Math.min(20, Math.max(10, yLabelLength * 0.8));
241
+ this.margin.left = Math.max(this.margin.left, 40 + extraLeftMargin);
242
+ }
243
+ // Ensure minimum margins for axes
244
+ if (options.showXAxis !== false) {
245
+ this.margin.bottom = Math.max(this.margin.bottom, 35);
246
+ }
247
+ if (options.showYAxis !== false) {
248
+ this.margin.left = Math.max(this.margin.left, 45);
249
+ }
250
+ }
251
+ /**
252
+ * Creates x and y scales for the chart
253
+ */
254
+ setupScales(data) {
255
+ // Get the bar width percentage (default 80%)
256
+ const barWidthPercent = this.effectiveOptions().barWidth ?? 60 / 100;
257
+ // Calculate padding based on barWidth (inverse relationship)
258
+ const padding = Math.max(0.1, 1 - barWidthPercent);
259
+ // Create x scale (band scale for categorical data)
260
+ this.xScale = this.d3
261
+ .scaleBand()
262
+ .domain(data.map((d) => d.label))
263
+ .range([0, this.width])
264
+ .padding(padding);
265
+ // Create y scale (linear scale for values)
266
+ this.yScale = this.d3
267
+ .scaleLinear()
268
+ .domain([0, this.d3.max(data, (d) => d.value) || 0])
269
+ .nice()
270
+ .range([this.height, 0]);
271
+ }
272
+ /**
273
+ * Creates x and y axes with grid lines
274
+ */
275
+ createAxes(options) {
276
+ // Only create axes if they are enabled in options
277
+ const showXAxis = options.showXAxis !== false;
278
+ const showYAxis = options.showYAxis !== false;
279
+ const showGrid = options.showGrid !== false;
280
+ const isRtl = this.platform.isRtl();
281
+ // Create a group for all axes
282
+ const axesGroup = this.chart.append('g').attr('class', 'ax-bar-chart-axes');
283
+ if (showXAxis) {
284
+ // Create X axis
285
+ this.xAxis = axesGroup
286
+ .append('g')
287
+ .attr('class', 'ax-bar-chart-axis-x')
288
+ .attr('transform', `translate(0,${this.height})`)
289
+ .call(this.d3.axisBottom(this.xScale));
290
+ // Style the axis text
291
+ const dynamicXAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.width / 45)));
292
+ this.xAxis
293
+ .selectAll('text')
294
+ .style('font-size', `${dynamicXAxisTickFontSize}px`)
295
+ .style('font-weight', '400')
296
+ .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)');
297
+ // Style all lines in the x-axis (path, ticks)
298
+ this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
299
+ // Add X axis label if provided
300
+ if (options.xAxisLabel) {
301
+ const labelY = this.height + this.margin.bottom * 0.8;
302
+ axesGroup
303
+ .append('text')
304
+ .attr('class', 'ax-bar-chart-axis-label ax-x-axis-label')
305
+ .attr('text-anchor', 'middle')
306
+ .attr('dominant-baseline', 'middle')
307
+ .attr('x', this.width / 2)
308
+ .attr('y', labelY)
309
+ .style('font-size', '14px')
310
+ .style('font-weight', '500')
311
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
312
+ .text(options.xAxisLabel);
313
+ }
314
+ }
315
+ if (showYAxis) {
316
+ // Create Y axis
317
+ this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(this.d3.axisLeft(this.yScale));
318
+ // Style the axis text
319
+ const dynamicYAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.height / 30)));
320
+ const yTickTexts = this.yAxis
321
+ .selectAll('text')
322
+ .style('font-size', `${dynamicYAxisTickFontSize}px`)
323
+ .style('font-weight', '400')
324
+ .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)');
325
+ if (isRtl) {
326
+ yTickTexts.attr('text-anchor', 'start');
327
+ }
328
+ // Style all lines in the y-axis (path, ticks)
329
+ this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
330
+ // Add Y axis label if provided
331
+ if (options.yAxisLabel) {
332
+ const labelX = -this.height / 2;
333
+ const labelY = -this.margin.left * 0.8;
334
+ axesGroup
335
+ .append('text')
336
+ .attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
337
+ .attr('text-anchor', 'middle')
338
+ .attr('dominant-baseline', 'middle')
339
+ .attr('transform', 'rotate(-90)')
340
+ .attr('x', labelX)
341
+ .attr('y', labelY)
342
+ .style('font-size', '14px')
343
+ .style('font-weight', '500')
344
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
345
+ .text(options.yAxisLabel);
346
+ }
347
+ }
348
+ if (showGrid) {
349
+ // Add horizontal grid lines
350
+ this.chart
351
+ .append('g')
352
+ .attr('class', 'ax-bar-chart-grid')
353
+ .call(this.d3
354
+ .axisLeft(this.yScale)
355
+ .tickSize(-this.width)
356
+ .tickFormat(() => ''))
357
+ .selectAll('line')
358
+ .style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))')
359
+ .style('stroke-opacity', 0.2);
360
+ // Remove unneeded elements from grid
361
+ this.chart.select('.ax-bar-chart-grid').selectAll('path, text').remove();
362
+ }
363
+ }
364
+ /**
365
+ * Renders the bars with animations
366
+ */
367
+ renderBars(data) {
368
+ // Filter out hidden bars
369
+ const visibleData = data.filter((d) => !this.isBarHidden(d.id));
370
+ const originalFullData = this.data(); // Get the original full data set
371
+ // Reset animation state
372
+ this._initialAnimationComplete.set(false);
373
+ // Get corner radius from options
374
+ const radius = this.effectiveOptions().cornerRadius;
375
+ // Get animation options
376
+ const animationDuration = this.effectiveOptions().animationDuration;
377
+ const animationEasing = this.getEasingFunction(this.effectiveOptions().animationEasing);
378
+ // Create a container for all bars
379
+ const barsContainer = this.chart.append('g').attr('class', 'ax-bar-chart-bars-container');
380
+ // Create groups for each bar to handle transformations
381
+ const barGroups = barsContainer
382
+ .selectAll('.ax-bar-chart-bar-group')
383
+ .data(visibleData)
384
+ .enter()
385
+ .append('g')
386
+ .style('cursor', 'pointer')
387
+ .attr('class', 'ax-bar-chart-bar-group');
388
+ // Add bars inside groups
389
+ const bars = barGroups
390
+ .append('rect')
391
+ .attr('class', 'ax-bar-chart-bar')
392
+ .attr('x', (d) => this.xScale(d.label))
393
+ .attr('width', this.xScale.bandwidth())
394
+ .attr('y', this.height) // Start from bottom for animation
395
+ .attr('height', 0) // Start with height 0 for animation
396
+ .attr('rx', radius) // Rounded corners
397
+ .attr('ry', radius) // Rounded corners
398
+ .attr('fill', (d) => {
399
+ // Find the index of the current bar (d) in the original data array
400
+ const originalIndex = originalFullData.findIndex((item) => item.id === d.id);
401
+ // Use the original color if provided, otherwise get color from palette using original index
402
+ // Fallback to index 0 if not found, though this shouldn't happen with valid data.
403
+ return d.color || this.getColor(originalIndex !== -1 ? originalIndex : 0);
404
+ });
405
+ // Add data labels if they're enabled
406
+ if (this.effectiveOptions().showDataLabels !== false) {
407
+ barGroups
408
+ .append('text')
409
+ .attr('class', 'ax-bar-chart-data-label')
410
+ .attr('text-anchor', 'middle')
411
+ .attr('x', (d) => this.xScale(d.label) + this.xScale.bandwidth() / 2)
412
+ .attr('y', this.height) // Start from bottom for animation
413
+ .style('font-size', 'clamp(8px, 2vmin, 12px)')
414
+ .style('font-weight', '500')
415
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-data-labels-color))')
416
+ .style('opacity', 0) // Start invisible for animation
417
+ .text((d) => d.value);
418
+ // Animate data labels
419
+ barGroups
420
+ .selectAll('.ax-bar-chart-data-label')
421
+ .transition()
422
+ .duration(animationDuration)
423
+ .delay((d, i) => i * 50 + 100) // Slightly delayed after bar animation
424
+ .attr('y', (d) => this.yScale(d.value) - 8) // Position above bar
425
+ .style('opacity', 1)
426
+ .ease(animationEasing);
427
+ }
428
+ // Set up event handlers on each group
429
+ barGroups
430
+ .on('mouseenter', (event, d) => {
431
+ // Only apply hover effects if initial animation is complete
432
+ if (!this._initialAnimationComplete())
433
+ return;
434
+ const barEl = this.d3.select(event.currentTarget).select('rect');
435
+ // Standard hover effect - darken the bar slightly and add a subtle shadow
436
+ barEl.transition().duration(150).style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
437
+ this.handleBarHover(event, d);
438
+ })
439
+ .on('mousemove', (event) => {
440
+ // Only update tooltip if initial animation is complete
441
+ if (this._initialAnimationComplete()) {
442
+ this.updateTooltipPosition(event);
443
+ }
444
+ })
445
+ .on('mouseleave', (event, d) => {
446
+ // Only apply hover effects if initial animation is complete
447
+ if (!this._initialAnimationComplete())
448
+ return;
449
+ const barEl = this.d3.select(event.currentTarget).select('rect');
450
+ // Remove hover effect
451
+ barEl.transition().duration(150).style('filter', null);
452
+ this._tooltipVisible.set(false);
453
+ })
454
+ .on('click', (event, d) => {
455
+ // Only trigger click events if initial animation is complete
456
+ if (this._initialAnimationComplete()) {
457
+ this.handleBarClick(event, d);
458
+ }
459
+ });
460
+ // Add animation
461
+ bars
462
+ .transition()
463
+ .duration(animationDuration)
464
+ .delay((d, i) => i * 50) // Stagger each bar animation
465
+ .attr('y', (d) => this.yScale(d.value))
466
+ .attr('height', (d) => this.height - this.yScale(d.value))
467
+ .ease(animationEasing) // Use the configured easing function
468
+ .end() // Wait for all animations to complete
469
+ .then(() => {
470
+ // Mark animation as complete to enable hover effects
471
+ this._initialAnimationComplete.set(true);
472
+ });
473
+ }
474
+ /**
475
+ * Gets the appropriate D3 easing function based on the option string
476
+ */
477
+ getEasingFunction(easing) {
478
+ switch (easing) {
479
+ case 'linear':
480
+ return this.d3.easeLinear;
481
+ case 'ease':
482
+ return this.d3.easePolyInOut;
483
+ case 'ease-in':
484
+ return this.d3.easePolyIn;
485
+ case 'ease-out':
486
+ return this.d3.easePolyOut;
487
+ case 'ease-in-out':
488
+ return this.d3.easePolyInOut;
489
+ case 'cubic':
490
+ return this.d3.easeCubic;
491
+ case 'cubic-in':
492
+ return this.d3.easeCubicIn;
493
+ case 'cubic-out':
494
+ return this.d3.easeCubicOut;
495
+ case 'cubic-in-out':
496
+ return this.d3.easeCubicInOut;
497
+ default:
498
+ return this.d3.easeCubicOut; // Default easing
499
+ }
500
+ }
501
+ /**
502
+ * Handles bar hover event and shows tooltip
503
+ */
504
+ handleBarHover(event, datum) {
505
+ if (this.effectiveOptions().showTooltip !== false) {
506
+ const index = this.data().findIndex((item) => item.id === datum.id);
507
+ const color = datum.color || this.getColor(index);
508
+ // Calculate percentage of total
509
+ const total = this.data().reduce((sum, item) => sum + item.value, 0);
510
+ const percentage = total > 0 ? ((datum.value / total) * 100).toFixed(1) : '0';
511
+ this._tooltipData.set({
512
+ title: datum.tooltipLabel || datum.label,
513
+ value: datum.value.toString(),
514
+ percentage: `${percentage}%`,
515
+ color: color,
516
+ });
517
+ this.updateTooltipPosition(event);
518
+ this._tooltipVisible.set(true);
519
+ }
520
+ }
521
+ /**
522
+ * Updates tooltip position based on mouse coordinates
523
+ */
524
+ updateTooltipPosition(event) {
525
+ const container = this.chartContainerEl().nativeElement.getBoundingClientRect();
526
+ const tooltipEl = this.chartContainerEl().nativeElement.querySelector('.chart-tooltip');
527
+ if (!tooltipEl) {
528
+ const x = event.clientX - container.left;
529
+ const y = event.clientY - container.top;
530
+ this._tooltipPosition.set({ x, y });
531
+ return;
532
+ }
533
+ const tooltipRect = tooltipEl.getBoundingClientRect();
534
+ let x = event.clientX - container.left;
535
+ const y = event.clientY - container.top;
536
+ // Adjust position if near the right edge
537
+ if (x + tooltipRect.width + 10 > container.width) {
538
+ x = x - tooltipRect.width - 10;
539
+ }
540
+ else {
541
+ x = x + 10;
542
+ }
543
+ this._tooltipPosition.set({ x, y });
544
+ }
545
+ /**
546
+ * Handles bar click event
547
+ */
548
+ handleBarClick(event, datum) {
549
+ this.barClick.emit({ item: datum, event: { nativeEvent: event, sender: this } });
550
+ }
551
+ /**
552
+ * Shows a message when no data is available
553
+ */
554
+ showNoDataMessage(containerElement) {
555
+ // Clear existing contents first
556
+ this.d3.select(containerElement).selectAll('*').remove();
557
+ const messageContainer = this.d3
558
+ .select(containerElement)
559
+ .append('div')
560
+ // Apply generic container class, specific bar chart background class, and existing specific class
561
+ .attr('class', 'ax-chart-message-container ax-bar-chart-message-background ax-bar-chart-no-data-message');
562
+ // Add an icon
563
+ messageContainer
564
+ .append('div')
565
+ // Apply generic icon class and specific bar chart icon class
566
+ .attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
567
+ .html('<i class="fa-light fa-chart-column fa-2x"></i>');
568
+ // Add text message
569
+ messageContainer
570
+ .append('div')
571
+ // Apply generic text class and specific bar chart text class
572
+ .attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
573
+ .text('No data available');
574
+ // Add help text
575
+ messageContainer
576
+ .append('div')
577
+ // Apply generic help class and specific bar chart help class
578
+ .attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
579
+ .text('Please provide data to display the chart');
580
+ }
581
+ /**
582
+ * Shows a message when all bars are hidden
583
+ */
584
+ showAllBarsHiddenMessage(containerElement) {
585
+ // Clear existing contents first
586
+ this.d3.select(containerElement).selectAll('*').remove();
587
+ const messageContainer = this.d3
588
+ .select(containerElement)
589
+ .append('div')
590
+ // Apply generic container class, specific bar chart background class, and existing specific class
591
+ .attr('class', 'ax-chart-message-container ax-bar-chart-message-background ax-bar-chart-no-data-message');
592
+ // Add an icon
593
+ messageContainer
594
+ .append('div')
595
+ // Apply generic icon class and specific bar chart icon class
596
+ .attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
597
+ .html('<i class="fa-light fa-eye-slash fa-2x"></i>');
598
+ // Add text message
599
+ messageContainer
600
+ .append('div')
601
+ // Apply generic text class and specific bar chart text class
602
+ .attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
603
+ .text('All bars are hidden');
604
+ // Add help text
605
+ messageContainer
606
+ .append('div')
607
+ // Apply generic help class and specific bar chart help class
608
+ .attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
609
+ .text('Click on legend items to show bars');
610
+ }
611
+ /**
612
+ * Gets the color for a bar based on its index
613
+ */
614
+ getColor(index) {
615
+ return getChartColor(index, this.chartColors);
616
+ }
617
+ /**
618
+ * Checks if a bar is hidden
619
+ */
620
+ isBarHidden(id) {
621
+ return this.hiddenBars.has(id);
622
+ }
623
+ /**
624
+ * Implementation of AXChartLegendCompatible interface
625
+ * Returns legend items based on the chart data
626
+ */
627
+ getLegendItems() {
628
+ const data = this.data() || [];
629
+ const total = data.reduce((sum, item) => sum + (typeof item.value === 'number' ? item.value : 0), 0);
630
+ return data.map((item, index) => {
631
+ const value = typeof item.value === 'number' ? item.value : 0;
632
+ const percentage = total > 0 ? (value / total) * 100 : 0;
633
+ return {
634
+ id: item.id,
635
+ name: item.label || `Item ${index + 1}`,
636
+ value: item.value,
637
+ percentage,
638
+ color: item.color || this.getColor(index),
639
+ hidden: this.isBarHidden(item.id),
640
+ };
641
+ });
642
+ }
643
+ /**
644
+ * Implementation of AXChartLegendCompatible interface
645
+ * Highlights a specific bar by ID
646
+ */
647
+ highlightSegment(id) {
648
+ if (!this.svg)
649
+ return;
650
+ // If the bar is hidden, we shouldn't try to highlight it
651
+ if (id !== null && this.isBarHidden(id)) {
652
+ // Just unhighlight everything when trying to highlight a hidden bar
653
+ this.svg
654
+ .selectAll('.ax-bar-chart-bar')
655
+ .classed('ax-bar-chart-highlighted', false)
656
+ .classed('ax-bar-chart-dimmed', false)
657
+ .attr('opacity', 1)
658
+ .attr('filter', null)
659
+ .attr('stroke', null)
660
+ .attr('stroke-width', null)
661
+ .attr('stroke-opacity', null);
662
+ return;
663
+ }
664
+ if (id === null) {
665
+ // If id is null, unhighlight everything
666
+ this.svg
667
+ .selectAll('.ax-bar-chart-bar')
668
+ .classed('ax-bar-chart-highlighted', false)
669
+ .classed('ax-bar-chart-dimmed', false)
670
+ .attr('opacity', 1)
671
+ .attr('filter', null)
672
+ .attr('stroke', null)
673
+ .attr('stroke-width', null)
674
+ .attr('stroke-opacity', null);
675
+ return;
676
+ }
677
+ // Find the target bar
678
+ const targetBar = this.svg.selectAll('.ax-bar-chart-bar').filter((d) => d?.id === id);
679
+ // Safety check - if no matching bar is found, do nothing
680
+ if (targetBar.empty())
681
+ return;
682
+ // Check if the target bar is currently highlighted
683
+ const isCurrentlyHighlighted = targetBar.classed('ax-bar-chart-highlighted');
684
+ if (isCurrentlyHighlighted) {
685
+ // If already highlighted, unhighlight all bars
686
+ this.svg
687
+ .selectAll('.ax-bar-chart-bar')
688
+ .classed('ax-bar-chart-highlighted', false)
689
+ .classed('ax-bar-chart-dimmed', false)
690
+ .attr('opacity', 1)
691
+ .attr('filter', null)
692
+ .attr('stroke', null)
693
+ .attr('stroke-width', null)
694
+ .attr('stroke-opacity', null);
695
+ }
696
+ else {
697
+ // Reset all bars first
698
+ this.svg
699
+ .selectAll('.ax-bar-chart-bar')
700
+ .classed('ax-bar-chart-highlighted', false)
701
+ .classed('ax-bar-chart-dimmed', false)
702
+ .attr('opacity', 1)
703
+ .attr('filter', null)
704
+ .attr('stroke', null)
705
+ .attr('stroke-width', null)
706
+ .attr('stroke-opacity', null);
707
+ // Highlight the target bar
708
+ targetBar
709
+ .classed('ax-bar-chart-highlighted', true)
710
+ .attr('filter', 'drop-shadow(0 0 4px rgba(0, 0, 0, 0.3))')
711
+ .attr('stroke', '#000')
712
+ .attr('stroke-width', '1px')
713
+ .attr('stroke-opacity', '0.3')
714
+ .style('transition', 'all 0.2s ease-in-out');
715
+ // Dim other bars
716
+ this.svg
717
+ .selectAll('.ax-bar-chart-bar')
718
+ .filter((d) => d?.id !== id)
719
+ .classed('ax-bar-chart-dimmed', true)
720
+ .attr('opacity', 0.5)
721
+ .style('transition', 'opacity 0.2s ease-in-out');
722
+ }
723
+ }
724
+ /**
725
+ * Implementation of AXChartLegendCompatible interface
726
+ * Toggles visibility of a bar by ID
727
+ * @returns New visibility state (true = visible, false = hidden)
728
+ */
729
+ toggleSegment(id) {
730
+ const wasAllHidden = this.data().length > 0 && this.data().every((d) => this.isBarHidden(d.id));
731
+ if (this.hiddenBars.has(id)) {
732
+ // Making a bar visible
733
+ this.hiddenBars.delete(id);
734
+ // If all bars were previously hidden, we need to ensure the message is cleared
735
+ if (wasAllHidden) {
736
+ // Force complete DOM cleanup and redraw
737
+ if (this.chartContainerEl()?.nativeElement) {
738
+ // Clear everything in the container
739
+ this.d3?.select(this.chartContainerEl().nativeElement).selectAll('*').remove();
740
+ // Force full redraw
741
+ setTimeout(() => {
742
+ this.createChart();
743
+ }, 0);
744
+ }
745
+ }
746
+ else {
747
+ // Normal update for other cases
748
+ this.updateChart();
749
+ }
750
+ }
751
+ else {
752
+ // Hiding a bar
753
+ this.hiddenBars.add(id);
754
+ this.updateChart();
755
+ }
756
+ // Return the new visibility state
757
+ return !this.hiddenBars.has(id);
758
+ }
759
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: AXBarChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
760
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.4", type: AXBarChartComponent, isStandalone: true, selector: "ax-bar-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-bar-chart-bg-color));padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
761
+ }
762
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: AXBarChartComponent, decorators: [{
763
+ type: Component,
764
+ args: [{ selector: 'ax-bar-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-bar-chart-bg-color));padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"] }]
765
+ }], ctorParameters: () => [] });
766
+
767
+ /**
768
+ * Generated bundle index. Do not edit.
769
+ */
770
+
771
+ export { AXBarChartComponent, AXBarChartDefaultConfig, AX_BAR_CHART_CONFIG, barChartConfig };
772
+ //# sourceMappingURL=acorex-charts-bar-chart.mjs.map