@internetstiftelsen/charts 0.9.2 → 0.10.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 +136 -2
- package/dist/bar.d.ts +3 -1
- package/dist/bar.js +167 -327
- package/dist/base-chart.d.ts +16 -1
- package/dist/base-chart.js +89 -30
- package/dist/chart-group.d.ts +121 -0
- package/dist/chart-group.js +1097 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-chart.js +1 -1
- package/dist/gauge-chart.js +1 -1
- 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 +35 -29
- package/dist/pie-chart.js +1 -1
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +163 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +3 -3
- package/dist/types.d.ts +16 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/xy-chart.d.ts +16 -1
- package/dist/xy-chart.js +317 -102
- package/docs/chart-group.md +213 -0
- package/docs/components.md +308 -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 +502 -0
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExportHooks } from './types.js';
|
|
2
|
-
export type ChartComponentType = 'line' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
2
|
+
export type ChartComponentType = 'line' | 'scatter' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
3
3
|
export interface ChartComponentBase {
|
|
4
4
|
type: ChartComponentType;
|
|
5
5
|
}
|
package/dist/donut-chart.js
CHANGED
|
@@ -122,7 +122,7 @@ export class DonutChart extends RadialChartBase {
|
|
|
122
122
|
...(this.centerContent ? [this.centerContent] : []),
|
|
123
123
|
...this.getBaseExportComponents({
|
|
124
124
|
tooltip: true,
|
|
125
|
-
legend: this.
|
|
125
|
+
legend: this.shouldIncludeLegendInExport(),
|
|
126
126
|
}),
|
|
127
127
|
];
|
|
128
128
|
}
|
package/dist/gauge-chart.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type LegendVisibilityMap = Record<string, boolean>;
|
|
2
|
+
type LegendStateMutationOptions = {
|
|
3
|
+
silent?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare class LegendStateController {
|
|
6
|
+
private visibilityState;
|
|
7
|
+
private readonly changeCallbacks;
|
|
8
|
+
clone(): LegendStateController;
|
|
9
|
+
hasSeries(dataKey: string): boolean;
|
|
10
|
+
isSeriesVisible(dataKey: string): boolean;
|
|
11
|
+
ensureSeries(dataKeys: Iterable<string>, options?: LegendStateMutationOptions): void;
|
|
12
|
+
setSeriesVisible(dataKey: string, visible: boolean, options?: LegendStateMutationOptions): void;
|
|
13
|
+
toggleSeries(dataKey: string, options?: LegendStateMutationOptions): void;
|
|
14
|
+
setVisibilityMap(visibility: LegendVisibilityMap, options?: LegendStateMutationOptions): void;
|
|
15
|
+
subscribe(callback: () => void): () => void;
|
|
16
|
+
toVisibilityMap(): LegendVisibilityMap;
|
|
17
|
+
private triggerChange;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class LegendStateController {
|
|
2
|
+
constructor() {
|
|
3
|
+
Object.defineProperty(this, "visibilityState", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: new Map()
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "changeCallbacks", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: new Set()
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
clone() {
|
|
17
|
+
const controller = new LegendStateController();
|
|
18
|
+
controller.visibilityState = new Map(this.visibilityState);
|
|
19
|
+
return controller;
|
|
20
|
+
}
|
|
21
|
+
hasSeries(dataKey) {
|
|
22
|
+
return this.visibilityState.has(dataKey);
|
|
23
|
+
}
|
|
24
|
+
isSeriesVisible(dataKey) {
|
|
25
|
+
return this.visibilityState.get(dataKey) ?? true;
|
|
26
|
+
}
|
|
27
|
+
ensureSeries(dataKeys, options) {
|
|
28
|
+
let changed = false;
|
|
29
|
+
for (const dataKey of dataKeys) {
|
|
30
|
+
if (this.visibilityState.has(dataKey)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
this.visibilityState.set(dataKey, true);
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
if (changed && !options?.silent) {
|
|
37
|
+
this.triggerChange();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setSeriesVisible(dataKey, visible, options) {
|
|
41
|
+
const currentValue = this.visibilityState.get(dataKey);
|
|
42
|
+
if (currentValue === visible) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.visibilityState.set(dataKey, visible);
|
|
46
|
+
if (!options?.silent) {
|
|
47
|
+
this.triggerChange();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
toggleSeries(dataKey, options) {
|
|
51
|
+
this.setSeriesVisible(dataKey, !this.isSeriesVisible(dataKey), options);
|
|
52
|
+
}
|
|
53
|
+
setVisibilityMap(visibility, options) {
|
|
54
|
+
let changed = false;
|
|
55
|
+
Object.entries(visibility).forEach(([dataKey, visible]) => {
|
|
56
|
+
const currentValue = this.visibilityState.get(dataKey);
|
|
57
|
+
if (currentValue === visible) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.visibilityState.set(dataKey, visible);
|
|
61
|
+
changed = true;
|
|
62
|
+
});
|
|
63
|
+
if (changed && !options?.silent) {
|
|
64
|
+
this.triggerChange();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
subscribe(callback) {
|
|
68
|
+
this.changeCallbacks.add(callback);
|
|
69
|
+
return () => {
|
|
70
|
+
this.changeCallbacks.delete(callback);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
toVisibilityMap() {
|
|
74
|
+
return Object.fromEntries(this.visibilityState.entries());
|
|
75
|
+
}
|
|
76
|
+
triggerChange() {
|
|
77
|
+
this.changeCallbacks.forEach((callback) => {
|
|
78
|
+
callback();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/legend.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { LegendConfig, ChartTheme, LegendSeries, ExportHooks, LegendConfigBase, LegendMode } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
+
import { LegendStateController } from './legend-state.js';
|
|
4
5
|
export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
5
6
|
readonly type: "legend";
|
|
6
7
|
mode: LegendMode;
|
|
@@ -13,7 +14,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
13
14
|
private readonly itemSpacingX?;
|
|
14
15
|
private readonly itemSpacingY?;
|
|
15
16
|
private readonly gapBetweenBoxAndText;
|
|
16
|
-
private
|
|
17
|
+
private stateController;
|
|
18
|
+
private stateControllerCleanup;
|
|
17
19
|
private onToggleCallback?;
|
|
18
20
|
private onChangeCallbacks;
|
|
19
21
|
private estimatedLayout;
|
|
@@ -22,6 +24,7 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
22
24
|
getExportConfig(): LegendConfigBase;
|
|
23
25
|
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent<LegendConfigBase>;
|
|
24
26
|
setToggleCallback(callback: () => void): void;
|
|
27
|
+
setStateController(controller: LegendStateController): void;
|
|
25
28
|
isInlineMode(): boolean;
|
|
26
29
|
isDisconnectedMode(): boolean;
|
|
27
30
|
isHiddenMode(): boolean;
|
|
@@ -50,5 +53,5 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
50
53
|
private positionRows;
|
|
51
54
|
private getLayoutSignature;
|
|
52
55
|
private getFallbackRowHeight;
|
|
53
|
-
private
|
|
56
|
+
private bindStateController;
|
|
54
57
|
}
|
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,
|
|
@@ -108,6 +115,8 @@ export class Legend {
|
|
|
108
115
|
this.itemSpacingX = config?.itemSpacingX;
|
|
109
116
|
this.itemSpacingY = config?.itemSpacingY;
|
|
110
117
|
this.exportHooks = config?.exportHooks;
|
|
118
|
+
this.stateController = new LegendStateController();
|
|
119
|
+
this.bindStateController(this.stateController);
|
|
111
120
|
}
|
|
112
121
|
getExportConfig() {
|
|
113
122
|
return {
|
|
@@ -127,16 +136,19 @@ export class Legend {
|
|
|
127
136
|
...merged,
|
|
128
137
|
exportHooks: this.exportHooks,
|
|
129
138
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
const clonedStateController = this.stateController.clone();
|
|
140
|
+
clonedStateController.subscribe(() => {
|
|
141
|
+
this.stateController.setVisibilityMap(clonedStateController.toVisibilityMap());
|
|
142
|
+
});
|
|
143
|
+
legend.setStateController(clonedStateController);
|
|
135
144
|
return legend;
|
|
136
145
|
}
|
|
137
146
|
setToggleCallback(callback) {
|
|
138
147
|
this.onToggleCallback = callback;
|
|
139
148
|
}
|
|
149
|
+
setStateController(controller) {
|
|
150
|
+
this.bindStateController(controller);
|
|
151
|
+
}
|
|
140
152
|
isInlineMode() {
|
|
141
153
|
return this.mode === 'inline';
|
|
142
154
|
}
|
|
@@ -159,22 +171,16 @@ export class Legend {
|
|
|
159
171
|
};
|
|
160
172
|
}
|
|
161
173
|
isSeriesVisible(dataKey) {
|
|
162
|
-
return this.
|
|
174
|
+
return this.stateController.isSeriesVisible(dataKey);
|
|
163
175
|
}
|
|
164
176
|
setSeriesVisible(dataKey, visible) {
|
|
165
|
-
this.
|
|
166
|
-
this.triggerChange();
|
|
177
|
+
this.stateController.setSeriesVisible(dataKey, visible);
|
|
167
178
|
}
|
|
168
179
|
toggleSeries(dataKey) {
|
|
169
|
-
|
|
170
|
-
this.visibilityState.set(dataKey, !currentState);
|
|
171
|
-
this.triggerChange();
|
|
180
|
+
this.stateController.toggleSeries(dataKey);
|
|
172
181
|
}
|
|
173
182
|
setVisibilityMap(visibility) {
|
|
174
|
-
|
|
175
|
-
this.visibilityState.set(dataKey, isVisible);
|
|
176
|
-
});
|
|
177
|
-
this.triggerChange();
|
|
183
|
+
this.stateController.setVisibilityMap(visibility);
|
|
178
184
|
}
|
|
179
185
|
estimateLayoutSpace(series, theme, width, svg) {
|
|
180
186
|
const signature = this.getLayoutSignature(series, width, theme);
|
|
@@ -269,7 +275,7 @@ export class Legend {
|
|
|
269
275
|
.attr('width', theme.legend.boxSize)
|
|
270
276
|
.attr('height', theme.legend.boxSize)
|
|
271
277
|
.attr('fill', (d) => {
|
|
272
|
-
const isVisible = this.
|
|
278
|
+
const isVisible = this.stateController.isSeriesVisible(d.dataKey);
|
|
273
279
|
return isVisible ? d.color : theme.legend.uncheckedColor;
|
|
274
280
|
})
|
|
275
281
|
.attr('rx', 3);
|
|
@@ -283,7 +289,7 @@ export class Legend {
|
|
|
283
289
|
.attr('stroke-linecap', 'round')
|
|
284
290
|
.attr('stroke-linejoin', 'round')
|
|
285
291
|
.style('display', (d) => {
|
|
286
|
-
const isVisible = this.
|
|
292
|
+
const isVisible = this.stateController.isSeriesVisible(d.dataKey);
|
|
287
293
|
return isVisible ? 'block' : 'none';
|
|
288
294
|
});
|
|
289
295
|
// Add label text
|
|
@@ -296,7 +302,7 @@ export class Legend {
|
|
|
296
302
|
.text((d) => d.label);
|
|
297
303
|
}
|
|
298
304
|
isLegendItemVisible(dataKey) {
|
|
299
|
-
return this.
|
|
305
|
+
return this.stateController.isSeriesVisible(dataKey);
|
|
300
306
|
}
|
|
301
307
|
isToggleActivationKey(key) {
|
|
302
308
|
return key === 'Enter' || key === ' ' || key === 'Spacebar';
|
|
@@ -304,11 +310,7 @@ export class Legend {
|
|
|
304
310
|
computeLayout(series, theme, width, svg) {
|
|
305
311
|
const settings = this.resolveLayoutSettings(theme);
|
|
306
312
|
const legendItems = this.buildLegendItems(series);
|
|
307
|
-
legendItems.
|
|
308
|
-
if (!this.visibilityState.has(item.dataKey)) {
|
|
309
|
-
this.visibilityState.set(item.dataKey, true);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
313
|
+
this.stateController.ensureSeries(legendItems.map((item) => item.dataKey), { silent: true });
|
|
312
314
|
const measuredItems = this.measureLegendItemWidths(legendItems, theme, svg);
|
|
313
315
|
const rows = this.buildRows(measuredItems, width, settings);
|
|
314
316
|
const positionedItems = this.positionRows(rows, width, settings);
|
|
@@ -439,10 +441,14 @@ export class Legend {
|
|
|
439
441
|
getFallbackRowHeight(theme) {
|
|
440
442
|
return Math.max(theme.legend.boxSize, theme.legend.fontSize);
|
|
441
443
|
}
|
|
442
|
-
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
445
|
-
|
|
444
|
+
bindStateController(controller) {
|
|
445
|
+
this.stateControllerCleanup?.();
|
|
446
|
+
this.stateController = controller;
|
|
447
|
+
this.stateControllerCleanup = controller.subscribe(() => {
|
|
448
|
+
this.onToggleCallback?.();
|
|
449
|
+
this.onChangeCallbacks.forEach((callback) => {
|
|
450
|
+
callback();
|
|
451
|
+
});
|
|
446
452
|
});
|
|
447
453
|
}
|
|
448
454
|
}
|
package/dist/pie-chart.js
CHANGED
|
@@ -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,163 @@
|
|
|
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 = String(value);
|
|
113
|
+
const xPos = getXPosition(d);
|
|
114
|
+
const yPos = y(value) || 0;
|
|
115
|
+
const tempText = labelGroup
|
|
116
|
+
.append('text')
|
|
117
|
+
.style('font-size', `${fontSize}px`)
|
|
118
|
+
.style('font-family', fontFamily)
|
|
119
|
+
.style('font-weight', fontWeight)
|
|
120
|
+
.text(valueText);
|
|
121
|
+
const textBBox = tempText.node().getBBox();
|
|
122
|
+
const boxWidth = textBBox.width + padding * 2;
|
|
123
|
+
const boxHeight = textBBox.height + padding * 2;
|
|
124
|
+
const labelX = xPos;
|
|
125
|
+
let labelY;
|
|
126
|
+
let shouldRender = true;
|
|
127
|
+
labelY = yPos - boxHeight / 2 - pointSize - 4;
|
|
128
|
+
if (labelY - boxHeight / 2 < plotTop + 4) {
|
|
129
|
+
labelY = yPos + boxHeight / 2 + pointSize + 4;
|
|
130
|
+
if (labelY + boxHeight / 2 > plotBottom - 4) {
|
|
131
|
+
shouldRender = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
tempText.remove();
|
|
135
|
+
if (shouldRender) {
|
|
136
|
+
const group = labelGroup.append('g');
|
|
137
|
+
group
|
|
138
|
+
.append('rect')
|
|
139
|
+
.attr('x', labelX - boxWidth / 2)
|
|
140
|
+
.attr('y', labelY - boxHeight / 2)
|
|
141
|
+
.attr('width', boxWidth)
|
|
142
|
+
.attr('height', boxHeight)
|
|
143
|
+
.attr('rx', borderRadius)
|
|
144
|
+
.attr('ry', borderRadius)
|
|
145
|
+
.attr('fill', background)
|
|
146
|
+
.attr('stroke', border)
|
|
147
|
+
.attr('stroke-width', 1);
|
|
148
|
+
group
|
|
149
|
+
.append('text')
|
|
150
|
+
.attr('x', labelX)
|
|
151
|
+
.attr('y', labelY)
|
|
152
|
+
.attr('text-anchor', 'middle')
|
|
153
|
+
.attr('dominant-baseline', 'central')
|
|
154
|
+
.style('font-size', `${fontSize}px`)
|
|
155
|
+
.style('font-family', fontFamily)
|
|
156
|
+
.style('font-weight', fontWeight)
|
|
157
|
+
.style('fill', color)
|
|
158
|
+
.style('pointer-events', 'none')
|
|
159
|
+
.text(valueText);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
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);
|
package/dist/types.d.ts
CHANGED
|
@@ -176,6 +176,7 @@ export type BarValueLabelConfig = ValueLabelConfig & {
|
|
|
176
176
|
position?: 'inside' | 'outside';
|
|
177
177
|
insidePosition?: 'top' | 'middle' | 'bottom';
|
|
178
178
|
};
|
|
179
|
+
export type BarSide = 'left' | 'right';
|
|
179
180
|
export type LineConfigBase = {
|
|
180
181
|
dataKey: string;
|
|
181
182
|
stroke?: string;
|
|
@@ -185,11 +186,21 @@ export type LineConfigBase = {
|
|
|
185
186
|
export type LineConfig = LineConfigBase & {
|
|
186
187
|
exportHooks?: ExportHooks<LineConfigBase>;
|
|
187
188
|
};
|
|
189
|
+
export type ScatterConfigBase = {
|
|
190
|
+
dataKey: string;
|
|
191
|
+
stroke?: string;
|
|
192
|
+
pointSize?: number;
|
|
193
|
+
valueLabel?: LineValueLabelConfig;
|
|
194
|
+
};
|
|
195
|
+
export type ScatterConfig = ScatterConfigBase & {
|
|
196
|
+
exportHooks?: ExportHooks<ScatterConfigBase>;
|
|
197
|
+
};
|
|
188
198
|
export type BarConfigBase = {
|
|
189
199
|
dataKey: string;
|
|
190
200
|
fill?: string;
|
|
191
201
|
colorAdapter?: (data: DataItem, index: number) => string;
|
|
192
202
|
maxBarSize?: number;
|
|
203
|
+
side?: BarSide;
|
|
193
204
|
valueLabel?: BarValueLabelConfig;
|
|
194
205
|
};
|
|
195
206
|
export type BarConfig = BarConfigBase & {
|
|
@@ -328,6 +339,7 @@ export type ScaleConfig = {
|
|
|
328
339
|
range?: number[];
|
|
329
340
|
padding?: number;
|
|
330
341
|
groupGap?: number;
|
|
342
|
+
reverse?: boolean;
|
|
331
343
|
nice?: boolean;
|
|
332
344
|
min?: number;
|
|
333
345
|
max?: number;
|
|
@@ -344,6 +356,10 @@ export type BarStackingContext = {
|
|
|
344
356
|
totalSeries: number;
|
|
345
357
|
cumulativeData: Map<string, number>;
|
|
346
358
|
totalData: Map<string, number>;
|
|
359
|
+
positiveCumulativeData: Map<string, number>;
|
|
360
|
+
negativeCumulativeData: Map<string, number>;
|
|
361
|
+
positiveTotalData: Map<string, number>;
|
|
362
|
+
negativeTotalData: Map<string, number>;
|
|
347
363
|
gap: number;
|
|
348
364
|
nextLayerData?: Map<string, number>;
|
|
349
365
|
};
|
package/dist/validation.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export declare class ChartValidator {
|
|
|
26
26
|
* Validates scale configuration
|
|
27
27
|
*/
|
|
28
28
|
static validateScaleConfig(scaleType: string, domain: ScaleDomainValue[] | undefined): void;
|
|
29
|
+
/**
|
|
30
|
+
* Validates that explicit numeric bar domains include zero so bars retain a truthful baseline
|
|
31
|
+
*/
|
|
32
|
+
static validateBarDomainIncludesZero(domain: ScaleDomainValue[] | undefined): void;
|
|
29
33
|
/**
|
|
30
34
|
* Warns about potential issues without throwing errors
|
|
31
35
|
*/
|
package/dist/validation.js
CHANGED
|
@@ -91,6 +91,25 @@ export class ChartValidator {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Validates that explicit numeric bar domains include zero so bars retain a truthful baseline
|
|
96
|
+
*/
|
|
97
|
+
static validateBarDomainIncludesZero(domain) {
|
|
98
|
+
if (!domain || domain.length < 2) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const numericDomain = domain.filter((value) => {
|
|
102
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
103
|
+
});
|
|
104
|
+
if (numericDomain.length < 2) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const minValue = Math.min(...numericDomain);
|
|
108
|
+
const maxValue = Math.max(...numericDomain);
|
|
109
|
+
if (minValue > 0 || maxValue < 0) {
|
|
110
|
+
throw new ChartValidationError('Bar charts require explicit numeric domains to include 0 so bars can render from a zero baseline');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
94
113
|
/**
|
|
95
114
|
* Warns about potential issues without throwing errors
|
|
96
115
|
*/
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseChart, type BaseChartConfig, type BaseLayoutContext, type BaseRenderContext } from './base-chart.js';
|
|
2
2
|
import type { ChartComponentBase } from './chart-interface.js';
|
|
3
|
-
import { type AreaStackConfig, type BarStackConfig, type LegendSeries, type Orientation } from './types.js';
|
|
3
|
+
import { type AreaStackConfig, type AxisScaleConfig, type BarStackConfig, type LegendSeries, type Orientation, type ScaleType } from './types.js';
|
|
4
4
|
export type XYChartConfig = BaseChartConfig & {
|
|
5
5
|
orientation?: Orientation;
|
|
6
6
|
barStack?: BarStackConfig;
|
|
@@ -13,6 +13,7 @@ export declare class XYChart extends BaseChart {
|
|
|
13
13
|
private barStackReverseSeries;
|
|
14
14
|
private areaStackMode;
|
|
15
15
|
private readonly orientation;
|
|
16
|
+
private scaleConfigOverride;
|
|
16
17
|
constructor(config: XYChartConfig);
|
|
17
18
|
addChild(component: ChartComponentBase): this;
|
|
18
19
|
protected getExportComponents(): ChartComponentBase[];
|
|
@@ -23,18 +24,32 @@ export declare class XYChart extends BaseChart {
|
|
|
23
24
|
private getXKey;
|
|
24
25
|
protected getLegendSeries(): LegendSeries[];
|
|
25
26
|
private getCategoryScaleType;
|
|
27
|
+
getOrientation(): Orientation;
|
|
28
|
+
getValueAxisScaleType(): ScaleType | null;
|
|
29
|
+
getValueAxisDomain(): [number, number] | null;
|
|
30
|
+
getBaseValueAxisDomain(): [number, number] | null;
|
|
31
|
+
setScaleConfigOverride(override: AxisScaleConfig | null, rerender?: boolean): this;
|
|
32
|
+
private resolveValueAxisDomain;
|
|
26
33
|
private getVisibleSeries;
|
|
27
34
|
private getDisplaySeries;
|
|
28
35
|
private resolveSeriesDefaults;
|
|
29
36
|
private shouldReplaceSeriesColor;
|
|
30
37
|
private cloneSeriesWithOverride;
|
|
31
38
|
private setupScales;
|
|
39
|
+
private get resolvedScaleConfig();
|
|
40
|
+
private getResolvedAxisConfigs;
|
|
41
|
+
private getAxisConfigsForScaleConfig;
|
|
32
42
|
private isHorizontalOrientation;
|
|
43
|
+
private getSeriesTypeName;
|
|
33
44
|
private validateSeriesOrientation;
|
|
34
45
|
private collectSeriesValues;
|
|
46
|
+
private getBarPercentDomain;
|
|
47
|
+
private getBarValueDomain;
|
|
35
48
|
private getStackedAreaGroups;
|
|
36
49
|
private buildBandDomainWithGroupGaps;
|
|
50
|
+
private getScaleRange;
|
|
37
51
|
private createScale;
|
|
52
|
+
private resolveScaleDomain;
|
|
38
53
|
private getSeriesTooltipValue;
|
|
39
54
|
private renderSeries;
|
|
40
55
|
private computeStackingData;
|