@internetstiftelsen/charts 0.9.2 → 0.10.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/README.md +137 -3
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +20 -1
- package/dist/bar.js +395 -519
- package/dist/base-chart.d.ts +21 -1
- package/dist/base-chart.js +166 -93
- package/dist/chart-group.d.ts +137 -0
- package/dist/chart-group.js +1155 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +30 -15
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +229 -133
- package/dist/legend-state.d.ts +19 -0
- package/dist/legend-state.js +81 -0
- package/dist/legend.d.ts +5 -2
- package/dist/legend.js +45 -38
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +45 -19
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +165 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +21 -25
- package/dist/types.d.ts +19 -1
- package/dist/utils.js +11 -19
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +40 -1
- package/dist/xy-chart.js +488 -165
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/chart-group.md +213 -0
- package/docs/components.md +321 -0
- package/docs/donut-chart.md +193 -0
- package/docs/gauge-chart.md +175 -0
- package/docs/getting-started.md +311 -0
- package/docs/pie-chart.md +123 -0
- package/docs/theming.md +162 -0
- package/docs/word-cloud-chart.md +98 -0
- package/docs/xy-chart.md +517 -0
- package/package.json +6 -4
package/dist/legend.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { select } from 'd3';
|
|
2
2
|
import { getSeriesColor } from './types.js';
|
|
3
|
+
import { LegendStateController } from './legend-state.js';
|
|
3
4
|
import { getContrastTextColor, mergeDeep } from './utils.js';
|
|
4
5
|
export class Legend {
|
|
5
6
|
constructor(config) {
|
|
@@ -69,11 +70,17 @@ export class Legend {
|
|
|
69
70
|
writable: true,
|
|
70
71
|
value: 8
|
|
71
72
|
});
|
|
72
|
-
Object.defineProperty(this, "
|
|
73
|
+
Object.defineProperty(this, "stateController", {
|
|
73
74
|
enumerable: true,
|
|
74
75
|
configurable: true,
|
|
75
76
|
writable: true,
|
|
76
|
-
value:
|
|
77
|
+
value: void 0
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(this, "stateControllerCleanup", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: true,
|
|
82
|
+
writable: true,
|
|
83
|
+
value: null
|
|
77
84
|
});
|
|
78
85
|
Object.defineProperty(this, "onToggleCallback", {
|
|
79
86
|
enumerable: true,
|
|
@@ -99,15 +106,18 @@ export class Legend {
|
|
|
99
106
|
writable: true,
|
|
100
107
|
value: null
|
|
101
108
|
});
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
109
|
+
const { mode = 'inline', position = 'bottom', disconnectedTarget, marginTop = 20, marginBottom = 10, paddingX, itemSpacingX, itemSpacingY, exportHooks, } = config ?? {};
|
|
110
|
+
this.mode = mode;
|
|
111
|
+
this.position = position;
|
|
112
|
+
this.disconnectedTarget = disconnectedTarget;
|
|
113
|
+
this.marginTop = marginTop;
|
|
114
|
+
this.marginBottom = marginBottom;
|
|
115
|
+
this.paddingX = paddingX;
|
|
116
|
+
this.itemSpacingX = itemSpacingX;
|
|
117
|
+
this.itemSpacingY = itemSpacingY;
|
|
118
|
+
this.exportHooks = exportHooks;
|
|
119
|
+
this.stateController = new LegendStateController();
|
|
120
|
+
this.bindStateController(this.stateController);
|
|
111
121
|
}
|
|
112
122
|
getExportConfig() {
|
|
113
123
|
return {
|
|
@@ -127,16 +137,19 @@ export class Legend {
|
|
|
127
137
|
...merged,
|
|
128
138
|
exportHooks: this.exportHooks,
|
|
129
139
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const clonedStateController = this.stateController.clone();
|
|
141
|
+
clonedStateController.subscribe(() => {
|
|
142
|
+
this.stateController.setVisibilityMap(clonedStateController.toVisibilityMap());
|
|
143
|
+
});
|
|
144
|
+
legend.setStateController(clonedStateController);
|
|
135
145
|
return legend;
|
|
136
146
|
}
|
|
137
147
|
setToggleCallback(callback) {
|
|
138
148
|
this.onToggleCallback = callback;
|
|
139
149
|
}
|
|
150
|
+
setStateController(controller) {
|
|
151
|
+
this.bindStateController(controller);
|
|
152
|
+
}
|
|
140
153
|
isInlineMode() {
|
|
141
154
|
return this.mode === 'inline';
|
|
142
155
|
}
|
|
@@ -159,22 +172,16 @@ export class Legend {
|
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
174
|
isSeriesVisible(dataKey) {
|
|
162
|
-
return this.
|
|
175
|
+
return this.stateController.isSeriesVisible(dataKey);
|
|
163
176
|
}
|
|
164
177
|
setSeriesVisible(dataKey, visible) {
|
|
165
|
-
this.
|
|
166
|
-
this.triggerChange();
|
|
178
|
+
this.stateController.setSeriesVisible(dataKey, visible);
|
|
167
179
|
}
|
|
168
180
|
toggleSeries(dataKey) {
|
|
169
|
-
|
|
170
|
-
this.visibilityState.set(dataKey, !currentState);
|
|
171
|
-
this.triggerChange();
|
|
181
|
+
this.stateController.toggleSeries(dataKey);
|
|
172
182
|
}
|
|
173
183
|
setVisibilityMap(visibility) {
|
|
174
|
-
|
|
175
|
-
this.visibilityState.set(dataKey, isVisible);
|
|
176
|
-
});
|
|
177
|
-
this.triggerChange();
|
|
184
|
+
this.stateController.setVisibilityMap(visibility);
|
|
178
185
|
}
|
|
179
186
|
estimateLayoutSpace(series, theme, width, svg) {
|
|
180
187
|
const signature = this.getLayoutSignature(series, width, theme);
|
|
@@ -269,7 +276,7 @@ export class Legend {
|
|
|
269
276
|
.attr('width', theme.legend.boxSize)
|
|
270
277
|
.attr('height', theme.legend.boxSize)
|
|
271
278
|
.attr('fill', (d) => {
|
|
272
|
-
const isVisible = this.
|
|
279
|
+
const isVisible = this.stateController.isSeriesVisible(d.dataKey);
|
|
273
280
|
return isVisible ? d.color : theme.legend.uncheckedColor;
|
|
274
281
|
})
|
|
275
282
|
.attr('rx', 3);
|
|
@@ -283,7 +290,7 @@ export class Legend {
|
|
|
283
290
|
.attr('stroke-linecap', 'round')
|
|
284
291
|
.attr('stroke-linejoin', 'round')
|
|
285
292
|
.style('display', (d) => {
|
|
286
|
-
const isVisible = this.
|
|
293
|
+
const isVisible = this.stateController.isSeriesVisible(d.dataKey);
|
|
287
294
|
return isVisible ? 'block' : 'none';
|
|
288
295
|
});
|
|
289
296
|
// Add label text
|
|
@@ -296,7 +303,7 @@ export class Legend {
|
|
|
296
303
|
.text((d) => d.label);
|
|
297
304
|
}
|
|
298
305
|
isLegendItemVisible(dataKey) {
|
|
299
|
-
return this.
|
|
306
|
+
return this.stateController.isSeriesVisible(dataKey);
|
|
300
307
|
}
|
|
301
308
|
isToggleActivationKey(key) {
|
|
302
309
|
return key === 'Enter' || key === ' ' || key === 'Spacebar';
|
|
@@ -304,11 +311,7 @@ export class Legend {
|
|
|
304
311
|
computeLayout(series, theme, width, svg) {
|
|
305
312
|
const settings = this.resolveLayoutSettings(theme);
|
|
306
313
|
const legendItems = this.buildLegendItems(series);
|
|
307
|
-
legendItems.
|
|
308
|
-
if (!this.visibilityState.has(item.dataKey)) {
|
|
309
|
-
this.visibilityState.set(item.dataKey, true);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
314
|
+
this.stateController.ensureSeries(legendItems.map((item) => item.dataKey), { silent: true });
|
|
312
315
|
const measuredItems = this.measureLegendItemWidths(legendItems, theme, svg);
|
|
313
316
|
const rows = this.buildRows(measuredItems, width, settings);
|
|
314
317
|
const positionedItems = this.positionRows(rows, width, settings);
|
|
@@ -439,10 +442,14 @@ export class Legend {
|
|
|
439
442
|
getFallbackRowHeight(theme) {
|
|
440
443
|
return Math.max(theme.legend.boxSize, theme.legend.fontSize);
|
|
441
444
|
}
|
|
442
|
-
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
445
|
-
|
|
445
|
+
bindStateController(controller) {
|
|
446
|
+
this.stateControllerCleanup?.();
|
|
447
|
+
this.stateController = controller;
|
|
448
|
+
this.stateControllerCleanup = controller.subscribe(() => {
|
|
449
|
+
this.onToggleCallback?.();
|
|
450
|
+
this.onChangeCallbacks.forEach((callback) => {
|
|
451
|
+
callback();
|
|
452
|
+
});
|
|
446
453
|
});
|
|
447
454
|
}
|
|
448
455
|
}
|
package/dist/line.js
CHANGED
|
@@ -123,7 +123,9 @@ export class Line {
|
|
|
123
123
|
const plotBottom = y.range()[0];
|
|
124
124
|
data.forEach((d) => {
|
|
125
125
|
const value = parseValue(d[this.dataKey]);
|
|
126
|
-
const valueText =
|
|
126
|
+
const valueText = config.formatter
|
|
127
|
+
? config.formatter(this.dataKey, value, d)
|
|
128
|
+
: String(value);
|
|
127
129
|
const xPos = getXPosition(d);
|
|
128
130
|
const yPos = y(value) || 0;
|
|
129
131
|
// Create temporary text to measure dimensions
|
package/dist/pie-chart.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ export declare class PieChart extends RadialChartBase {
|
|
|
46
46
|
private segments;
|
|
47
47
|
constructor(config: PieChartConfig);
|
|
48
48
|
private validatePieData;
|
|
49
|
+
private validatePieConfig;
|
|
50
|
+
private validateValueLabelConfig;
|
|
51
|
+
private validateDataItems;
|
|
49
52
|
private prepareSegments;
|
|
50
53
|
private warnOnTinySlices;
|
|
51
54
|
protected getExportComponents(): ChartComponentBase[];
|
package/dist/pie-chart.js
CHANGED
|
@@ -8,6 +8,17 @@ const FULL_CIRCLE_RADIANS = Math.PI * 2;
|
|
|
8
8
|
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
9
9
|
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
10
10
|
const TINY_SLICE_THRESHOLD_RATIO = 0.03;
|
|
11
|
+
const DEFAULT_PIE_VALUE_LABEL = {
|
|
12
|
+
show: false,
|
|
13
|
+
position: 'auto',
|
|
14
|
+
minInsidePercentage: 8,
|
|
15
|
+
outsideOffset: 16,
|
|
16
|
+
insideMargin: 8,
|
|
17
|
+
minVerticalSpacing: 14,
|
|
18
|
+
formatter: (label, value, _data, _percentage) => {
|
|
19
|
+
return `${label}: ${value}`;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
11
22
|
export class PieChart extends RadialChartBase {
|
|
12
23
|
constructor(config) {
|
|
13
24
|
super(config);
|
|
@@ -71,25 +82,31 @@ export class PieChart extends RadialChartBase {
|
|
|
71
82
|
writable: true,
|
|
72
83
|
value: []
|
|
73
84
|
});
|
|
74
|
-
const pieConfig =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const pieConfig = {
|
|
86
|
+
innerRadius: 0,
|
|
87
|
+
startAngle: 0,
|
|
88
|
+
endAngle: FULL_CIRCLE_RADIANS,
|
|
89
|
+
padAngle: this.theme.donut.padAngle,
|
|
90
|
+
cornerRadius: this.theme.donut.cornerRadius,
|
|
91
|
+
sort: 'none',
|
|
92
|
+
...config.pie,
|
|
93
|
+
};
|
|
94
|
+
const resolvedConfig = {
|
|
95
|
+
valueKey: 'value',
|
|
96
|
+
labelKey: 'name',
|
|
97
|
+
...config,
|
|
98
|
+
};
|
|
99
|
+
this.innerRadiusRatio = pieConfig.innerRadius;
|
|
100
|
+
this.startAngle = pieConfig.startAngle;
|
|
101
|
+
this.endAngle = pieConfig.endAngle;
|
|
102
|
+
this.padAngle = pieConfig.padAngle;
|
|
103
|
+
this.cornerRadius = pieConfig.cornerRadius;
|
|
104
|
+
this.sort = pieConfig.sort;
|
|
105
|
+
this.valueKey = resolvedConfig.valueKey;
|
|
106
|
+
this.labelKey = resolvedConfig.labelKey;
|
|
84
107
|
this.valueLabel = {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
minInsidePercentage: config.valueLabel?.minInsidePercentage ?? 8,
|
|
88
|
-
outsideOffset: config.valueLabel?.outsideOffset ?? 16,
|
|
89
|
-
insideMargin: config.valueLabel?.insideMargin ?? 8,
|
|
90
|
-
minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
|
|
91
|
-
formatter: config.valueLabel?.formatter ??
|
|
92
|
-
((label, value, _data, _percentage) => `${label}: ${value}`),
|
|
108
|
+
...DEFAULT_PIE_VALUE_LABEL,
|
|
109
|
+
...config.valueLabel,
|
|
93
110
|
};
|
|
94
111
|
this.initializeDataState();
|
|
95
112
|
}
|
|
@@ -97,6 +114,11 @@ export class PieChart extends RadialChartBase {
|
|
|
97
114
|
ChartValidator.validateDataKey(this.data, this.labelKey, 'PieChart');
|
|
98
115
|
ChartValidator.validateDataKey(this.data, this.valueKey, 'PieChart');
|
|
99
116
|
ChartValidator.validateNumericData(this.data, this.valueKey, 'PieChart');
|
|
117
|
+
this.validatePieConfig();
|
|
118
|
+
this.validateValueLabelConfig();
|
|
119
|
+
this.validateDataItems();
|
|
120
|
+
}
|
|
121
|
+
validatePieConfig() {
|
|
100
122
|
if (this.innerRadiusRatio < 0 || this.innerRadiusRatio > 1) {
|
|
101
123
|
throw new Error(`PieChart: pie.innerRadius must be between 0 and 1, received '${this.innerRadiusRatio}'`);
|
|
102
124
|
}
|
|
@@ -106,6 +128,8 @@ export class PieChart extends RadialChartBase {
|
|
|
106
128
|
if (this.padAngle < 0) {
|
|
107
129
|
throw new Error(`PieChart: pie.padAngle must be >= 0, received '${this.padAngle}'`);
|
|
108
130
|
}
|
|
131
|
+
}
|
|
132
|
+
validateValueLabelConfig() {
|
|
109
133
|
if (this.valueLabel.minInsidePercentage < 0 ||
|
|
110
134
|
this.valueLabel.minInsidePercentage > 100) {
|
|
111
135
|
throw new Error(`PieChart: valueLabel.minInsidePercentage must be between 0 and 100, received '${this.valueLabel.minInsidePercentage}'`);
|
|
@@ -119,6 +143,8 @@ export class PieChart extends RadialChartBase {
|
|
|
119
143
|
if (this.valueLabel.minVerticalSpacing < 0) {
|
|
120
144
|
throw new Error(`PieChart: valueLabel.minVerticalSpacing must be >= 0, received '${this.valueLabel.minVerticalSpacing}'`);
|
|
121
145
|
}
|
|
146
|
+
}
|
|
147
|
+
validateDataItems() {
|
|
122
148
|
for (const [index, item] of this.data.entries()) {
|
|
123
149
|
const label = String(item[this.labelKey] ?? '').trim();
|
|
124
150
|
if (!label) {
|
|
@@ -173,7 +199,7 @@ export class PieChart extends RadialChartBase {
|
|
|
173
199
|
return this.getBaseExportComponents({
|
|
174
200
|
title: true,
|
|
175
201
|
tooltip: true,
|
|
176
|
-
legend: this.
|
|
202
|
+
legend: this.shouldIncludeLegendInExport(),
|
|
177
203
|
});
|
|
178
204
|
}
|
|
179
205
|
update(data) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChartTheme, D3Scale, DataItem, ExportHooks, LineValueLabelConfig, ScaleType, ScatterConfig, ScatterConfigBase } from './types.js';
|
|
2
|
+
import type { ChartComponent } from './chart-interface.js';
|
|
3
|
+
import type { Selection } from 'd3';
|
|
4
|
+
export declare class Scatter implements ChartComponent<ScatterConfigBase> {
|
|
5
|
+
readonly type: "scatter";
|
|
6
|
+
readonly dataKey: string;
|
|
7
|
+
readonly stroke: string;
|
|
8
|
+
readonly pointSize?: number;
|
|
9
|
+
readonly valueLabel?: LineValueLabelConfig;
|
|
10
|
+
readonly exportHooks?: ExportHooks<ScatterConfigBase>;
|
|
11
|
+
constructor(config: ScatterConfig);
|
|
12
|
+
getExportConfig(): ScatterConfigBase;
|
|
13
|
+
createExportComponent(override?: Partial<ScatterConfigBase>): ChartComponent<ScatterConfigBase>;
|
|
14
|
+
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
|
|
15
|
+
private renderValueLabels;
|
|
16
|
+
}
|
package/dist/scatter.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mergeDeep, sanitizeForCSS } from './utils.js';
|
|
2
|
+
import { getScalePosition } from './scale-utils.js';
|
|
3
|
+
export class Scatter {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
Object.defineProperty(this, "type", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: 'scatter'
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "dataKey", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "stroke", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "pointSize", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "valueLabel", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "exportHooks", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
41
|
+
this.dataKey = config.dataKey;
|
|
42
|
+
this.stroke = config.stroke || '#8884d8';
|
|
43
|
+
this.pointSize = config.pointSize;
|
|
44
|
+
this.valueLabel = config.valueLabel;
|
|
45
|
+
this.exportHooks = config.exportHooks;
|
|
46
|
+
}
|
|
47
|
+
getExportConfig() {
|
|
48
|
+
return {
|
|
49
|
+
dataKey: this.dataKey,
|
|
50
|
+
stroke: this.stroke,
|
|
51
|
+
pointSize: this.pointSize,
|
|
52
|
+
valueLabel: this.valueLabel,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
createExportComponent(override) {
|
|
56
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
57
|
+
return new Scatter({
|
|
58
|
+
...merged,
|
|
59
|
+
exportHooks: this.exportHooks,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
|
|
63
|
+
const getXPosition = (d) => {
|
|
64
|
+
return (getScalePosition(x, d[xKey], xScaleType) +
|
|
65
|
+
(x.bandwidth ? x.bandwidth() / 2 : 0));
|
|
66
|
+
};
|
|
67
|
+
const hasValidValue = (d) => {
|
|
68
|
+
const value = d[this.dataKey];
|
|
69
|
+
if (value === null || value === undefined) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return Number.isFinite(parseValue(value));
|
|
73
|
+
};
|
|
74
|
+
const validData = data.filter(hasValidValue);
|
|
75
|
+
const pointSize = this.pointSize ?? theme.line.point.size;
|
|
76
|
+
const pointStrokeWidth = theme.line.point.strokeWidth;
|
|
77
|
+
const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
|
|
78
|
+
const pointColor = theme.line.point.color || this.stroke;
|
|
79
|
+
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
80
|
+
plotGroup
|
|
81
|
+
.selectAll(`.scatter-point-${sanitizedKey}`)
|
|
82
|
+
.data(validData)
|
|
83
|
+
.join('circle')
|
|
84
|
+
.attr('class', `scatter-point-${sanitizedKey}`)
|
|
85
|
+
.attr('cx', getXPosition)
|
|
86
|
+
.attr('cy', (d) => y(parseValue(d[this.dataKey])) || 0)
|
|
87
|
+
.attr('r', pointSize)
|
|
88
|
+
.attr('fill', pointColor)
|
|
89
|
+
.attr('stroke', pointStrokeColor)
|
|
90
|
+
.attr('stroke-width', pointStrokeWidth);
|
|
91
|
+
if (this.valueLabel?.show) {
|
|
92
|
+
this.renderValueLabels(plotGroup, validData, y, parseValue, theme, getXPosition, pointSize);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition, pointSize) {
|
|
96
|
+
const config = this.valueLabel;
|
|
97
|
+
const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
|
|
98
|
+
const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
|
|
99
|
+
const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
|
|
100
|
+
const color = config.color ?? theme.valueLabel.color;
|
|
101
|
+
const background = config.background ?? theme.valueLabel.background;
|
|
102
|
+
const border = config.border ?? theme.valueLabel.border;
|
|
103
|
+
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
104
|
+
const padding = config.padding ?? theme.valueLabel.padding;
|
|
105
|
+
const labelGroup = plotGroup
|
|
106
|
+
.append('g')
|
|
107
|
+
.attr('class', `scatter-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
108
|
+
const plotTop = y.range()[1];
|
|
109
|
+
const plotBottom = y.range()[0];
|
|
110
|
+
data.forEach((d) => {
|
|
111
|
+
const value = parseValue(d[this.dataKey]);
|
|
112
|
+
const valueText = config.formatter
|
|
113
|
+
? config.formatter(this.dataKey, value, d)
|
|
114
|
+
: String(value);
|
|
115
|
+
const xPos = getXPosition(d);
|
|
116
|
+
const yPos = y(value) || 0;
|
|
117
|
+
const tempText = labelGroup
|
|
118
|
+
.append('text')
|
|
119
|
+
.style('font-size', `${fontSize}px`)
|
|
120
|
+
.style('font-family', fontFamily)
|
|
121
|
+
.style('font-weight', fontWeight)
|
|
122
|
+
.text(valueText);
|
|
123
|
+
const textBBox = tempText.node().getBBox();
|
|
124
|
+
const boxWidth = textBBox.width + padding * 2;
|
|
125
|
+
const boxHeight = textBBox.height + padding * 2;
|
|
126
|
+
const labelX = xPos;
|
|
127
|
+
let labelY;
|
|
128
|
+
let shouldRender = true;
|
|
129
|
+
labelY = yPos - boxHeight / 2 - pointSize - 4;
|
|
130
|
+
if (labelY - boxHeight / 2 < plotTop + 4) {
|
|
131
|
+
labelY = yPos + boxHeight / 2 + pointSize + 4;
|
|
132
|
+
if (labelY + boxHeight / 2 > plotBottom - 4) {
|
|
133
|
+
shouldRender = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
tempText.remove();
|
|
137
|
+
if (shouldRender) {
|
|
138
|
+
const group = labelGroup.append('g');
|
|
139
|
+
group
|
|
140
|
+
.append('rect')
|
|
141
|
+
.attr('x', labelX - boxWidth / 2)
|
|
142
|
+
.attr('y', labelY - boxHeight / 2)
|
|
143
|
+
.attr('width', boxWidth)
|
|
144
|
+
.attr('height', boxHeight)
|
|
145
|
+
.attr('rx', borderRadius)
|
|
146
|
+
.attr('ry', borderRadius)
|
|
147
|
+
.attr('fill', background)
|
|
148
|
+
.attr('stroke', border)
|
|
149
|
+
.attr('stroke-width', 1);
|
|
150
|
+
group
|
|
151
|
+
.append('text')
|
|
152
|
+
.attr('x', labelX)
|
|
153
|
+
.attr('y', labelY)
|
|
154
|
+
.attr('text-anchor', 'middle')
|
|
155
|
+
.attr('dominant-baseline', 'central')
|
|
156
|
+
.style('font-size', `${fontSize}px`)
|
|
157
|
+
.style('font-family', fontFamily)
|
|
158
|
+
.style('font-weight', fontWeight)
|
|
159
|
+
.style('fill', color)
|
|
160
|
+
.style('pointer-events', 'none')
|
|
161
|
+
.text(valueText);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ChartComponent } from './chart-interface.js';
|
|
|
4
4
|
import type { Line } from './line.js';
|
|
5
5
|
import type { Bar } from './bar.js';
|
|
6
6
|
import type { Area } from './area.js';
|
|
7
|
+
import type { Scatter } from './scatter.js';
|
|
7
8
|
import type { PlotAreaBounds } from './layout-manager.js';
|
|
8
9
|
export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
9
10
|
readonly id = "iisChartTooltip";
|
|
@@ -21,6 +22,6 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
21
22
|
getExportConfig(): TooltipConfigBase;
|
|
22
23
|
createExportComponent(override?: Partial<TooltipConfigBase>): ChartComponent<TooltipConfigBase>;
|
|
23
24
|
initialize(theme: ChartTheme): void;
|
|
24
|
-
attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar | Area)[], xKey: string, x: D3Scale, y: D3Scale, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: unknown) => number, isHorizontal?: boolean, categoryScaleType?: ScaleType, resolveSeriesValue?: (series: Line | Bar | Area, dataPoint: DataItem, index: number) => number): void;
|
|
25
|
+
attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar | Area | Scatter)[], xKey: string, x: D3Scale, y: D3Scale, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: unknown) => number, isHorizontal?: boolean, categoryScaleType?: ScaleType, resolveSeriesValue?: (series: Line | Bar | Area | Scatter, dataPoint: DataItem, index: number) => number): void;
|
|
25
26
|
cleanup(): void;
|
|
26
27
|
}
|
package/dist/tooltip.js
CHANGED
|
@@ -214,10 +214,10 @@ export class Tooltip {
|
|
|
214
214
|
.attr('aria-hidden', 'true')
|
|
215
215
|
.style('fill', 'none')
|
|
216
216
|
.style('pointer-events', 'all');
|
|
217
|
-
const
|
|
217
|
+
const pointSeries = series.filter((s) => s.type === 'line' || s.type === 'area' || s.type === 'scatter');
|
|
218
218
|
const barSeries = series.filter((s) => s.type === 'bar');
|
|
219
219
|
const hasBarSeries = barSeries.length > 0;
|
|
220
|
-
const focusCircles =
|
|
220
|
+
const focusCircles = pointSeries.map((s) => {
|
|
221
221
|
const seriesColor = getSeriesColor(s);
|
|
222
222
|
return svg
|
|
223
223
|
.append('circle')
|
|
@@ -243,7 +243,7 @@ export class Tooltip {
|
|
|
243
243
|
const showTooltipAtIndex = (closestIndex) => {
|
|
244
244
|
const dataPoint = data[closestIndex];
|
|
245
245
|
const dataPointPosition = dataPointPositions[closestIndex];
|
|
246
|
-
|
|
246
|
+
pointSeries.forEach((s, i) => {
|
|
247
247
|
const value = resolveSeriesValue(s, dataPoint, closestIndex);
|
|
248
248
|
if (!Number.isFinite(value)) {
|
|
249
249
|
focusCircles[i].style('opacity', 0);
|
|
@@ -420,6 +420,22 @@ export class Tooltip {
|
|
|
420
420
|
.attr('aria-label', (dataPoint) => buildAccessibleLabel(dataPoint))
|
|
421
421
|
.style('pointer-events', 'none');
|
|
422
422
|
const focusTargetNodes = focusTargets.nodes();
|
|
423
|
+
const getNextFocusTargetIndex = (currentIndex, key) => {
|
|
424
|
+
switch (key) {
|
|
425
|
+
case 'ArrowRight':
|
|
426
|
+
case 'ArrowDown':
|
|
427
|
+
return currentIndex + 1;
|
|
428
|
+
case 'ArrowLeft':
|
|
429
|
+
case 'ArrowUp':
|
|
430
|
+
return currentIndex - 1;
|
|
431
|
+
case 'Home':
|
|
432
|
+
return 0;
|
|
433
|
+
case 'End':
|
|
434
|
+
return focusTargetNodes.length - 1;
|
|
435
|
+
default:
|
|
436
|
+
return -1;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
423
439
|
focusTargets
|
|
424
440
|
.on('focus', function () {
|
|
425
441
|
const currentIndex = focusTargetNodes.indexOf(this);
|
|
@@ -441,28 +457,8 @@ export class Tooltip {
|
|
|
441
457
|
if (currentIndex === -1) {
|
|
442
458
|
return;
|
|
443
459
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
case 'ArrowRight':
|
|
447
|
-
case 'ArrowDown':
|
|
448
|
-
nextIndex = currentIndex + 1;
|
|
449
|
-
break;
|
|
450
|
-
case 'ArrowLeft':
|
|
451
|
-
case 'ArrowUp':
|
|
452
|
-
nextIndex = currentIndex - 1;
|
|
453
|
-
break;
|
|
454
|
-
case 'Home':
|
|
455
|
-
nextIndex = 0;
|
|
456
|
-
break;
|
|
457
|
-
case 'End':
|
|
458
|
-
nextIndex = focusTargetNodes.length - 1;
|
|
459
|
-
break;
|
|
460
|
-
default:
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (nextIndex === null ||
|
|
464
|
-
nextIndex < 0 ||
|
|
465
|
-
nextIndex >= focusTargetNodes.length) {
|
|
460
|
+
const nextIndex = getNextFocusTargetIndex(currentIndex, event.key);
|
|
461
|
+
if (nextIndex < 0 || nextIndex >= focusTargetNodes.length) {
|
|
466
462
|
return;
|
|
467
463
|
}
|
|
468
464
|
event.preventDefault();
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type DeepPartial<T> = {
|
|
|
3
3
|
};
|
|
4
4
|
export type DataValue = string | number | boolean | Date | null | undefined;
|
|
5
5
|
export type DataItem = Record<string, unknown>;
|
|
6
|
+
export type SeriesValueFormatter = (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
6
7
|
export type GroupedDataGroup = {
|
|
7
8
|
group: string;
|
|
8
9
|
data: DataItem[];
|
|
@@ -167,6 +168,7 @@ export type ValueLabelConfig = {
|
|
|
167
168
|
border?: string;
|
|
168
169
|
borderRadius?: number;
|
|
169
170
|
padding?: number;
|
|
171
|
+
formatter?: SeriesValueFormatter;
|
|
170
172
|
};
|
|
171
173
|
export type LineValueLabelConfig = ValueLabelConfig & {
|
|
172
174
|
show?: boolean;
|
|
@@ -176,6 +178,7 @@ export type BarValueLabelConfig = ValueLabelConfig & {
|
|
|
176
178
|
position?: 'inside' | 'outside';
|
|
177
179
|
insidePosition?: 'top' | 'middle' | 'bottom';
|
|
178
180
|
};
|
|
181
|
+
export type BarSide = 'left' | 'right';
|
|
179
182
|
export type LineConfigBase = {
|
|
180
183
|
dataKey: string;
|
|
181
184
|
stroke?: string;
|
|
@@ -185,11 +188,21 @@ export type LineConfigBase = {
|
|
|
185
188
|
export type LineConfig = LineConfigBase & {
|
|
186
189
|
exportHooks?: ExportHooks<LineConfigBase>;
|
|
187
190
|
};
|
|
191
|
+
export type ScatterConfigBase = {
|
|
192
|
+
dataKey: string;
|
|
193
|
+
stroke?: string;
|
|
194
|
+
pointSize?: number;
|
|
195
|
+
valueLabel?: LineValueLabelConfig;
|
|
196
|
+
};
|
|
197
|
+
export type ScatterConfig = ScatterConfigBase & {
|
|
198
|
+
exportHooks?: ExportHooks<ScatterConfigBase>;
|
|
199
|
+
};
|
|
188
200
|
export type BarConfigBase = {
|
|
189
201
|
dataKey: string;
|
|
190
202
|
fill?: string;
|
|
191
203
|
colorAdapter?: (data: DataItem, index: number) => string;
|
|
192
204
|
maxBarSize?: number;
|
|
205
|
+
side?: BarSide;
|
|
193
206
|
valueLabel?: BarValueLabelConfig;
|
|
194
207
|
};
|
|
195
208
|
export type BarConfig = BarConfigBase & {
|
|
@@ -265,7 +278,7 @@ export type GridConfig = GridConfigBase & {
|
|
|
265
278
|
exportHooks?: ExportHooks<GridConfigBase>;
|
|
266
279
|
};
|
|
267
280
|
export type TooltipConfigBase = {
|
|
268
|
-
formatter?:
|
|
281
|
+
formatter?: SeriesValueFormatter;
|
|
269
282
|
labelFormatter?: (label: string, data: DataItem) => string;
|
|
270
283
|
customFormatter?: (data: DataItem, series: {
|
|
271
284
|
dataKey: string;
|
|
@@ -328,6 +341,7 @@ export type ScaleConfig = {
|
|
|
328
341
|
range?: number[];
|
|
329
342
|
padding?: number;
|
|
330
343
|
groupGap?: number;
|
|
344
|
+
reverse?: boolean;
|
|
331
345
|
nice?: boolean;
|
|
332
346
|
min?: number;
|
|
333
347
|
max?: number;
|
|
@@ -344,6 +358,10 @@ export type BarStackingContext = {
|
|
|
344
358
|
totalSeries: number;
|
|
345
359
|
cumulativeData: Map<string, number>;
|
|
346
360
|
totalData: Map<string, number>;
|
|
361
|
+
positiveCumulativeData: Map<string, number>;
|
|
362
|
+
negativeCumulativeData: Map<string, number>;
|
|
363
|
+
positiveTotalData: Map<string, number>;
|
|
364
|
+
negativeTotalData: Map<string, number>;
|
|
347
365
|
gap: number;
|
|
348
366
|
nextLayerData?: Map<string, number>;
|
|
349
367
|
};
|