@acorex/charts 20.1.25 → 20.1.26
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 +36 -1
- package/donut-chart/index.d.ts +31 -1
- package/fesm2022/acorex-charts-bar-chart.mjs +343 -249
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +34 -20
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-gauge-chart.mjs +7 -2
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +22 -5
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +31 -8
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/gauge-chart/index.d.ts +14 -1
- package/hierarchy-chart/index.d.ts +22 -1
- package/line-chart/index.d.ts +28 -1
- package/package.json +1 -1
|
@@ -17,6 +17,14 @@ const AXBarChartDefaultConfig = {
|
|
|
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',
|
|
@@ -94,6 +102,21 @@ class AXBarChartComponent extends NXComponent {
|
|
|
94
102
|
...this.options(),
|
|
95
103
|
};
|
|
96
104
|
}, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
|
|
105
|
+
// Messages with defaults
|
|
106
|
+
effectiveMessages = computed(() => {
|
|
107
|
+
const defaultMessages = {
|
|
108
|
+
noData: 'No data available',
|
|
109
|
+
noDataHelp: 'Please provide data to display the chart',
|
|
110
|
+
allHidden: 'All bars are hidden',
|
|
111
|
+
allHiddenHelp: 'Click on legend items to show bars',
|
|
112
|
+
noDataIcon: 'fa-light fa-chart-column',
|
|
113
|
+
allHiddenIcon: 'fa-light fa-eye-slash',
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
...defaultMessages,
|
|
117
|
+
...this.effectiveOptions().messages,
|
|
118
|
+
};
|
|
119
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : []));
|
|
97
120
|
// Track hidden bars
|
|
98
121
|
hiddenBars = new Set();
|
|
99
122
|
// Track hidden series for clustered charts (by series label)
|
|
@@ -173,10 +196,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
173
196
|
hasAnyVisible = visibleSingleData.length > 0;
|
|
174
197
|
}
|
|
175
198
|
// Clear existing chart SVG and messages (do not remove tooltip component)
|
|
176
|
-
this.d3
|
|
177
|
-
.select(containerElement)
|
|
178
|
-
.selectAll('svg, .ax-chart-message-container')
|
|
179
|
-
.remove();
|
|
199
|
+
this.d3.select(containerElement).selectAll('svg, .ax-chart-message-container').remove();
|
|
180
200
|
// Early return if no data
|
|
181
201
|
if (!inputValue.length) {
|
|
182
202
|
this.showNoDataMessage(containerElement);
|
|
@@ -242,34 +262,39 @@ class AXBarChartComponent extends NXComponent {
|
|
|
242
262
|
this.width = Math.max(containerWidth, minDim) - this.margin.left - this.margin.right;
|
|
243
263
|
this.height = Math.max(containerHeight, minDim) - this.margin.top - this.margin.bottom;
|
|
244
264
|
}
|
|
245
|
-
// Create responsive SVG
|
|
265
|
+
// Create responsive SVG with proper coordinate system
|
|
266
|
+
const totalWidth = this.width + this.margin.left + this.margin.right;
|
|
267
|
+
const totalHeight = this.height + this.margin.top + this.margin.bottom;
|
|
246
268
|
const svg = this.d3
|
|
247
269
|
.select(containerElement)
|
|
248
270
|
.append('svg')
|
|
249
271
|
.attr('width', '100%')
|
|
250
272
|
.attr('height', '100%')
|
|
251
|
-
.attr('viewBox', `0 0 ${
|
|
273
|
+
.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`)
|
|
252
274
|
.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
253
275
|
this.svg = svg;
|
|
254
|
-
// Create chart group
|
|
255
|
-
this.chart = this.svg
|
|
276
|
+
// Create chart group positioned at margin offsets
|
|
277
|
+
this.chart = this.svg
|
|
278
|
+
.append('g')
|
|
279
|
+
.attr('class', 'chart-content')
|
|
280
|
+
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
|
|
256
281
|
}
|
|
257
282
|
/**
|
|
258
283
|
* Calculates chart margins based on options
|
|
259
284
|
*/
|
|
260
285
|
calculateMargins(options, containerWidth) {
|
|
261
|
-
// Start with
|
|
286
|
+
// Start with scientifically calculated base margins
|
|
262
287
|
this.margin = {
|
|
263
288
|
top: 20,
|
|
264
|
-
right:
|
|
265
|
-
bottom:
|
|
266
|
-
left:
|
|
289
|
+
right: 25,
|
|
290
|
+
bottom: 40,
|
|
291
|
+
left: 60,
|
|
267
292
|
};
|
|
268
293
|
const raw = (this.data() || []);
|
|
269
294
|
const isClustered = this.isClusteredData(raw);
|
|
270
295
|
const barCount = isClustered
|
|
271
296
|
? raw.length
|
|
272
|
-
:
|
|
297
|
+
: raw.filter((d) => !this.isBarHidden(d.id)).length;
|
|
273
298
|
// Pre-emptively increase bottom margin if x-axis labels are likely to be rotated.
|
|
274
299
|
// This is an estimation before scales and full dimensions are calculated.
|
|
275
300
|
if (barCount > 0 && containerWidth > 0) {
|
|
@@ -285,29 +310,24 @@ class AXBarChartComponent extends NXComponent {
|
|
|
285
310
|
return visibleData.reduce((a, b) => (a.length > b.label.length ? a : b.label), '');
|
|
286
311
|
})();
|
|
287
312
|
// Estimate font size using the same logic as in createAxes, but with workingWidth.
|
|
288
|
-
const estimatedFontSize = Math.max(
|
|
289
|
-
// Estimate label width (using 0.
|
|
290
|
-
const estimatedLongestLabelWidth = longestLabel.length * estimatedFontSize * 0.
|
|
313
|
+
const estimatedFontSize = Math.max(10, Math.min(14, Math.round(workingWidth / 50)));
|
|
314
|
+
// Estimate label width (using 0.65 as character width-to-height ratio for better Persian text support).
|
|
315
|
+
const estimatedLongestLabelWidth = longestLabel.length * estimatedFontSize * 0.65;
|
|
291
316
|
// If estimated label width is greater than the space available, rotation will occur.
|
|
292
317
|
if (estimatedLongestLabelWidth > availableWidthPerBar) {
|
|
293
318
|
// Add space for rotated labels. The height of the rotated label's bounding box
|
|
294
|
-
// is roughly its length * sin(45 degrees). Add
|
|
295
|
-
const requiredExtraMargin = estimatedLongestLabelWidth * Math.sin(Math.PI / 4) +
|
|
296
|
-
this.margin.bottom += Math.min(
|
|
319
|
+
// is roughly its length * sin(45 degrees). Add extra padding for Persian text.
|
|
320
|
+
const requiredExtraMargin = estimatedLongestLabelWidth * Math.sin(Math.PI / 4) + 20;
|
|
321
|
+
this.margin.bottom += Math.min(100, requiredExtraMargin); // Increased cap for Persian text.
|
|
322
|
+
// Also ensure minimum space for axis title if present
|
|
323
|
+
if (options.xAxisLabel) {
|
|
324
|
+
// Add substantial extra space for axis title when labels are rotated
|
|
325
|
+
// Account for the increased spacing needed for rotated text bounding box
|
|
326
|
+
this.margin.bottom += 30; // Further increased to match the positioning calculation
|
|
327
|
+
}
|
|
297
328
|
}
|
|
298
329
|
}
|
|
299
330
|
}
|
|
300
|
-
// Adjust margins if axis labels are present
|
|
301
|
-
if (options.xAxisLabel) {
|
|
302
|
-
const xLabelLength = options.xAxisLabel.length;
|
|
303
|
-
const extraBottomMargin = Math.min(20, Math.max(10, xLabelLength * 0.8));
|
|
304
|
-
this.margin.bottom = Math.max(this.margin.bottom, 30 + extraBottomMargin);
|
|
305
|
-
}
|
|
306
|
-
if (options.yAxisLabel) {
|
|
307
|
-
const yLabelLength = options.yAxisLabel.length;
|
|
308
|
-
const extraLeftMargin = Math.min(20, Math.max(10, yLabelLength * 0.8));
|
|
309
|
-
this.margin.left = Math.max(this.margin.left, 40 + extraLeftMargin);
|
|
310
|
-
}
|
|
311
331
|
// Ensure enough left margin for Y-axis tick labels (avoid clipping large numbers)
|
|
312
332
|
if (options.showYAxis !== false && barCount > 0) {
|
|
313
333
|
const maxValue = (() => {
|
|
@@ -330,20 +350,24 @@ class AXBarChartComponent extends NXComponent {
|
|
|
330
350
|
})();
|
|
331
351
|
// Use a localized string to approximate default axis formatter (e.g., includes thousands separators)
|
|
332
352
|
const exampleTickLabel = Number.isFinite(maxValue) ? maxValue.toLocaleString() : '00000';
|
|
333
|
-
// Approximate font size used in axis creation (fallback to 13px here since height isn't finalized yet)
|
|
334
|
-
const estimatedYAxisTickFontSize = 13;
|
|
335
|
-
// Estimate label width: characters * font size * average char width ratio + padding
|
|
336
|
-
const estimatedTickLabelWidth = exampleTickLabel.length * estimatedYAxisTickFontSize * 0.6 + 12;
|
|
337
353
|
// Reserve additional space for tick mark and padding
|
|
338
|
-
|
|
354
|
+
const maxYAxisLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
355
|
+
const baseYAxisSpace = 25; // Space for axis line and padding
|
|
356
|
+
const yAxisTitleSpace = options.yAxisLabel ? 35 : 0; // Space for Y-axis title if present
|
|
357
|
+
this.margin.left = Math.max(this.margin.left, maxYAxisLabelWidth + baseYAxisSpace + yAxisTitleSpace);
|
|
339
358
|
}
|
|
340
|
-
// Ensure minimum margins
|
|
359
|
+
// Ensure minimum margins meet scientific requirements
|
|
341
360
|
if (options.showXAxis !== false) {
|
|
342
|
-
this.margin.bottom = Math.max(this.margin.bottom,
|
|
361
|
+
this.margin.bottom = Math.max(this.margin.bottom, 40); // Minimum for X-axis
|
|
343
362
|
}
|
|
344
363
|
if (options.showYAxis !== false) {
|
|
345
|
-
this.margin.left = Math.max(this.margin.left,
|
|
364
|
+
this.margin.left = Math.max(this.margin.left, 60); // Minimum for Y-axis
|
|
346
365
|
}
|
|
366
|
+
// Ensure final margin integrity
|
|
367
|
+
this.margin.top = Math.max(this.margin.top, 20);
|
|
368
|
+
this.margin.right = Math.max(this.margin.right, 25);
|
|
369
|
+
this.margin.bottom = Math.max(this.margin.bottom, 40);
|
|
370
|
+
this.margin.left = Math.max(this.margin.left, 60);
|
|
347
371
|
}
|
|
348
372
|
/**
|
|
349
373
|
* Creates x and y scales for the chart
|
|
@@ -380,11 +404,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
380
404
|
.range([0, this.width])
|
|
381
405
|
.padding(outerPadding);
|
|
382
406
|
// Inner band for series
|
|
383
|
-
this.xSubScale = this.d3
|
|
384
|
-
.scaleBand()
|
|
385
|
-
.domain(visibleSeries)
|
|
386
|
-
.range([0, this.xScale.bandwidth()])
|
|
387
|
-
.padding(0.1);
|
|
407
|
+
this.xSubScale = this.d3.scaleBand().domain(visibleSeries).range([0, this.xScale.bandwidth()]).padding(0.1);
|
|
388
408
|
// Y scale across all visible values
|
|
389
409
|
const allVisibleValues = [];
|
|
390
410
|
for (const g of groups) {
|
|
@@ -401,112 +421,178 @@ class AXBarChartComponent extends NXComponent {
|
|
|
401
421
|
* Creates x and y axes with grid lines
|
|
402
422
|
*/
|
|
403
423
|
createAxes(options) {
|
|
404
|
-
// Only create axes if they are enabled in options
|
|
405
424
|
const showXAxis = options.showXAxis !== false;
|
|
406
425
|
const showYAxis = options.showYAxis !== false;
|
|
407
426
|
const showGrid = options.showGrid !== false;
|
|
408
|
-
// Create a group for all axes
|
|
409
427
|
const axesGroup = this.chart.append('g').attr('class', 'ax-bar-chart-axes');
|
|
410
428
|
if (showXAxis) {
|
|
411
|
-
|
|
412
|
-
const visibleDataForTicks = (this.data() || []).filter((d) => !this.isBarHidden(d.id));
|
|
413
|
-
const idToLabel = new Map(visibleDataForTicks.map((d) => [d.id, d.label]));
|
|
414
|
-
// Create X axis
|
|
415
|
-
this.xAxis = axesGroup
|
|
416
|
-
.append('g')
|
|
417
|
-
.attr('class', 'ax-bar-chart-axis-x')
|
|
418
|
-
.attr('transform', `translate(0,${this.height})`)
|
|
419
|
-
.call(this.d3.axisBottom(this.xScale).tickFormat((id) => idToLabel.get(id) ?? String(id)));
|
|
420
|
-
// Style the axis text
|
|
421
|
-
const dynamicXAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.width / 45)));
|
|
422
|
-
const xAxisTicks = this.xAxis
|
|
423
|
-
.selectAll('text')
|
|
424
|
-
.style('font-size', `${dynamicXAxisTickFontSize}px`)
|
|
425
|
-
.style('font-weight', '400')
|
|
426
|
-
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
427
|
-
.attr('direction', 'ltr');
|
|
428
|
-
// Automatically rotate labels if they are likely to overlap
|
|
429
|
-
if (this.xScale.domain().length > 0) {
|
|
430
|
-
const step = this.xScale.step();
|
|
431
|
-
const labelsForRotation = Array.from(idToLabel.values());
|
|
432
|
-
const longestLabel = labelsForRotation.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
433
|
-
// Using 0.55 as a safer estimate for char width-to-height ratio
|
|
434
|
-
const estimatedLongestLabelWidth = longestLabel.length * dynamicXAxisTickFontSize * 0.55;
|
|
435
|
-
if (estimatedLongestLabelWidth > step) {
|
|
436
|
-
xAxisTicks
|
|
437
|
-
.attr('transform', 'rotate(-45)')
|
|
438
|
-
.style('text-anchor', 'end')
|
|
439
|
-
.attr('dx', '-0.8em')
|
|
440
|
-
.attr('dy', '0.15em');
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
// Style all lines in the x-axis (path, ticks)
|
|
444
|
-
this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
445
|
-
// Add X axis label if provided
|
|
446
|
-
if (options.xAxisLabel) {
|
|
447
|
-
const labelY = this.height + this.margin.bottom * 0.8;
|
|
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', 'middle')
|
|
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(options.xAxisLabel);
|
|
460
|
-
}
|
|
429
|
+
this.createXAxis(axesGroup, options);
|
|
461
430
|
}
|
|
462
431
|
if (showYAxis) {
|
|
463
|
-
|
|
464
|
-
this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(this.d3.axisLeft(this.yScale));
|
|
465
|
-
// Style the axis text
|
|
466
|
-
const dynamicYAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.height / 30)));
|
|
467
|
-
this.yAxis
|
|
468
|
-
.selectAll('text')
|
|
469
|
-
.style('font-size', `${dynamicYAxisTickFontSize}px`)
|
|
470
|
-
.style('font-weight', '400')
|
|
471
|
-
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
472
|
-
.attr('text-anchor', 'end')
|
|
473
|
-
.attr('direction', 'ltr');
|
|
474
|
-
// Style all lines in the y-axis (path, ticks)
|
|
475
|
-
this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
476
|
-
// Add Y axis label if provided
|
|
477
|
-
if (options.yAxisLabel) {
|
|
478
|
-
const labelX = -this.height / 2;
|
|
479
|
-
const labelY = -this.margin.left * 0.8;
|
|
480
|
-
axesGroup
|
|
481
|
-
.append('text')
|
|
482
|
-
.attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
|
|
483
|
-
.attr('text-anchor', 'middle')
|
|
484
|
-
.attr('dominant-baseline', 'middle')
|
|
485
|
-
.attr('transform', 'rotate(-90)')
|
|
486
|
-
.attr('x', labelX)
|
|
487
|
-
.attr('y', labelY)
|
|
488
|
-
.attr('direction', 'ltr')
|
|
489
|
-
.style('font-size', '14px')
|
|
490
|
-
.style('font-weight', '500')
|
|
491
|
-
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
492
|
-
.text(options.yAxisLabel);
|
|
493
|
-
}
|
|
432
|
+
this.createYAxis(axesGroup, options);
|
|
494
433
|
}
|
|
495
434
|
if (showGrid) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
this.
|
|
435
|
+
this.createGridLines();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
createXAxis(axesGroup, options) {
|
|
439
|
+
// Build a map from id to label for tick formatting
|
|
440
|
+
const visibleDataForTicks = (this.data() || []).filter((d) => !this.isBarHidden(d.id));
|
|
441
|
+
const idToLabel = new Map(visibleDataForTicks.map((d) => [d.id, d.label]));
|
|
442
|
+
// Create X axis
|
|
443
|
+
this.xAxis = axesGroup
|
|
444
|
+
.append('g')
|
|
445
|
+
.attr('class', 'ax-bar-chart-axis-x')
|
|
446
|
+
.attr('transform', `translate(0,${this.height})`)
|
|
447
|
+
.call(this.d3.axisBottom(this.xScale).tickFormat((id) => idToLabel.get(id) ?? String(id)));
|
|
448
|
+
// Style the axis text - optimized font size for Persian text
|
|
449
|
+
const dynamicFontSize = Math.max(10, Math.min(14, Math.round(this.width / 50)));
|
|
450
|
+
const xAxisTicks = this.xAxis
|
|
451
|
+
.selectAll('text')
|
|
452
|
+
.style('font-size', `${dynamicFontSize}px`)
|
|
453
|
+
.style('font-weight', '400')
|
|
454
|
+
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
455
|
+
.attr('direction', 'ltr');
|
|
456
|
+
// Handle label rotation for long Persian text
|
|
457
|
+
const isRotated = this.shouldRotateXAxisLabels(idToLabel, dynamicFontSize);
|
|
458
|
+
if (isRotated) {
|
|
459
|
+
xAxisTicks.attr('transform', 'rotate(-45)').style('text-anchor', 'end').attr('dx', '-0.8em').attr('dy', '0.15em');
|
|
460
|
+
}
|
|
461
|
+
// Style axis lines
|
|
462
|
+
this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
463
|
+
// Add X axis title with proper spacing
|
|
464
|
+
if (options.xAxisLabel) {
|
|
465
|
+
this.createXAxisTitle(axesGroup, options.xAxisLabel, isRotated);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
createYAxis(axesGroup, options) {
|
|
469
|
+
// Create Y axis
|
|
470
|
+
this.yAxis = axesGroup.append('g').attr('class', 'ax-bar-chart-axis-y').call(this.d3.axisLeft(this.yScale));
|
|
471
|
+
// Style the axis text
|
|
472
|
+
const dynamicFontSize = Math.max(11, Math.min(15, Math.round(this.height / 30)));
|
|
473
|
+
this.yAxis
|
|
474
|
+
.selectAll('text')
|
|
475
|
+
.style('font-size', `${dynamicFontSize}px`)
|
|
476
|
+
.style('font-weight', '400')
|
|
477
|
+
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
478
|
+
.attr('text-anchor', 'end')
|
|
479
|
+
.attr('direction', 'ltr');
|
|
480
|
+
// Style axis lines
|
|
481
|
+
this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
482
|
+
// Add Y axis title
|
|
483
|
+
if (options.yAxisLabel) {
|
|
484
|
+
this.createYAxisTitle(axesGroup, options.yAxisLabel);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
shouldRotateXAxisLabels(idToLabel, fontSize) {
|
|
488
|
+
if (this.xScale.domain().length === 0)
|
|
489
|
+
return false;
|
|
490
|
+
const step = this.xScale.step();
|
|
491
|
+
const labels = Array.from(idToLabel.values());
|
|
492
|
+
const longestLabel = labels.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
493
|
+
const estimatedWidth = longestLabel.length * fontSize * 0.65; // Persian text ratio
|
|
494
|
+
return estimatedWidth > step;
|
|
495
|
+
}
|
|
496
|
+
createXAxisTitle(axesGroup, title, isRotated) {
|
|
497
|
+
// Calculate the exact position relative to the SVG coordinate system
|
|
498
|
+
// X-axis is positioned at Y = this.height within the chart group
|
|
499
|
+
// Chart group is translated by (margin.left, margin.top)
|
|
500
|
+
// So X-axis is at Y = margin.top + this.height in SVG coordinates
|
|
501
|
+
let titleOffset;
|
|
502
|
+
if (isRotated) {
|
|
503
|
+
// For 45° rotated text, we need to account for the diagonal bounding box
|
|
504
|
+
// The rotated text extends both horizontally and vertically from the baseline
|
|
505
|
+
const fontSize = 13; // Approximate font size for tick labels
|
|
506
|
+
const textLength = title.length;
|
|
507
|
+
// For 45° rotation, vertical extent = textLength * fontSize * sin(45°) + fontSize * cos(45°)
|
|
508
|
+
const verticalExtent = textLength * fontSize * 0.707 + fontSize * 0.707;
|
|
509
|
+
titleOffset = verticalExtent + 20; // Add padding
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
// For horizontal text, account for font height + padding
|
|
513
|
+
const fontSize = 13; // Approximate font size for tick labels
|
|
514
|
+
titleOffset = fontSize + 12; // Font height + padding
|
|
509
515
|
}
|
|
516
|
+
// Position the title below the X-axis line
|
|
517
|
+
const labelY = this.height + titleOffset;
|
|
518
|
+
axesGroup
|
|
519
|
+
.append('text')
|
|
520
|
+
.attr('class', 'ax-bar-chart-axis-label ax-x-axis-label')
|
|
521
|
+
.attr('text-anchor', 'middle')
|
|
522
|
+
.attr('dominant-baseline', 'hanging') // Align to top of text for better positioning
|
|
523
|
+
.attr('x', this.width / 2)
|
|
524
|
+
.attr('y', labelY)
|
|
525
|
+
.attr('direction', 'ltr')
|
|
526
|
+
.style('font-size', '14px')
|
|
527
|
+
.style('font-weight', '500')
|
|
528
|
+
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
529
|
+
.text(title);
|
|
530
|
+
}
|
|
531
|
+
createYAxisTitle(axesGroup, title) {
|
|
532
|
+
// Calculate the exact position for Y-axis title
|
|
533
|
+
// Y-axis tick labels use text-anchor: end, so they extend LEFT of the Y-axis line
|
|
534
|
+
// Y-axis is at X = 0 within the chart group
|
|
535
|
+
// Chart group is translated by (margin.left, margin.top)
|
|
536
|
+
// So Y-axis is at X = margin.left in SVG coordinates
|
|
537
|
+
const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
538
|
+
const padding = 15; // Padding between tick labels and axis title
|
|
539
|
+
// Position title to the left of the tick labels
|
|
540
|
+
// Since text-anchor: end aligns the text to the Y-axis, we need to go further left
|
|
541
|
+
const labelY = -maxTickLabelWidth - padding;
|
|
542
|
+
// Center the title vertically
|
|
543
|
+
const labelX = -this.height / 2;
|
|
544
|
+
axesGroup
|
|
545
|
+
.append('text')
|
|
546
|
+
.attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
|
|
547
|
+
.attr('text-anchor', 'middle')
|
|
548
|
+
.attr('dominant-baseline', 'middle')
|
|
549
|
+
.attr('transform', 'rotate(-90)')
|
|
550
|
+
.attr('x', labelX)
|
|
551
|
+
.attr('y', labelY)
|
|
552
|
+
.attr('direction', 'ltr')
|
|
553
|
+
.style('font-size', '14px')
|
|
554
|
+
.style('font-weight', '500')
|
|
555
|
+
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
556
|
+
.text(title);
|
|
557
|
+
}
|
|
558
|
+
calculateMaxYAxisTickLabelWidth() {
|
|
559
|
+
// Calculate the maximum width needed for Y-axis tick labels
|
|
560
|
+
const raw = (this.data() || []);
|
|
561
|
+
const isClustered = this.isClusteredData(raw);
|
|
562
|
+
let maxValue = 0;
|
|
563
|
+
if (isClustered) {
|
|
564
|
+
const groups = raw;
|
|
565
|
+
const seriesLabels = groups.length > 0 ? this.getClusterSeriesLabels(groups) : [];
|
|
566
|
+
const visibleSeries = seriesLabels.filter((s) => !this.hiddenSeries.has(s));
|
|
567
|
+
for (const g of groups) {
|
|
568
|
+
visibleSeries.forEach((_, idx) => {
|
|
569
|
+
const item = g.chartData[idx];
|
|
570
|
+
if (item && typeof item.value === 'number')
|
|
571
|
+
maxValue = Math.max(maxValue, item.value);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
const visibleData = raw.filter((d) => !this.isBarHidden(d.id));
|
|
577
|
+
maxValue = visibleData.reduce((acc, d) => (d.value > acc ? d.value : acc), 0);
|
|
578
|
+
}
|
|
579
|
+
const tickLabelText = Number.isFinite(maxValue) ? maxValue.toLocaleString() : '00000';
|
|
580
|
+
const fontSize = 13; // Approximate font size for Y-axis tick labels
|
|
581
|
+
return tickLabelText.length * fontSize * 0.6 + 8; // Character width + padding
|
|
582
|
+
}
|
|
583
|
+
createGridLines() {
|
|
584
|
+
this.chart
|
|
585
|
+
.append('g')
|
|
586
|
+
.attr('class', 'ax-bar-chart-grid')
|
|
587
|
+
.call(this.d3
|
|
588
|
+
.axisLeft(this.yScale)
|
|
589
|
+
.tickSize(-this.width)
|
|
590
|
+
.tickFormat(() => ''))
|
|
591
|
+
.selectAll('line')
|
|
592
|
+
.style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))')
|
|
593
|
+
.style('stroke-opacity', 0.2);
|
|
594
|
+
// Remove unneeded elements from grid
|
|
595
|
+
this.chart.select('.ax-bar-chart-grid').selectAll('path, text').remove();
|
|
510
596
|
}
|
|
511
597
|
/**
|
|
512
598
|
* Creates axes for clustered charts
|
|
@@ -517,83 +603,41 @@ class AXBarChartComponent extends NXComponent {
|
|
|
517
603
|
const showGrid = options.showGrid !== false;
|
|
518
604
|
const axesGroup = this.chart.append('g').attr('class', 'ax-bar-chart-axes');
|
|
519
605
|
if (showXAxis) {
|
|
520
|
-
|
|
521
|
-
this.xAxis = axesGroup
|
|
522
|
-
.append('g')
|
|
523
|
-
.attr('class', 'ax-bar-chart-axis-x')
|
|
524
|
-
.attr('transform', `translate(0,${this.height})`)
|
|
525
|
-
.call(this.d3.axisBottom(this.xScale).tickFormat((id) => idToLabel.get(id) ?? String(id)));
|
|
526
|
-
const dynamicXAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.width / 45)));
|
|
527
|
-
const xAxisTicks = this.xAxis
|
|
528
|
-
.selectAll('text')
|
|
529
|
-
.style('font-size', `${dynamicXAxisTickFontSize}px`)
|
|
530
|
-
.style('font-weight', '400')
|
|
531
|
-
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
532
|
-
.attr('direction', 'ltr');
|
|
533
|
-
if (this.xScale.domain().length > 0) {
|
|
534
|
-
const step = this.xScale.step();
|
|
535
|
-
const labelsForRotation = Array.from(idToLabel.values());
|
|
536
|
-
const longestLabel = labelsForRotation.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
537
|
-
const estimatedLongestLabelWidth = longestLabel.length * dynamicXAxisTickFontSize * 0.55;
|
|
538
|
-
if (estimatedLongestLabelWidth > step) {
|
|
539
|
-
xAxisTicks.attr('transform', 'rotate(-45)').style('text-anchor', 'end').attr('dx', '-0.8em').attr('dy', '0.15em');
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
543
|
-
if (options.xAxisLabel) {
|
|
544
|
-
const labelY = this.height + this.margin.bottom * 0.8;
|
|
545
|
-
axesGroup
|
|
546
|
-
.append('text')
|
|
547
|
-
.attr('class', 'ax-bar-chart-axis-label ax-x-axis-label')
|
|
548
|
-
.attr('text-anchor', 'middle')
|
|
549
|
-
.attr('dominant-baseline', 'middle')
|
|
550
|
-
.attr('x', this.width / 2)
|
|
551
|
-
.attr('y', labelY)
|
|
552
|
-
.attr('direction', 'ltr')
|
|
553
|
-
.style('font-size', '14px')
|
|
554
|
-
.style('font-weight', '500')
|
|
555
|
-
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
556
|
-
.text(options.xAxisLabel);
|
|
557
|
-
}
|
|
606
|
+
this.createXAxisClustered(axesGroup, options, groups);
|
|
558
607
|
}
|
|
559
608
|
if (showYAxis) {
|
|
560
|
-
this.
|
|
561
|
-
const dynamicYAxisTickFontSize = Math.max(11, Math.min(15, Math.round(this.height / 30)));
|
|
562
|
-
this.yAxis
|
|
563
|
-
.selectAll('text')
|
|
564
|
-
.style('font-size', `${dynamicYAxisTickFontSize}px`)
|
|
565
|
-
.style('font-weight', '400')
|
|
566
|
-
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
567
|
-
.attr('text-anchor', 'end')
|
|
568
|
-
.attr('direction', 'ltr');
|
|
569
|
-
this.yAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
570
|
-
if (options.yAxisLabel) {
|
|
571
|
-
const labelX = -this.height / 2;
|
|
572
|
-
const labelY = -this.margin.left * 0.8;
|
|
573
|
-
axesGroup
|
|
574
|
-
.append('text')
|
|
575
|
-
.attr('class', 'ax-bar-chart-axis-label ax-y-axis-label')
|
|
576
|
-
.attr('text-anchor', 'middle')
|
|
577
|
-
.attr('dominant-baseline', 'middle')
|
|
578
|
-
.attr('transform', 'rotate(-90)')
|
|
579
|
-
.attr('x', labelX)
|
|
580
|
-
.attr('y', labelY)
|
|
581
|
-
.attr('direction', 'ltr')
|
|
582
|
-
.style('font-size', '14px')
|
|
583
|
-
.style('font-weight', '500')
|
|
584
|
-
.style('fill', 'rgb(var(--ax-comp-bar-chart-axis-label-color))')
|
|
585
|
-
.text(options.yAxisLabel);
|
|
586
|
-
}
|
|
609
|
+
this.createYAxis(axesGroup, options);
|
|
587
610
|
}
|
|
588
611
|
if (showGrid) {
|
|
589
|
-
this.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
612
|
+
this.createGridLines();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
createXAxisClustered(axesGroup, options, groups) {
|
|
616
|
+
const idToLabel = new Map(groups.map((g) => [g.id, g.label]));
|
|
617
|
+
// Create X axis
|
|
618
|
+
this.xAxis = axesGroup
|
|
619
|
+
.append('g')
|
|
620
|
+
.attr('class', 'ax-bar-chart-axis-x')
|
|
621
|
+
.attr('transform', `translate(0,${this.height})`)
|
|
622
|
+
.call(this.d3.axisBottom(this.xScale).tickFormat((id) => idToLabel.get(id) ?? String(id)));
|
|
623
|
+
// Style the axis text - optimized font size for Persian text
|
|
624
|
+
const dynamicFontSize = Math.max(10, Math.min(14, Math.round(this.width / 50)));
|
|
625
|
+
const xAxisTicks = this.xAxis
|
|
626
|
+
.selectAll('text')
|
|
627
|
+
.style('font-size', `${dynamicFontSize}px`)
|
|
628
|
+
.style('font-weight', '400')
|
|
629
|
+
.style('fill', 'rgba(var(--ax-comp-bar-chart-labels-color), 0.7)')
|
|
630
|
+
.attr('direction', 'ltr');
|
|
631
|
+
// Handle label rotation for long Persian text
|
|
632
|
+
const isRotated = this.shouldRotateXAxisLabels(idToLabel, dynamicFontSize);
|
|
633
|
+
if (isRotated) {
|
|
634
|
+
xAxisTicks.attr('transform', 'rotate(-45)').style('text-anchor', 'end').attr('dx', '-0.8em').attr('dy', '0.15em');
|
|
635
|
+
}
|
|
636
|
+
// Style axis lines
|
|
637
|
+
this.xAxis.selectAll('line, path').style('stroke', 'rgb(var(--ax-comp-bar-chart-grid-lines-color))');
|
|
638
|
+
// Add X axis title with proper spacing
|
|
639
|
+
if (options.xAxisLabel) {
|
|
640
|
+
this.createXAxisTitle(axesGroup, options.xAxisLabel, isRotated);
|
|
597
641
|
}
|
|
598
642
|
}
|
|
599
643
|
/**
|
|
@@ -622,14 +666,24 @@ class AXBarChartComponent extends NXComponent {
|
|
|
622
666
|
.attr('class', 'ax-bar-chart-bar-group');
|
|
623
667
|
// Add bars inside groups
|
|
624
668
|
const bars = barGroups
|
|
625
|
-
.append('
|
|
669
|
+
.append('path')
|
|
626
670
|
.attr('class', 'ax-bar-chart-bar')
|
|
627
|
-
.attr('
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
.
|
|
631
|
-
|
|
632
|
-
|
|
671
|
+
.attr('d', (d) => {
|
|
672
|
+
const x = this.xScale(d.id);
|
|
673
|
+
const width = this.xScale.bandwidth();
|
|
674
|
+
const y = this.height - 0.5; // Add small gap to prevent overlap with x-axis
|
|
675
|
+
const height = 0;
|
|
676
|
+
// Create path with only top corners rounded
|
|
677
|
+
if (radius > 0 && height > 0) {
|
|
678
|
+
const topY = y - height;
|
|
679
|
+
// Limit radius to prevent it from exceeding half the bar width
|
|
680
|
+
const effectiveRadius = Math.min(radius, width / 2);
|
|
681
|
+
return `M${x},${y} L${x},${topY + effectiveRadius} Q${x},${topY} ${x + effectiveRadius},${topY} L${x + width - effectiveRadius},${topY} Q${x + width},${topY} ${x + width},${topY + effectiveRadius} L${x + width},${y} Z`;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
return `M${x},${y} L${x},${y - height} L${x + width},${y - height} L${x + width},${y} Z`;
|
|
685
|
+
}
|
|
686
|
+
})
|
|
633
687
|
.attr('fill', (d) => {
|
|
634
688
|
// Find the index of the current bar (d) in the original data array
|
|
635
689
|
const originalIndex = originalFullData.findIndex((item) => item.id === d.id);
|
|
@@ -666,7 +720,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
666
720
|
// Only apply hover effects if initial animation is complete
|
|
667
721
|
if (!this._initialAnimationComplete())
|
|
668
722
|
return;
|
|
669
|
-
const barEl = this.d3.select(event.currentTarget).select('
|
|
723
|
+
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
670
724
|
// Standard hover effect - darken the bar slightly and add a subtle shadow
|
|
671
725
|
barEl.transition().duration(150).style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
|
|
672
726
|
this.handleBarHover(event, d);
|
|
@@ -681,7 +735,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
681
735
|
// Only apply hover effects if initial animation is complete
|
|
682
736
|
if (!this._initialAnimationComplete())
|
|
683
737
|
return;
|
|
684
|
-
const barEl = this.d3.select(event.currentTarget).select('
|
|
738
|
+
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
685
739
|
// Remove hover effect
|
|
686
740
|
barEl.transition().duration(150).style('filter', null);
|
|
687
741
|
this._tooltipVisible.set(false);
|
|
@@ -697,8 +751,24 @@ class AXBarChartComponent extends NXComponent {
|
|
|
697
751
|
.transition()
|
|
698
752
|
.duration(animationDuration)
|
|
699
753
|
.delay((d, i) => i * 50) // Stagger each bar animation
|
|
700
|
-
.
|
|
701
|
-
|
|
754
|
+
.attrTween('d', (d) => {
|
|
755
|
+
const x = this.xScale(d.id);
|
|
756
|
+
const width = this.xScale.bandwidth();
|
|
757
|
+
const finalHeight = this.height - this.yScale(d.value);
|
|
758
|
+
return (t) => {
|
|
759
|
+
const currentHeight = finalHeight * t;
|
|
760
|
+
const currentY = this.height - currentHeight;
|
|
761
|
+
const bottomY = this.height - 0.5; // Add small gap to prevent overlap with x-axis
|
|
762
|
+
if (radius > 0 && currentHeight > 0) {
|
|
763
|
+
// Limit radius to prevent it from exceeding half the bar width
|
|
764
|
+
const effectiveRadius = Math.min(radius, width / 2);
|
|
765
|
+
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`;
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
return `M${x},${bottomY} L${x},${currentY} L${x + width},${currentY} L${x + width},${bottomY} Z`;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
})
|
|
702
772
|
.ease(animationEasing) // Use the configured easing function
|
|
703
773
|
.end() // Wait for all animations to complete
|
|
704
774
|
.then(() => {
|
|
@@ -732,15 +802,25 @@ class AXBarChartComponent extends NXComponent {
|
|
|
732
802
|
.style('cursor', 'pointer')
|
|
733
803
|
.attr('class', 'ax-bar-chart-bar-group');
|
|
734
804
|
const bars = barGroups
|
|
735
|
-
.append('
|
|
805
|
+
.append('path')
|
|
736
806
|
.attr('class', 'ax-bar-chart-bar')
|
|
737
807
|
.attr('data-series', (d) => d.seriesLabel)
|
|
738
|
-
.attr('
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
.
|
|
742
|
-
|
|
743
|
-
|
|
808
|
+
.attr('d', (d) => {
|
|
809
|
+
const x = this.xSubScale(d.seriesLabel);
|
|
810
|
+
const width = this.xSubScale.bandwidth();
|
|
811
|
+
const y = this.height - 0.5; // Add small gap to prevent overlap with x-axis
|
|
812
|
+
const height = 0;
|
|
813
|
+
// Create path with only top corners rounded
|
|
814
|
+
if (radius > 0 && height > 0) {
|
|
815
|
+
const topY = y - height;
|
|
816
|
+
// Limit radius to prevent it from exceeding half the bar width
|
|
817
|
+
const effectiveRadius = Math.min(radius, width / 2);
|
|
818
|
+
return `M${x},${y} L${x},${topY + effectiveRadius} Q${x},${topY} ${x + effectiveRadius},${topY} L${x + width - effectiveRadius},${topY} Q${x + width},${topY} ${x + width},${topY + effectiveRadius} L${x + width},${y} Z`;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
return `M${x},${y} L${x},${y - height} L${x + width},${y - height} L${x + width},${y} Z`;
|
|
822
|
+
}
|
|
823
|
+
})
|
|
744
824
|
.attr('fill', (d) => {
|
|
745
825
|
const item = d.group.chartData[d.seriesIndex];
|
|
746
826
|
const colorIndex = d.seriesIndex;
|
|
@@ -777,7 +857,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
777
857
|
.on('mouseenter', (event, d) => {
|
|
778
858
|
if (!this._initialAnimationComplete())
|
|
779
859
|
return;
|
|
780
|
-
const barEl = this.d3.select(event.currentTarget).select('
|
|
860
|
+
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
781
861
|
barEl.transition().duration(150).style('filter', 'brightness(0.7) drop-shadow(0 0 2px rgba(0,0,0,0.1))');
|
|
782
862
|
const item = d.group.chartData[d.seriesIndex];
|
|
783
863
|
if (item)
|
|
@@ -790,7 +870,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
790
870
|
.on('mouseleave', (event) => {
|
|
791
871
|
if (!this._initialAnimationComplete())
|
|
792
872
|
return;
|
|
793
|
-
const barEl = this.d3.select(event.currentTarget).select('
|
|
873
|
+
const barEl = this.d3.select(event.currentTarget).select('path');
|
|
794
874
|
barEl.transition().duration(150).style('filter', null);
|
|
795
875
|
this._tooltipVisible.set(false);
|
|
796
876
|
})
|
|
@@ -805,13 +885,24 @@ class AXBarChartComponent extends NXComponent {
|
|
|
805
885
|
.transition()
|
|
806
886
|
.duration(animationDuration)
|
|
807
887
|
.delay((_, i) => i * 50)
|
|
808
|
-
.
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
})
|
|
812
|
-
.attr('height', (d) => {
|
|
888
|
+
.attrTween('d', (d) => {
|
|
889
|
+
const x = this.xSubScale(d.seriesLabel);
|
|
890
|
+
const width = this.xSubScale.bandwidth();
|
|
813
891
|
const item = d.group.chartData[d.seriesIndex];
|
|
814
|
-
|
|
892
|
+
const finalHeight = item ? this.height - this.yScale(item.value) : 0;
|
|
893
|
+
return (t) => {
|
|
894
|
+
const currentHeight = finalHeight * t;
|
|
895
|
+
const currentY = this.height - currentHeight;
|
|
896
|
+
const bottomY = this.height - 0.5; // Add small gap to prevent overlap with x-axis
|
|
897
|
+
if (radius > 0 && currentHeight > 0) {
|
|
898
|
+
// Limit radius to prevent it from exceeding half the bar width
|
|
899
|
+
const effectiveRadius = Math.min(radius, width / 2);
|
|
900
|
+
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`;
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
return `M${x},${bottomY} L${x},${currentY} L${x + width},${currentY} L${x + width},${bottomY} Z`;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
815
906
|
})
|
|
816
907
|
.ease(animationEasing)
|
|
817
908
|
.end()
|
|
@@ -853,7 +944,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
853
944
|
const isClustered = this.isClusteredData(dataValue);
|
|
854
945
|
const flatData = isClustered
|
|
855
946
|
? dataValue.flatMap((g) => g.chartData)
|
|
856
|
-
:
|
|
947
|
+
: dataValue || [];
|
|
857
948
|
const index = flatData.findIndex((item) => item.id === datum.id && item.label === datum.label);
|
|
858
949
|
const color = datum.color || this.getColor(index !== -1 ? index : 0);
|
|
859
950
|
// Calculate percentage of total
|
|
@@ -862,7 +953,7 @@ class AXBarChartComponent extends NXComponent {
|
|
|
862
953
|
: flatData.reduce((sum, item) => sum + item.value, 0);
|
|
863
954
|
const percentage = total > 0 ? ((datum.value / total) * 100).toFixed(1) : '0';
|
|
864
955
|
this._tooltipData.set({
|
|
865
|
-
title: group ? `${datum.tooltipLabel || datum.label} — ${group.label}` :
|
|
956
|
+
title: group ? `${datum.tooltipLabel || datum.label} — ${group.label}` : datum.tooltipLabel || datum.label,
|
|
866
957
|
value: datum.value.toString(),
|
|
867
958
|
percentage: `${percentage}%`,
|
|
868
959
|
color: color,
|
|
@@ -917,19 +1008,19 @@ class AXBarChartComponent extends NXComponent {
|
|
|
917
1008
|
.append('div')
|
|
918
1009
|
// Apply generic icon class and specific bar chart icon class
|
|
919
1010
|
.attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
|
|
920
|
-
.html(
|
|
1011
|
+
.html(`<i class="${this.effectiveMessages().noDataIcon} fa-2x"></i>`);
|
|
921
1012
|
// Add text message
|
|
922
1013
|
messageContainer
|
|
923
1014
|
.append('div')
|
|
924
1015
|
// Apply generic text class and specific bar chart text class
|
|
925
1016
|
.attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
|
|
926
|
-
.text(
|
|
1017
|
+
.text(this.effectiveMessages().noData);
|
|
927
1018
|
// Add help text
|
|
928
1019
|
messageContainer
|
|
929
1020
|
.append('div')
|
|
930
1021
|
// Apply generic help class and specific bar chart help class
|
|
931
1022
|
.attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
|
|
932
|
-
.text(
|
|
1023
|
+
.text(this.effectiveMessages().noDataHelp);
|
|
933
1024
|
}
|
|
934
1025
|
/**
|
|
935
1026
|
* Shows a message when all bars are hidden
|
|
@@ -947,19 +1038,19 @@ class AXBarChartComponent extends NXComponent {
|
|
|
947
1038
|
.append('div')
|
|
948
1039
|
// Apply generic icon class and specific bar chart icon class
|
|
949
1040
|
.attr('class', 'ax-chart-message-icon ax-bar-chart-no-data-icon')
|
|
950
|
-
.html(
|
|
1041
|
+
.html(`<i class="${this.effectiveMessages().allHiddenIcon} fa-2x"></i>`);
|
|
951
1042
|
// Add text message
|
|
952
1043
|
messageContainer
|
|
953
1044
|
.append('div')
|
|
954
1045
|
// Apply generic text class and specific bar chart text class
|
|
955
1046
|
.attr('class', 'ax-chart-message-text ax-bar-chart-no-data-text')
|
|
956
|
-
.text(
|
|
1047
|
+
.text(this.effectiveMessages().allHidden);
|
|
957
1048
|
// Add help text
|
|
958
1049
|
messageContainer
|
|
959
1050
|
.append('div')
|
|
960
1051
|
// Apply generic help class and specific bar chart help class
|
|
961
1052
|
.attr('class', 'ax-chart-message-help ax-bar-chart-no-data-help')
|
|
962
|
-
.text(
|
|
1053
|
+
.text(this.effectiveMessages().allHiddenHelp);
|
|
963
1054
|
}
|
|
964
1055
|
/**
|
|
965
1056
|
* Gets the color for a bar based on its index
|
|
@@ -1111,7 +1202,10 @@ class AXBarChartComponent extends NXComponent {
|
|
|
1111
1202
|
this.hiddenBars.delete(id);
|
|
1112
1203
|
if (wasAllHidden) {
|
|
1113
1204
|
if (this.chartContainerEl()?.nativeElement) {
|
|
1114
|
-
this.d3
|
|
1205
|
+
this.d3
|
|
1206
|
+
?.select(this.chartContainerEl().nativeElement)
|
|
1207
|
+
.selectAll('svg, .ax-chart-message-container')
|
|
1208
|
+
.remove();
|
|
1115
1209
|
setTimeout(() => {
|
|
1116
1210
|
this.createChart();
|
|
1117
1211
|
}, 0);
|
|
@@ -1153,11 +1247,11 @@ class AXBarChartComponent extends NXComponent {
|
|
|
1153
1247
|
return !this.hiddenSeries.has(id);
|
|
1154
1248
|
}
|
|
1155
1249
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.8", ngImport: i0, type: AXBarChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1156
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.1.8", type: AXBarChartComponent, isStandalone: true, selector: "ax-bar-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-bar-chart-bg-color));padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1250
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.1.8", type: AXBarChartComponent, isStandalone: true, selector: "ax-bar-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart 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;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1157
1251
|
}
|
|
1158
1252
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.8", ngImport: i0, type: AXBarChartComponent, decorators: [{
|
|
1159
1253
|
type: Component,
|
|
1160
|
-
args: [{ selector: 'ax-bar-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-bar-chart-bg-color));padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"] }]
|
|
1254
|
+
args: [{ selector: 'ax-bar-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-bar-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-bar-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-bar-chart-axis-label-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-data-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-bar-chart-bg-color: var(--ax-sys-color-lightest-surface)}ax-bar-chart .ax-bar-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;color:rgb(var(--ax-sys-color-on-lightest-surface));background-color:rgb(var(--ax-comp-bar-chart-bg-color))}ax-bar-chart .ax-bar-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-bar-chart .ax-bar-chart 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;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-bar-chart .ax-bar-chart-no-data-message .ax-bar-chart-no-data-help{font-size:.8rem;opacity:.6}\n"] }]
|
|
1161
1255
|
}], ctorParameters: () => [] });
|
|
1162
1256
|
|
|
1163
1257
|
/**
|