@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.
Files changed (46) hide show
  1. package/README.md +137 -3
  2. package/dist/area.d.ts +2 -0
  3. package/dist/area.js +39 -31
  4. package/dist/bar.d.ts +20 -1
  5. package/dist/bar.js +395 -519
  6. package/dist/base-chart.d.ts +21 -1
  7. package/dist/base-chart.js +166 -93
  8. package/dist/chart-group.d.ts +137 -0
  9. package/dist/chart-group.js +1155 -0
  10. package/dist/chart-interface.d.ts +1 -1
  11. package/dist/donut-center-content.d.ts +1 -0
  12. package/dist/donut-center-content.js +21 -38
  13. package/dist/donut-chart.js +30 -15
  14. package/dist/gauge-chart.d.ts +20 -0
  15. package/dist/gauge-chart.js +229 -133
  16. package/dist/legend-state.d.ts +19 -0
  17. package/dist/legend-state.js +81 -0
  18. package/dist/legend.d.ts +5 -2
  19. package/dist/legend.js +45 -38
  20. package/dist/line.js +3 -1
  21. package/dist/pie-chart.d.ts +3 -0
  22. package/dist/pie-chart.js +45 -19
  23. package/dist/scatter.d.ts +16 -0
  24. package/dist/scatter.js +165 -0
  25. package/dist/tooltip.d.ts +2 -1
  26. package/dist/tooltip.js +21 -25
  27. package/dist/types.d.ts +19 -1
  28. package/dist/utils.js +11 -19
  29. package/dist/validation.d.ts +4 -0
  30. package/dist/validation.js +19 -0
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-chart.d.ts +40 -1
  34. package/dist/xy-chart.js +488 -165
  35. package/dist/y-axis.d.ts +7 -2
  36. package/dist/y-axis.js +99 -10
  37. package/docs/chart-group.md +213 -0
  38. package/docs/components.md +321 -0
  39. package/docs/donut-chart.md +193 -0
  40. package/docs/gauge-chart.md +175 -0
  41. package/docs/getting-started.md +311 -0
  42. package/docs/pie-chart.md +123 -0
  43. package/docs/theming.md +162 -0
  44. package/docs/word-cloud-chart.md +98 -0
  45. package/docs/xy-chart.md +517 -0
  46. 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, "visibilityState", {
73
+ Object.defineProperty(this, "stateController", {
73
74
  enumerable: true,
74
75
  configurable: true,
75
76
  writable: true,
76
- value: new Map()
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
- this.mode = config?.mode ?? 'inline';
103
- this.position = config?.position || 'bottom';
104
- this.disconnectedTarget = config?.disconnectedTarget;
105
- this.marginTop = config?.marginTop ?? 20;
106
- this.marginBottom = config?.marginBottom ?? 10;
107
- this.paddingX = config?.paddingX;
108
- this.itemSpacingX = config?.itemSpacingX;
109
- this.itemSpacingY = config?.itemSpacingY;
110
- this.exportHooks = config?.exportHooks;
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
- legend.visibilityState = new Map(this.visibilityState);
131
- legend.onToggleCallback = () => {
132
- this.visibilityState = new Map(legend.visibilityState);
133
- this.onToggleCallback?.();
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.visibilityState.get(dataKey) ?? true;
175
+ return this.stateController.isSeriesVisible(dataKey);
163
176
  }
164
177
  setSeriesVisible(dataKey, visible) {
165
- this.visibilityState.set(dataKey, visible);
166
- this.triggerChange();
178
+ this.stateController.setSeriesVisible(dataKey, visible);
167
179
  }
168
180
  toggleSeries(dataKey) {
169
- const currentState = this.visibilityState.get(dataKey) ?? true;
170
- this.visibilityState.set(dataKey, !currentState);
171
- this.triggerChange();
181
+ this.stateController.toggleSeries(dataKey);
172
182
  }
173
183
  setVisibilityMap(visibility) {
174
- Object.entries(visibility).forEach(([dataKey, isVisible]) => {
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.visibilityState.get(d.dataKey) ?? true;
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.visibilityState.get(d.dataKey) ?? true;
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.visibilityState.get(dataKey) ?? true;
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.forEach((item) => {
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
- triggerChange() {
443
- this.onToggleCallback?.();
444
- this.onChangeCallbacks.forEach((callback) => {
445
- callback();
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 = String(value);
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
@@ -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 = config.pie ?? {};
75
- this.innerRadiusRatio = pieConfig.innerRadius ?? 0;
76
- this.startAngle = pieConfig.startAngle ?? 0;
77
- this.endAngle = pieConfig.endAngle ?? FULL_CIRCLE_RADIANS;
78
- this.padAngle = pieConfig.padAngle ?? this.theme.donut.padAngle;
79
- this.cornerRadius =
80
- pieConfig.cornerRadius ?? this.theme.donut.cornerRadius;
81
- this.sort = pieConfig.sort ?? 'none';
82
- this.valueKey = config.valueKey ?? 'value';
83
- this.labelKey = config.labelKey ?? 'name';
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
- show: config.valueLabel?.show ?? false,
86
- position: config.valueLabel?.position ?? 'auto',
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.legend?.isInlineMode(),
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
+ }
@@ -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 lineSeries = series.filter((s) => s.type === 'line' || s.type === 'area');
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 = lineSeries.map((s) => {
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
- lineSeries.forEach((s, i) => {
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
- let nextIndex = null;
445
- switch (event.key) {
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?: (dataKey: string, value: DataValue, data: DataItem) => string;
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
  };