@internetstiftelsen/charts 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -8
- package/area.d.ts +0 -1
- package/area.js +2 -19
- package/bar.d.ts +0 -1
- package/bar.js +64 -136
- package/base-chart.d.ts +54 -5
- package/base-chart.js +260 -73
- package/donut-chart.d.ts +7 -9
- package/donut-chart.js +51 -131
- package/gauge-chart.d.ts +18 -7
- package/gauge-chart.js +315 -106
- package/line.js +3 -25
- package/package.json +3 -1
- package/pie-chart.d.ts +7 -11
- package/pie-chart.js +30 -153
- package/radial-chart-base.d.ts +25 -0
- package/radial-chart-base.js +77 -0
- package/scale-utils.d.ts +3 -0
- package/scale-utils.js +14 -0
- package/types.d.ts +2 -0
- package/utils.d.ts +7 -0
- package/utils.js +24 -0
- package/word-cloud-chart.d.ts +32 -0
- package/word-cloud-chart.js +199 -0
- package/xy-chart.d.ts +8 -4
- package/xy-chart.js +127 -127
package/gauge-chart.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { arc, select } from 'd3';
|
|
2
|
-
import { BaseChart } from './base-chart.js';
|
|
1
|
+
import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, select, } from 'd3';
|
|
2
|
+
import { BaseChart, } from './base-chart.js';
|
|
3
3
|
import { DEFAULT_COLOR_PALETTE } from './theme.js';
|
|
4
4
|
import { ChartValidator } from './validation.js';
|
|
5
5
|
const DEFAULT_START_ANGLE = -Math.PI * 0.75;
|
|
@@ -10,6 +10,9 @@ const DEFAULT_VALUE_KEY = 'value';
|
|
|
10
10
|
const DEFAULT_HALF_CIRCLE = false;
|
|
11
11
|
const DEFAULT_MIN_VALUE = 0;
|
|
12
12
|
const DEFAULT_MAX_VALUE = 100;
|
|
13
|
+
const DEFAULT_ANIMATE = false;
|
|
14
|
+
const DEFAULT_ANIMATION_DURATION_MS = 700;
|
|
15
|
+
const DEFAULT_ANIMATION_EASING_PRESET = 'ease-in-out';
|
|
13
16
|
const DEFAULT_INNER_RADIUS_RATIO = 0.68;
|
|
14
17
|
const DEFAULT_CORNER_RADIUS = 4;
|
|
15
18
|
const DEFAULT_TRACK_COLOR = '#e5e7eb';
|
|
@@ -45,6 +48,14 @@ const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
|
|
|
45
48
|
const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
|
|
46
49
|
const TOOLTIP_OFFSET_PX = 12;
|
|
47
50
|
const EDGE_MARGIN_PX = 10;
|
|
51
|
+
const GAUGE_ANIMATION_EASING_PRESETS = {
|
|
52
|
+
linear: easeLinear,
|
|
53
|
+
'ease-in': easeCubicIn,
|
|
54
|
+
'ease-out': easeCubicOut,
|
|
55
|
+
'ease-in-out': easeCubicInOut,
|
|
56
|
+
'bounce-out': easeBounceOut,
|
|
57
|
+
'elastic-out': easeElasticOut,
|
|
58
|
+
};
|
|
48
59
|
const DUMMY_ARC_DATUM = {
|
|
49
60
|
innerRadius: 0,
|
|
50
61
|
outerRadius: 0,
|
|
@@ -96,6 +107,12 @@ export class GaugeChart extends BaseChart {
|
|
|
96
107
|
writable: true,
|
|
97
108
|
value: void 0
|
|
98
109
|
});
|
|
110
|
+
Object.defineProperty(this, "animation", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: void 0
|
|
115
|
+
});
|
|
99
116
|
Object.defineProperty(this, "halfCircle", {
|
|
100
117
|
enumerable: true,
|
|
101
118
|
configurable: true,
|
|
@@ -216,6 +233,12 @@ export class GaugeChart extends BaseChart {
|
|
|
216
233
|
writable: true,
|
|
217
234
|
value: null
|
|
218
235
|
});
|
|
236
|
+
Object.defineProperty(this, "lastRenderedValue", {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true,
|
|
240
|
+
value: null
|
|
241
|
+
});
|
|
219
242
|
Object.defineProperty(this, "defaultFormat", {
|
|
220
243
|
enumerable: true,
|
|
221
244
|
configurable: true,
|
|
@@ -235,6 +258,7 @@ export class GaugeChart extends BaseChart {
|
|
|
235
258
|
this.targetValueKey = config.targetValueKey;
|
|
236
259
|
this.minValue = gauge.min ?? DEFAULT_MIN_VALUE;
|
|
237
260
|
this.maxValue = gauge.max ?? DEFAULT_MAX_VALUE;
|
|
261
|
+
this.animation = this.normalizeAnimationConfig(gauge.animate);
|
|
238
262
|
this.halfCircle = gauge.halfCircle ?? DEFAULT_HALF_CIRCLE;
|
|
239
263
|
this.startAngle =
|
|
240
264
|
gauge.startAngle ??
|
|
@@ -260,7 +284,7 @@ export class GaugeChart extends BaseChart {
|
|
|
260
284
|
this.valueLabelStyle = this.normalizeValueLabelStyle(gauge.valueLabelStyle);
|
|
261
285
|
this.validateGaugeConfig();
|
|
262
286
|
this.segments = this.prepareSegments();
|
|
263
|
-
this.
|
|
287
|
+
this.initializeDataState();
|
|
264
288
|
}
|
|
265
289
|
normalizeNeedleConfig(config) {
|
|
266
290
|
if (config === false) {
|
|
@@ -328,6 +352,147 @@ export class GaugeChart extends BaseChart {
|
|
|
328
352
|
formatter: config?.formatter ?? this.defaultFormat,
|
|
329
353
|
};
|
|
330
354
|
}
|
|
355
|
+
normalizeAnimationConfig(config) {
|
|
356
|
+
if (config === undefined) {
|
|
357
|
+
return {
|
|
358
|
+
show: DEFAULT_ANIMATE,
|
|
359
|
+
duration: DEFAULT_ANIMATION_DURATION_MS,
|
|
360
|
+
easing: GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (typeof config === 'boolean') {
|
|
364
|
+
return {
|
|
365
|
+
show: config,
|
|
366
|
+
duration: DEFAULT_ANIMATION_DURATION_MS,
|
|
367
|
+
easing: GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
show: config.show ?? true,
|
|
372
|
+
duration: config.duration ?? DEFAULT_ANIMATION_DURATION_MS,
|
|
373
|
+
easing: this.resolveAnimationEasing(config.easing),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
resolveAnimationEasing(easing) {
|
|
377
|
+
if (!easing) {
|
|
378
|
+
return GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
|
|
379
|
+
}
|
|
380
|
+
if (typeof easing === 'function') {
|
|
381
|
+
return easing;
|
|
382
|
+
}
|
|
383
|
+
if (easing in GAUGE_ANIMATION_EASING_PRESETS) {
|
|
384
|
+
return GAUGE_ANIMATION_EASING_PRESETS[easing];
|
|
385
|
+
}
|
|
386
|
+
if (easing.startsWith('linear(')) {
|
|
387
|
+
const parsedCssLinear = this.parseCssLinearEasing(easing);
|
|
388
|
+
if (parsedCssLinear) {
|
|
389
|
+
return parsedCssLinear;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
ChartValidator.warn(`GaugeChart: unsupported gauge.animate.easing '${easing}', falling back to '${DEFAULT_ANIMATION_EASING_PRESET}'`);
|
|
393
|
+
return GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
|
|
394
|
+
}
|
|
395
|
+
parseCssLinearEasing(cssLinearEasing) {
|
|
396
|
+
const normalized = cssLinearEasing.trim();
|
|
397
|
+
if (!normalized.startsWith('linear(') || !normalized.endsWith(')')) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const body = normalized.slice('linear('.length, -1);
|
|
401
|
+
const tokens = body
|
|
402
|
+
.split(',')
|
|
403
|
+
.map((token) => token.trim())
|
|
404
|
+
.filter((token) => token.length > 0);
|
|
405
|
+
if (tokens.length < 2) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const rawStops = tokens.map((token) => {
|
|
409
|
+
const parts = token.split(/\s+/).filter(Boolean);
|
|
410
|
+
if (parts.length === 0 || parts.length > 2) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const value = Number(parts[0]);
|
|
414
|
+
if (!Number.isFinite(value)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
if (parts.length === 1) {
|
|
418
|
+
return { value, position: undefined };
|
|
419
|
+
}
|
|
420
|
+
const percentageToken = parts[1];
|
|
421
|
+
if (!percentageToken.endsWith('%')) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const percentageValue = Number(percentageToken.slice(0, -1));
|
|
425
|
+
if (!Number.isFinite(percentageValue)) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
value,
|
|
430
|
+
position: percentageValue / 100,
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
if (rawStops.some((stop) => stop === null)) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const stops = rawStops;
|
|
437
|
+
const positions = stops.map((stop) => stop.position);
|
|
438
|
+
if (positions[0] === undefined) {
|
|
439
|
+
positions[0] = 0;
|
|
440
|
+
}
|
|
441
|
+
if (positions[positions.length - 1] === undefined) {
|
|
442
|
+
positions[positions.length - 1] = 1;
|
|
443
|
+
}
|
|
444
|
+
let previousDefinedIndex = 0;
|
|
445
|
+
for (let currentIndex = 1; currentIndex < positions.length; currentIndex += 1) {
|
|
446
|
+
const currentPosition = positions[currentIndex];
|
|
447
|
+
if (currentPosition === undefined) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const previousPosition = positions[previousDefinedIndex];
|
|
451
|
+
if (currentPosition < previousPosition) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const missingCount = currentIndex - previousDefinedIndex - 1;
|
|
455
|
+
if (missingCount > 0) {
|
|
456
|
+
for (let missingIndex = 1; missingIndex <= missingCount; missingIndex += 1) {
|
|
457
|
+
const ratio = missingIndex / (missingCount + 1);
|
|
458
|
+
positions[previousDefinedIndex + missingIndex] =
|
|
459
|
+
previousPosition +
|
|
460
|
+
(currentPosition - previousPosition) * ratio;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
previousDefinedIndex = currentIndex;
|
|
464
|
+
}
|
|
465
|
+
const easingPoints = stops.map((stop, index) => {
|
|
466
|
+
return {
|
|
467
|
+
value: stop.value,
|
|
468
|
+
position: positions[index],
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
return (progress) => {
|
|
472
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
473
|
+
if (clamped <= easingPoints[0].position) {
|
|
474
|
+
return easingPoints[0].value;
|
|
475
|
+
}
|
|
476
|
+
const lastPoint = easingPoints[easingPoints.length - 1];
|
|
477
|
+
if (clamped >= lastPoint.position) {
|
|
478
|
+
return lastPoint.value;
|
|
479
|
+
}
|
|
480
|
+
for (let index = 0; index < easingPoints.length - 1; index += 1) {
|
|
481
|
+
const left = easingPoints[index];
|
|
482
|
+
const right = easingPoints[index + 1];
|
|
483
|
+
if (clamped > right.position) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const span = right.position - left.position;
|
|
487
|
+
if (span <= 0) {
|
|
488
|
+
return right.value;
|
|
489
|
+
}
|
|
490
|
+
const localProgress = (clamped - left.position) / span;
|
|
491
|
+
return left.value + (right.value - left.value) * localProgress;
|
|
492
|
+
}
|
|
493
|
+
return lastPoint.value;
|
|
494
|
+
};
|
|
495
|
+
}
|
|
331
496
|
normalizeTickLabelStyle(config) {
|
|
332
497
|
return {
|
|
333
498
|
fontSize: config?.fontSize ?? DEFAULT_TICK_LABEL_FONT_SIZE,
|
|
@@ -392,6 +557,10 @@ export class GaugeChart extends BaseChart {
|
|
|
392
557
|
if (this.marker.width <= 0) {
|
|
393
558
|
throw new Error(`GaugeChart: gauge.marker.width must be > 0, received '${this.marker.width}'`);
|
|
394
559
|
}
|
|
560
|
+
if (!Number.isFinite(this.animation.duration) ||
|
|
561
|
+
this.animation.duration < 0) {
|
|
562
|
+
throw new Error(`GaugeChart: gauge.animate.duration must be >= 0, received '${this.animation.duration}'`);
|
|
563
|
+
}
|
|
395
564
|
this.validateSegments(this.configuredSegments);
|
|
396
565
|
}
|
|
397
566
|
validateSegments(segments) {
|
|
@@ -502,59 +671,17 @@ export class GaugeChart extends BaseChart {
|
|
|
502
671
|
};
|
|
503
672
|
});
|
|
504
673
|
}
|
|
505
|
-
addChild(component) {
|
|
506
|
-
const type = component.type;
|
|
507
|
-
if (type === 'tooltip') {
|
|
508
|
-
this.tooltip = component;
|
|
509
|
-
}
|
|
510
|
-
else if (type === 'legend') {
|
|
511
|
-
this.legend = component;
|
|
512
|
-
this.legend.setToggleCallback(() => {
|
|
513
|
-
if (!this.container) {
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
this.update(this.data);
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
else if (type === 'title') {
|
|
520
|
-
this.title = component;
|
|
521
|
-
}
|
|
522
|
-
return this;
|
|
523
|
-
}
|
|
524
674
|
getExportComponents() {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
components.push(this.tooltip);
|
|
531
|
-
}
|
|
532
|
-
if (this.legend?.isInlineMode()) {
|
|
533
|
-
components.push(this.legend);
|
|
534
|
-
}
|
|
535
|
-
return components;
|
|
675
|
+
return this.getBaseExportComponents({
|
|
676
|
+
title: true,
|
|
677
|
+
tooltip: true,
|
|
678
|
+
legend: this.legend?.isInlineMode(),
|
|
679
|
+
});
|
|
536
680
|
}
|
|
537
681
|
update(data) {
|
|
538
|
-
this.
|
|
539
|
-
this.refreshResolvedValues();
|
|
682
|
+
this.lastRenderedValue = this.value;
|
|
540
683
|
super.update(data);
|
|
541
684
|
}
|
|
542
|
-
getLayoutComponents() {
|
|
543
|
-
const components = [];
|
|
544
|
-
if (this.title) {
|
|
545
|
-
components.push(this.title);
|
|
546
|
-
}
|
|
547
|
-
if (this.legend) {
|
|
548
|
-
components.push(this.legend);
|
|
549
|
-
}
|
|
550
|
-
return components;
|
|
551
|
-
}
|
|
552
|
-
prepareLayout() {
|
|
553
|
-
const svgNode = this.svg?.node();
|
|
554
|
-
if (svgNode && this.legend?.isInlineMode()) {
|
|
555
|
-
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
685
|
createExportChart() {
|
|
559
686
|
return new GaugeChart({
|
|
560
687
|
data: this.data,
|
|
@@ -567,6 +694,7 @@ export class GaugeChart extends BaseChart {
|
|
|
567
694
|
targetValue: this.configuredTargetValue,
|
|
568
695
|
min: this.minValue,
|
|
569
696
|
max: this.maxValue,
|
|
697
|
+
animate: false,
|
|
570
698
|
halfCircle: this.halfCircle,
|
|
571
699
|
startAngle: this.startAngle,
|
|
572
700
|
endAngle: this.endAngle,
|
|
@@ -590,17 +718,14 @@ export class GaugeChart extends BaseChart {
|
|
|
590
718
|
},
|
|
591
719
|
});
|
|
592
720
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const titlePosition = this.layoutManager.getComponentPosition(this.title);
|
|
600
|
-
this.title.render(this.svg, this.theme, this.width, titlePosition.x, titlePosition.y);
|
|
601
|
-
}
|
|
721
|
+
syncDerivedState() {
|
|
722
|
+
this.refreshResolvedValues();
|
|
723
|
+
}
|
|
724
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
725
|
+
svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
|
|
726
|
+
this.renderTitle(svg);
|
|
602
727
|
if (this.tooltip) {
|
|
603
|
-
this.tooltip.initialize(this.
|
|
728
|
+
this.tooltip.initialize(this.renderTheme);
|
|
604
729
|
}
|
|
605
730
|
const labelAllowance = this.ticks.show && this.ticks.showLabels
|
|
606
731
|
? this.ticks.size + this.ticks.labelOffset + 14
|
|
@@ -608,29 +733,30 @@ export class GaugeChart extends BaseChart {
|
|
|
608
733
|
? this.ticks.size + 10
|
|
609
734
|
: 8;
|
|
610
735
|
let outerRadius;
|
|
611
|
-
const centerX =
|
|
736
|
+
const centerX = plotArea.left + plotArea.width / 2;
|
|
612
737
|
let centerY;
|
|
613
738
|
if (this.halfCircle) {
|
|
614
739
|
const valueSpace = this.showValue
|
|
615
740
|
? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
|
|
616
741
|
: DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
|
|
617
|
-
const maxHorizontalRadius =
|
|
618
|
-
const maxVerticalRadius =
|
|
742
|
+
const maxHorizontalRadius = plotArea.width / 2 - labelAllowance - 8;
|
|
743
|
+
const maxVerticalRadius = plotArea.height - valueSpace - labelAllowance - 8;
|
|
619
744
|
outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
|
|
620
|
-
centerY =
|
|
745
|
+
centerY = plotArea.top + labelAllowance + outerRadius + 4;
|
|
621
746
|
}
|
|
622
747
|
else {
|
|
623
|
-
const maxRadius = Math.min(
|
|
748
|
+
const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
|
|
624
749
|
outerRadius = Math.max(24, maxRadius - labelAllowance);
|
|
625
|
-
centerY =
|
|
750
|
+
centerY = plotArea.top + plotArea.height / 2;
|
|
626
751
|
}
|
|
627
752
|
const innerRadius = this.thickness !== null
|
|
628
753
|
? Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1))
|
|
629
754
|
: Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
|
|
630
|
-
const gaugeGroup =
|
|
755
|
+
const gaugeGroup = plotGroup
|
|
631
756
|
.append('g')
|
|
632
757
|
.attr('class', 'gauge')
|
|
633
758
|
.attr('transform', `translate(${centerX}, ${centerY})`);
|
|
759
|
+
const animationStartValue = this.resolveAnimationStartValue();
|
|
634
760
|
this.renderTrack(gaugeGroup, innerRadius, outerRadius);
|
|
635
761
|
const visibleSegments = this.getVisibleSegments();
|
|
636
762
|
if (visibleSegments.length > 0) {
|
|
@@ -645,7 +771,7 @@ export class GaugeChart extends BaseChart {
|
|
|
645
771
|
const shouldRenderProgress = visibleSegments.length === 0;
|
|
646
772
|
if (shouldRenderProgress) {
|
|
647
773
|
const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
|
|
648
|
-
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer);
|
|
774
|
+
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer, animationStartValue);
|
|
649
775
|
}
|
|
650
776
|
if (this.ticks.show &&
|
|
651
777
|
this.ticks.count > 0 &&
|
|
@@ -653,21 +779,30 @@ export class GaugeChart extends BaseChart {
|
|
|
653
779
|
this.renderTicks(gaugeGroup, outerRadius);
|
|
654
780
|
}
|
|
655
781
|
if (this.showValue) {
|
|
656
|
-
this.renderValueText(gaugeGroup, outerRadius);
|
|
782
|
+
this.renderValueText(gaugeGroup, outerRadius, animationStartValue);
|
|
657
783
|
}
|
|
658
784
|
if (this.targetValue !== null) {
|
|
659
785
|
this.renderTargetMarker(gaugeGroup, innerRadius, outerRadius);
|
|
660
786
|
}
|
|
661
787
|
if (this.needle.show) {
|
|
662
|
-
this.renderNeedle(gaugeGroup, innerRadius, outerRadius);
|
|
788
|
+
this.renderNeedle(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
663
789
|
}
|
|
664
790
|
else if (this.marker.show) {
|
|
665
|
-
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius);
|
|
791
|
+
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
666
792
|
}
|
|
667
793
|
if (this.tooltip) {
|
|
668
794
|
this.attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor);
|
|
669
795
|
}
|
|
670
|
-
this.
|
|
796
|
+
this.renderInlineLegend(svg);
|
|
797
|
+
}
|
|
798
|
+
resolveAnimationStartValue() {
|
|
799
|
+
if (this.lastRenderedValue === null) {
|
|
800
|
+
return this.minValue;
|
|
801
|
+
}
|
|
802
|
+
return Math.min(this.maxValue, Math.max(this.minValue, this.lastRenderedValue));
|
|
803
|
+
}
|
|
804
|
+
shouldAnimateTransition(startValue) {
|
|
805
|
+
return this.animation.show && startValue !== this.value;
|
|
671
806
|
}
|
|
672
807
|
buildAriaLabel() {
|
|
673
808
|
const statusLabel = this.findSegmentStatusLabel();
|
|
@@ -689,11 +824,8 @@ export class GaugeChart extends BaseChart {
|
|
|
689
824
|
return null;
|
|
690
825
|
}
|
|
691
826
|
getVisibleSegments() {
|
|
692
|
-
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
return this.segments.filter((segment) => {
|
|
696
|
-
return this.legend.isSeriesVisible(segment.legendLabel);
|
|
827
|
+
return this.filterVisibleItems(this.segments, (segment) => {
|
|
828
|
+
return segment.legendLabel;
|
|
697
829
|
});
|
|
698
830
|
}
|
|
699
831
|
resolveProgressColor(segments) {
|
|
@@ -811,18 +943,44 @@ export class GaugeChart extends BaseChart {
|
|
|
811
943
|
.attr('stroke-opacity', 0.95)
|
|
812
944
|
.style('pointer-events', 'none');
|
|
813
945
|
}
|
|
814
|
-
renderProgress(gaugeGroup, progressColor, innerRadius, outerRadius) {
|
|
946
|
+
renderProgress(gaugeGroup, progressColor, innerRadius, outerRadius, startValue) {
|
|
815
947
|
const progressArc = arc()
|
|
816
948
|
.innerRadius(innerRadius)
|
|
817
949
|
.outerRadius(outerRadius)
|
|
818
950
|
.startAngle(this.startAngle)
|
|
819
|
-
.endAngle(
|
|
951
|
+
.endAngle((datum) => datum.endAngle)
|
|
820
952
|
.cornerRadius(this.cornerRadius);
|
|
821
|
-
|
|
953
|
+
const startAngle = this.valueToAngle(startValue);
|
|
954
|
+
const endAngle = this.valueToAngle(this.value);
|
|
955
|
+
const startPath = progressArc({
|
|
956
|
+
...DUMMY_ARC_DATUM,
|
|
957
|
+
endAngle: startAngle,
|
|
958
|
+
});
|
|
959
|
+
const endPath = progressArc({
|
|
960
|
+
...DUMMY_ARC_DATUM,
|
|
961
|
+
endAngle,
|
|
962
|
+
});
|
|
963
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
964
|
+
const progressPath = gaugeGroup
|
|
822
965
|
.append('path')
|
|
823
966
|
.attr('class', 'gauge-progress')
|
|
824
|
-
.attr('fill', progressColor)
|
|
825
|
-
|
|
967
|
+
.attr('fill', progressColor);
|
|
968
|
+
if (shouldAnimate) {
|
|
969
|
+
const angleInterpolator = interpolateNumber(startAngle, endAngle);
|
|
970
|
+
progressPath
|
|
971
|
+
.attr('d', startPath)
|
|
972
|
+
.transition()
|
|
973
|
+
.duration(this.animation.duration)
|
|
974
|
+
.ease(this.animation.easing)
|
|
975
|
+
.attrTween('d', () => {
|
|
976
|
+
return (progress) => progressArc({
|
|
977
|
+
...DUMMY_ARC_DATUM,
|
|
978
|
+
endAngle: angleInterpolator(progress),
|
|
979
|
+
}) ?? '';
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
progressPath.attr('d', endPath);
|
|
826
984
|
}
|
|
827
985
|
renderTicks(gaugeGroup, outerRadius) {
|
|
828
986
|
const tickGroup = gaugeGroup.append('g').attr('class', 'gauge-ticks');
|
|
@@ -889,21 +1047,43 @@ export class GaugeChart extends BaseChart {
|
|
|
889
1047
|
.attr('stroke-width', DEFAULT_TARGET_MARKER_STROKE_WIDTH)
|
|
890
1048
|
.attr('stroke-linecap', 'round');
|
|
891
1049
|
}
|
|
892
|
-
renderNeedle(gaugeGroup, innerRadius, outerRadius) {
|
|
1050
|
+
renderNeedle(gaugeGroup, innerRadius, outerRadius, startValue) {
|
|
893
1051
|
const needleAngle = this.valueToAngle(this.value);
|
|
1052
|
+
const startNeedleAngle = this.valueToAngle(startValue);
|
|
894
1053
|
const maxLength = Math.max(innerRadius + 2, outerRadius - 2);
|
|
895
1054
|
const length = maxLength * this.needle.lengthRatio;
|
|
896
1055
|
const needlePoint = this.pointAt(needleAngle, length);
|
|
897
|
-
|
|
1056
|
+
const startNeedlePoint = this.pointAt(startNeedleAngle, length);
|
|
1057
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1058
|
+
const initialNeedlePoint = shouldAnimate
|
|
1059
|
+
? startNeedlePoint
|
|
1060
|
+
: needlePoint;
|
|
1061
|
+
const needleLine = gaugeGroup
|
|
898
1062
|
.append('line')
|
|
899
1063
|
.attr('class', 'gauge-needle')
|
|
900
1064
|
.attr('x1', 0)
|
|
901
1065
|
.attr('y1', 0)
|
|
902
|
-
.attr('x2',
|
|
903
|
-
.attr('y2',
|
|
1066
|
+
.attr('x2', initialNeedlePoint.x)
|
|
1067
|
+
.attr('y2', initialNeedlePoint.y)
|
|
904
1068
|
.attr('stroke', this.needle.color)
|
|
905
1069
|
.attr('stroke-width', this.needle.width)
|
|
906
1070
|
.attr('stroke-linecap', 'round');
|
|
1071
|
+
if (shouldAnimate) {
|
|
1072
|
+
const angleInterpolator = interpolateNumber(startNeedleAngle, needleAngle);
|
|
1073
|
+
needleLine
|
|
1074
|
+
.transition()
|
|
1075
|
+
.duration(this.animation.duration)
|
|
1076
|
+
.ease(this.animation.easing)
|
|
1077
|
+
.tween('needle-rotation', () => {
|
|
1078
|
+
return (progress) => {
|
|
1079
|
+
const interpolatedAngle = angleInterpolator(progress);
|
|
1080
|
+
const interpolatedPoint = this.pointAt(interpolatedAngle, length);
|
|
1081
|
+
needleLine
|
|
1082
|
+
.attr('x2', interpolatedPoint.x)
|
|
1083
|
+
.attr('y2', interpolatedPoint.y);
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
907
1087
|
gaugeGroup
|
|
908
1088
|
.append('circle')
|
|
909
1089
|
.attr('class', 'gauge-needle-cap')
|
|
@@ -912,28 +1092,55 @@ export class GaugeChart extends BaseChart {
|
|
|
912
1092
|
.attr('r', this.needle.capRadius)
|
|
913
1093
|
.attr('fill', this.needle.color);
|
|
914
1094
|
}
|
|
915
|
-
renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius) {
|
|
1095
|
+
renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, startValue) {
|
|
916
1096
|
const markerAngle = this.valueToAngle(this.value);
|
|
1097
|
+
const startMarkerAngle = this.valueToAngle(startValue);
|
|
917
1098
|
const markerInner = innerRadius + 1;
|
|
918
1099
|
const markerOuter = Math.max(markerInner + 1, outerRadius - 1);
|
|
919
1100
|
const innerPoint = this.pointAt(markerAngle, markerInner);
|
|
920
1101
|
const outerPoint = this.pointAt(markerAngle, markerOuter);
|
|
921
|
-
|
|
1102
|
+
const startInnerPoint = this.pointAt(startMarkerAngle, markerInner);
|
|
1103
|
+
const startOuterPoint = this.pointAt(startMarkerAngle, markerOuter);
|
|
1104
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1105
|
+
const initialInnerPoint = shouldAnimate ? startInnerPoint : innerPoint;
|
|
1106
|
+
const initialOuterPoint = shouldAnimate ? startOuterPoint : outerPoint;
|
|
1107
|
+
const markerLine = gaugeGroup
|
|
922
1108
|
.append('line')
|
|
923
1109
|
.attr('class', 'gauge-marker')
|
|
924
|
-
.attr('x1',
|
|
925
|
-
.attr('y1',
|
|
926
|
-
.attr('x2',
|
|
927
|
-
.attr('y2',
|
|
1110
|
+
.attr('x1', initialInnerPoint.x)
|
|
1111
|
+
.attr('y1', initialInnerPoint.y)
|
|
1112
|
+
.attr('x2', initialOuterPoint.x)
|
|
1113
|
+
.attr('y2', initialOuterPoint.y)
|
|
928
1114
|
.attr('stroke', this.marker.color)
|
|
929
1115
|
.attr('stroke-width', this.marker.width)
|
|
930
1116
|
.attr('stroke-linecap', 'round');
|
|
1117
|
+
if (shouldAnimate) {
|
|
1118
|
+
const angleInterpolator = interpolateNumber(startMarkerAngle, markerAngle);
|
|
1119
|
+
markerLine
|
|
1120
|
+
.transition()
|
|
1121
|
+
.duration(this.animation.duration)
|
|
1122
|
+
.ease(this.animation.easing)
|
|
1123
|
+
.tween('marker-sweep', () => {
|
|
1124
|
+
return (progress) => {
|
|
1125
|
+
const interpolatedAngle = angleInterpolator(progress);
|
|
1126
|
+
const interpolatedInner = this.pointAt(interpolatedAngle, markerInner);
|
|
1127
|
+
const interpolatedOuter = this.pointAt(interpolatedAngle, markerOuter);
|
|
1128
|
+
markerLine
|
|
1129
|
+
.attr('x1', interpolatedInner.x)
|
|
1130
|
+
.attr('y1', interpolatedInner.y)
|
|
1131
|
+
.attr('x2', interpolatedOuter.x)
|
|
1132
|
+
.attr('y2', interpolatedOuter.y);
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
931
1136
|
}
|
|
932
|
-
renderValueText(gaugeGroup, outerRadius) {
|
|
1137
|
+
renderValueText(gaugeGroup, outerRadius, startValue) {
|
|
933
1138
|
const mainValueY = this.halfCircle
|
|
934
1139
|
? DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y
|
|
935
1140
|
: Math.max(DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y, outerRadius * 0.58);
|
|
936
|
-
|
|
1141
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1142
|
+
const initialValue = shouldAnimate ? startValue : this.value;
|
|
1143
|
+
const valueText = gaugeGroup
|
|
937
1144
|
.append('text')
|
|
938
1145
|
.attr('class', 'gauge-value')
|
|
939
1146
|
.attr('x', 0)
|
|
@@ -943,7 +1150,19 @@ export class GaugeChart extends BaseChart {
|
|
|
943
1150
|
.attr('font-weight', this.valueLabelStyle.fontWeight)
|
|
944
1151
|
.attr('font-family', this.valueLabelStyle.fontFamily)
|
|
945
1152
|
.attr('fill', this.valueLabelStyle.color)
|
|
946
|
-
.text(this.valueFormatter(
|
|
1153
|
+
.text(this.valueFormatter(initialValue));
|
|
1154
|
+
if (shouldAnimate) {
|
|
1155
|
+
valueText
|
|
1156
|
+
.transition()
|
|
1157
|
+
.duration(this.animation.duration)
|
|
1158
|
+
.ease(this.animation.easing)
|
|
1159
|
+
.tween('text', () => {
|
|
1160
|
+
return (progress) => {
|
|
1161
|
+
const currentValue = startValue + (this.value - startValue) * progress;
|
|
1162
|
+
valueText.text(this.valueFormatter(currentValue));
|
|
1163
|
+
};
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
947
1166
|
}
|
|
948
1167
|
attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor) {
|
|
949
1168
|
const interactionArc = arc()
|
|
@@ -1039,16 +1258,6 @@ export class GaugeChart extends BaseChart {
|
|
|
1039
1258
|
EDGE_MARGIN_PX));
|
|
1040
1259
|
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
1041
1260
|
}
|
|
1042
|
-
renderLegend() {
|
|
1043
|
-
if (!this.legend ||
|
|
1044
|
-
!this.legend.isInlineMode() ||
|
|
1045
|
-
!this.svg ||
|
|
1046
|
-
this.segments.length === 0) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const legendPosition = this.layoutManager.getComponentPosition(this.legend);
|
|
1050
|
-
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPosition.x, legendPosition.y);
|
|
1051
|
-
}
|
|
1052
1261
|
getLegendSeries() {
|
|
1053
1262
|
return this.segments.map((segment) => {
|
|
1054
1263
|
return {
|
package/line.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { line } from 'd3';
|
|
2
2
|
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
3
|
+
import { getScalePosition } from './scale-utils.js';
|
|
3
4
|
export class Line {
|
|
4
5
|
constructor(config) {
|
|
5
6
|
Object.defineProperty(this, "type", {
|
|
@@ -61,31 +62,8 @@ export class Line {
|
|
|
61
62
|
}
|
|
62
63
|
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
|
|
63
64
|
const getXPosition = (d) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let scaledValue;
|
|
67
|
-
switch (xScaleType) {
|
|
68
|
-
case 'band':
|
|
69
|
-
// Band scale - use string
|
|
70
|
-
scaledValue = String(xValue);
|
|
71
|
-
break;
|
|
72
|
-
case 'time':
|
|
73
|
-
// Time scale - convert to Date
|
|
74
|
-
scaledValue =
|
|
75
|
-
xValue instanceof Date
|
|
76
|
-
? xValue
|
|
77
|
-
: new Date(String(xValue));
|
|
78
|
-
break;
|
|
79
|
-
case 'linear':
|
|
80
|
-
case 'log':
|
|
81
|
-
// Linear/log scale - convert to number
|
|
82
|
-
scaledValue =
|
|
83
|
-
typeof xValue === 'number' ? xValue : Number(xValue);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
const scaled = x(scaledValue);
|
|
87
|
-
// Handle band scales with bandwidth
|
|
88
|
-
return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
65
|
+
return (getScalePosition(x, d[xKey], xScaleType) +
|
|
66
|
+
(x.bandwidth ? x.bandwidth() / 2 : 0));
|
|
89
67
|
};
|
|
90
68
|
// Helper to check if a data point has a valid (non-null) value
|
|
91
69
|
const hasValidValue = (d) => {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.9.0",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"class-variance-authority": "^0.7.1",
|
|
41
41
|
"clsx": "^2.1.1",
|
|
42
42
|
"d3": "^7.9.0",
|
|
43
|
+
"d3-cloud": "^1.2.9",
|
|
43
44
|
"handsontable": "^16.2.0",
|
|
44
45
|
"lucide-react": "^0.548.0",
|
|
45
46
|
"react": "^19.2.4",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"@testing-library/dom": "^10.4.1",
|
|
60
61
|
"@testing-library/jest-dom": "^6.9.1",
|
|
61
62
|
"@testing-library/react": "^16.3.2",
|
|
63
|
+
"@types/d3-cloud": "^1.2.9",
|
|
62
64
|
"@types/node": "^24.10.13",
|
|
63
65
|
"@types/react": "^19.2.14",
|
|
64
66
|
"@types/react-dom": "^19.2.3",
|