@acorex/charts 21.0.2-next.8 → 21.0.3-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/acorex-charts-bar-chart.mjs +16 -16
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-legend.mjs +11 -11
- package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-tooltip.mjs +10 -10
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +21 -20
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-funnel-chart.mjs +12 -12
- package/fesm2022/acorex-charts-funnel-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-gauge-chart.mjs +13 -13
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-heatmap-chart.mjs +32 -17
- package/fesm2022/acorex-charts-heatmap-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +34 -27
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +358 -204
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts.mjs +4 -4
- package/fesm2022/acorex-charts.mjs.map +1 -1
- package/package.json +2 -1
- package/types/acorex-charts-heatmap-chart.d.ts +14 -0
- package/types/acorex-charts-hierarchy-chart.d.ts +5 -2
- package/types/acorex-charts-line-chart.d.ts +54 -7
|
@@ -5,10 +5,10 @@ import { InjectionToken, input, output, viewChild, signal, inject, computed, eff
|
|
|
5
5
|
|
|
6
6
|
const AXLineChartDefaultConfig = {
|
|
7
7
|
margins: {
|
|
8
|
-
top:
|
|
9
|
-
right:
|
|
10
|
-
bottom:
|
|
11
|
-
left:
|
|
8
|
+
top: 12,
|
|
9
|
+
right: 12,
|
|
10
|
+
bottom: 8,
|
|
11
|
+
left: 8,
|
|
12
12
|
},
|
|
13
13
|
showXAxis: true,
|
|
14
14
|
showYAxis: true,
|
|
@@ -51,32 +51,33 @@ function lineChartConfig(config = {}) {
|
|
|
51
51
|
* Line Chart Component for rendering data as lines with interactive hover effects and animations
|
|
52
52
|
*/
|
|
53
53
|
class AXLineChartComponent extends AXChartComponent {
|
|
54
|
-
data = input([], ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
55
|
-
options = input({}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
54
|
+
data = input([], ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
55
|
+
options = input({}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
56
56
|
pointClick = output();
|
|
57
57
|
chartContainerEl = viewChild.required('chartContainer');
|
|
58
58
|
d3;
|
|
59
59
|
svg;
|
|
60
60
|
chart;
|
|
61
61
|
xScale;
|
|
62
|
+
xScaleKind = 'linear';
|
|
62
63
|
yScale;
|
|
63
64
|
xAxis;
|
|
64
65
|
yAxis;
|
|
65
66
|
width;
|
|
66
67
|
height;
|
|
67
68
|
margin = { top: 20, right: 25, bottom: 40, left: 50 };
|
|
68
|
-
_tooltipVisible = signal(false, ...(ngDevMode ? [{ debugName: "_tooltipVisible" }] : []));
|
|
69
|
-
_tooltipPosition = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "_tooltipPosition" }] : []));
|
|
69
|
+
_tooltipVisible = signal(false, ...(ngDevMode ? [{ debugName: "_tooltipVisible" }] : /* istanbul ignore next */ []));
|
|
70
|
+
_tooltipPosition = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "_tooltipPosition" }] : /* istanbul ignore next */ []));
|
|
70
71
|
_tooltipData = signal({
|
|
71
72
|
title: '',
|
|
72
73
|
value: '0',
|
|
73
74
|
percentage: '0%',
|
|
74
75
|
color: '',
|
|
75
|
-
}, ...(ngDevMode ? [{ debugName: "_tooltipData" }] : []));
|
|
76
|
+
}, ...(ngDevMode ? [{ debugName: "_tooltipData" }] : /* istanbul ignore next */ []));
|
|
76
77
|
_tooltipRafId = null;
|
|
77
78
|
_pendingTooltipCoords = null;
|
|
78
|
-
_initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : []));
|
|
79
|
-
_rendered = signal(false, ...(ngDevMode ? [{ debugName: "_rendered" }] : []));
|
|
79
|
+
_initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : /* istanbul ignore next */ []));
|
|
80
|
+
_rendered = signal(false, ...(ngDevMode ? [{ debugName: "_rendered" }] : /* istanbul ignore next */ []));
|
|
80
81
|
hiddenSeries = new Set();
|
|
81
82
|
_fullNormalizedData = [];
|
|
82
83
|
tooltipVisible = this._tooltipVisible.asReadonly();
|
|
@@ -91,7 +92,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
91
92
|
...this.configToken,
|
|
92
93
|
...this.options(),
|
|
93
94
|
};
|
|
94
|
-
}, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
|
|
95
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : /* istanbul ignore next */ []));
|
|
95
96
|
// Messages with defaults
|
|
96
97
|
effectiveMessages = computed(() => {
|
|
97
98
|
const defaultMessages = {
|
|
@@ -106,18 +107,21 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
106
107
|
...defaultMessages,
|
|
107
108
|
...this.effectiveOptions().messages,
|
|
108
109
|
};
|
|
109
|
-
}, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : []));
|
|
110
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : /* istanbul ignore next */ []));
|
|
110
111
|
// Layout & Dimensions
|
|
111
112
|
MIN_DIMENSION = 100;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
DEFAULT_MARGIN_TOP = 12;
|
|
114
|
+
DEFAULT_MARGIN_RIGHT = 12;
|
|
115
|
+
DEFAULT_MARGIN_BOTTOM = 8;
|
|
116
|
+
DEFAULT_MARGIN_LEFT = 8;
|
|
117
|
+
MIN_MARGIN_BOTTOM = 28;
|
|
118
|
+
MIN_MARGIN_LEFT = 36;
|
|
119
|
+
X_AXIS_TITLE_GAP = 10;
|
|
120
|
+
TICK_AREA_PADDING = 4;
|
|
121
|
+
AXIS_TICK_PADDING = 6;
|
|
122
|
+
ROTATION_TOLERANCE = 0.92;
|
|
123
|
+
FORCE_ROTATE_LABEL_LENGTH = 14;
|
|
124
|
+
Y_AXIS_TITLE_PADDING = 10;
|
|
121
125
|
// Styling & Visual
|
|
122
126
|
DEFAULT_LINE_WIDTH = 2;
|
|
123
127
|
DEFAULT_POINT_RADIUS = 4;
|
|
@@ -136,16 +140,178 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
136
140
|
POINT_ANIMATION_DELAY_RATIO = 0.5;
|
|
137
141
|
POINT_ANIMATION_DURATION_RATIO = 0.3;
|
|
138
142
|
// Text & Labels
|
|
139
|
-
AXIS_LABEL_FONT_SIZE = 14;
|
|
140
143
|
CHAR_WIDTH_RATIO = 0.6;
|
|
141
144
|
FONT_WIDTH_MULTIPLIER = 0.6;
|
|
142
145
|
MAX_LABEL_LENGTH = 20;
|
|
143
|
-
MIN_FONT_SIZE_X =
|
|
144
|
-
MAX_FONT_SIZE_X =
|
|
146
|
+
MIN_FONT_SIZE_X = 10;
|
|
147
|
+
MAX_FONT_SIZE_X = 16;
|
|
145
148
|
MIN_FONT_SIZE_Y = 11;
|
|
146
|
-
MAX_FONT_SIZE_Y =
|
|
147
|
-
FONT_PADDING =
|
|
148
|
-
|
|
149
|
+
MAX_FONT_SIZE_Y = 16;
|
|
150
|
+
FONT_PADDING = 6;
|
|
151
|
+
xAxisLabelLayout = {
|
|
152
|
+
fontSize: 12,
|
|
153
|
+
willRotate: false,
|
|
154
|
+
tickAreaHeight: 24,
|
|
155
|
+
displayedTickCount: 1,
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Resolves X-axis tick font size, rotation, and reserved tick area height.
|
|
159
|
+
*/
|
|
160
|
+
resolveXAxisLabelLayout(chartWidth, categoricalLabels) {
|
|
161
|
+
if (!categoricalLabels || categoricalLabels.length === 0) {
|
|
162
|
+
const displayedTickCount = Math.min(12, Math.max(5, Math.floor(chartWidth / 80)));
|
|
163
|
+
const fontSize = this.getXAxisTickFontSize(chartWidth, displayedTickCount);
|
|
164
|
+
const tickAreaHeight = fontSize + this.TICK_AREA_PADDING + this.AXIS_TICK_PADDING;
|
|
165
|
+
return { fontSize, willRotate: false, tickAreaHeight, displayedTickCount };
|
|
166
|
+
}
|
|
167
|
+
const itemCount = categoricalLabels.length;
|
|
168
|
+
const longestLabel = categoricalLabels.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
169
|
+
const effectiveLength = this.getEffectiveXLabelLength(longestLabel, itemCount);
|
|
170
|
+
const step = itemCount > 1 ? chartWidth / (itemCount - 1) : chartWidth;
|
|
171
|
+
let fontSize = this.getXAxisTickFontSize(chartWidth, itemCount);
|
|
172
|
+
let effectiveWidth = effectiveLength * fontSize * this.CHAR_WIDTH_RATIO;
|
|
173
|
+
while (effectiveWidth > step * this.ROTATION_TOLERANCE && fontSize > this.MIN_FONT_SIZE_X) {
|
|
174
|
+
fontSize--;
|
|
175
|
+
effectiveWidth = effectiveLength * fontSize * this.CHAR_WIDTH_RATIO;
|
|
176
|
+
}
|
|
177
|
+
const willRotate = longestLabel.length >= this.FORCE_ROTATE_LABEL_LENGTH ||
|
|
178
|
+
effectiveWidth > step * this.ROTATION_TOLERANCE;
|
|
179
|
+
if (willRotate && effectiveWidth > step) {
|
|
180
|
+
while (effectiveWidth > step * 1.35 && fontSize > this.MIN_FONT_SIZE_X) {
|
|
181
|
+
fontSize--;
|
|
182
|
+
effectiveWidth = effectiveLength * fontSize * this.CHAR_WIDTH_RATIO;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const tickAreaHeight = this.estimateXAxisTickAreaHeight(fontSize, effectiveWidth, willRotate);
|
|
186
|
+
let displayedTickCount = itemCount;
|
|
187
|
+
if (itemCount > this.VERY_MANY_ITEMS_THRESHOLD) {
|
|
188
|
+
displayedTickCount = Math.ceil(itemCount / 5);
|
|
189
|
+
}
|
|
190
|
+
else if (itemCount > this.MANY_ITEMS_THRESHOLD) {
|
|
191
|
+
displayedTickCount = Math.ceil(itemCount / 2);
|
|
192
|
+
}
|
|
193
|
+
return { fontSize, willRotate, tickAreaHeight, displayedTickCount };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Returns truncated label length used for layout calculations.
|
|
197
|
+
*/
|
|
198
|
+
getEffectiveXLabelLength(label, itemCount) {
|
|
199
|
+
const maxLength = itemCount > this.MANY_ITEMS_THRESHOLD ? 10 : this.MAX_LABEL_LENGTH;
|
|
200
|
+
return Math.min(label.length, maxLength);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Estimates vertical space required below the X-axis for tick labels.
|
|
204
|
+
*/
|
|
205
|
+
estimateXAxisTickAreaHeight(fontSize, effectiveLabelWidth, willRotate) {
|
|
206
|
+
if (willRotate) {
|
|
207
|
+
return effectiveLabelWidth * Math.SQRT1_2 + fontSize * Math.SQRT1_2 + this.TICK_AREA_PADDING + this.AXIS_TICK_PADDING;
|
|
208
|
+
}
|
|
209
|
+
return fontSize + this.TICK_AREA_PADDING + this.AXIS_TICK_PADDING;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Measures rendered X-axis tick label area using the axis group bounding box.
|
|
213
|
+
*/
|
|
214
|
+
measureXAxisTickAreaHeight(axisGroup, fontSize, fallbackHeight) {
|
|
215
|
+
if (!axisGroup) {
|
|
216
|
+
return fallbackHeight;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const bbox = axisGroup.getBBox();
|
|
220
|
+
const measured = Math.max(0, bbox.y + bbox.height) + this.TICK_AREA_PADDING;
|
|
221
|
+
return Math.max(measured, fallbackHeight);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return fallbackHeight;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Measures rendered Y-axis tick label width using SVG bounding boxes.
|
|
229
|
+
*/
|
|
230
|
+
measureMaxYAxisTickLabelWidth(tickNodes, fontSize) {
|
|
231
|
+
try {
|
|
232
|
+
if (tickNodes.length > 0) {
|
|
233
|
+
return Math.max(...tickNodes.map((node) => node.getBBox().width)) + this.FONT_PADDING;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Fall through to estimation
|
|
238
|
+
}
|
|
239
|
+
return this.calculateMaxYAxisTickLabelWidth(fontSize);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Calculates required bottom margin for X-axis ticks, optional rotation, and title.
|
|
243
|
+
*/
|
|
244
|
+
calculateRequiredBottomMargin(options, chartWidth, categoricalLabels) {
|
|
245
|
+
const baseBottom = options.margins?.bottom ?? this.DEFAULT_MARGIN_BOTTOM;
|
|
246
|
+
if (options.showXAxis === false) {
|
|
247
|
+
return Math.max(baseBottom, 8);
|
|
248
|
+
}
|
|
249
|
+
const layout = this.resolveXAxisLabelLayout(chartWidth, categoricalLabels);
|
|
250
|
+
this.xAxisLabelLayout.fontSize = layout.fontSize;
|
|
251
|
+
this.xAxisLabelLayout.willRotate = layout.willRotate;
|
|
252
|
+
this.xAxisLabelLayout.tickAreaHeight = layout.tickAreaHeight;
|
|
253
|
+
this.xAxisLabelLayout.displayedTickCount = layout.displayedTickCount;
|
|
254
|
+
const titleBlock = options.xAxisLabel
|
|
255
|
+
? this.X_AXIS_TITLE_GAP + this.getAxisTitleFontSize(layout.fontSize) + 2
|
|
256
|
+
: 0;
|
|
257
|
+
const rotatedBuffer = layout.willRotate ? 12 : 4;
|
|
258
|
+
return Math.max(baseBottom, this.MIN_MARGIN_BOTTOM, layout.tickAreaHeight + titleBlock + rotatedBuffer);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculates required left margin for Y-axis ticks and title.
|
|
262
|
+
*/
|
|
263
|
+
calculateRequiredLeftMargin(options, chartHeight) {
|
|
264
|
+
const baseLeft = options.margins?.left ?? this.DEFAULT_MARGIN_LEFT;
|
|
265
|
+
if (options.showYAxis === false) {
|
|
266
|
+
return baseLeft;
|
|
267
|
+
}
|
|
268
|
+
const fontSize = this.getYAxisTickFontSize(chartHeight);
|
|
269
|
+
const tickLabelWidth = this.calculateMaxYAxisTickLabelWidth(fontSize);
|
|
270
|
+
const titleBlock = options.yAxisLabel
|
|
271
|
+
? this.Y_AXIS_TITLE_PADDING + this.getAxisTitleFontSize(fontSize) + 4
|
|
272
|
+
: 0;
|
|
273
|
+
return Math.max(baseLeft, this.MIN_MARGIN_LEFT, tickLabelWidth + this.AXIS_TICK_PADDING + titleBlock + 4);
|
|
274
|
+
}
|
|
275
|
+
createXAxisTitle(axesGroup, title, tickAreaHeight, tickFontSize) {
|
|
276
|
+
const titleFontSize = this.getAxisTitleFontSize(tickFontSize);
|
|
277
|
+
const titleY = Math.min(this.height + tickAreaHeight + this.X_AXIS_TITLE_GAP, this.height + this.margin.bottom - titleFontSize - 2);
|
|
278
|
+
axesGroup
|
|
279
|
+
.append('text')
|
|
280
|
+
.attr('class', 'ax-line-chart-axis-label ax-x-axis-label')
|
|
281
|
+
.attr('text-anchor', 'middle')
|
|
282
|
+
.attr('dominant-baseline', 'hanging')
|
|
283
|
+
.attr('x', this.width / 2)
|
|
284
|
+
.attr('y', titleY)
|
|
285
|
+
.attr('direction', 'ltr')
|
|
286
|
+
.attr('style', `
|
|
287
|
+
font-size: ${titleFontSize}px;
|
|
288
|
+
font-weight: 500;
|
|
289
|
+
fill: rgb(var(--ax-comp-line-chart-text-color));
|
|
290
|
+
pointer-events: none;
|
|
291
|
+
`)
|
|
292
|
+
.text(title);
|
|
293
|
+
}
|
|
294
|
+
createYAxisTitle(axesGroup, title, tickFontSize, maxTickLabelWidth) {
|
|
295
|
+
const titleFontSize = this.getAxisTitleFontSize(tickFontSize);
|
|
296
|
+
const labelY = -maxTickLabelWidth - this.Y_AXIS_TITLE_PADDING;
|
|
297
|
+
const labelX = -this.height / 2;
|
|
298
|
+
axesGroup
|
|
299
|
+
.append('text')
|
|
300
|
+
.attr('class', 'ax-line-chart-axis-label ax-y-axis-label')
|
|
301
|
+
.attr('text-anchor', 'middle')
|
|
302
|
+
.attr('dominant-baseline', 'middle')
|
|
303
|
+
.attr('transform', 'rotate(-90)')
|
|
304
|
+
.attr('x', labelX)
|
|
305
|
+
.attr('y', labelY)
|
|
306
|
+
.attr('direction', 'ltr')
|
|
307
|
+
.attr('style', `
|
|
308
|
+
font-size: ${titleFontSize}px;
|
|
309
|
+
font-weight: 500;
|
|
310
|
+
fill: rgb(var(--ax-comp-line-chart-text-color));
|
|
311
|
+
pointer-events: none;
|
|
312
|
+
`)
|
|
313
|
+
.text(title);
|
|
314
|
+
}
|
|
149
315
|
// Data & Performance
|
|
150
316
|
MAX_POINTS_TO_RENDER = 100;
|
|
151
317
|
POINT_COORDINATE_PRECISION = 10;
|
|
@@ -172,10 +338,41 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
172
338
|
return series.id || series.label || `series-${series.originalIndex}`;
|
|
173
339
|
}
|
|
174
340
|
/**
|
|
175
|
-
* Calculates
|
|
341
|
+
* Calculates adaptive X-axis tick font size based on chart width and label count.
|
|
342
|
+
*/
|
|
343
|
+
getXAxisTickFontSize(chartWidth, itemCount = 1) {
|
|
344
|
+
let size = chartWidth / 42;
|
|
345
|
+
if (itemCount > this.VERY_MANY_ITEMS_THRESHOLD) {
|
|
346
|
+
size *= 0.65;
|
|
347
|
+
}
|
|
348
|
+
else if (itemCount > this.MANY_ITEMS_THRESHOLD) {
|
|
349
|
+
size *= 0.8;
|
|
350
|
+
}
|
|
351
|
+
return Math.max(this.MIN_FONT_SIZE_X, Math.min(this.MAX_FONT_SIZE_X, Math.round(size)));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Calculates adaptive Y-axis tick font size based on chart height.
|
|
355
|
+
*/
|
|
356
|
+
getYAxisTickFontSize(chartHeight) {
|
|
357
|
+
const size = chartHeight / 28;
|
|
358
|
+
return Math.max(this.MIN_FONT_SIZE_Y, Math.min(this.MAX_FONT_SIZE_Y, Math.round(size)));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Calculates axis title font size relative to tick labels.
|
|
362
|
+
*/
|
|
363
|
+
getAxisTitleFontSize(tickFontSize) {
|
|
364
|
+
return Math.min(15, Math.max(12, tickFontSize));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Estimates inner chart dimensions before margins are finalized.
|
|
176
368
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
369
|
+
estimateChartDimensions(containerWidth, containerHeight, options) {
|
|
370
|
+
const outerWidth = options.width ?? Math.max(containerWidth, 1);
|
|
371
|
+
const outerHeight = options.height ?? Math.max(containerHeight, 1);
|
|
372
|
+
return {
|
|
373
|
+
width: Math.max(outerWidth - this.margin.left - this.margin.right, this.MIN_DIMENSION),
|
|
374
|
+
height: Math.max(outerHeight - this.margin.top - this.margin.bottom, this.MIN_DIMENSION),
|
|
375
|
+
};
|
|
179
376
|
}
|
|
180
377
|
/**
|
|
181
378
|
* Creates a unique key for point coordinates (for overlap detection)
|
|
@@ -202,7 +399,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
202
399
|
/**
|
|
203
400
|
* Calculates maximum width needed for Y-axis tick labels
|
|
204
401
|
*/
|
|
205
|
-
calculateMaxYAxisTickLabelWidth() {
|
|
402
|
+
calculateMaxYAxisTickLabelWidth(fontSize) {
|
|
206
403
|
const allSeriesData = this._fullNormalizedData.filter((s) => !this.hiddenSeries.has(this.getSeriesIdentifier(s)));
|
|
207
404
|
let maxValue = 0;
|
|
208
405
|
let minValue = 0;
|
|
@@ -219,7 +416,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
219
416
|
// Check both max and min (for negative values)
|
|
220
417
|
const maxAbsValue = Math.max(Math.abs(maxValue), Math.abs(minValue));
|
|
221
418
|
const tickLabelText = Number.isFinite(maxAbsValue) ? formatLargeNumber(maxAbsValue) : '00000';
|
|
222
|
-
return tickLabelText.length *
|
|
419
|
+
return tickLabelText.length * fontSize * this.FONT_WIDTH_MULTIPLIER + this.FONT_PADDING;
|
|
223
420
|
}
|
|
224
421
|
ngOnInit() {
|
|
225
422
|
this.loadD3();
|
|
@@ -230,7 +427,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
230
427
|
if (this._rendered()) {
|
|
231
428
|
this.updateChart();
|
|
232
429
|
}
|
|
233
|
-
}, ...(ngDevMode ? [{ debugName: "#effect" }] : []));
|
|
430
|
+
}, ...(ngDevMode ? [{ debugName: "#effect" }] : /* istanbul ignore next */ []));
|
|
234
431
|
ngAfterViewInit() {
|
|
235
432
|
this._initialized.set(true);
|
|
236
433
|
if (this.d3 && this.chartContainerEl()) {
|
|
@@ -359,102 +556,64 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
359
556
|
}
|
|
360
557
|
this._pendingTooltipCoords = null;
|
|
361
558
|
if (this.svg) {
|
|
362
|
-
this.d3
|
|
559
|
+
this.d3
|
|
560
|
+
?.select(this.chartContainerEl()?.nativeElement)
|
|
561
|
+
.selectAll('svg, .ax-line-chart-no-data-message, .ax-line-chart-all-hidden-message')
|
|
562
|
+
.remove();
|
|
363
563
|
this.svg = null;
|
|
364
564
|
this.chart = null;
|
|
365
565
|
}
|
|
366
566
|
this._tooltipVisible.set(false);
|
|
367
567
|
}
|
|
368
568
|
setupDimensions(containerElement, options) {
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
const minDim = Math.min(this.MIN_CONTAINER_DIMENSION, containerWidth, containerHeight);
|
|
569
|
+
const containerWidth = Math.max(containerElement.clientWidth, 1);
|
|
570
|
+
const containerHeight = Math.max(containerElement.clientHeight, 1);
|
|
571
|
+
this.calculateMargins(options, containerWidth, containerHeight);
|
|
373
572
|
if (options.width && options.height) {
|
|
374
573
|
this.width = options.width - this.margin.left - this.margin.right;
|
|
375
574
|
this.height = options.height - this.margin.top - this.margin.bottom;
|
|
376
575
|
}
|
|
377
576
|
else {
|
|
378
|
-
this.width =
|
|
379
|
-
this.height =
|
|
577
|
+
this.width = containerWidth - this.margin.left - this.margin.right;
|
|
578
|
+
this.height = containerHeight - this.margin.top - this.margin.bottom;
|
|
380
579
|
}
|
|
381
580
|
this.width = Math.max(this.width, this.MIN_DIMENSION);
|
|
382
581
|
this.height = Math.max(this.height, this.MIN_DIMENSION);
|
|
383
582
|
const totalWidth = this.width + this.margin.left + this.margin.right;
|
|
384
583
|
const totalHeight = this.height + this.margin.top + this.margin.bottom;
|
|
385
|
-
const viewBoxWidth = totalWidth + this.CHART_EDGE_PADDING * 2;
|
|
386
|
-
const viewBoxHeight = totalHeight + this.CHART_EDGE_PADDING * 2;
|
|
387
584
|
const svg = this.d3
|
|
388
585
|
.select(containerElement)
|
|
389
586
|
.append('svg')
|
|
390
587
|
.attr('width', '100%')
|
|
391
588
|
.attr('height', '100%')
|
|
392
|
-
.attr('viewBox', `0 0 ${
|
|
589
|
+
.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`)
|
|
393
590
|
.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
394
591
|
this.svg = svg;
|
|
395
592
|
this.chart = this.svg
|
|
396
593
|
.append('g')
|
|
397
|
-
.attr('transform', `translate(${this.margin.left
|
|
594
|
+
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
|
|
398
595
|
}
|
|
399
|
-
calculateMargins(options, containerWidth) {
|
|
596
|
+
calculateMargins(options, containerWidth, containerHeight) {
|
|
400
597
|
this.margin = {
|
|
401
598
|
top: options.margins?.top ?? this.DEFAULT_MARGIN_TOP,
|
|
402
599
|
right: options.margins?.right ?? this.DEFAULT_MARGIN_RIGHT,
|
|
403
600
|
bottom: options.margins?.bottom ?? this.DEFAULT_MARGIN_BOTTOM,
|
|
404
601
|
left: options.margins?.left ?? this.DEFAULT_MARGIN_LEFT,
|
|
405
602
|
};
|
|
603
|
+
const estimatedDimensions = this.estimateChartDimensions(containerWidth, containerHeight, options);
|
|
406
604
|
const allDataPoints = this._fullNormalizedData.flatMap((series) => series.data);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const labelCount = allXValues.size;
|
|
413
|
-
if (labelCount > 0 && containerWidth > 0) {
|
|
414
|
-
const workingWidth = containerWidth - this.margin.left - this.margin.right;
|
|
415
|
-
if (workingWidth > 0) {
|
|
416
|
-
const availableWidthPerLabel = workingWidth / labelCount;
|
|
417
|
-
const labels = Array.from(allXValues);
|
|
418
|
-
const longestLabel = labels.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
419
|
-
const estimatedFontSize = this.calculateDynamicFontSize(workingWidth, 45, this.MIN_FONT_SIZE_X, this.MAX_FONT_SIZE_X);
|
|
420
|
-
// Account for label truncation in width calculation
|
|
421
|
-
const maxLabelLength = labelCount > this.MANY_ITEMS_THRESHOLD ? 10 : this.MAX_LABEL_LENGTH;
|
|
422
|
-
const effectiveLabelLength = Math.min(longestLabel.length, maxLabelLength);
|
|
423
|
-
const estimatedLongestLabelWidth = effectiveLabelLength * estimatedFontSize * this.CHAR_WIDTH_RATIO;
|
|
424
|
-
if (estimatedLongestLabelWidth > availableWidthPerLabel) {
|
|
425
|
-
// Calculate diagonal height when rotated -45 degrees
|
|
426
|
-
const diagonalHeight = estimatedLongestLabelWidth * Math.sin(Math.PI / 4);
|
|
427
|
-
const requiredExtraMargin = diagonalHeight + 15; // Extra padding for safety
|
|
428
|
-
this.margin.bottom += Math.min(this.MAX_EXTRA_MARGIN, requiredExtraMargin);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (options.xAxisLabel) {
|
|
435
|
-
const xLabelLength = options.xAxisLabel.length;
|
|
436
|
-
const extraBottomMargin = Math.min(20, Math.max(10, xLabelLength * 0.8));
|
|
437
|
-
this.margin.bottom = Math.max(this.margin.bottom, 40 + extraBottomMargin);
|
|
438
|
-
}
|
|
439
|
-
if (options.yAxisLabel) {
|
|
440
|
-
// Calculate space needed for Y-axis: tick labels + padding + title
|
|
441
|
-
const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
442
|
-
const yAxisTitleThickness = 20; // Height of rotated title text
|
|
443
|
-
const yAxisTitlePadding = this.Y_AXIS_PADDING;
|
|
444
|
-
const totalYAxisWidth = maxTickLabelWidth + yAxisTitlePadding + yAxisTitleThickness;
|
|
445
|
-
this.margin.left = Math.max(this.margin.left, totalYAxisWidth);
|
|
446
|
-
}
|
|
447
|
-
else if (options.showYAxis !== false) {
|
|
448
|
-
// Just tick labels, no title
|
|
449
|
-
const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
450
|
-
this.margin.left = Math.max(this.margin.left, maxTickLabelWidth + 10);
|
|
451
|
-
}
|
|
452
|
-
if (options.showXAxis !== false) {
|
|
453
|
-
this.margin.bottom = Math.max(this.margin.bottom, this.MIN_MARGIN_BOTTOM);
|
|
454
|
-
}
|
|
455
|
-
if (options.showYAxis !== false) {
|
|
456
|
-
this.margin.left = Math.max(this.margin.left, this.MIN_MARGIN_LEFT);
|
|
605
|
+
const allNumericX = allDataPoints.length === 0 || allDataPoints.every((d) => typeof d.x === 'number');
|
|
606
|
+
let categoricalLabels = null;
|
|
607
|
+
if (!allNumericX) {
|
|
608
|
+
const visibleSeries = this._fullNormalizedData.filter((s) => !this.hiddenSeries.has(this.getSeriesIdentifier(s)));
|
|
609
|
+
categoricalLabels = [...new Set(visibleSeries.flatMap((series) => series.data.map((d) => String(d.x))))];
|
|
457
610
|
}
|
|
611
|
+
this.margin.bottom = this.calculateRequiredBottomMargin(options, estimatedDimensions.width, categoricalLabels);
|
|
612
|
+
this.margin.left = this.calculateRequiredLeftMargin(options, estimatedDimensions.height);
|
|
613
|
+
// Second pass: margins affect inner chart size, which affects label layout
|
|
614
|
+
const refinedDimensions = this.estimateChartDimensions(containerWidth, containerHeight, options);
|
|
615
|
+
this.margin.bottom = this.calculateRequiredBottomMargin(options, refinedDimensions.width, categoricalLabels);
|
|
616
|
+
this.margin.left = this.calculateRequiredLeftMargin(options, refinedDimensions.height);
|
|
458
617
|
}
|
|
459
618
|
setupScales(data) {
|
|
460
619
|
// Expects already filtered data for scales
|
|
@@ -469,52 +628,79 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
469
628
|
}
|
|
470
629
|
const allNumericX = allDataPoints.every((d) => typeof d.x === 'number');
|
|
471
630
|
if (allNumericX) {
|
|
631
|
+
this.xScaleKind = 'linear';
|
|
632
|
+
const xMin = this.d3.min(allDataPoints, (d) => d.x) ?? 0;
|
|
472
633
|
const xMax = this.d3.max(allDataPoints, (d) => d.x) ?? 0;
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
634
|
+
if (xMin === xMax) {
|
|
635
|
+
this.xScale = this.d3
|
|
636
|
+
.scaleLinear()
|
|
637
|
+
.domain([xMin - 1, xMax + 1])
|
|
638
|
+
.range([0, this.width]);
|
|
476
639
|
}
|
|
477
640
|
else {
|
|
478
|
-
|
|
479
|
-
this.xScale = this.d3.scaleLinear().domain([0, xMax]).range([0, this.width]);
|
|
641
|
+
this.xScale = this.d3.scaleLinear().domain([xMin, xMax]).range([0, this.width]);
|
|
480
642
|
}
|
|
481
643
|
}
|
|
482
644
|
else {
|
|
645
|
+
this.xScaleKind = 'point';
|
|
646
|
+
const xDomain = [];
|
|
647
|
+
const seenX = new Set();
|
|
648
|
+
for (const point of allDataPoints) {
|
|
649
|
+
const key = String(point.x);
|
|
650
|
+
if (!seenX.has(key)) {
|
|
651
|
+
seenX.add(key);
|
|
652
|
+
xDomain.push(key);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
483
655
|
this.xScale = this.d3
|
|
484
|
-
.
|
|
485
|
-
.domain(
|
|
656
|
+
.scalePoint()
|
|
657
|
+
.domain(xDomain)
|
|
486
658
|
.range([0, this.width])
|
|
487
|
-
.
|
|
488
|
-
.paddingOuter(0);
|
|
659
|
+
.padding(0);
|
|
489
660
|
}
|
|
490
661
|
const yAxisStartsAtZero = chartOptions.yAxisStartsAtZero !== false;
|
|
491
|
-
|
|
662
|
+
const dataYMin = this.d3.min(allDataPoints, (d) => d.y) ?? 0;
|
|
663
|
+
const dataYMax = this.d3.max(allDataPoints, (d) => d.y) ?? 0;
|
|
664
|
+
let yMin;
|
|
665
|
+
let yMax;
|
|
492
666
|
if (yAxisStartsAtZero) {
|
|
493
|
-
yMin = Math.min(0,
|
|
667
|
+
yMin = Math.min(0, dataYMin);
|
|
668
|
+
yMax = Math.max(0, dataYMax);
|
|
494
669
|
}
|
|
495
|
-
|
|
496
|
-
|
|
670
|
+
else {
|
|
671
|
+
yMin = dataYMin;
|
|
672
|
+
yMax = dataYMax;
|
|
673
|
+
}
|
|
674
|
+
const yRange = yMax - yMin || 1;
|
|
497
675
|
this.yScale = this.d3
|
|
498
676
|
.scaleLinear()
|
|
499
677
|
.domain([yMin, yMax + yRange * paddingMultiplier])
|
|
500
678
|
.nice()
|
|
501
679
|
.range([this.height, 0]);
|
|
680
|
+
if (yAxisStartsAtZero && dataYMin >= 0) {
|
|
681
|
+
this.yScale.domain([0, this.yScale.domain()[1]]);
|
|
682
|
+
}
|
|
502
683
|
}
|
|
503
684
|
createAxes(options) {
|
|
504
685
|
const showXAxis = options.showXAxis !== false;
|
|
505
686
|
const showYAxis = options.showYAxis !== false;
|
|
506
687
|
const showGrid = options.showGrid !== false;
|
|
507
|
-
const
|
|
688
|
+
const isPointScale = this.xScaleKind === 'point';
|
|
689
|
+
const pointScale = isPointScale ? this.xScale : null;
|
|
508
690
|
const isRtl = document.documentElement.dir === 'rtl' || document.body.dir === 'rtl';
|
|
509
691
|
const axesGroup = this.chart.append('g').attr('class', 'ax-line-chart-axes');
|
|
510
692
|
if (showXAxis) {
|
|
511
693
|
let xAxisGenerator;
|
|
512
694
|
let itemCount = 0;
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
695
|
+
if (isPointScale && pointScale) {
|
|
696
|
+
itemCount = pointScale.domain().length;
|
|
697
|
+
const pointLayout = this.resolveXAxisLabelLayout(this.width, pointScale.domain());
|
|
698
|
+
this.xAxisLabelLayout.fontSize = pointLayout.fontSize;
|
|
699
|
+
this.xAxisLabelLayout.willRotate = pointLayout.willRotate;
|
|
700
|
+
this.xAxisLabelLayout.tickAreaHeight = pointLayout.tickAreaHeight;
|
|
701
|
+
this.xAxisLabelLayout.displayedTickCount = pointLayout.displayedTickCount;
|
|
516
702
|
// Smart tick reduction for many items (like bar chart)
|
|
517
|
-
let tickValues =
|
|
703
|
+
let tickValues = pointScale.domain();
|
|
518
704
|
if (itemCount > this.VERY_MANY_ITEMS_THRESHOLD) {
|
|
519
705
|
// Show every 5th tick for 50+ items
|
|
520
706
|
tickValues = tickValues.filter((_d, i) => i % 5 === 0);
|
|
@@ -524,7 +710,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
524
710
|
tickValues = tickValues.filter((_d, i) => i % 2 === 0);
|
|
525
711
|
}
|
|
526
712
|
xAxisGenerator = this.d3
|
|
527
|
-
.axisBottom(
|
|
713
|
+
.axisBottom(pointScale)
|
|
528
714
|
.tickValues(tickValues)
|
|
529
715
|
.tickSize(5)
|
|
530
716
|
.tickPadding(8)
|
|
@@ -567,21 +753,17 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
567
753
|
.attr('stroke', 'rgba(var(--ax-comp-line-chart-grid-lines-color), 0.2)')
|
|
568
754
|
.attr('stroke-dasharray', '2,2')
|
|
569
755
|
.attr('stroke-opacity', '0.5');
|
|
570
|
-
const dynamicXAxisTickFontSize =
|
|
756
|
+
const dynamicXAxisTickFontSize = isPointScale
|
|
757
|
+
? this.xAxisLabelLayout.fontSize
|
|
758
|
+
: this.getXAxisTickFontSize(this.width, itemCount);
|
|
571
759
|
const xAxisTicks = this.xAxis
|
|
572
760
|
.selectAll('text')
|
|
573
761
|
.style('font-size', `${dynamicXAxisTickFontSize}px`)
|
|
574
762
|
.style('font-weight', '400')
|
|
575
|
-
.style('fill', 'rgba(var(--ax-comp-line-chart-labels-color), 0.7)')
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const step = this.xScale.step();
|
|
580
|
-
const longestLabel = this.xScale.domain().reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
581
|
-
// Using char width ratio constant for better estimate
|
|
582
|
-
const estimatedLongestLabelWidth = longestLabel.length * dynamicXAxisTickFontSize * this.CHAR_WIDTH_RATIO;
|
|
583
|
-
if (estimatedLongestLabelWidth > step) {
|
|
584
|
-
labelsAreRotated = true;
|
|
763
|
+
.style('fill', 'rgba(var(--ax-comp-line-chart-labels-color), 0.7)')
|
|
764
|
+
.attr('direction', 'ltr');
|
|
765
|
+
if (isPointScale && pointScale && pointScale.domain().length > 0) {
|
|
766
|
+
if (this.xAxisLabelLayout.willRotate) {
|
|
585
767
|
xAxisTicks
|
|
586
768
|
.attr('transform', 'rotate(-45)')
|
|
587
769
|
.style('text-anchor', 'end')
|
|
@@ -589,24 +771,9 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
589
771
|
.attr('dy', '0.15em');
|
|
590
772
|
}
|
|
591
773
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (shouldShowXAxisTitle) {
|
|
596
|
-
axesGroup
|
|
597
|
-
.append('text')
|
|
598
|
-
.attr('class', 'ax-line-chart-axis-label ax-x-axis-label')
|
|
599
|
-
.attr('text-anchor', 'middle')
|
|
600
|
-
.attr('x', this.width / 2)
|
|
601
|
-
.attr('y', this.height + this.margin.bottom - 5)
|
|
602
|
-
.attr('direction', 'ltr')
|
|
603
|
-
.attr('style', `
|
|
604
|
-
font-size: 14px;
|
|
605
|
-
font-weight: 500;
|
|
606
|
-
fill: rgb(var(--ax-comp-line-chart-text-color));
|
|
607
|
-
pointer-events: none;
|
|
608
|
-
`)
|
|
609
|
-
.text(options.xAxisLabel);
|
|
774
|
+
const xTickAreaHeight = this.measureXAxisTickAreaHeight(this.xAxis.node(), dynamicXAxisTickFontSize, this.xAxisLabelLayout.tickAreaHeight);
|
|
775
|
+
if (options.xAxisLabel) {
|
|
776
|
+
this.createXAxisTitle(axesGroup, options.xAxisLabel, xTickAreaHeight, dynamicXAxisTickFontSize);
|
|
610
777
|
}
|
|
611
778
|
}
|
|
612
779
|
if (showYAxis) {
|
|
@@ -635,7 +802,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
635
802
|
.attr('stroke', 'rgba(var(--ax-comp-line-chart-grid-lines-color), 0.2)')
|
|
636
803
|
.attr('stroke-dasharray', '2,2')
|
|
637
804
|
.attr('stroke-opacity', '0.5');
|
|
638
|
-
const dynamicYAxisTickFontSize = this.
|
|
805
|
+
const dynamicYAxisTickFontSize = this.getYAxisTickFontSize(this.height);
|
|
639
806
|
const yTickTexts = this.yAxis.selectAll('text').attr('style', `
|
|
640
807
|
font-size: ${dynamicYAxisTickFontSize}px;
|
|
641
808
|
font-weight: 400;
|
|
@@ -644,31 +811,13 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
644
811
|
if (isRtl) {
|
|
645
812
|
yTickTexts.attr('text-anchor', 'start');
|
|
646
813
|
}
|
|
814
|
+
else {
|
|
815
|
+
yTickTexts.attr('text-anchor', 'end').attr('direction', 'ltr');
|
|
816
|
+
}
|
|
817
|
+
const yTickNodes = (yTickTexts.nodes() || []);
|
|
818
|
+
const maxTickLabelWidth = this.measureMaxYAxisTickLabelWidth(yTickNodes, dynamicYAxisTickFontSize);
|
|
647
819
|
if (options.yAxisLabel) {
|
|
648
|
-
|
|
649
|
-
// Position it to the left of the tick labels with proper spacing
|
|
650
|
-
const maxTickLabelWidth = this.calculateMaxYAxisTickLabelWidth();
|
|
651
|
-
const padding = this.Y_AXIS_PADDING;
|
|
652
|
-
// Position title to the left of the tick labels
|
|
653
|
-
const labelY = -maxTickLabelWidth - padding;
|
|
654
|
-
// Center the title vertically
|
|
655
|
-
const labelX = -this.height / 2;
|
|
656
|
-
axesGroup
|
|
657
|
-
.append('text')
|
|
658
|
-
.attr('class', 'ax-line-chart-axis-label ax-y-axis-label')
|
|
659
|
-
.attr('text-anchor', 'middle')
|
|
660
|
-
.attr('dominant-baseline', 'middle')
|
|
661
|
-
.attr('transform', 'rotate(-90)')
|
|
662
|
-
.attr('x', labelX)
|
|
663
|
-
.attr('y', labelY)
|
|
664
|
-
.attr('direction', 'ltr')
|
|
665
|
-
.attr('style', `
|
|
666
|
-
font-size: 14px;
|
|
667
|
-
font-weight: 500;
|
|
668
|
-
fill: rgb(var(--ax-comp-line-chart-text-color));
|
|
669
|
-
pointer-events: none;
|
|
670
|
-
`)
|
|
671
|
-
.text(options.yAxisLabel);
|
|
820
|
+
this.createYAxisTitle(axesGroup, options.yAxisLabel, dynamicYAxisTickFontSize, maxTickLabelWidth);
|
|
672
821
|
}
|
|
673
822
|
}
|
|
674
823
|
if (showGrid) {
|
|
@@ -701,7 +850,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
701
850
|
.axisBottom(this.xScale)
|
|
702
851
|
.tickSize(-this.height)
|
|
703
852
|
.tickFormat(() => '')
|
|
704
|
-
.tickValues(
|
|
853
|
+
.tickValues(isPointScale ? undefined : this.xScale.ticks()));
|
|
705
854
|
// Style the vertical grid path and lines
|
|
706
855
|
xGrid.selectAll('path').attr('stroke-width', '0');
|
|
707
856
|
xGrid
|
|
@@ -713,16 +862,14 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
713
862
|
}
|
|
714
863
|
}
|
|
715
864
|
}
|
|
865
|
+
getXPosition(point) {
|
|
866
|
+
if (this.xScaleKind === 'point') {
|
|
867
|
+
return this.xScale(String(point.x)) ?? 0;
|
|
868
|
+
}
|
|
869
|
+
return this.xScale(point.x);
|
|
870
|
+
}
|
|
716
871
|
renderLines(allSeriesData) {
|
|
717
|
-
const
|
|
718
|
-
const getX = (d) => {
|
|
719
|
-
if (isBandScale) {
|
|
720
|
-
return this.xScale(String(d.x)) + this.xScale.bandwidth() / 2;
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
return this.xScale(d.x);
|
|
724
|
-
}
|
|
725
|
-
};
|
|
872
|
+
const getX = (d) => this.getXPosition(d);
|
|
726
873
|
const lineGenerator = this.d3
|
|
727
874
|
.line()
|
|
728
875
|
.x(getX)
|
|
@@ -898,23 +1045,23 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
898
1045
|
else {
|
|
899
1046
|
this.handlePointHover(event, d, series, series.originalIndex);
|
|
900
1047
|
}
|
|
1048
|
+
const pointRadius = this.effectiveOptions().pointRadius ?? this.DEFAULT_POINT_RADIUS;
|
|
901
1049
|
this.d3
|
|
902
1050
|
.select(event.target)
|
|
903
|
-
.
|
|
904
|
-
.
|
|
905
|
-
.attr('
|
|
906
|
-
.attr('stroke-width', 2)
|
|
1051
|
+
.interrupt()
|
|
1052
|
+
.attr('r', pointRadius * this.POINT_HIGHLIGHT_MULTIPLIER)
|
|
1053
|
+
.attr('stroke-width', this.POINT_HOVER_STROKE_WIDTH)
|
|
907
1054
|
.attr('stroke', `rgb(var(--ax-comp-line-chart-bg-color))`);
|
|
908
1055
|
})
|
|
909
1056
|
.on('mousemove', (event) => this.updateTooltipPosition(event))
|
|
910
1057
|
.on('mouseleave', (event) => {
|
|
911
1058
|
this._tooltipVisible.set(false);
|
|
1059
|
+
const pointRadius = this.effectiveOptions().pointRadius ?? this.DEFAULT_POINT_RADIUS;
|
|
912
1060
|
this.d3
|
|
913
1061
|
.select(event.target)
|
|
914
|
-
.
|
|
915
|
-
.
|
|
916
|
-
.attr('
|
|
917
|
-
.attr('stroke-width', 1)
|
|
1062
|
+
.interrupt()
|
|
1063
|
+
.attr('r', pointRadius)
|
|
1064
|
+
.attr('stroke-width', this.POINT_STROKE_WIDTH)
|
|
918
1065
|
.attr('stroke', '#fff');
|
|
919
1066
|
})
|
|
920
1067
|
.on('click', (event, d) => {
|
|
@@ -1009,14 +1156,7 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
1009
1156
|
}
|
|
1010
1157
|
}
|
|
1011
1158
|
showCrosshairLines(dataPoint) {
|
|
1012
|
-
const
|
|
1013
|
-
let x;
|
|
1014
|
-
if (isBandScale) {
|
|
1015
|
-
x = this.xScale(String(dataPoint.x)) + this.xScale.bandwidth() / 2;
|
|
1016
|
-
}
|
|
1017
|
-
else {
|
|
1018
|
-
x = this.xScale(dataPoint.x);
|
|
1019
|
-
}
|
|
1159
|
+
const x = this.getXPosition(dataPoint);
|
|
1020
1160
|
const y = this.yScale(dataPoint.y);
|
|
1021
1161
|
let crosshairGroup = this.chart.select('.ax-line-chart-crosshair');
|
|
1022
1162
|
if (crosshairGroup.empty()) {
|
|
@@ -1052,19 +1192,33 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
1052
1192
|
this._pendingTooltipCoords = { x: event.clientX, y: event.clientY };
|
|
1053
1193
|
if (this._tooltipRafId != null)
|
|
1054
1194
|
return;
|
|
1195
|
+
this.scheduleTooltipPosition(0);
|
|
1196
|
+
}
|
|
1197
|
+
scheduleTooltipPosition(attempt) {
|
|
1055
1198
|
this._tooltipRafId = requestAnimationFrame(() => {
|
|
1056
|
-
this._tooltipRafId = null;
|
|
1057
1199
|
const coords = this._pendingTooltipCoords;
|
|
1058
|
-
if (!coords)
|
|
1200
|
+
if (!coords) {
|
|
1201
|
+
this._tooltipRafId = null;
|
|
1059
1202
|
return;
|
|
1203
|
+
}
|
|
1060
1204
|
const containerEl = this.chartContainerEl()?.nativeElement;
|
|
1061
|
-
if (!containerEl)
|
|
1205
|
+
if (!containerEl) {
|
|
1206
|
+
this._tooltipRafId = null;
|
|
1062
1207
|
return;
|
|
1063
|
-
|
|
1208
|
+
}
|
|
1064
1209
|
const tooltipEl = containerEl.querySelector('.chart-tooltip');
|
|
1210
|
+
if (!tooltipEl && this._tooltipVisible() && attempt < 3) {
|
|
1211
|
+
this.scheduleTooltipPosition(attempt + 1);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
this._tooltipRafId = null;
|
|
1215
|
+
const rect = containerEl.getBoundingClientRect();
|
|
1065
1216
|
const tooltipRect = tooltipEl ? tooltipEl.getBoundingClientRect() : null;
|
|
1066
1217
|
const pos = computeTooltipPosition(rect, tooltipRect, coords.x, coords.y, this.TOOLTIP_GAP);
|
|
1067
|
-
this._tooltipPosition
|
|
1218
|
+
const prev = this._tooltipPosition();
|
|
1219
|
+
if (prev.x !== pos.x || prev.y !== pos.y) {
|
|
1220
|
+
this._tooltipPosition.set(pos);
|
|
1221
|
+
}
|
|
1068
1222
|
});
|
|
1069
1223
|
}
|
|
1070
1224
|
// computeTooltipPosition moved to shared chart utils
|
|
@@ -1161,12 +1315,12 @@ class AXLineChartComponent extends AXChartComponent {
|
|
|
1161
1315
|
this.showCrosshairLines(overlappingPoints[0].point);
|
|
1162
1316
|
}
|
|
1163
1317
|
}
|
|
1164
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1165
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.
|
|
1318
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXLineChartComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1319
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.9", type: AXLineChartComponent, isStandalone: true, selector: "ax-line-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: { pointClick: "pointClick" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-line-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"false\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-line-chart{display:block;width:100%;height:100%;min-height:0;box-sizing:border-box;--ax-comp-line-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-axis-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-bg-color: 0, 0, 0, 0;--ax-comp-line-chart-text-color: var(--ax-sys-color-on-lightest-surface)}ax-line-chart .ax-line-chart{width:100%;height:100%;position:relative;box-sizing:border-box;padding:clamp(.25rem,.6vw,.5rem);border-radius:.5rem;overflow:hidden;contain:layout style;color:rgba(var(--ax-comp-line-chart-text-color));background-color:rgb(var(--ax-comp-line-chart-bg-color))}ax-line-chart .ax-line-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden;flex-shrink:0}ax-line-chart .ax-line-chart svg g:has(text){font-family:inherit}ax-line-chart .ax-line-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-line-chart-bg-color));padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px}ax-line-chart .ax-line-chart-no-data-message .ax-line-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-line-chart .ax-line-chart-no-data-message .ax-line-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-line-chart .ax-line-chart-no-data-message .ax-line-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 });
|
|
1166
1320
|
}
|
|
1167
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1321
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXLineChartComponent, decorators: [{
|
|
1168
1322
|
type: Component,
|
|
1169
|
-
args: [{ selector: 'ax-line-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-line-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"false\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-line-chart{display:block;width:100%;height:100%;min-height:0;box-sizing:border-box;--ax-comp-line-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-axis-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-bg-color: 0, 0, 0, 0;--ax-comp-line-chart-text-color: var(--ax-sys-color-on-lightest-surface)}ax-line-chart .ax-line-chart{width:100%;height:100%;position:relative;box-sizing:border-box;padding:clamp(.5rem
|
|
1323
|
+
args: [{ selector: 'ax-line-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-line-chart\" #chartContainer>\n <!-- Shared tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"false\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-line-chart{display:block;width:100%;height:100%;min-height:0;box-sizing:border-box;--ax-comp-line-chart-labels-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-axis-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-grid-lines-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-line-chart-bg-color: 0, 0, 0, 0;--ax-comp-line-chart-text-color: var(--ax-sys-color-on-lightest-surface)}ax-line-chart .ax-line-chart{width:100%;height:100%;position:relative;box-sizing:border-box;padding:clamp(.25rem,.6vw,.5rem);border-radius:.5rem;overflow:hidden;contain:layout style;color:rgba(var(--ax-comp-line-chart-text-color));background-color:rgb(var(--ax-comp-line-chart-bg-color))}ax-line-chart .ax-line-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden;flex-shrink:0}ax-line-chart .ax-line-chart svg g:has(text){font-family:inherit}ax-line-chart .ax-line-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-line-chart-bg-color));padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px}ax-line-chart .ax-line-chart-no-data-message .ax-line-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-line-chart .ax-line-chart-no-data-message .ax-line-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-line-chart .ax-line-chart-no-data-message .ax-line-chart-no-data-help{font-size:.8rem;opacity:.6}\n"] }]
|
|
1170
1324
|
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], chartContainerEl: [{ type: i0.ViewChild, args: ['chartContainer', { isSignal: true }] }] } });
|
|
1171
1325
|
|
|
1172
1326
|
/**
|