@acorex/charts 20.2.0-next.9 → 20.2.0

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.
@@ -1,7 +1,6 @@
1
1
  import { NXComponent } from '@acorex/cdk/common';
2
- import { AX_CHART_COLOR_PALETTE, getChartColor } from '@acorex/charts';
2
+ import { AX_CHART_COLOR_PALETTE, getEasingFunction, computeTooltipPosition, getChartColor } from '@acorex/charts';
3
3
  import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
4
- import { AXPlatform } from '@acorex/core/platform';
5
4
  import * as i0 from '@angular/core';
6
5
  import { InjectionToken, inject, input, output, viewChild, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
7
6
  import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
@@ -13,10 +12,19 @@ const AXBarChartDefaultConfig = {
13
12
  showGrid: true,
14
13
  showDataLabels: true,
15
14
  showTooltip: true,
15
+ rotateXAxisLabels: 'auto',
16
16
  barWidth: 80,
17
17
  cornerRadius: 4,
18
18
  animationDuration: 800,
19
19
  animationEasing: 'cubic-out',
20
+ messages: {
21
+ noData: 'No data available',
22
+ noDataHelp: 'Please provide data to display the chart',
23
+ allHidden: 'All bars are hidden',
24
+ allHiddenHelp: 'Click on legend items to show bars',
25
+ noDataIcon: 'fa-light fa-chart-column',
26
+ allHiddenIcon: 'fa-light fa-eye-slash',
27
+ },
20
28
  };
21
29
  const AX_BAR_CHART_CONFIG = new InjectionToken('AX_BAR_CHART_CONFIG', {
22
30
  providedIn: 'root',
@@ -56,12 +64,17 @@ class AXBarChartComponent extends NXComponent {
56
64
  chart;
57
65
  xScale;
58
66
  yScale;
67
+ xSubScale;
59
68
  xAxis;
60
69
  yAxis;
61
70
  // Chart dimensions
62
71
  width;
63
72
  height;
64
- margin = { top: 20, right: 20, bottom: 30, left: 40 };
73
+ margin = { top: 0, right: 0, bottom: 0, left: 0 };
74
+ // Character width ratio heuristic for mixed Latin/Persian texts
75
+ CHAR_WIDTH_RATIO = 0.65;
76
+ // Tooltip gap in pixels for consistent spacing
77
+ TOOLTIP_GAP = 10;
65
78
  // Animation state
66
79
  _initialAnimationComplete = signal(false, ...(ngDevMode ? [{ debugName: "_initialAnimationComplete" }] : []));
67
80
  // Tooltip state
@@ -85,7 +98,7 @@ class AXBarChartComponent extends NXComponent {
85
98
  // Inject the chart colors
86
99
  chartColors = inject(AX_CHART_COLOR_PALETTE);
87
100
  // Inject AXPlatform
88
- platform = inject(AXPlatform);
101
+ // Removed unused AXPlatform injection
89
102
  // Configuration with defaults
90
103
  effectiveOptions = computed(() => {
91
104
  return {
@@ -93,8 +106,33 @@ class AXBarChartComponent extends NXComponent {
93
106
  ...this.options(),
94
107
  };
95
108
  }, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
109
+ // Messages with defaults
110
+ effectiveMessages = computed(() => {
111
+ const defaultMessages = {
112
+ noData: 'No data available',
113
+ noDataHelp: 'Please provide data to display the chart',
114
+ allHidden: 'All bars are hidden',
115
+ allHiddenHelp: 'Click on legend items to show bars',
116
+ noDataIcon: 'fa-light fa-chart-column',
117
+ allHiddenIcon: 'fa-light fa-eye-slash',
118
+ };
119
+ return {
120
+ ...defaultMessages,
121
+ ...this.effectiveOptions().messages,
122
+ };
123
+ }, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : []));
96
124
  // Track hidden bars
97
125
  hiddenBars = new Set();
126
+ // Track hidden series for clustered charts (by series label)
127
+ hiddenSeries = new Set();
128
+ // Helpers
129
+ isClusteredData(value) {
130
+ return Array.isArray(value) && value.length > 0 && value[0] && 'chartData' in value[0];
131
+ }
132
+ getClusterSeriesLabels(groups) {
133
+ const first = groups[0]?.chartData ?? [];
134
+ return first.map((d) => d.label);
135
+ }
98
136
  constructor() {
99
137
  super();
100
138
  // Dynamically load D3 and initialize the chart when the component is ready
@@ -144,29 +182,49 @@ class AXBarChartComponent extends NXComponent {
144
182
  if (!this.d3 || !this.chartContainerEl()?.nativeElement)
145
183
  return;
146
184
  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();
185
+ const inputValue = (this.data() || []);
186
+ const isClustered = this.isClusteredData(inputValue);
187
+ // Visibility calculation
188
+ let hasAnyVisible = false;
189
+ let visibleSingleData = [];
190
+ let groupedData = [];
191
+ if (isClustered) {
192
+ groupedData = inputValue;
193
+ const seriesLabels = groupedData.length > 0 ? this.getClusterSeriesLabels(groupedData) : [];
194
+ const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
195
+ hasAnyVisible = groupedData.some((g) => visibleSeries.some((_, idx) => !!g.chartData[idx]));
196
+ }
197
+ else {
198
+ const single = inputValue || [];
199
+ visibleSingleData = single.filter((d) => !this.isBarHidden(d.id));
200
+ hasAnyVisible = visibleSingleData.length > 0;
201
+ }
202
+ // Clear existing chart SVG and messages (do not remove tooltip component)
203
+ this.d3.select(containerElement).selectAll('svg, .ax-chart-message-container').remove();
152
204
  // Early return if no data
153
- if (!data.length) {
205
+ if (!inputValue.length) {
154
206
  this.showNoDataMessage(containerElement);
155
207
  return;
156
208
  }
157
209
  // Early return if all bars are hidden
158
- if (visibleData.length === 0) {
210
+ if (!hasAnyVisible) {
159
211
  this.showAllBarsHiddenMessage(containerElement);
160
212
  return;
161
213
  }
162
214
  // Get options and setup dimensions
163
215
  const chartOptions = this.effectiveOptions();
164
216
  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);
217
+ // Create scales and axes and render based on mode
218
+ if (isClustered) {
219
+ this.setupScalesClustered(groupedData);
220
+ this.createAxesClustered(chartOptions, groupedData);
221
+ this.renderBarsClustered(groupedData);
222
+ }
223
+ else {
224
+ this.setupScales(visibleSingleData);
225
+ this.createAxes(chartOptions);
226
+ this.renderBars(visibleSingleData);
227
+ }
170
228
  }
171
229
  /**
172
230
  * Updates the chart when inputs change
@@ -178,100 +236,101 @@ class AXBarChartComponent extends NXComponent {
178
236
  * Cleans up chart resources
179
237
  */
180
238
  cleanupChart() {
181
- if (this.svg) {
182
- this.d3?.select(this.chartContainerEl()?.nativeElement).selectAll('svg').remove();
183
- this.svg = null;
184
- this.chart = null;
239
+ const container = this.chartContainerEl()?.nativeElement;
240
+ if (container) {
241
+ // Only remove chart SVG and message containers to preserve tooltip component
242
+ this.d3?.select(container).selectAll('svg, .ax-chart-message-container').remove();
185
243
  }
244
+ this.svg = null;
245
+ this.chart = null;
186
246
  this._tooltipVisible.set(false);
187
247
  }
188
248
  /**
189
249
  * Sets up chart dimensions and creates SVG with responsive attributes
190
250
  */
191
251
  setupDimensions(containerElement, options) {
192
- // Calculate margins based on options
193
- this.calculateMargins(options, containerElement.clientWidth);
252
+ // Remove outer margins; calculate only internal paddings for axes and labels
253
+ this.margin = { top: 0, right: 0, bottom: 0, left: 0 };
194
254
  // Get container dimensions
195
255
  const containerWidth = containerElement.clientWidth;
196
256
  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;
257
+ // Axis visibility
258
+ const showXAxis = options.showXAxis !== false;
259
+ const showYAxis = options.showYAxis !== false;
260
+ // Internal left padding for Y-axis ticks/labels/title
261
+ const axisAndTickPadding = 10;
262
+ const yAxisTitleThickness = options.yAxisLabel ? 14 : 0; // rotated label thickness along X
263
+ const yAxisTitlePadding = options.yAxisLabel ? 14 : 0; // gap between ticks and title
264
+ const leftPaddingForYAxis = showYAxis
265
+ ? this.calculateMaxYAxisTickLabelWidth() + axisAndTickPadding + yAxisTitlePadding + yAxisTitleThickness
266
+ : 0;
267
+ // Estimate internal bottom padding for X-axis ticks/labels/title
268
+ let bottomPaddingForXAxis = 0;
269
+ if (showXAxis) {
270
+ const raw = (this.data() || []);
271
+ const isClustered = this.isClusteredData(raw);
272
+ const barCount = isClustered ? raw.length : raw.length;
273
+ const longestLabel = (() => {
274
+ if (isClustered) {
275
+ const groups = raw;
276
+ return groups.reduce((acc, g) => (acc.length > (g.label?.length ?? 0) ? acc : (g.label ?? '')), '');
277
+ }
278
+ const flat = raw || [];
279
+ return flat.reduce((a, b) => (a.length > b.label.length ? a : b.label), '');
280
+ })();
281
+ const baseWidth = options.width ?? containerWidth;
282
+ const workingWidth = Math.max(1, baseWidth - leftPaddingForYAxis);
283
+ const dynamicFontSize = Math.max(10, Math.min(14, Math.round(workingWidth / 50)));
284
+ const estimatedLongestLabelWidth = longestLabel.length * dynamicFontSize * 0.65;
285
+ const availableWidthPerBar = barCount > 0 ? workingWidth / barCount : workingWidth;
286
+ const willRotate = options.rotateXAxisLabels === true
287
+ ? true
288
+ : options.rotateXAxisLabels === false
289
+ ? false
290
+ : estimatedLongestLabelWidth > availableWidthPerBar;
291
+ const tickAreaHeight = willRotate
292
+ ? estimatedLongestLabelWidth * Math.SQRT1_2 + dynamicFontSize * Math.SQRT1_2 + 6
293
+ : dynamicFontSize + 8;
294
+ const gapBelowTicks = options.xAxisLabel && !willRotate ? 14 : 0; // matches renderer
295
+ const titleBlockHeight = options.xAxisLabel && !willRotate ? 24 : 0; // 14px font + ~10px gap
296
+ bottomPaddingForXAxis = tickAreaHeight + gapBelowTicks + titleBlockHeight;
297
+ }
298
+ // Determine plotting dimensions with sensible fallbacks
299
+ const fallbackPlotHeight = 240; // ensures visible plot when container has no height
300
+ const baseWidth = options.width ?? containerWidth;
301
+ const totalWidth = Math.max(200, baseWidth > 0 ? baseWidth : 200);
302
+ const plotWidth = Math.max(0, totalWidth - leftPaddingForYAxis);
303
+ let plotHeight;
304
+ if (options.height && options.height > 0) {
305
+ plotHeight = Math.max(120, options.height - bottomPaddingForXAxis);
306
+ }
307
+ else if (containerHeight > 0) {
308
+ plotHeight = Math.max(120, containerHeight - bottomPaddingForXAxis, fallbackPlotHeight);
203
309
  }
204
310
  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;
311
+ plotHeight = fallbackPlotHeight;
208
312
  }
209
- // Create responsive SVG that scales with its container
313
+ this.width = plotWidth;
314
+ this.height = plotHeight;
315
+ // Create SVG with explicit pixel dimensions to avoid collapse
316
+ // Add a small internal top padding to avoid clipping the top-most Y-axis label
317
+ const topPaddingForTopEdge = 6;
318
+ const totalHeight = this.height + bottomPaddingForXAxis + topPaddingForTopEdge;
210
319
  const svg = this.d3
211
320
  .select(containerElement)
212
321
  .append('svg')
213
322
  .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}`)
323
+ .attr('height', String(totalHeight))
324
+ .attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`)
216
325
  .attr('preserveAspectRatio', 'xMidYMid meet');
217
326
  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, containerWidth) {
225
- // Start with default margins
226
- this.margin = {
227
- top: 20,
228
- right: 20,
229
- bottom: 30,
230
- left: 40,
231
- };
232
- const visibleData = this.data().filter((d) => !this.isBarHidden(d.id));
233
- const barCount = visibleData.length;
234
- // Pre-emptively increase bottom margin if x-axis labels are likely to be rotated.
235
- // This is an estimation before scales and full dimensions are calculated.
236
- if (barCount > 0 && containerWidth > 0) {
237
- const workingWidth = containerWidth - this.margin.left - this.margin.right;
238
- if (workingWidth > 0) {
239
- const availableWidthPerBar = workingWidth / barCount;
240
- const longestLabel = visibleData.reduce((a, b) => (a.label.length > b.label.length ? a : b), {
241
- label: '',
242
- }).label;
243
- // Estimate font size using the same logic as in createAxes, but with workingWidth.
244
- const estimatedFontSize = Math.max(11, Math.min(15, Math.round(workingWidth / 45)));
245
- // Estimate label width (using 0.6 as an average character width-to-height ratio).
246
- const estimatedLongestLabelWidth = longestLabel.length * estimatedFontSize * 0.6;
247
- // If estimated label width is greater than the space available, rotation will occur.
248
- if (estimatedLongestLabelWidth > availableWidthPerBar) {
249
- // Add space for rotated labels. The height of the rotated label's bounding box
250
- // is roughly its length * sin(45 degrees). Add some padding.
251
- const requiredExtraMargin = estimatedLongestLabelWidth * Math.sin(Math.PI / 4) + 10;
252
- this.margin.bottom += Math.min(60, requiredExtraMargin); // Cap max extra margin.
253
- }
254
- }
255
- }
256
- // Adjust margins if axis labels are present
257
- if (options.xAxisLabel) {
258
- const xLabelLength = options.xAxisLabel.length;
259
- const extraBottomMargin = Math.min(20, Math.max(10, xLabelLength * 0.8));
260
- this.margin.bottom = Math.max(this.margin.bottom, 30 + extraBottomMargin);
261
- }
262
- if (options.yAxisLabel) {
263
- const yLabelLength = options.yAxisLabel.length;
264
- const extraLeftMargin = Math.min(20, Math.max(10, yLabelLength * 0.8));
265
- this.margin.left = Math.max(this.margin.left, 40 + extraLeftMargin);
266
- }
267
- // Ensure minimum margins for axes
268
- if (options.showXAxis !== false) {
269
- this.margin.bottom = Math.max(this.margin.bottom, 35);
270
- }
271
- if (options.showYAxis !== false) {
272
- this.margin.left = Math.max(this.margin.left, 45);
273
- }
327
+ // Create chart group translated by internal paddings (no external margins)
328
+ this.chart = this.svg
329
+ .append('g')
330
+ .attr('class', 'chart-content')
331
+ .attr('transform', `translate(${leftPaddingForYAxis},${topPaddingForTopEdge})`);
274
332
  }
333
+ // calculateMargins was unused; removed to simplify and reduce maintenance surface
275
334
  /**
276
335
  * Creates x and y scales for the chart
277
336
  */
@@ -283,7 +342,7 @@ class AXBarChartComponent extends NXComponent {
283
342
  // Create x scale (band scale for categorical data)
284
343
  this.xScale = this.d3
285
344
  .scaleBand()
286
- .domain(data.map((d) => d.label))
345
+ .domain(data.map((d) => d.id))
287
346
  .range([0, this.width])
288
347
  .padding(padding);
289
348
  // Create y scale (linear scale for values)
@@ -293,126 +352,214 @@ class AXBarChartComponent extends NXComponent {
293
352
  .nice()
294
353
  .range([this.height, 0]);
295
354
  }
355
+ /**
356
+ * Creates x and y scales for clustered charts
357
+ */
358
+ setupScalesClustered(groups) {
359
+ const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
360
+ const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
361
+ // Outer band for clusters
362
+ const outerPadding = 0.2;
363
+ this.xScale = this.d3
364
+ .scaleBand()
365
+ .domain(groups.map((g) => g.id))
366
+ .range([0, this.width])
367
+ .padding(outerPadding);
368
+ // Inner band for series
369
+ this.xSubScale = this.d3.scaleBand().domain(visibleSeries).range([0, this.xScale.bandwidth()]).padding(0.1);
370
+ // Y scale across all visible values
371
+ const allVisibleValues = [];
372
+ for (const g of groups) {
373
+ visibleSeries.forEach((_, idx) => {
374
+ const item = g.chartData[idx];
375
+ if (item && typeof item.value === 'number')
376
+ allVisibleValues.push(item.value);
377
+ });
378
+ }
379
+ const yMax = this.d3.max(allVisibleValues) || 0;
380
+ this.yScale = this.d3.scaleLinear().domain([0, yMax]).nice().range([this.height, 0]);
381
+ }
296
382
  /**
297
383
  * Creates x and y axes with grid lines
298
384
  */
299
385
  createAxes(options) {
300
- // Only create axes if they are enabled in options
301
386
  const showXAxis = options.showXAxis !== false;
302
387
  const showYAxis = options.showYAxis !== false;
303
388
  const showGrid = options.showGrid !== false;
304
- const isRtl = this.platform.isRtl();
305
- // Create a group for all axes
306
389
  const axesGroup = this.chart.append('g').attr('class', 'ax-bar-chart-axes');
307
390
  if (showXAxis) {
308
- // Create X axis
309
- this.xAxis = axesGroup
310
- .append('g')
311
- .attr('class', 'ax-bar-chart-axis-x')
312
- .attr('transform', `translate(0,${this.height})`)
313
- .call(this.d3.axisBottom(this.xScale));
314
- // Style the axis text
315
- const dynamicXAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.width / 45)));
316
- const xAxisTicks = this.xAxis
317
- .selectAll('text')
318
- .style('font-size', `${dynamicXAxisTickFontSize}px`)
319
- .style('font-weight', '400')
320
- .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)');
321
- // Automatically rotate labels if they are likely to overlap
322
- if (this.xScale.domain().length > 0) {
323
- const step = this.xScale.step();
324
- const longestLabel = this.xScale.domain().reduce((a, b) => (a.length > b.length ? a : b), '');
325
- // Using 0.55 as a safer estimate for char width-to-height ratio
326
- const estimatedLongestLabelWidth = longestLabel.length * dynamicXAxisTickFontSize * 0.55;
327
- if (estimatedLongestLabelWidth > step) {
328
- xAxisTicks
329
- .attr('transform', 'rotate(-45)')
330
- .style('text-anchor', 'end')
331
- .attr('dx', '-0.8em')
332
- .attr('dy', '0.15em');
333
- }
334
- }
335
- // Style all lines in the x-axis (path, ticks)
336
- this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
337
- // Add X axis label if provided
338
- if (options.xAxisLabel) {
339
- const labelY = this.height + this.margin.bottom * 0.8;
340
- axesGroup
341
- .append('text')
342
- .attr('class', 'ax-bar-chart-axis-label ax-x-axis-label')
343
- .attr('text-anchor', 'middle')
344
- .attr('dominant-baseline', 'middle')
345
- .attr('x', this.width / 2)
346
- .attr('y', labelY)
347
- .style('font-size', '14px')
348
- .style('font-weight', '500')
349
- .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
350
- .text(options.xAxisLabel);
351
- }
391
+ this.createXAxis(axesGroup, options);
352
392
  }
353
393
  if (showYAxis) {
354
- // Create Y axis
355
- this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(this.d3.axisLeft(this.yScale));
356
- // Style the axis text
357
- const dynamicYAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.height / 30)));
358
- const yTickTexts = this.yAxis
359
- .selectAll('text')
360
- .style('font-size', `${dynamicYAxisTickFontSize}px`)
361
- .style('font-weight', '400')
362
- .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)');
363
- if (isRtl) {
364
- yTickTexts.attr('text-anchor', 'start');
365
- }
366
- // Style all lines in the y-axis (path, ticks)
367
- this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
368
- // Add Y axis label if provided
369
- if (options.yAxisLabel) {
370
- const labelX = -this.height / 2;
371
- const labelY = -this.margin.left * 0.8;
372
- axesGroup
373
- .append('text')
374
- .attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
375
- .attr('text-anchor', 'middle')
376
- .attr('dominant-baseline', 'middle')
377
- .attr('transform', 'rotate(-90)')
378
- .attr('x', labelX)
379
- .attr('y', labelY)
380
- .style('font-size', '14px')
381
- .style('font-weight', '500')
382
- .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
383
- .text(options.yAxisLabel);
394
+ this.createYAxis(axesGroup, options);
395
+ }
396
+ if (showGrid) {
397
+ this.createGridLines();
398
+ }
399
+ }
400
+ createXAxis(axesGroup, options) {
401
+ const visibleDataForTicks = (this.data() || []).filter((d) => !this.isBarHidden(d.id));
402
+ const idToLabel = new Map(visibleDataForTicks.map((d) => [d.id, d.label]));
403
+ this.buildXAxisCommon(axesGroup, idToLabel, options);
404
+ }
405
+ createYAxis(axesGroup, options) {
406
+ // Create Y axis
407
+ this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(this.d3.axisLeft(this.yScale));
408
+ // Style the axis text
409
+ const dynamicFontSize = this.getYAxisTickFontSizeBasedOnHeight();
410
+ this.yAxis
411
+ .selectAll('text')
412
+ .style('font-size', `${dynamicFontSize}px`)
413
+ .style('font-weight', '400')
414
+ .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
415
+ .attr('text-anchor', 'end')
416
+ .attr('direction', 'ltr');
417
+ // Style axis lines
418
+ this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
419
+ // Add Y axis title
420
+ if (options.yAxisLabel) {
421
+ this.createYAxisTitle(axesGroup, options.yAxisLabel);
422
+ }
423
+ }
424
+ shouldRotateXAxisLabels(idToLabel, fontSize) {
425
+ const options = this.effectiveOptions();
426
+ // Check if user explicitly set rotation option
427
+ if (options.rotateXAxisLabels === true)
428
+ return true;
429
+ if (options.rotateXAxisLabels === false)
430
+ return false;
431
+ // If 'auto' or undefined, use intelligent calculation
432
+ if (this.xScale.domain().length === 0)
433
+ return false;
434
+ const step = this.xScale.step();
435
+ const labels = Array.from(idToLabel.values());
436
+ const longestLabel = labels.reduce((a, b) => (a.length > b.length ? a : b), '');
437
+ const estimatedWidth = longestLabel.length * fontSize * this.CHAR_WIDTH_RATIO;
438
+ // Heuristic: avoid rotation when there are few categories (<= 6) even if estimation is slightly larger
439
+ const domainCount = this.xScale.domain().length;
440
+ if (domainCount <= 6) {
441
+ return estimatedWidth > step * 1.4; // be more tolerant for small datasets
442
+ }
443
+ return estimatedWidth > step;
444
+ }
445
+ createXAxisTitle(axesGroup, title, tickAreaHeight, gapBelowTicks = 8) {
446
+ // Title positioned directly below tick area with a consistent gap
447
+ const labelY = this.height + tickAreaHeight + gapBelowTicks;
448
+ axesGroup
449
+ .append('text')
450
+ .attr('class', 'ax-bar-chart-axis-label ax-x-axis-label')
451
+ .attr('text-anchor', 'middle')
452
+ .attr('dominant-baseline', 'hanging') // Align to top of text for better positioning
453
+ .attr('x', this.width / 2)
454
+ .attr('y', labelY)
455
+ .attr('direction', 'ltr')
456
+ .style('font-size', '14px')
457
+ .style('font-weight', '500')
458
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
459
+ .text(title);
460
+ }
461
+ createYAxisTitle(axesGroup, title) {
462
+ // Calculate the exact position for Y-axis title
463
+ // Y-axis tick labels use text-anchor: end, so they extend LEFT of the Y-axis line
464
+ // Y-axis is at X = 0 within the chart group
465
+ // Chart group is translated by (margin.left, margin.top)
466
+ // So Y-axis is at X = margin.left in SVG coordinates
467
+ const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
468
+ const padding = 20; // Increased padding between tick labels and axis title
469
+ // Position title to the left of the tick labels
470
+ // Since text-anchor: end aligns the text to the Y-axis, we need to go further left
471
+ const labelY = -maxTickLabelWidth - padding;
472
+ // Center the title vertically
473
+ const labelX = -this.height / 2;
474
+ axesGroup
475
+ .append('text')
476
+ .attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
477
+ .attr('text-anchor', 'middle')
478
+ .attr('dominant-baseline', 'middle')
479
+ .attr('transform', 'rotate(-90)')
480
+ .attr('x', labelX)
481
+ .attr('y', labelY)
482
+ .attr('direction', 'ltr')
483
+ .style('font-size', '14px')
484
+ .style('font-weight', '500')
485
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
486
+ .text(title);
487
+ }
488
+ calculateMaxYAxisTickLabelWidth() {
489
+ // Calculate the maximum width needed for Y-axis tick labels
490
+ const raw = (this.data() || []);
491
+ const isClustered = this.isClusteredData(raw);
492
+ let maxValue = 0;
493
+ if (isClustered) {
494
+ const groups = raw;
495
+ const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
496
+ const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
497
+ for (const g of groups) {
498
+ visibleSeries.forEach((_, idx) => {
499
+ const item = g.chartData[idx];
500
+ if (item && typeof item.value === 'number')
501
+ maxValue = Math.max(maxValue, item.value);
502
+ });
384
503
  }
385
504
  }
505
+ else {
506
+ const visibleData = raw.filter((d) => !this.isBarHidden(d.id));
507
+ maxValue = visibleData.reduce((acc, d) => (d.value > acc ? d.value : acc), 0);
508
+ }
509
+ const tickLabelText = Number.isFinite(maxValue) ? maxValue.toLocaleString() : '00000';
510
+ const fontSize = 13; // Approximate font size for Y-axis tick labels
511
+ return tickLabelText.length * fontSize * 0.6 + 8; // Character width + padding
512
+ }
513
+ createGridLines() {
514
+ this.chart
515
+ .append('g')
516
+ .attr('class', 'ax-bar-chart-grid')
517
+ .call(this.d3
518
+ .axisLeft(this.yScale)
519
+ .tickSize(-this.width)
520
+ .tickFormat(() => ''))
521
+ .selectAll('line')
522
+ .style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))')
523
+ .style('stroke-opacity', 0.2);
524
+ // Remove unneeded elements from grid
525
+ this.chart.select('.ax-bar-chart-grid').selectAll('path, text').remove();
526
+ }
527
+ /**
528
+ * Creates axes for clustered charts
529
+ */
530
+ createAxesClustered(options, groups) {
531
+ const showXAxis = options.showXAxis !== false;
532
+ const showYAxis = options.showYAxis !== false;
533
+ const showGrid = options.showGrid !== false;
534
+ const axesGroup = this.chart.append('g').attr('class', 'ax-bar-chart-axes');
535
+ if (showXAxis) {
536
+ this.createXAxisClustered(axesGroup, options, groups);
537
+ }
538
+ if (showYAxis) {
539
+ this.createYAxis(axesGroup, options);
540
+ }
386
541
  if (showGrid) {
387
- // Add horizontal grid lines
388
- this.chart
389
- .append('g')
390
- .attr('class', 'ax-bar-chart-grid')
391
- .call(this.d3
392
- .axisLeft(this.yScale)
393
- .tickSize(-this.width)
394
- .tickFormat(() => ''))
395
- .selectAll('line')
396
- .style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))')
397
- .style('stroke-opacity', 0.2);
398
- // Remove unneeded elements from grid
399
- this.chart.select('.ax-bar-chart-grid').selectAll('path, text').remove();
542
+ this.createGridLines();
400
543
  }
401
544
  }
545
+ createXAxisClustered(axesGroup, options, groups) {
546
+ const idToLabel = new Map(groups.map((g) => [g.id, g.label]));
547
+ this.buildXAxisCommon(axesGroup, idToLabel, options);
548
+ }
402
549
  /**
403
550
  * Renders the bars with animations
404
551
  */
405
552
  renderBars(data) {
406
553
  // Filter out hidden bars
407
554
  const visibleData = data.filter((d) => !this.isBarHidden(d.id));
408
- const originalFullData = this.data(); // Get the original full data set
555
+ const originalFullData = this.data() || [];
409
556
  // Reset animation state
410
557
  this._initialAnimationComplete.set(false);
411
558
  // Get corner radius from options
412
559
  const radius = this.effectiveOptions().cornerRadius;
413
560
  // Get animation options
414
561
  const animationDuration = this.effectiveOptions().animationDuration;
415
- const animationEasing = this.getEasingFunction(this.effectiveOptions().animationEasing);
562
+ const animationEasing = getEasingFunction(this.d3, this.effectiveOptions().animationEasing);
416
563
  // Create a container for all bars
417
564
  const barsContainer = this.chart.append('g').attr('class', 'ax-bar-chart-bars-container');
418
565
  // Create groups for each bar to handle transformations
@@ -425,14 +572,14 @@ class AXBarChartComponent extends NXComponent {
425
572
  .attr('class', 'ax-bar-chart-bar-group');
426
573
  // Add bars inside groups
427
574
  const bars = barGroups
428
- .append('rect')
575
+ .append('path')
429
576
  .attr('class', 'ax-bar-chart-bar')
430
- .attr('x', (d) => this.xScale(d.label))
431
- .attr('width', this.xScale.bandwidth())
432
- .attr('y', this.height) // Start from bottom for animation
433
- .attr('height', 0) // Start with height 0 for animation
434
- .attr('rx', radius) // Rounded corners
435
- .attr('ry', radius) // Rounded corners
577
+ .attr('d', (d) => {
578
+ const x = this.xScale(d.id);
579
+ const width = this.xScale.bandwidth();
580
+ const y = this.height - 0.5; // initial height is zero
581
+ return `M${x},${y} L${x},${y} L${x + width},${y} L${x + width},${y} Z`;
582
+ })
436
583
  .attr('fill', (d) => {
437
584
  // Find the index of the current bar (d) in the original data array
438
585
  const originalIndex = originalFullData.findIndex((item) => item.id === d.id);
@@ -446,7 +593,7 @@ class AXBarChartComponent extends NXComponent {
446
593
  .append('text')
447
594
  .attr('class', 'ax-bar-chart-data-label')
448
595
  .attr('text-anchor', 'middle')
449
- .attr('x', (d) => this.xScale(d.label) + this.xScale.bandwidth() / 2)
596
+ .attr('x', (d) => this.xScale(d.id) + this.xScale.bandwidth() / 2)
450
597
  .attr('y', this.height) // Start from bottom for animation
451
598
  .style('font-size', 'clamp(8px, 2vmin, 12px)')
452
599
  .style('font-weight', '500')
@@ -469,7 +616,7 @@ class AXBarChartComponent extends NXComponent {
469
616
  // Only apply hover effects if initial animation is complete
470
617
  if (!this._initialAnimationComplete())
471
618
  return;
472
- const barEl = this.d3.select(event.currentTarget).select('rect');
619
+ const barEl = this.d3.select(event.currentTarget).select('path');
473
620
  // Standard hover effect - darken the bar slightly and add a subtle shadow
474
621
  barEl.transition().duration(150).style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
475
622
  this.handleBarHover(event, d);
@@ -480,11 +627,11 @@ class AXBarChartComponent extends NXComponent {
480
627
  this.updateTooltipPosition(event);
481
628
  }
482
629
  })
483
- .on('mouseleave', (event, d) => {
630
+ .on('mouseleave', (event) => {
484
631
  // Only apply hover effects if initial animation is complete
485
632
  if (!this._initialAnimationComplete())
486
633
  return;
487
- const barEl = this.d3.select(event.currentTarget).select('rect');
634
+ const barEl = this.d3.select(event.currentTarget).select('path');
488
635
  // Remove hover effect
489
636
  barEl.transition().duration(150).style('filter', null);
490
637
  this._tooltipVisible.set(false);
@@ -500,8 +647,24 @@ class AXBarChartComponent extends NXComponent {
500
647
  .transition()
501
648
  .duration(animationDuration)
502
649
  .delay((d, i) => i * 50) // Stagger each bar animation
503
- .attr('y', (d) => this.yScale(d.value))
504
- .attr('height', (d) => this.height - this.yScale(d.value))
650
+ .attrTween('d', (d) => {
651
+ const x = this.xScale(d.id);
652
+ const width = this.xScale.bandwidth();
653
+ const finalHeight = this.height - this.yScale(d.value);
654
+ return (t) => {
655
+ const currentHeight = finalHeight * t;
656
+ const currentY = this.height - currentHeight;
657
+ const bottomY = this.height - 0.5; // Add small gap to prevent overlap with x-axis
658
+ if (radius > 0 && currentHeight > 0) {
659
+ // Limit radius to prevent it from exceeding half the bar width
660
+ const effectiveRadius = Math.min(radius, width / 2);
661
+ return `M${x},${bottomY} L${x},${currentY + effectiveRadius} Q${x},${currentY} ${x + effectiveRadius},${currentY} L${x + width - effectiveRadius},${currentY} Q${x + width},${currentY} ${x + width},${currentY + effectiveRadius} L${x + width},${bottomY} Z`;
662
+ }
663
+ else {
664
+ return `M${x},${bottomY} L${x},${currentY} L${x + width},${currentY} L${x + width},${bottomY} Z`;
665
+ }
666
+ };
667
+ })
505
668
  .ease(animationEasing) // Use the configured easing function
506
669
  .end() // Wait for all animations to complete
507
670
  .then(() => {
@@ -510,44 +673,150 @@ class AXBarChartComponent extends NXComponent {
510
673
  });
511
674
  }
512
675
  /**
513
- * Gets the appropriate D3 easing function based on the option string
676
+ * Renders bars for clustered charts
514
677
  */
515
- getEasingFunction(easing) {
516
- switch (easing) {
517
- case 'linear':
518
- return this.d3.easeLinear;
519
- case 'ease':
520
- return this.d3.easePolyInOut;
521
- case 'ease-in':
522
- return this.d3.easePolyIn;
523
- case 'ease-out':
524
- return this.d3.easePolyOut;
525
- case 'ease-in-out':
526
- return this.d3.easePolyInOut;
527
- case 'cubic':
528
- return this.d3.easeCubic;
529
- case 'cubic-in':
530
- return this.d3.easeCubicIn;
531
- case 'cubic-out':
532
- return this.d3.easeCubicOut;
533
- case 'cubic-in-out':
534
- return this.d3.easeCubicInOut;
535
- default:
536
- return this.d3.easeCubicOut; // Default easing
678
+ renderBarsClustered(groups) {
679
+ const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
680
+ const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
681
+ this._initialAnimationComplete.set(false);
682
+ const radius = this.effectiveOptions().cornerRadius;
683
+ const animationDuration = this.effectiveOptions().animationDuration;
684
+ const animationEasing = getEasingFunction(this.d3, this.effectiveOptions().animationEasing);
685
+ const barsContainer = this.chart.append('g').attr('class', 'ax-bar-chart-bars-container');
686
+ const groupSel = barsContainer
687
+ .selectAll('.ax-bar-chart-group')
688
+ .data(groups)
689
+ .enter()
690
+ .append('g')
691
+ .attr('class', 'ax-bar-chart-group')
692
+ .attr('transform', (g) => `translate(${this.xScale(g.id)},0)`);
693
+ const barGroups = groupSel
694
+ .selectAll('.ax-bar-chart-bar-group')
695
+ .data((g) => visibleSeries.map((label, idx) => ({ group: g, seriesLabel: label, seriesIndex: idx })))
696
+ .enter()
697
+ .append('g')
698
+ .style('cursor', 'pointer')
699
+ .attr('class', 'ax-bar-chart-bar-group');
700
+ const bars = barGroups
701
+ .append('path')
702
+ .attr('class', 'ax-bar-chart-bar')
703
+ .attr('data-series', (d) => d.seriesLabel)
704
+ .attr('d', (d) => {
705
+ const x = this.xSubScale(d.seriesLabel);
706
+ const width = this.xSubScale.bandwidth();
707
+ const y = this.height - 0.5; // initial height is zero
708
+ return `M${x},${y} L${x},${y} L${x + width},${y} L${x + width},${y} Z`;
709
+ })
710
+ .attr('fill', (d) => {
711
+ const item = d.group.chartData[d.seriesIndex];
712
+ const colorIndex = d.seriesIndex;
713
+ return item?.color || this.getColor(colorIndex);
714
+ });
715
+ if (this.effectiveOptions().showDataLabels !== false) {
716
+ barGroups
717
+ .append('text')
718
+ .attr('class', 'ax-bar-chart-data-label')
719
+ .attr('text-anchor', 'middle')
720
+ .attr('x', (d) => (this.xSubScale(d.seriesLabel) || 0) + this.xSubScale.bandwidth() / 2)
721
+ .attr('y', this.height)
722
+ .style('font-size', 'clamp(8px, 2vmin, 12px)')
723
+ .style('font-weight', '500')
724
+ .style('fill', 'rgb(var(--ax-comp-bar-chart-data-labels-color))')
725
+ .style('opacity', 0)
726
+ .text((d) => {
727
+ const item = d.group.chartData[d.seriesIndex];
728
+ return item ? item.value : '';
729
+ });
730
+ barGroups
731
+ .selectAll('.ax-bar-chart-data-label')
732
+ .transition()
733
+ .duration(animationDuration)
734
+ .delay((_, i) => i * 50 + 100)
735
+ .attr('y', (d) => {
736
+ const item = d.group.chartData[d.seriesIndex];
737
+ return item ? this.yScale(item.value) - 8 : this.height;
738
+ })
739
+ .style('opacity', 1)
740
+ .ease(animationEasing);
537
741
  }
742
+ barGroups
743
+ .on('mouseenter', (event, d) => {
744
+ if (!this._initialAnimationComplete())
745
+ return;
746
+ const barEl = this.d3.select(event.currentTarget).select('path');
747
+ barEl.transition().duration(150).style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
748
+ const item = d.group.chartData[d.seriesIndex];
749
+ if (item)
750
+ this.handleBarHover(event, item, d.group);
751
+ })
752
+ .on('mousemove', (event) => {
753
+ if (this._initialAnimationComplete())
754
+ this.updateTooltipPosition(event);
755
+ })
756
+ .on('mouseleave', (event) => {
757
+ if (!this._initialAnimationComplete())
758
+ return;
759
+ const barEl = this.d3.select(event.currentTarget).select('path');
760
+ barEl.transition().duration(150).style('filter', null);
761
+ this._tooltipVisible.set(false);
762
+ })
763
+ .on('click', (event, d) => {
764
+ if (!this._initialAnimationComplete())
765
+ return;
766
+ const item = d.group.chartData[d.seriesIndex];
767
+ if (item)
768
+ this.handleBarClick(event, item);
769
+ });
770
+ bars
771
+ .transition()
772
+ .duration(animationDuration)
773
+ .delay((_, i) => i * 50)
774
+ .attrTween('d', (d) => {
775
+ const x = this.xSubScale(d.seriesLabel);
776
+ const width = this.xSubScale.bandwidth();
777
+ const item = d.group.chartData[d.seriesIndex];
778
+ const finalHeight = item ? this.height - this.yScale(item.value) : 0;
779
+ return (t) => {
780
+ const currentHeight = finalHeight * t;
781
+ const currentY = this.height - currentHeight;
782
+ const bottomY = this.height - 0.5; // Add small gap to prevent overlap with x-axis
783
+ if (radius > 0 && currentHeight > 0) {
784
+ // Limit radius to prevent it from exceeding half the bar width
785
+ const effectiveRadius = Math.min(radius, width / 2);
786
+ return `M${x},${bottomY} L${x},${currentY + effectiveRadius} Q${x},${currentY} ${x + effectiveRadius},${currentY} L${x + width - effectiveRadius},${currentY} Q${x + width},${currentY} ${x + width},${currentY + effectiveRadius} L${x + width},${bottomY} Z`;
787
+ }
788
+ else {
789
+ return `M${x},${bottomY} L${x},${currentY} L${x + width},${currentY} L${x + width},${bottomY} Z`;
790
+ }
791
+ };
792
+ })
793
+ .ease(animationEasing)
794
+ .end()
795
+ .then(() => this._initialAnimationComplete.set(true));
538
796
  }
797
+ /**
798
+ * Gets the appropriate D3 easing function based on the option string
799
+ */
800
+ // getEasingFunction moved to shared chart utils
539
801
  /**
540
802
  * Handles bar hover event and shows tooltip
541
803
  */
542
- handleBarHover(event, datum) {
804
+ handleBarHover(event, datum, group) {
543
805
  if (this.effectiveOptions().showTooltip !== false) {
544
- const index = this.data().findIndex((item) => item.id === datum.id);
545
- const color = datum.color || this.getColor(index);
806
+ const dataValue = this.data();
807
+ const isClustered = this.isClusteredData(dataValue);
808
+ const flatData = isClustered
809
+ ? dataValue.flatMap((g) => g.chartData)
810
+ : dataValue || [];
811
+ const index = flatData.findIndex((item) => item.id === datum.id && item.label === datum.label);
812
+ const color = datum.color || this.getColor(index !== -1 ? index : 0);
546
813
  // Calculate percentage of total
547
- const total = this.data().reduce((sum, item) => sum + item.value, 0);
814
+ const total = isClustered
815
+ ? dataValue.reduce((sum, g) => sum + g.chartData.reduce((s, it) => s + it.value, 0), 0)
816
+ : flatData.reduce((sum, item) => sum + item.value, 0);
548
817
  const percentage = total > 0 ? ((datum.value / total) * 100).toFixed(1) : '0';
549
818
  this._tooltipData.set({
550
- title: datum.tooltipLabel || datum.label,
819
+ title: group ? `${datum.tooltipLabel || datum.label} — ${group.label}` : datum.tooltipLabel || datum.label,
551
820
  value: datum.value.toString(),
552
821
  percentage: `${percentage}%`,
553
822
  color: color,
@@ -560,25 +829,14 @@ class AXBarChartComponent extends NXComponent {
560
829
  * Updates tooltip position based on mouse coordinates
561
830
  */
562
831
  updateTooltipPosition(event) {
563
- const container = this.chartContainerEl().nativeElement.getBoundingClientRect();
564
- const tooltipEl = this.chartContainerEl().nativeElement.querySelector('.chart-tooltip');
565
- if (!tooltipEl) {
566
- const x = event.clientX - container.left;
567
- const y = event.clientY - container.top;
568
- this._tooltipPosition.set({ x, y });
832
+ const containerEl = this.chartContainerEl()?.nativeElement;
833
+ if (!containerEl)
569
834
  return;
570
- }
571
- const tooltipRect = tooltipEl.getBoundingClientRect();
572
- let x = event.clientX - container.left;
573
- const y = event.clientY - container.top;
574
- // Adjust position if near the right edge
575
- if (x + tooltipRect.width + 10 > container.width) {
576
- x = x - tooltipRect.width - 10;
577
- }
578
- else {
579
- x = x + 10;
580
- }
581
- this._tooltipPosition.set({ x, y });
835
+ const containerRect = containerEl.getBoundingClientRect();
836
+ const tooltipEl = containerEl.querySelector('.chart-tooltip');
837
+ const tooltipRect = tooltipEl ? tooltipEl.getBoundingClientRect() : null;
838
+ const pos = computeTooltipPosition(containerRect, tooltipRect, event.clientX, event.clientY, this.TOOLTIP_GAP);
839
+ this._tooltipPosition.set(pos);
582
840
  }
583
841
  /**
584
842
  * Handles bar click event
@@ -590,61 +848,15 @@ class AXBarChartComponent extends NXComponent {
590
848
  * Shows a message when no data is available
591
849
  */
592
850
  showNoDataMessage(containerElement) {
593
- // Clear existing contents first
594
- this.d3.select(containerElement).selectAll('*').remove();
595
- const messageContainer = this.d3
596
- .select(containerElement)
597
- .append('div')
598
- // Apply generic container class, specific bar chart background class, and existing specific class
599
- .attr('class', 'ax-chart-message-container ax-bar-chart-message-background ax-bar-chart-no-data-message');
600
- // Add an icon
601
- messageContainer
602
- .append('div')
603
- // Apply generic icon class and specific bar chart icon class
604
- .attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
605
- .html('<i class="fa-light fa-chart-column fa-2x"></i>');
606
- // Add text message
607
- messageContainer
608
- .append('div')
609
- // Apply generic text class and specific bar chart text class
610
- .attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
611
- .text('No data available');
612
- // Add help text
613
- messageContainer
614
- .append('div')
615
- // Apply generic help class and specific bar chart help class
616
- .attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
617
- .text('Please provide data to display the chart');
851
+ this.clearChartArea(containerElement);
852
+ this.showMessage(containerElement, this.effectiveMessages().noDataIcon, this.effectiveMessages().noData, this.effectiveMessages().noDataHelp, 'ax-bar-chart-no-data-message');
618
853
  }
619
854
  /**
620
855
  * Shows a message when all bars are hidden
621
856
  */
622
857
  showAllBarsHiddenMessage(containerElement) {
623
- // Clear existing contents first
624
- this.d3.select(containerElement).selectAll('*').remove();
625
- const messageContainer = this.d3
626
- .select(containerElement)
627
- .append('div')
628
- // Apply generic container class, specific bar chart background class, and existing specific class
629
- .attr('class', 'ax-chart-message-container ax-bar-chart-message-background ax-bar-chart-no-data-message');
630
- // Add an icon
631
- messageContainer
632
- .append('div')
633
- // Apply generic icon class and specific bar chart icon class
634
- .attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
635
- .html('<i class="fa-light fa-eye-slash fa-2x"></i>');
636
- // Add text message
637
- messageContainer
638
- .append('div')
639
- // Apply generic text class and specific bar chart text class
640
- .attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
641
- .text('All bars are hidden');
642
- // Add help text
643
- messageContainer
644
- .append('div')
645
- // Apply generic help class and specific bar chart help class
646
- .attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
647
- .text('Click on legend items to show bars');
858
+ this.clearChartArea(containerElement);
859
+ this.showMessage(containerElement, this.effectiveMessages().allHiddenIcon, this.effectiveMessages().allHidden, this.effectiveMessages().allHiddenHelp, 'ax-bar-chart-no-data-message');
648
860
  }
649
861
  /**
650
862
  * Gets the color for a bar based on its index
@@ -663,11 +875,34 @@ class AXBarChartComponent extends NXComponent {
663
875
  * Returns legend items based on the chart data
664
876
  */
665
877
  getLegendItems() {
666
- const data = this.data() || [];
667
- const total = data.reduce((sum, item) => sum + (typeof item.value === 'number' ? item.value : 0), 0);
668
- return data.map((item, index) => {
669
- const value = typeof item.value === 'number' ? item.value : 0;
670
- const percentage = total > 0 ? (value / total) * 100 : 0;
878
+ const value = (this.data() || []);
879
+ if (this.isClusteredData(value)) {
880
+ const groups = value;
881
+ const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
882
+ const total = groups.reduce((sum, g) => sum + g.chartData.reduce((s, it) => s + (typeof it.value === 'number' ? it.value : 0), 0), 0);
883
+ return seriesLabels.map((label, index) => {
884
+ const seriesTotal = groups.reduce((s, g) => {
885
+ const item = g.chartData[index];
886
+ const v = item && typeof item.value === 'number' ? item.value : 0;
887
+ return s + v;
888
+ }, 0);
889
+ const percentage = total > 0 ? (seriesTotal / total) * 100 : 0;
890
+ const firstItem = groups[0]?.chartData[index];
891
+ return {
892
+ id: label,
893
+ name: label || `Series ${index + 1}`,
894
+ value: seriesTotal,
895
+ percentage,
896
+ color: firstItem?.color || this.getColor(index),
897
+ hidden: this.hiddenSeries.has(label),
898
+ };
899
+ });
900
+ }
901
+ const flat = value;
902
+ const total = flat.reduce((sum, item) => sum + (typeof item.value === 'number' ? item.value : 0), 0);
903
+ return flat.map((item, index) => {
904
+ const val = typeof item.value === 'number' ? item.value : 0;
905
+ const percentage = total > 0 ? (val / total) * 100 : 0;
671
906
  return {
672
907
  id: item.id,
673
908
  name: item.label || `Item ${index + 1}`,
@@ -685,9 +920,9 @@ class AXBarChartComponent extends NXComponent {
685
920
  highlightSegment(id) {
686
921
  if (!this.svg)
687
922
  return;
688
- // If the bar is hidden, we shouldn't try to highlight it
689
- if (id !== null && this.isBarHidden(id)) {
690
- // Just unhighlight everything when trying to highlight a hidden bar
923
+ const value = (this.data() || []);
924
+ const isClustered = this.isClusteredData(value);
925
+ const resetAll = () => {
691
926
  this.svg
692
927
  .selectAll('.ax-bar-chart-bar')
693
928
  .classed('ax-bar-chart-highlighted', false)
@@ -697,63 +932,63 @@ class AXBarChartComponent extends NXComponent {
697
932
  .attr('stroke', null)
698
933
  .attr('stroke-width', null)
699
934
  .attr('stroke-opacity', null);
935
+ };
936
+ if (id === null) {
937
+ resetAll();
700
938
  return;
701
939
  }
702
- if (id === null) {
703
- // If id is null, unhighlight everything
704
- this.svg
705
- .selectAll('.ax-bar-chart-bar')
706
- .classed('ax-bar-chart-highlighted', false)
707
- .classed('ax-bar-chart-dimmed', false)
708
- .attr('opacity', 1)
709
- .attr('filter', null)
710
- .attr('stroke', null)
711
- .attr('stroke-width', null)
712
- .attr('stroke-opacity', null);
940
+ if (!isClustered) {
941
+ if (this.isBarHidden(id)) {
942
+ resetAll();
943
+ return;
944
+ }
945
+ const targetBar = this.svg.selectAll('.ax-bar-chart-bar').filter((d) => d?.id === id);
946
+ if (targetBar.empty())
947
+ return;
948
+ const isCurrentlyHighlighted = targetBar.classed('ax-bar-chart-highlighted');
949
+ if (isCurrentlyHighlighted) {
950
+ resetAll();
951
+ }
952
+ else {
953
+ resetAll();
954
+ targetBar
955
+ .classed('ax-bar-chart-highlighted', true)
956
+ .attr('filter', 'drop-shadow(0 0 4px rgba(0, 0, 0, 0.3))')
957
+ .attr('stroke', '#000')
958
+ .attr('stroke-width', '1px')
959
+ .attr('stroke-opacity', '0.3')
960
+ .style('transition', 'all 0.2s ease-in-out');
961
+ this.svg
962
+ .selectAll('.ax-bar-chart-bar')
963
+ .filter((d) => d?.id !== id)
964
+ .classed('ax-bar-chart-dimmed', true)
965
+ .attr('opacity', 0.5)
966
+ .style('transition', 'opacity 0.2s ease-in-out');
967
+ }
713
968
  return;
714
969
  }
715
- // Find the target bar
716
- const targetBar = this.svg.selectAll('.ax-bar-chart-bar').filter((d) => d?.id === id);
717
- // Safety check - if no matching bar is found, do nothing
718
- if (targetBar.empty())
970
+ const targetBars = this.svg.selectAll('.ax-bar-chart-bar').filter((_, i, nodes) => {
971
+ const node = nodes[i];
972
+ return node.getAttribute('data-series') === id;
973
+ });
974
+ if (targetBars.empty())
719
975
  return;
720
- // Check if the target bar is currently highlighted
721
- const isCurrentlyHighlighted = targetBar.classed('ax-bar-chart-highlighted');
722
- if (isCurrentlyHighlighted) {
723
- // If already highlighted, unhighlight all bars
724
- this.svg
725
- .selectAll('.ax-bar-chart-bar')
726
- .classed('ax-bar-chart-highlighted', false)
727
- .classed('ax-bar-chart-dimmed', false)
728
- .attr('opacity', 1)
729
- .attr('filter', null)
730
- .attr('stroke', null)
731
- .attr('stroke-width', null)
732
- .attr('stroke-opacity', null);
976
+ const isHighlighted = targetBars.classed('ax-bar-chart-highlighted');
977
+ if (isHighlighted) {
978
+ resetAll();
733
979
  }
734
980
  else {
735
- // Reset all bars first
736
- this.svg
737
- .selectAll('.ax-bar-chart-bar')
738
- .classed('ax-bar-chart-highlighted', false)
739
- .classed('ax-bar-chart-dimmed', false)
740
- .attr('opacity', 1)
741
- .attr('filter', null)
742
- .attr('stroke', null)
743
- .attr('stroke-width', null)
744
- .attr('stroke-opacity', null);
745
- // Highlight the target bar
746
- targetBar
981
+ resetAll();
982
+ targetBars
747
983
  .classed('ax-bar-chart-highlighted', true)
748
984
  .attr('filter', 'drop-shadow(0 0 4px rgba(0, 0, 0, 0.3))')
749
985
  .attr('stroke', '#000')
750
986
  .attr('stroke-width', '1px')
751
987
  .attr('stroke-opacity', '0.3')
752
988
  .style('transition', 'all 0.2s ease-in-out');
753
- // Dim other bars
754
989
  this.svg
755
990
  .selectAll('.ax-bar-chart-bar')
756
- .filter((d) => d?.id !== id)
991
+ .filter((_, i, nodes) => nodes[i].getAttribute('data-series') !== id)
757
992
  .classed('ax-bar-chart-dimmed', true)
758
993
  .attr('opacity', 0.5)
759
994
  .style('transition', 'opacity 0.2s ease-in-out');
@@ -765,41 +1000,118 @@ class AXBarChartComponent extends NXComponent {
765
1000
  * @returns New visibility state (true = visible, false = hidden)
766
1001
  */
767
1002
  toggleSegment(id) {
768
- const wasAllHidden = this.data().length > 0 && this.data().every((d) => this.isBarHidden(d.id));
769
- if (this.hiddenBars.has(id)) {
770
- // Making a bar visible
771
- this.hiddenBars.delete(id);
772
- // If all bars were previously hidden, we need to ensure the message is cleared
1003
+ const value = (this.data() || []);
1004
+ const isClustered = this.isClusteredData(value);
1005
+ if (!isClustered) {
1006
+ const wasAllHidden = value.length > 0 && value.every((d) => this.isBarHidden(d.id));
1007
+ if (this.hiddenBars.has(id)) {
1008
+ this.hiddenBars.delete(id);
1009
+ if (wasAllHidden) {
1010
+ if (this.chartContainerEl()?.nativeElement) {
1011
+ this.d3
1012
+ ?.select(this.chartContainerEl().nativeElement)
1013
+ .selectAll('svg, .ax-chart-message-container')
1014
+ .remove();
1015
+ setTimeout(() => {
1016
+ this.createChart();
1017
+ }, 0);
1018
+ }
1019
+ }
1020
+ else {
1021
+ this.updateChart();
1022
+ }
1023
+ }
1024
+ else {
1025
+ this.hiddenBars.add(id);
1026
+ this.updateChart();
1027
+ }
1028
+ return !this.hiddenBars.has(id);
1029
+ }
1030
+ // Clustered mode: toggle series by label
1031
+ const groups = value;
1032
+ const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
1033
+ const seriesCount = seriesLabels.length;
1034
+ const wasAllHidden = seriesCount > 0 && this.hiddenSeries.size >= seriesCount;
1035
+ if (this.hiddenSeries.has(id)) {
1036
+ this.hiddenSeries.delete(id);
773
1037
  if (wasAllHidden) {
774
- // Force complete DOM cleanup and redraw
775
1038
  if (this.chartContainerEl()?.nativeElement) {
776
- // Clear everything in the container
777
- this.d3?.select(this.chartContainerEl().nativeElement).selectAll('*').remove();
778
- // Force full redraw
1039
+ this.d3?.select(this.chartContainerEl().nativeElement).selectAll('svg, .ax-chart-message-container').remove();
779
1040
  setTimeout(() => {
780
1041
  this.createChart();
781
1042
  }, 0);
782
1043
  }
783
1044
  }
784
1045
  else {
785
- // Normal update for other cases
786
1046
  this.updateChart();
787
1047
  }
788
1048
  }
789
1049
  else {
790
- // Hiding a bar
791
- this.hiddenBars.add(id);
1050
+ this.hiddenSeries.add(id);
792
1051
  this.updateChart();
793
1052
  }
794
- // Return the new visibility state
795
- return !this.hiddenBars.has(id);
1053
+ return !this.hiddenSeries.has(id);
1054
+ }
1055
+ // ===== Helper utilities for modularity and maintainability =====
1056
+ getContainerElement() {
1057
+ return this.chartContainerEl()?.nativeElement ?? null;
1058
+ }
1059
+ clearChartArea(containerElement) {
1060
+ this.d3.select(containerElement).selectAll('svg, .ax-chart-message-container').remove();
1061
+ }
1062
+ getXAxisTickFontSizeBasedOnWidth() {
1063
+ return Math.max(10, Math.min(14, Math.round(this.width / 50)));
1064
+ }
1065
+ getYAxisTickFontSizeBasedOnHeight() {
1066
+ return Math.max(11, Math.min(15, Math.round(this.height / 30)));
1067
+ }
1068
+ buildXAxisCommon(axesGroup, idToLabel, options) {
1069
+ this.xAxis = axesGroup
1070
+ .append('g')
1071
+ .attr('class', 'ax-bar-chart-axis-x')
1072
+ .attr('transform', `translate(0,${this.height})`)
1073
+ .call(this.d3.axisBottom(this.xScale).tickFormat((id) => idToLabel.get(id) ?? String(id)));
1074
+ const dynamicFontSize = this.getXAxisTickFontSizeBasedOnWidth();
1075
+ const xAxisTicks = this.xAxis
1076
+ .selectAll('text')
1077
+ .style('font-size', `${dynamicFontSize}px`)
1078
+ .style('font-weight', '400')
1079
+ .style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
1080
+ .attr('direction', 'ltr');
1081
+ const isRotated = this.shouldRotateXAxisLabels(idToLabel, dynamicFontSize);
1082
+ if (isRotated) {
1083
+ xAxisTicks.attr('transform', 'rotate(-45)').style('text-anchor', 'end').attr('dx', '-0.8em').attr('dy', '0.15em');
1084
+ }
1085
+ this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
1086
+ if (options.xAxisLabel && !isRotated) {
1087
+ const tickNodes = (this.xAxis.selectAll('text').nodes() || []);
1088
+ const measuredTickHeight = tickNodes.length > 0
1089
+ ? Math.max(...tickNodes.map((n) => (n && n.getBBox ? n.getBBox().height : dynamicFontSize)))
1090
+ : dynamicFontSize;
1091
+ const tickAreaHeight = measuredTickHeight + 6;
1092
+ const gapBelowTicks = 14;
1093
+ this.createXAxisTitle(axesGroup, options.xAxisLabel, tickAreaHeight, gapBelowTicks);
1094
+ }
1095
+ }
1096
+ // computeTooltipPosition moved to shared chart utils
1097
+ showMessage(containerElement, iconClass, text, helpText, specificClass) {
1098
+ const container = this.d3
1099
+ .select(containerElement)
1100
+ .append('div')
1101
+ .attr('class', `ax-chart-message-container ax-bar-chart-message-background ${specificClass}`);
1102
+ container
1103
+ .append('div')
1104
+ .attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
1105
+ .html(`<i class="${iconClass} fa-2x"></i>`);
1106
+ container.append('div').attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text').text(text);
1107
+ container.append('div').attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help').text(helpText);
796
1108
  }
797
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: AXBarChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
798
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.1.3", 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 });
1109
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXBarChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1110
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.3", 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\" role=\"img\" #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: 0, 0, 0, 0}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;background-color:rgba(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:hidden}ax-bar-chart .ax-bar-chart svg g:has(text){font-family:inherit}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;border:1px solid rgba(var(--ax-sys-color-surface));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 });
799
1111
  }
800
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: AXBarChartComponent, decorators: [{
1112
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXBarChartComponent, decorators: [{
801
1113
  type: Component,
802
- 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"] }]
1114
+ args: [{ selector: 'ax-bar-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-bar-chart\" role=\"img\" #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: 0, 0, 0, 0}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;background-color:rgba(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:hidden}ax-bar-chart .ax-bar-chart svg g:has(text){font-family:inherit}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;border:1px solid rgba(var(--ax-sys-color-surface));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"] }]
803
1115
  }], ctorParameters: () => [] });
804
1116
 
805
1117
  /**