@acorex/charts 21.0.0-next.2 → 21.0.0-next.21
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 +67 -7
- package/donut-chart/index.d.ts +46 -8
- package/fesm2022/acorex-charts-bar-chart.mjs +342 -124
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-legend.mjs +4 -4
- package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-tooltip.mjs +4 -4
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +298 -200
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-gauge-chart.mjs +93 -68
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +22 -16
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +295 -153
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts.mjs +93 -7
- package/fesm2022/acorex-charts.mjs.map +1 -1
- package/gauge-chart/index.d.ts +10 -7
- package/hierarchy-chart/index.d.ts +4 -5
- package/index.d.ts +43 -4
- package/line-chart/index.d.ts +70 -7
- package/package.json +1 -3
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AX_CHART_COLOR_PALETTE, getEasingFunction, computeTooltipPosition, getChartColor } from '@acorex/charts';
|
|
1
|
+
import { AXChartComponent, AX_CHART_COLOR_PALETTE, formatLargeNumber, getEasingFunction, getChartColor } from '@acorex/charts';
|
|
3
2
|
import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
|
|
4
3
|
import * as i0 from '@angular/core';
|
|
5
|
-
import { InjectionToken,
|
|
6
|
-
import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
|
|
7
|
-
import { set } from 'lodash-es';
|
|
4
|
+
import { InjectionToken, input, output, viewChild, signal, inject, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
|
|
8
5
|
|
|
9
6
|
const AXBarChartDefaultConfig = {
|
|
10
7
|
showXAxis: true,
|
|
@@ -28,11 +25,7 @@ const AXBarChartDefaultConfig = {
|
|
|
28
25
|
};
|
|
29
26
|
const AX_BAR_CHART_CONFIG = new InjectionToken('AX_BAR_CHART_CONFIG', {
|
|
30
27
|
providedIn: 'root',
|
|
31
|
-
factory: () =>
|
|
32
|
-
const global = inject(AX_GLOBAL_CONFIG);
|
|
33
|
-
set(global, 'chart.barChart', AXBarChartDefaultConfig);
|
|
34
|
-
return AXBarChartDefaultConfig;
|
|
35
|
-
},
|
|
28
|
+
factory: () => AXBarChartDefaultConfig,
|
|
36
29
|
});
|
|
37
30
|
function barChartConfig(config = {}) {
|
|
38
31
|
const result = {
|
|
@@ -46,7 +39,7 @@ function barChartConfig(config = {}) {
|
|
|
46
39
|
* Bar Chart Component
|
|
47
40
|
* Renders data as vertical bars with interactive hover effects and animations
|
|
48
41
|
*/
|
|
49
|
-
class AXBarChartComponent extends
|
|
42
|
+
class AXBarChartComponent extends AXChartComponent {
|
|
50
43
|
// Inputs
|
|
51
44
|
/** Chart data input */
|
|
52
45
|
data = input([], ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
@@ -59,7 +52,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
59
52
|
chartContainerEl = viewChild.required('chartContainer');
|
|
60
53
|
// D3 reference - loaded asynchronously
|
|
61
54
|
d3;
|
|
62
|
-
// Chart elements
|
|
55
|
+
// Chart elements - using proper D3 types
|
|
63
56
|
svg;
|
|
64
57
|
chart;
|
|
65
58
|
xScale;
|
|
@@ -71,10 +64,42 @@ class AXBarChartComponent extends NXComponent {
|
|
|
71
64
|
width;
|
|
72
65
|
height;
|
|
73
66
|
margin = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
74
|
-
//
|
|
67
|
+
// Constants for layout calculations
|
|
75
68
|
CHAR_WIDTH_RATIO = 0.65;
|
|
76
|
-
// Tooltip gap in pixels for consistent spacing
|
|
77
69
|
TOOLTIP_GAP = 10;
|
|
70
|
+
AXIS_TICK_PADDING = 10;
|
|
71
|
+
Y_AXIS_TITLE_THICKNESS = 14;
|
|
72
|
+
Y_AXIS_TITLE_PADDING = 14;
|
|
73
|
+
X_AXIS_TITLE_FONT_SIZE = 14;
|
|
74
|
+
X_AXIS_TITLE_GAP = 14;
|
|
75
|
+
X_AXIS_TITLE_BLOCK_HEIGHT = 24;
|
|
76
|
+
TOP_EDGE_PADDING = 6;
|
|
77
|
+
FALLBACK_PLOT_HEIGHT = 240;
|
|
78
|
+
MIN_PLOT_HEIGHT = 120;
|
|
79
|
+
MIN_TOTAL_WIDTH = 200;
|
|
80
|
+
OUTER_PADDING_CLUSTERED = 0.2;
|
|
81
|
+
INNER_PADDING_CLUSTERED = 0.1;
|
|
82
|
+
MIN_PADDING = 0.1;
|
|
83
|
+
HOVER_TRANSITION_DURATION = 150;
|
|
84
|
+
DEFAULT_ANIMATION_DELAY = 50;
|
|
85
|
+
LABEL_ANIMATION_DELAY = 100;
|
|
86
|
+
MIN_TOP_SPACE_FOR_LABEL = 20;
|
|
87
|
+
LABEL_OFFSET = 8;
|
|
88
|
+
ROTATION_TOLERANCE_SMALL_DATASET = 1.4;
|
|
89
|
+
SMALL_DATASET_THRESHOLD = 6;
|
|
90
|
+
Y_AXIS_PADDING = 20;
|
|
91
|
+
TICK_AREA_PADDING = 6;
|
|
92
|
+
TICK_AREA_GAP = 8;
|
|
93
|
+
DEFAULT_FONT_SIZE = 13;
|
|
94
|
+
FONT_WIDTH_MULTIPLIER = 0.6;
|
|
95
|
+
FONT_PADDING = 8;
|
|
96
|
+
MAX_LABEL_LENGTH = 20;
|
|
97
|
+
MIN_FONT_SIZE_X_AXIS = 8;
|
|
98
|
+
MAX_FONT_SIZE_X_AXIS = 14;
|
|
99
|
+
MIN_FONT_SIZE_Y_AXIS = 9;
|
|
100
|
+
MAX_FONT_SIZE_Y_AXIS = 15;
|
|
101
|
+
MANY_ITEMS_THRESHOLD = 20;
|
|
102
|
+
VERY_MANY_ITEMS_THRESHOLD = 50;
|
|
78
103
|
// Animation state
|
|
79
104
|
_initialAnimationComplete = signal(false, ...(ngDevMode ? [{ debugName: "_initialAnimationComplete" }] : []));
|
|
80
105
|
// Tooltip state
|
|
@@ -121,17 +146,39 @@ class AXBarChartComponent extends NXComponent {
|
|
|
121
146
|
...this.effectiveOptions().messages,
|
|
122
147
|
};
|
|
123
148
|
}, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : []));
|
|
124
|
-
// Track hidden bars
|
|
149
|
+
// Track hidden bars and series
|
|
125
150
|
hiddenBars = new Set();
|
|
126
|
-
// Track hidden series for clustered charts (by series label)
|
|
127
151
|
hiddenSeries = new Set();
|
|
128
|
-
//
|
|
152
|
+
// Computed values for better performance
|
|
153
|
+
isClusteredMode = computed(() => {
|
|
154
|
+
const value = this.data();
|
|
155
|
+
return Array.isArray(value) && value.length > 0 && 'chartData' in value[0];
|
|
156
|
+
}, ...(ngDevMode ? [{ debugName: "isClusteredMode" }] : []));
|
|
157
|
+
visibleData = computed(() => {
|
|
158
|
+
const value = this.data() || [];
|
|
159
|
+
if (this.isClusteredMode()) {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
return value.filter((d) => !this.hiddenBars.has(d.id));
|
|
163
|
+
}, ...(ngDevMode ? [{ debugName: "visibleData" }] : []));
|
|
164
|
+
hasVisibleData = computed(() => {
|
|
165
|
+
const value = this.data();
|
|
166
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
167
|
+
return false;
|
|
168
|
+
if (this.isClusteredMode()) {
|
|
169
|
+
const groups = value;
|
|
170
|
+
const seriesLabels = this.getClusterSeriesLabels(groups);
|
|
171
|
+
const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
|
|
172
|
+
return groups.some((g) => visibleSeries.some((_, idx) => !!g.chartData[idx]));
|
|
173
|
+
}
|
|
174
|
+
return value.some((d) => !this.hiddenBars.has(d.id));
|
|
175
|
+
}, ...(ngDevMode ? [{ debugName: "hasVisibleData" }] : []));
|
|
176
|
+
// Type guard helper
|
|
129
177
|
isClusteredData(value) {
|
|
130
|
-
return Array.isArray(value) && value.length > 0 &&
|
|
178
|
+
return Array.isArray(value) && value.length > 0 && 'chartData' in value[0];
|
|
131
179
|
}
|
|
132
180
|
getClusterSeriesLabels(groups) {
|
|
133
|
-
|
|
134
|
-
return first.map((d) => d.label);
|
|
181
|
+
return groups[0]?.chartData?.map((d) => d.label) ?? [];
|
|
135
182
|
}
|
|
136
183
|
constructor() {
|
|
137
184
|
super();
|
|
@@ -184,25 +231,14 @@ class AXBarChartComponent extends NXComponent {
|
|
|
184
231
|
const containerElement = this.chartContainerEl().nativeElement;
|
|
185
232
|
const inputValue = (this.data() || []);
|
|
186
233
|
const isClustered = this.isClusteredData(inputValue);
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
}
|
|
234
|
+
// Use computed visibility
|
|
235
|
+
const hasAnyVisible = this.hasVisibleData();
|
|
236
|
+
const visibleSingleData = isClustered ? [] : this.visibleData();
|
|
237
|
+
const groupedData = isClustered ? inputValue : [];
|
|
202
238
|
// Clear existing chart SVG and messages (do not remove tooltip component)
|
|
203
239
|
this.d3.select(containerElement).selectAll('svg, .ax-chart-message-container').remove();
|
|
204
240
|
// Early return if no data
|
|
205
|
-
if (!inputValue.length) {
|
|
241
|
+
if (!Array.isArray(inputValue) || inputValue.length === 0) {
|
|
206
242
|
this.showNoDataMessage(containerElement);
|
|
207
243
|
return;
|
|
208
244
|
}
|
|
@@ -241,8 +277,8 @@ class AXBarChartComponent extends NXComponent {
|
|
|
241
277
|
// Only remove chart SVG and message containers to preserve tooltip component
|
|
242
278
|
this.d3?.select(container).selectAll('svg, .ax-chart-message-container').remove();
|
|
243
279
|
}
|
|
244
|
-
this.svg =
|
|
245
|
-
this.chart =
|
|
280
|
+
this.svg = undefined;
|
|
281
|
+
this.chart = undefined;
|
|
246
282
|
this._tooltipVisible.set(false);
|
|
247
283
|
}
|
|
248
284
|
/**
|
|
@@ -258,11 +294,10 @@ class AXBarChartComponent extends NXComponent {
|
|
|
258
294
|
const showXAxis = options.showXAxis !== false;
|
|
259
295
|
const showYAxis = options.showYAxis !== false;
|
|
260
296
|
// Internal left padding for Y-axis ticks/labels/title
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const yAxisTitlePadding = options.yAxisLabel ? 14 : 0; // gap between ticks and title
|
|
297
|
+
const yAxisTitleThickness = options.yAxisLabel ? this.Y_AXIS_TITLE_THICKNESS : 0;
|
|
298
|
+
const yAxisTitlePadding = options.yAxisLabel ? this.Y_AXIS_TITLE_PADDING : 0;
|
|
264
299
|
const leftPaddingForYAxis = showYAxis
|
|
265
|
-
? this.calculateMaxYAxisTickLabelWidth() +
|
|
300
|
+
? this.calculateMaxYAxisTickLabelWidth() + this.AXIS_TICK_PADDING + yAxisTitlePadding + yAxisTitleThickness
|
|
266
301
|
: 0;
|
|
267
302
|
// Estimate internal bottom padding for X-axis ticks/labels/title
|
|
268
303
|
let bottomPaddingForXAxis = 0;
|
|
@@ -281,41 +316,29 @@ class AXBarChartComponent extends NXComponent {
|
|
|
281
316
|
const baseWidth = options.width ?? containerWidth;
|
|
282
317
|
const workingWidth = Math.max(1, baseWidth - leftPaddingForYAxis);
|
|
283
318
|
const dynamicFontSize = Math.max(10, Math.min(14, Math.round(workingWidth / 50)));
|
|
284
|
-
|
|
319
|
+
// Use truncated label length for calculation (same logic as actual rendering)
|
|
320
|
+
const maxLabelLength = barCount > this.MANY_ITEMS_THRESHOLD ? 10 : this.MAX_LABEL_LENGTH;
|
|
321
|
+
const effectiveLabelLength = Math.min(longestLabel.length, maxLabelLength);
|
|
322
|
+
const estimatedLongestLabelWidth = effectiveLabelLength * dynamicFontSize * 0.65;
|
|
285
323
|
const availableWidthPerBar = barCount > 0 ? workingWidth / barCount : workingWidth;
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
: options.rotateXAxisLabels === false
|
|
289
|
-
? false
|
|
290
|
-
: estimatedLongestLabelWidth > availableWidthPerBar;
|
|
324
|
+
const rotateOption = typeof options.rotateXAxisLabels === 'boolean' ? options.rotateXAxisLabels : undefined;
|
|
325
|
+
const willRotate = this.shouldLabelsRotate(rotateOption, estimatedLongestLabelWidth, availableWidthPerBar);
|
|
291
326
|
const tickAreaHeight = willRotate
|
|
292
|
-
? estimatedLongestLabelWidth * Math.SQRT1_2 + dynamicFontSize * Math.SQRT1_2 +
|
|
293
|
-
: dynamicFontSize +
|
|
294
|
-
const gapBelowTicks = options.xAxisLabel && !willRotate ?
|
|
295
|
-
const titleBlockHeight = options.xAxisLabel && !willRotate ?
|
|
327
|
+
? estimatedLongestLabelWidth * Math.SQRT1_2 + dynamicFontSize * Math.SQRT1_2 + this.TICK_AREA_PADDING
|
|
328
|
+
: dynamicFontSize + this.TICK_AREA_PADDING + 2;
|
|
329
|
+
const gapBelowTicks = options.xAxisLabel && !willRotate ? this.X_AXIS_TITLE_GAP : 0;
|
|
330
|
+
const titleBlockHeight = options.xAxisLabel && !willRotate ? this.X_AXIS_TITLE_BLOCK_HEIGHT : 0;
|
|
296
331
|
bottomPaddingForXAxis = tickAreaHeight + gapBelowTicks + titleBlockHeight;
|
|
297
332
|
}
|
|
298
333
|
// Determine plotting dimensions with sensible fallbacks
|
|
299
|
-
const fallbackPlotHeight = 240; // ensures visible plot when container has no height
|
|
300
334
|
const baseWidth = options.width ?? containerWidth;
|
|
301
|
-
const totalWidth = Math.max(
|
|
335
|
+
const totalWidth = Math.max(this.MIN_TOTAL_WIDTH, baseWidth > 0 ? baseWidth : this.MIN_TOTAL_WIDTH);
|
|
302
336
|
const plotWidth = Math.max(0, totalWidth - leftPaddingForYAxis);
|
|
303
|
-
|
|
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);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
plotHeight = fallbackPlotHeight;
|
|
312
|
-
}
|
|
337
|
+
const plotHeight = this.calculatePlotHeight(options.height, containerHeight, bottomPaddingForXAxis);
|
|
313
338
|
this.width = plotWidth;
|
|
314
339
|
this.height = plotHeight;
|
|
315
340
|
// Create SVG with explicit pixel dimensions to avoid collapse
|
|
316
|
-
|
|
317
|
-
const topPaddingForTopEdge = 6;
|
|
318
|
-
const totalHeight = this.height + bottomPaddingForXAxis + topPaddingForTopEdge;
|
|
341
|
+
const totalHeight = this.height + bottomPaddingForXAxis + this.TOP_EDGE_PADDING;
|
|
319
342
|
const svg = this.d3
|
|
320
343
|
.select(containerElement)
|
|
321
344
|
.append('svg')
|
|
@@ -328,17 +351,38 @@ class AXBarChartComponent extends NXComponent {
|
|
|
328
351
|
this.chart = this.svg
|
|
329
352
|
.append('g')
|
|
330
353
|
.attr('class', 'chart-content')
|
|
331
|
-
.attr('transform', `translate(${leftPaddingForYAxis},${
|
|
354
|
+
.attr('transform', `translate(${leftPaddingForYAxis},${this.TOP_EDGE_PADDING})`);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Calculates plot height based on options and container dimensions
|
|
358
|
+
*/
|
|
359
|
+
calculatePlotHeight(optionHeight, containerHeight, bottomPadding) {
|
|
360
|
+
if (optionHeight && optionHeight > 0) {
|
|
361
|
+
return Math.max(this.MIN_PLOT_HEIGHT, optionHeight - bottomPadding);
|
|
362
|
+
}
|
|
363
|
+
if (containerHeight > 0) {
|
|
364
|
+
return Math.max(this.MIN_PLOT_HEIGHT, containerHeight - bottomPadding, this.FALLBACK_PLOT_HEIGHT);
|
|
365
|
+
}
|
|
366
|
+
return this.FALLBACK_PLOT_HEIGHT;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Determines if X-axis labels should rotate
|
|
370
|
+
*/
|
|
371
|
+
shouldLabelsRotate(rotateOption, estimatedWidth, availableWidth) {
|
|
372
|
+
if (rotateOption === true)
|
|
373
|
+
return true;
|
|
374
|
+
if (rotateOption === false)
|
|
375
|
+
return false;
|
|
376
|
+
return estimatedWidth > availableWidth;
|
|
332
377
|
}
|
|
333
|
-
// calculateMargins was unused; removed to simplify and reduce maintenance surface
|
|
334
378
|
/**
|
|
335
379
|
* Creates x and y scales for the chart
|
|
336
380
|
*/
|
|
337
381
|
setupScales(data) {
|
|
338
|
-
// Get the bar width percentage (default
|
|
339
|
-
const barWidthPercent = this.effectiveOptions().barWidth ??
|
|
382
|
+
// Get the bar width percentage (default 60%)
|
|
383
|
+
const barWidthPercent = this.effectiveOptions().barWidth ?? 0.6;
|
|
340
384
|
// Calculate padding based on barWidth (inverse relationship)
|
|
341
|
-
const padding = Math.max(
|
|
385
|
+
const padding = Math.max(this.MIN_PADDING, 1 - barWidthPercent);
|
|
342
386
|
// Create x scale (band scale for categorical data)
|
|
343
387
|
this.xScale = this.d3
|
|
344
388
|
.scaleBand()
|
|
@@ -346,9 +390,12 @@ class AXBarChartComponent extends NXComponent {
|
|
|
346
390
|
.range([0, this.width])
|
|
347
391
|
.padding(padding);
|
|
348
392
|
// Create y scale (linear scale for values)
|
|
393
|
+
// Support negative values by calculating min/max
|
|
394
|
+
const minValue = this.d3.min(data, (d) => d.value) || 0;
|
|
395
|
+
const maxValue = this.d3.max(data, (d) => d.value) || 0;
|
|
349
396
|
this.yScale = this.d3
|
|
350
397
|
.scaleLinear()
|
|
351
|
-
.domain([0,
|
|
398
|
+
.domain([Math.min(0, minValue), Math.max(0, maxValue)])
|
|
352
399
|
.nice()
|
|
353
400
|
.range([this.height, 0]);
|
|
354
401
|
}
|
|
@@ -356,18 +403,21 @@ class AXBarChartComponent extends NXComponent {
|
|
|
356
403
|
* Creates x and y scales for clustered charts
|
|
357
404
|
*/
|
|
358
405
|
setupScalesClustered(groups) {
|
|
359
|
-
const seriesLabels =
|
|
406
|
+
const seriesLabels = this.getClusterSeriesLabels(groups);
|
|
360
407
|
const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
|
|
361
408
|
// Outer band for clusters
|
|
362
|
-
const outerPadding = 0.2;
|
|
363
409
|
this.xScale = this.d3
|
|
364
410
|
.scaleBand()
|
|
365
411
|
.domain(groups.map((g) => g.id))
|
|
366
412
|
.range([0, this.width])
|
|
367
|
-
.padding(
|
|
413
|
+
.padding(this.OUTER_PADDING_CLUSTERED);
|
|
368
414
|
// Inner band for series
|
|
369
|
-
this.xSubScale = this.d3
|
|
370
|
-
|
|
415
|
+
this.xSubScale = this.d3
|
|
416
|
+
.scaleBand()
|
|
417
|
+
.domain(visibleSeries)
|
|
418
|
+
.range([0, this.xScale.bandwidth()])
|
|
419
|
+
.padding(this.INNER_PADDING_CLUSTERED);
|
|
420
|
+
// Y scale across all visible values (support negative values)
|
|
371
421
|
const allVisibleValues = [];
|
|
372
422
|
for (const g of groups) {
|
|
373
423
|
visibleSeries.forEach((_, idx) => {
|
|
@@ -376,8 +426,13 @@ class AXBarChartComponent extends NXComponent {
|
|
|
376
426
|
allVisibleValues.push(item.value);
|
|
377
427
|
});
|
|
378
428
|
}
|
|
429
|
+
const yMin = this.d3.min(allVisibleValues) || 0;
|
|
379
430
|
const yMax = this.d3.max(allVisibleValues) || 0;
|
|
380
|
-
this.yScale = this.d3
|
|
431
|
+
this.yScale = this.d3
|
|
432
|
+
.scaleLinear()
|
|
433
|
+
.domain([Math.min(0, yMin), Math.max(0, yMax)])
|
|
434
|
+
.nice()
|
|
435
|
+
.range([this.height, 0]);
|
|
381
436
|
}
|
|
382
437
|
/**
|
|
383
438
|
* Creates x and y axes with grid lines
|
|
@@ -403,8 +458,20 @@ class AXBarChartComponent extends NXComponent {
|
|
|
403
458
|
this.buildXAxisCommon(axesGroup, idToLabel, options);
|
|
404
459
|
}
|
|
405
460
|
createYAxis(axesGroup, options) {
|
|
406
|
-
// Create Y axis
|
|
407
|
-
|
|
461
|
+
// Create Y axis with smart number formatting
|
|
462
|
+
const yAxisGenerator = this.d3.axisLeft(this.yScale).tickFormat((value) => {
|
|
463
|
+
const numValue = typeof value === 'number' ? value : value.valueOf();
|
|
464
|
+
return formatLargeNumber(numValue);
|
|
465
|
+
});
|
|
466
|
+
// Reduce tick count for better readability
|
|
467
|
+
const maxValue = this.yScale.domain()[1];
|
|
468
|
+
if (maxValue > 1e6) {
|
|
469
|
+
yAxisGenerator.ticks(6); // Fewer ticks for large numbers
|
|
470
|
+
}
|
|
471
|
+
else if (maxValue > 1e3) {
|
|
472
|
+
yAxisGenerator.ticks(8);
|
|
473
|
+
}
|
|
474
|
+
this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(yAxisGenerator);
|
|
408
475
|
// Style the axis text
|
|
409
476
|
const dynamicFontSize = this.getYAxisTickFontSizeBasedOnHeight();
|
|
410
477
|
this.yAxis
|
|
@@ -435,14 +502,14 @@ class AXBarChartComponent extends NXComponent {
|
|
|
435
502
|
const labels = Array.from(idToLabel.values());
|
|
436
503
|
const longestLabel = labels.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
437
504
|
const estimatedWidth = longestLabel.length * fontSize * this.CHAR_WIDTH_RATIO;
|
|
438
|
-
// Heuristic: avoid rotation when there are few categories
|
|
505
|
+
// Heuristic: avoid rotation when there are few categories even if estimation is slightly larger
|
|
439
506
|
const domainCount = this.xScale.domain().length;
|
|
440
|
-
if (domainCount <=
|
|
441
|
-
return estimatedWidth > step *
|
|
507
|
+
if (domainCount <= this.SMALL_DATASET_THRESHOLD) {
|
|
508
|
+
return estimatedWidth > step * this.ROTATION_TOLERANCE_SMALL_DATASET;
|
|
442
509
|
}
|
|
443
510
|
return estimatedWidth > step;
|
|
444
511
|
}
|
|
445
|
-
createXAxisTitle(axesGroup, title, tickAreaHeight, gapBelowTicks =
|
|
512
|
+
createXAxisTitle(axesGroup, title, tickAreaHeight, gapBelowTicks = this.TICK_AREA_GAP) {
|
|
446
513
|
// Title positioned directly below tick area with a consistent gap
|
|
447
514
|
const labelY = this.height + tickAreaHeight + gapBelowTicks;
|
|
448
515
|
axesGroup
|
|
@@ -453,7 +520,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
453
520
|
.attr('x', this.width / 2)
|
|
454
521
|
.attr('y', labelY)
|
|
455
522
|
.attr('direction', 'ltr')
|
|
456
|
-
.style('font-size',
|
|
523
|
+
.style('font-size', `${this.X_AXIS_TITLE_FONT_SIZE}px`)
|
|
457
524
|
.style('font-weight', '500')
|
|
458
525
|
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
459
526
|
.text(title);
|
|
@@ -465,7 +532,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
465
532
|
// Chart group is translated by (margin.left, margin.top)
|
|
466
533
|
// So Y-axis is at X = margin.left in SVG coordinates
|
|
467
534
|
const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
468
|
-
const padding =
|
|
535
|
+
const padding = this.Y_AXIS_PADDING;
|
|
469
536
|
// Position title to the left of the tick labels
|
|
470
537
|
// Since text-anchor: end aligns the text to the Y-axis, we need to go further left
|
|
471
538
|
const labelY = -maxTickLabelWidth - padding;
|
|
@@ -506,9 +573,9 @@ class AXBarChartComponent extends NXComponent {
|
|
|
506
573
|
const visibleData = raw.filter((d) => !this.isBarHidden(d.id));
|
|
507
574
|
maxValue = visibleData.reduce((acc, d) => (d.value > acc ? d.value : acc), 0);
|
|
508
575
|
}
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
return tickLabelText.length *
|
|
576
|
+
// Use formatted number for width calculation
|
|
577
|
+
const tickLabelText = Number.isFinite(maxValue) ? formatLargeNumber(maxValue) : '00000';
|
|
578
|
+
return tickLabelText.length * this.DEFAULT_FONT_SIZE * this.FONT_WIDTH_MULTIPLIER + this.FONT_PADDING;
|
|
512
579
|
}
|
|
513
580
|
createGridLines() {
|
|
514
581
|
this.chart
|
|
@@ -599,14 +666,30 @@ class AXBarChartComponent extends NXComponent {
|
|
|
599
666
|
.style('font-weight', '500')
|
|
600
667
|
.style('fill', 'rgb(var(--ax-comp-bar-chart-data-labels-color))')
|
|
601
668
|
.style('opacity', 0) // Start invisible for animation
|
|
602
|
-
.text((d) =>
|
|
603
|
-
|
|
669
|
+
.text((d) => {
|
|
670
|
+
// Format large numbers in data labels
|
|
671
|
+
return d.value >= 1000 ? formatLargeNumber(d.value) : d.value;
|
|
672
|
+
});
|
|
673
|
+
// Animate data labels with smart positioning to avoid clipping
|
|
674
|
+
const animationDelay = this.effectiveOptions().animationDelay ?? this.DEFAULT_ANIMATION_DELAY;
|
|
604
675
|
barGroups
|
|
605
676
|
.selectAll('.ax-bar-chart-data-label')
|
|
606
677
|
.transition()
|
|
607
678
|
.duration(animationDuration)
|
|
608
|
-
.delay((d, i) => i *
|
|
609
|
-
.attr('y', (d) =>
|
|
679
|
+
.delay((d, i) => i * animationDelay + this.LABEL_ANIMATION_DELAY)
|
|
680
|
+
.attr('y', (d) => {
|
|
681
|
+
const barTop = this.yScale(d.value);
|
|
682
|
+
// If bar is near the top (100% or close), position label inside the bar
|
|
683
|
+
if (barTop < this.MIN_TOP_SPACE_FOR_LABEL) {
|
|
684
|
+
return barTop + this.LABEL_OFFSET;
|
|
685
|
+
}
|
|
686
|
+
// Otherwise, position above the bar
|
|
687
|
+
return barTop - this.LABEL_OFFSET;
|
|
688
|
+
})
|
|
689
|
+
.attr('data-inside-bar', (d) => {
|
|
690
|
+
const barTop = this.yScale(d.value);
|
|
691
|
+
return barTop < this.MIN_TOP_SPACE_FOR_LABEL ? 'true' : 'false';
|
|
692
|
+
})
|
|
610
693
|
.style('opacity', 1)
|
|
611
694
|
.ease(animationEasing);
|
|
612
695
|
}
|
|
@@ -618,7 +701,10 @@ class AXBarChartComponent extends NXComponent {
|
|
|
618
701
|
return;
|
|
619
702
|
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
620
703
|
// Standard hover effect - darken the bar slightly and add a subtle shadow
|
|
621
|
-
barEl
|
|
704
|
+
barEl
|
|
705
|
+
.transition()
|
|
706
|
+
.duration(this.HOVER_TRANSITION_DURATION)
|
|
707
|
+
.style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
|
|
622
708
|
this.handleBarHover(event, d);
|
|
623
709
|
})
|
|
624
710
|
.on('mousemove', (event) => {
|
|
@@ -633,7 +719,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
633
719
|
return;
|
|
634
720
|
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
635
721
|
// Remove hover effect
|
|
636
|
-
barEl.transition().duration(
|
|
722
|
+
barEl.transition().duration(this.HOVER_TRANSITION_DURATION).style('filter', null);
|
|
637
723
|
this._tooltipVisible.set(false);
|
|
638
724
|
})
|
|
639
725
|
.on('click', (event, d) => {
|
|
@@ -643,10 +729,11 @@ class AXBarChartComponent extends NXComponent {
|
|
|
643
729
|
}
|
|
644
730
|
});
|
|
645
731
|
// Add animation
|
|
732
|
+
const animationDelay = this.effectiveOptions().animationDelay ?? this.DEFAULT_ANIMATION_DELAY;
|
|
646
733
|
bars
|
|
647
734
|
.transition()
|
|
648
735
|
.duration(animationDuration)
|
|
649
|
-
.delay((d, i) => i *
|
|
736
|
+
.delay((d, i) => i * animationDelay)
|
|
650
737
|
.attrTween('d', (d) => {
|
|
651
738
|
const x = this.xScale(d.id);
|
|
652
739
|
const width = this.xScale.bandwidth();
|
|
@@ -676,7 +763,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
676
763
|
* Renders bars for clustered charts
|
|
677
764
|
*/
|
|
678
765
|
renderBarsClustered(groups) {
|
|
679
|
-
const seriesLabels =
|
|
766
|
+
const seriesLabels = this.getClusterSeriesLabels(groups);
|
|
680
767
|
const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
|
|
681
768
|
this._initialAnimationComplete.set(false);
|
|
682
769
|
const radius = this.effectiveOptions().cornerRadius;
|
|
@@ -704,13 +791,12 @@ class AXBarChartComponent extends NXComponent {
|
|
|
704
791
|
.attr('d', (d) => {
|
|
705
792
|
const x = this.xSubScale(d.seriesLabel);
|
|
706
793
|
const width = this.xSubScale.bandwidth();
|
|
707
|
-
const y = this.height - 0.5;
|
|
794
|
+
const y = this.height - 0.5;
|
|
708
795
|
return `M${x},${y} L${x},${y} L${x + width},${y} L${x + width},${y} Z`;
|
|
709
796
|
})
|
|
710
797
|
.attr('fill', (d) => {
|
|
711
798
|
const item = d.group.chartData[d.seriesIndex];
|
|
712
|
-
|
|
713
|
-
return item?.color || this.getColor(colorIndex);
|
|
799
|
+
return item?.color || this.getColor(d.seriesIndex);
|
|
714
800
|
});
|
|
715
801
|
if (this.effectiveOptions().showDataLabels !== false) {
|
|
716
802
|
barGroups
|
|
@@ -724,17 +810,36 @@ class AXBarChartComponent extends NXComponent {
|
|
|
724
810
|
.style('fill', 'rgb(var(--ax-comp-bar-chart-data-labels-color))')
|
|
725
811
|
.style('opacity', 0)
|
|
726
812
|
.text((d) => {
|
|
727
|
-
const
|
|
728
|
-
|
|
813
|
+
const value = d.group.chartData[d.seriesIndex]?.value;
|
|
814
|
+
if (value === undefined)
|
|
815
|
+
return '';
|
|
816
|
+
// Format large numbers in data labels
|
|
817
|
+
return value >= 1000 ? formatLargeNumber(value) : value;
|
|
729
818
|
});
|
|
819
|
+
const animationDelay = this.effectiveOptions().animationDelay ?? this.DEFAULT_ANIMATION_DELAY;
|
|
730
820
|
barGroups
|
|
731
821
|
.selectAll('.ax-bar-chart-data-label')
|
|
732
822
|
.transition()
|
|
733
823
|
.duration(animationDuration)
|
|
734
|
-
.delay((
|
|
824
|
+
.delay((_d, i) => i * animationDelay + this.LABEL_ANIMATION_DELAY)
|
|
735
825
|
.attr('y', (d) => {
|
|
736
826
|
const item = d.group.chartData[d.seriesIndex];
|
|
737
|
-
|
|
827
|
+
if (!item)
|
|
828
|
+
return this.height;
|
|
829
|
+
const barTop = this.yScale(item.value);
|
|
830
|
+
// If bar is near the top (100% or close), position label inside the bar
|
|
831
|
+
if (barTop < this.MIN_TOP_SPACE_FOR_LABEL) {
|
|
832
|
+
return barTop + this.LABEL_OFFSET;
|
|
833
|
+
}
|
|
834
|
+
// Otherwise, position above the bar
|
|
835
|
+
return barTop - this.LABEL_OFFSET;
|
|
836
|
+
})
|
|
837
|
+
.attr('data-inside-bar', (d) => {
|
|
838
|
+
const item = d.group.chartData[d.seriesIndex];
|
|
839
|
+
if (!item)
|
|
840
|
+
return 'false';
|
|
841
|
+
const barTop = this.yScale(item.value);
|
|
842
|
+
return barTop < this.MIN_TOP_SPACE_FOR_LABEL ? 'true' : 'false';
|
|
738
843
|
})
|
|
739
844
|
.style('opacity', 1)
|
|
740
845
|
.ease(animationEasing);
|
|
@@ -744,7 +849,10 @@ class AXBarChartComponent extends NXComponent {
|
|
|
744
849
|
if (!this._initialAnimationComplete())
|
|
745
850
|
return;
|
|
746
851
|
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
747
|
-
barEl
|
|
852
|
+
barEl
|
|
853
|
+
.transition()
|
|
854
|
+
.duration(this.HOVER_TRANSITION_DURATION)
|
|
855
|
+
.style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
|
|
748
856
|
const item = d.group.chartData[d.seriesIndex];
|
|
749
857
|
if (item)
|
|
750
858
|
this.handleBarHover(event, item, d.group);
|
|
@@ -757,7 +865,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
757
865
|
if (!this._initialAnimationComplete())
|
|
758
866
|
return;
|
|
759
867
|
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
760
|
-
barEl.transition().duration(
|
|
868
|
+
barEl.transition().duration(this.HOVER_TRANSITION_DURATION).style('filter', null);
|
|
761
869
|
this._tooltipVisible.set(false);
|
|
762
870
|
})
|
|
763
871
|
.on('click', (event, d) => {
|
|
@@ -767,10 +875,11 @@ class AXBarChartComponent extends NXComponent {
|
|
|
767
875
|
if (item)
|
|
768
876
|
this.handleBarClick(event, item);
|
|
769
877
|
});
|
|
878
|
+
const animationDelay = this.effectiveOptions().animationDelay ?? this.DEFAULT_ANIMATION_DELAY;
|
|
770
879
|
bars
|
|
771
880
|
.transition()
|
|
772
881
|
.duration(animationDuration)
|
|
773
|
-
.delay((
|
|
882
|
+
.delay((_d, i) => i * animationDelay)
|
|
774
883
|
.attrTween('d', (d) => {
|
|
775
884
|
const x = this.xSubScale(d.seriesLabel);
|
|
776
885
|
const width = this.xSubScale.bandwidth();
|
|
@@ -817,7 +926,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
817
926
|
const percentage = total > 0 ? ((datum.value / total) * 100).toFixed(1) : '0';
|
|
818
927
|
this._tooltipData.set({
|
|
819
928
|
title: group ? `${datum.tooltipLabel || datum.label} — ${group.label}` : datum.tooltipLabel || datum.label,
|
|
820
|
-
value: datum.value
|
|
929
|
+
value: formatLargeNumber(datum.value),
|
|
821
930
|
percentage: `${percentage}%`,
|
|
822
931
|
color: color,
|
|
823
932
|
});
|
|
@@ -827,6 +936,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
827
936
|
}
|
|
828
937
|
/**
|
|
829
938
|
* Updates tooltip position based on mouse coordinates
|
|
939
|
+
* Enhanced to handle overflow issues by positioning tooltips more intelligently
|
|
830
940
|
*/
|
|
831
941
|
updateTooltipPosition(event) {
|
|
832
942
|
const containerEl = this.chartContainerEl()?.nativeElement;
|
|
@@ -835,9 +945,61 @@ class AXBarChartComponent extends NXComponent {
|
|
|
835
945
|
const containerRect = containerEl.getBoundingClientRect();
|
|
836
946
|
const tooltipEl = containerEl.querySelector('.chart-tooltip');
|
|
837
947
|
const tooltipRect = tooltipEl ? tooltipEl.getBoundingClientRect() : null;
|
|
838
|
-
|
|
948
|
+
// Enhanced positioning logic for better tooltip placement
|
|
949
|
+
const pos = this.computeEnhancedTooltipPosition(containerRect, tooltipRect, event.clientX, event.clientY);
|
|
839
950
|
this._tooltipPosition.set(pos);
|
|
840
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* Enhanced tooltip positioning that considers chart boundaries and prevents overflow
|
|
954
|
+
*/
|
|
955
|
+
computeEnhancedTooltipPosition(containerRect, tooltipRect, clientX, clientY) {
|
|
956
|
+
const cursorX = clientX - containerRect.left;
|
|
957
|
+
const cursorY = clientY - containerRect.top;
|
|
958
|
+
if (!tooltipRect) {
|
|
959
|
+
// Default positioning when tooltip dimensions are unknown
|
|
960
|
+
return {
|
|
961
|
+
x: Math.min(cursorX + this.TOOLTIP_GAP, containerRect.width - this.TOOLTIP_GAP),
|
|
962
|
+
y: Math.max(this.TOOLTIP_GAP, Math.min(cursorY, containerRect.height - this.TOOLTIP_GAP)),
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
// Calculate available space in each direction
|
|
966
|
+
const spaceRight = containerRect.width - cursorX;
|
|
967
|
+
const spaceLeft = cursorX;
|
|
968
|
+
const spaceBelow = containerRect.height - cursorY;
|
|
969
|
+
const spaceAbove = cursorY;
|
|
970
|
+
// Determine best horizontal position
|
|
971
|
+
let x;
|
|
972
|
+
if (spaceRight >= tooltipRect.width + this.TOOLTIP_GAP) {
|
|
973
|
+
// Position to the right of cursor
|
|
974
|
+
x = cursorX + this.TOOLTIP_GAP;
|
|
975
|
+
}
|
|
976
|
+
else if (spaceLeft >= tooltipRect.width + this.TOOLTIP_GAP) {
|
|
977
|
+
// Position to the left of cursor
|
|
978
|
+
x = cursorX - tooltipRect.width - this.TOOLTIP_GAP;
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
// Center horizontally if neither side has enough space
|
|
982
|
+
x = Math.max(this.TOOLTIP_GAP, Math.min((containerRect.width - tooltipRect.width) / 2, containerRect.width - tooltipRect.width - this.TOOLTIP_GAP));
|
|
983
|
+
}
|
|
984
|
+
// Calculate best vertical position
|
|
985
|
+
let y;
|
|
986
|
+
if (spaceBelow >= tooltipRect.height + this.TOOLTIP_GAP) {
|
|
987
|
+
// Position below cursor
|
|
988
|
+
y = cursorY + this.TOOLTIP_GAP;
|
|
989
|
+
}
|
|
990
|
+
else if (spaceAbove >= tooltipRect.height + this.TOOLTIP_GAP) {
|
|
991
|
+
// Position above cursor
|
|
992
|
+
y = cursorY - tooltipRect.height - this.TOOLTIP_GAP;
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
// Center vertically if neither direction has enough space
|
|
996
|
+
y = Math.max(this.TOOLTIP_GAP, Math.min((containerRect.height - tooltipRect.height) / 2, containerRect.height - tooltipRect.height - this.TOOLTIP_GAP));
|
|
997
|
+
}
|
|
998
|
+
// Ensure tooltip stays within container bounds
|
|
999
|
+
x = Math.max(this.TOOLTIP_GAP, Math.min(x, containerRect.width - tooltipRect.width - this.TOOLTIP_GAP));
|
|
1000
|
+
y = Math.max(this.TOOLTIP_GAP, Math.min(y, containerRect.height - tooltipRect.height - this.TOOLTIP_GAP));
|
|
1001
|
+
return { x, y };
|
|
1002
|
+
}
|
|
841
1003
|
/**
|
|
842
1004
|
* Handles bar click event
|
|
843
1005
|
*/
|
|
@@ -942,7 +1104,9 @@ class AXBarChartComponent extends NXComponent {
|
|
|
942
1104
|
resetAll();
|
|
943
1105
|
return;
|
|
944
1106
|
}
|
|
945
|
-
const targetBar = this.svg
|
|
1107
|
+
const targetBar = this.svg
|
|
1108
|
+
.selectAll('.ax-bar-chart-bar')
|
|
1109
|
+
.filter((d) => d?.id === id);
|
|
946
1110
|
if (targetBar.empty())
|
|
947
1111
|
return;
|
|
948
1112
|
const isCurrentlyHighlighted = targetBar.classed('ax-bar-chart-highlighted');
|
|
@@ -967,7 +1131,9 @@ class AXBarChartComponent extends NXComponent {
|
|
|
967
1131
|
}
|
|
968
1132
|
return;
|
|
969
1133
|
}
|
|
970
|
-
const targetBars = this.svg
|
|
1134
|
+
const targetBars = this.svg
|
|
1135
|
+
.selectAll('.ax-bar-chart-bar')
|
|
1136
|
+
.filter((_d, i, nodes) => {
|
|
971
1137
|
const node = nodes[i];
|
|
972
1138
|
return node.getAttribute('data-series') === id;
|
|
973
1139
|
});
|
|
@@ -988,7 +1154,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
988
1154
|
.style('transition', 'all 0.2s ease-in-out');
|
|
989
1155
|
this.svg
|
|
990
1156
|
.selectAll('.ax-bar-chart-bar')
|
|
991
|
-
.filter((
|
|
1157
|
+
.filter((_d, i, nodes) => nodes[i].getAttribute('data-series') !== id)
|
|
992
1158
|
.classed('ax-bar-chart-dimmed', true)
|
|
993
1159
|
.attr('opacity', 0.5)
|
|
994
1160
|
.style('transition', 'opacity 0.2s ease-in-out');
|
|
@@ -1059,18 +1225,63 @@ class AXBarChartComponent extends NXComponent {
|
|
|
1059
1225
|
clearChartArea(containerElement) {
|
|
1060
1226
|
this.d3.select(containerElement).selectAll('svg, .ax-chart-message-container').remove();
|
|
1061
1227
|
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Truncates long labels with ellipsis
|
|
1230
|
+
*/
|
|
1231
|
+
truncateLabel(label, maxLength = this.MAX_LABEL_LENGTH) {
|
|
1232
|
+
if (label.length <= maxLength)
|
|
1233
|
+
return label;
|
|
1234
|
+
return label.substring(0, maxLength - 1) + '…';
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Gets adaptive font size for X-axis based on width and item count
|
|
1238
|
+
*/
|
|
1062
1239
|
getXAxisTickFontSizeBasedOnWidth() {
|
|
1063
|
-
|
|
1240
|
+
const itemCount = this.xScale.domain().length;
|
|
1241
|
+
const baseSize = Math.round(this.width / 50);
|
|
1242
|
+
// Reduce font size for many items
|
|
1243
|
+
let adjustedSize = baseSize;
|
|
1244
|
+
if (itemCount > this.VERY_MANY_ITEMS_THRESHOLD) {
|
|
1245
|
+
adjustedSize = Math.round(baseSize * 0.7);
|
|
1246
|
+
}
|
|
1247
|
+
else if (itemCount > this.MANY_ITEMS_THRESHOLD) {
|
|
1248
|
+
adjustedSize = Math.round(baseSize * 0.85);
|
|
1249
|
+
}
|
|
1250
|
+
return Math.max(this.MIN_FONT_SIZE_X_AXIS, Math.min(this.MAX_FONT_SIZE_X_AXIS, adjustedSize));
|
|
1064
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Gets adaptive font size for Y-axis based on height
|
|
1254
|
+
*/
|
|
1065
1255
|
getYAxisTickFontSizeBasedOnHeight() {
|
|
1066
|
-
|
|
1256
|
+
const baseSize = Math.round(this.height / 30);
|
|
1257
|
+
return Math.max(this.MIN_FONT_SIZE_Y_AXIS, Math.min(this.MAX_FONT_SIZE_Y_AXIS, baseSize));
|
|
1067
1258
|
}
|
|
1068
1259
|
buildXAxisCommon(axesGroup, idToLabel, options) {
|
|
1260
|
+
const itemCount = this.xScale.domain().length;
|
|
1261
|
+
// Smart tick reduction for many items
|
|
1262
|
+
let tickValues = this.xScale.domain();
|
|
1263
|
+
if (itemCount > this.VERY_MANY_ITEMS_THRESHOLD) {
|
|
1264
|
+
// Show every 5th tick
|
|
1265
|
+
tickValues = tickValues.filter((_d, i) => i % 5 === 0);
|
|
1266
|
+
}
|
|
1267
|
+
else if (itemCount > this.MANY_ITEMS_THRESHOLD) {
|
|
1268
|
+
// Show every 2nd tick
|
|
1269
|
+
tickValues = tickValues.filter((_d, i) => i % 2 === 0);
|
|
1270
|
+
}
|
|
1271
|
+
const axis = this.d3
|
|
1272
|
+
.axisBottom(this.xScale)
|
|
1273
|
+
.tickValues(tickValues)
|
|
1274
|
+
.tickFormat((id) => {
|
|
1275
|
+
const label = idToLabel.get(id) ?? String(id);
|
|
1276
|
+
// Truncate long labels intelligently
|
|
1277
|
+
const maxLength = itemCount > this.MANY_ITEMS_THRESHOLD ? 10 : this.MAX_LABEL_LENGTH;
|
|
1278
|
+
return this.truncateLabel(label, maxLength);
|
|
1279
|
+
});
|
|
1069
1280
|
this.xAxis = axesGroup
|
|
1070
1281
|
.append('g')
|
|
1071
1282
|
.attr('class', 'ax-bar-chart-axis-x')
|
|
1072
1283
|
.attr('transform', `translate(0,${this.height})`)
|
|
1073
|
-
.call(
|
|
1284
|
+
.call(axis);
|
|
1074
1285
|
const dynamicFontSize = this.getXAxisTickFontSizeBasedOnWidth();
|
|
1075
1286
|
const xAxisTicks = this.xAxis
|
|
1076
1287
|
.selectAll('text')
|
|
@@ -1086,10 +1297,17 @@ class AXBarChartComponent extends NXComponent {
|
|
|
1086
1297
|
if (options.xAxisLabel && !isRotated) {
|
|
1087
1298
|
const tickNodes = (this.xAxis.selectAll('text').nodes() || []);
|
|
1088
1299
|
const measuredTickHeight = tickNodes.length > 0
|
|
1089
|
-
? Math.max(...tickNodes.map((n) =>
|
|
1300
|
+
? Math.max(...tickNodes.map((n) => {
|
|
1301
|
+
try {
|
|
1302
|
+
return n?.getBBox?.()?.height ?? dynamicFontSize;
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
return dynamicFontSize;
|
|
1306
|
+
}
|
|
1307
|
+
}))
|
|
1090
1308
|
: dynamicFontSize;
|
|
1091
|
-
const tickAreaHeight = measuredTickHeight +
|
|
1092
|
-
const gapBelowTicks =
|
|
1309
|
+
const tickAreaHeight = measuredTickHeight + this.TICK_AREA_PADDING;
|
|
1310
|
+
const gapBelowTicks = this.X_AXIS_TITLE_GAP;
|
|
1093
1311
|
this.createXAxisTitle(axesGroup, options.xAxisLabel, tickAreaHeight, gapBelowTicks);
|
|
1094
1312
|
}
|
|
1095
1313
|
}
|
|
@@ -1106,13 +1324,13 @@ class AXBarChartComponent extends NXComponent {
|
|
|
1106
1324
|
container.append('div').attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text').text(text);
|
|
1107
1325
|
container.append('div').attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help').text(helpText);
|
|
1108
1326
|
}
|
|
1109
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1110
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.
|
|
1327
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXBarChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1328
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.9", 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;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 .ax-bar-chart-data-label{pointer-events:none;-webkit-user-select:none;user-select:none}ax-bar-chart .ax-bar-chart .ax-bar-chart-data-label[data-inside-bar=true]{fill:#fff;font-weight:600;text-shadow:0 0 2px rgba(0,0,0,.3)}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 });
|
|
1111
1329
|
}
|
|
1112
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1330
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXBarChartComponent, decorators: [{
|
|
1113
1331
|
type: Component,
|
|
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;
|
|
1115
|
-
}], ctorParameters: () => [] });
|
|
1332
|
+
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;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 .ax-bar-chart-data-label{pointer-events:none;-webkit-user-select:none;user-select:none}ax-bar-chart .ax-bar-chart .ax-bar-chart-data-label[data-inside-bar=true]{fill:#fff;font-weight:600;text-shadow:0 0 2px rgba(0,0,0,.3)}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"] }]
|
|
1333
|
+
}], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], barClick: [{ type: i0.Output, args: ["barClick"] }], chartContainerEl: [{ type: i0.ViewChild, args: ['chartContainer', { isSignal: true }] }] } });
|
|
1116
1334
|
|
|
1117
1335
|
/**
|
|
1118
1336
|
* Generated bundle index. Do not edit.
|