@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.
- package/bar-chart/index.d.ts +74 -13
- package/chart-legend/index.d.ts +1 -1
- package/chart-tooltip/index.d.ts +11 -4
- package/donut-chart/index.d.ts +39 -6
- package/fesm2022/acorex-charts-bar-chart.mjs +672 -360
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-legend.mjs +12 -15
- package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-tooltip.mjs +38 -13
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +205 -228
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-gauge-chart.mjs +53 -86
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +29 -9
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +67 -60
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts.mjs +72 -1
- package/fesm2022/acorex-charts.mjs.map +1 -1
- package/gauge-chart/index.d.ts +20 -9
- package/hierarchy-chart/index.d.ts +27 -4
- package/index.d.ts +29 -1
- package/line-chart/index.d.ts +34 -5
- package/package.json +3 -1
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 (!
|
|
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 (
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.chart
|
|
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
|
-
//
|
|
193
|
-
this.
|
|
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
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
215
|
-
.attr('viewBox', `0 0 ${
|
|
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
|
|
219
|
-
this.chart = this.svg
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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()
|
|
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 =
|
|
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('
|
|
575
|
+
.append('path')
|
|
429
576
|
.attr('class', 'ax-bar-chart-bar')
|
|
430
|
-
.attr('
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
.
|
|
434
|
-
|
|
435
|
-
|
|
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.
|
|
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('
|
|
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
|
|
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('
|
|
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
|
-
.
|
|
504
|
-
|
|
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
|
-
*
|
|
676
|
+
* Renders bars for clustered charts
|
|
514
677
|
*/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
545
|
-
const
|
|
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 =
|
|
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
|
|
564
|
-
|
|
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
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
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
|
-
|
|
594
|
-
this.
|
|
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
|
-
|
|
624
|
-
this.
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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 (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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((
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
this.
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
this.hiddenBars.add(id);
|
|
1050
|
+
this.hiddenSeries.add(id);
|
|
792
1051
|
this.updateChart();
|
|
793
1052
|
}
|
|
794
|
-
|
|
795
|
-
|
|
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.
|
|
798
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.
|
|
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.
|
|
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:
|
|
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
|
/**
|