@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.
@@ -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
  }
@@ -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.legend?.isInlineMode(),
125
+ legend: this.shouldIncludeLegendInExport(),
126
126
  }),
127
127
  ];
128
128
  }
@@ -675,7 +675,7 @@ export class GaugeChart extends BaseChart {
675
675
  return this.getBaseExportComponents({
676
676
  title: true,
677
677
  tooltip: true,
678
- legend: this.legend?.isInlineMode(),
678
+ legend: this.shouldIncludeLegendInExport(),
679
679
  });
680
680
  }
681
681
  update(data) {
@@ -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 visibilityState;
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 triggerChange;
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, "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,
@@ -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
- legend.visibilityState = new Map(this.visibilityState);
131
- legend.onToggleCallback = () => {
132
- this.visibilityState = new Map(legend.visibilityState);
133
- this.onToggleCallback?.();
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.visibilityState.get(dataKey) ?? true;
174
+ return this.stateController.isSeriesVisible(dataKey);
163
175
  }
164
176
  setSeriesVisible(dataKey, visible) {
165
- this.visibilityState.set(dataKey, visible);
166
- this.triggerChange();
177
+ this.stateController.setSeriesVisible(dataKey, visible);
167
178
  }
168
179
  toggleSeries(dataKey) {
169
- const currentState = this.visibilityState.get(dataKey) ?? true;
170
- this.visibilityState.set(dataKey, !currentState);
171
- this.triggerChange();
180
+ this.stateController.toggleSeries(dataKey);
172
181
  }
173
182
  setVisibilityMap(visibility) {
174
- Object.entries(visibility).forEach(([dataKey, isVisible]) => {
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.visibilityState.get(d.dataKey) ?? true;
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.visibilityState.get(d.dataKey) ?? true;
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.visibilityState.get(dataKey) ?? true;
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.forEach((item) => {
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
- triggerChange() {
443
- this.onToggleCallback?.();
444
- this.onChangeCallbacks.forEach((callback) => {
445
- callback();
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
@@ -173,7 +173,7 @@ export class PieChart extends RadialChartBase {
173
173
  return this.getBaseExportComponents({
174
174
  title: true,
175
175
  tooltip: true,
176
- legend: this.legend?.isInlineMode(),
176
+ legend: this.shouldIncludeLegendInExport(),
177
177
  });
178
178
  }
179
179
  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,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 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);
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
  };
@@ -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
  */
@@ -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
  */
@@ -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;