@internetstiftelsen/charts 0.13.2 → 0.14.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,7 +1,9 @@
1
1
  import { BaseChart } from './base-chart.js';
2
2
  import { DEFAULT_CHART_HEIGHT } from './theme.js';
3
+ import { measureTextWidth, truncateText, wrapText } from './utils.js';
3
4
  const TOOLTIP_OFFSET_PX = 12;
4
5
  const EDGE_MARGIN_PX = 10;
6
+ const DEFAULT_LABEL_FONT_SIZE = 14;
5
7
  export class RadialChartBase extends BaseChart {
6
8
  initializeTooltip() {
7
9
  this.tooltip?.initialize(this.renderTheme);
@@ -27,6 +29,119 @@ export class RadialChartBase extends BaseChart {
27
29
  fill: item.color,
28
30
  }));
29
31
  }
32
+ renderRadialLabelText(textElement, text, options, verticalAnchor = 'middle') {
33
+ const layout = this.resolveRadialLabelLayout(text, options);
34
+ textElement.text(null).style('visibility', null);
35
+ if (layout.hidden) {
36
+ textElement.text(text).style('visibility', 'hidden');
37
+ return;
38
+ }
39
+ if (layout.lines.length === 1) {
40
+ textElement.text(layout.lines[0]);
41
+ this.addRadialLabelTitle(textElement, layout.titleText);
42
+ return;
43
+ }
44
+ const lineHeight = this.resolveRadialLabelLineHeight(options.fontSize);
45
+ const startDy = verticalAnchor === 'middle'
46
+ ? -((layout.lines.length - 1) * lineHeight) / 2
47
+ : 0;
48
+ const x = textElement.attr('x') ?? '0';
49
+ layout.lines.forEach((line, index) => {
50
+ textElement
51
+ .append('tspan')
52
+ .attr('x', x)
53
+ .attr('dy', index === 0 ? `${startDy}px` : `${lineHeight}px`)
54
+ .text(line);
55
+ });
56
+ this.addRadialLabelTitle(textElement, layout.titleText);
57
+ }
58
+ renderRadialStructuredLabelText(textElement, labelText, valueText, separator, options, verticalAnchor = 'middle') {
59
+ const layout = this.resolveRadialLabelLayout(labelText, options);
60
+ const fullText = `${labelText}${separator}${valueText}`;
61
+ textElement.text(null).style('visibility', null);
62
+ if (layout.hidden) {
63
+ textElement.text(fullText).style('visibility', 'hidden');
64
+ return;
65
+ }
66
+ if (layout.lines.length === 1) {
67
+ textElement.text(`${layout.lines[0]}${separator}${valueText}`);
68
+ this.addRadialLabelTitle(textElement, layout.titleText ? fullText : null);
69
+ return;
70
+ }
71
+ const lineHeight = this.resolveRadialLabelLineHeight(options.fontSize);
72
+ const startDy = verticalAnchor === 'middle'
73
+ ? -((layout.lines.length - 1) * lineHeight) / 2
74
+ : 0;
75
+ const x = textElement.attr('x') ?? '0';
76
+ const lastLineIndex = layout.lines.length - 1;
77
+ layout.lines.forEach((line, index) => {
78
+ const renderedLine = index === lastLineIndex
79
+ ? `${line}${separator}${valueText}`
80
+ : line;
81
+ textElement
82
+ .append('tspan')
83
+ .attr('x', x)
84
+ .attr('dy', index === 0 ? `${startDy}px` : `${lineHeight}px`)
85
+ .text(renderedLine);
86
+ });
87
+ this.addRadialLabelTitle(textElement, layout.titleText ? fullText : null);
88
+ }
89
+ measureRadialLabelDimensions(text, options) {
90
+ const layout = this.resolveRadialLabelLayout(text, options);
91
+ if (layout.hidden) {
92
+ return {
93
+ width: 0,
94
+ height: 0,
95
+ };
96
+ }
97
+ const svgNode = this.svg?.node();
98
+ const fontWeight = String(options.fontWeight);
99
+ const width = svgNode
100
+ ? layout.lines.reduce((maxWidth, line) => {
101
+ return Math.max(maxWidth, measureTextWidth(line, options.fontSize, options.fontFamily, fontWeight, svgNode));
102
+ }, 0)
103
+ : (options.maxLabelWidth ?? 0);
104
+ return {
105
+ width,
106
+ height: Math.max(layout.lines.length, 1) *
107
+ this.resolveRadialLabelLineHeight(options.fontSize),
108
+ };
109
+ }
110
+ measureRadialStructuredLabelDimensions(labelText, valueText, separator, options) {
111
+ const layout = this.resolveRadialLabelLayout(labelText, options);
112
+ if (layout.hidden) {
113
+ return {
114
+ width: 0,
115
+ height: 0,
116
+ };
117
+ }
118
+ const svgNode = this.svg?.node();
119
+ const fontWeight = String(options.fontWeight);
120
+ const lastLineIndex = layout.lines.length - 1;
121
+ const width = svgNode
122
+ ? layout.lines.reduce((maxWidth, line, index) => {
123
+ const renderedLine = index === lastLineIndex
124
+ ? `${line}${separator}${valueText}`
125
+ : line;
126
+ return Math.max(maxWidth, measureTextWidth(renderedLine, options.fontSize, options.fontFamily, fontWeight, svgNode));
127
+ }, 0)
128
+ : (options.maxLabelWidth ?? 0);
129
+ return {
130
+ width,
131
+ height: Math.max(layout.lines.length, 1) *
132
+ this.resolveRadialLabelLineHeight(options.fontSize),
133
+ };
134
+ }
135
+ resolveRadialLabelLineHeight(fontSize) {
136
+ if (typeof fontSize === 'number' && Number.isFinite(fontSize)) {
137
+ return fontSize * 1.2;
138
+ }
139
+ const parsedFontSize = Number.parseFloat(String(fontSize));
140
+ if (Number.isFinite(parsedFontSize)) {
141
+ return parsedFontSize * 1.2;
142
+ }
143
+ return DEFAULT_LABEL_FONT_SIZE * 1.2;
144
+ }
30
145
  showTooltipFromPointer(event, content) {
31
146
  if (!this.tooltip) {
32
147
  return;
@@ -84,6 +199,72 @@ export class RadialChartBase extends BaseChart {
84
199
  const y = Math.max(EDGE_MARGIN_PX, Math.min(rawY, window.innerHeight + window.scrollY - height - EDGE_MARGIN_PX));
85
200
  this.tooltip?.showAt(x, y);
86
201
  }
202
+ resolveRadialLabelLayout(text, options) {
203
+ const overflowContext = this.resolveRadialLabelOverflowContext(options.maxLabelWidth);
204
+ if (!overflowContext) {
205
+ return this.createVisibleRadialLabelLayout(text);
206
+ }
207
+ const { maxLabelWidth, svgNode } = overflowContext;
208
+ const fontWeight = String(options.fontWeight);
209
+ const textWidth = measureTextWidth(text, options.fontSize, options.fontFamily, fontWeight, svgNode);
210
+ if (textWidth <= maxLabelWidth) {
211
+ return this.createVisibleRadialLabelLayout(text);
212
+ }
213
+ return this.resolveOverflowingRadialLabelLayout(text, options, maxLabelWidth, fontWeight, svgNode);
214
+ }
215
+ resolveRadialLabelOverflowContext(maxLabelWidth) {
216
+ const svgNode = this.svg?.node();
217
+ if (maxLabelWidth === undefined ||
218
+ !Number.isFinite(maxLabelWidth) ||
219
+ maxLabelWidth <= 0 ||
220
+ !svgNode) {
221
+ return null;
222
+ }
223
+ return { maxLabelWidth, svgNode };
224
+ }
225
+ createVisibleRadialLabelLayout(text) {
226
+ return {
227
+ lines: [text],
228
+ hidden: false,
229
+ titleText: null,
230
+ };
231
+ }
232
+ resolveOverflowingRadialLabelLayout(text, options, maxLabelWidth, fontWeight, svgNode) {
233
+ if (options.oversizedBehavior === 'hide') {
234
+ if (options.forceVisible) {
235
+ return {
236
+ lines: [text],
237
+ hidden: false,
238
+ titleText: null,
239
+ };
240
+ }
241
+ return {
242
+ lines: [text],
243
+ hidden: true,
244
+ titleText: null,
245
+ };
246
+ }
247
+ if (options.oversizedBehavior === 'wrap') {
248
+ const lines = wrapText(text, maxLabelWidth, options.fontSize, options.fontFamily, fontWeight, svgNode);
249
+ return {
250
+ lines,
251
+ hidden: false,
252
+ titleText: lines.length > 1 ? text : null,
253
+ };
254
+ }
255
+ const result = truncateText(text, maxLabelWidth, options.fontSize, options.fontFamily, fontWeight, svgNode);
256
+ return {
257
+ lines: [result.text],
258
+ hidden: false,
259
+ titleText: result.truncated ? text : null,
260
+ };
261
+ }
262
+ addRadialLabelTitle(textElement, text) {
263
+ if (!text) {
264
+ return;
265
+ }
266
+ textElement.append('title').text(text);
267
+ }
87
268
  resolveRadialFontScale(outerRadius, theme) {
88
269
  const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
89
270
  const plotHeight = Math.max(1, referenceHeight - theme.margins.top - theme.margins.bottom);
package/dist/scatter.js CHANGED
@@ -183,6 +183,7 @@ export class Scatter {
183
183
  const border = config.border ?? theme.valueLabel.border;
184
184
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
185
185
  const padding = config.padding ?? theme.valueLabel.padding;
186
+ const forceVisible = config.forceVisible === true;
186
187
  const labelGroup = plotGroup
187
188
  .append('g')
188
189
  .attr('class', `scatter-value-labels-${sanitizeForCSS(this.dataKey)}`);
@@ -211,7 +212,7 @@ export class Scatter {
211
212
  if (labelY - boxHeight / 2 < plotTop + 4) {
212
213
  labelY = yPos + boxHeight / 2 + pointSize + 4;
213
214
  if (labelY + boxHeight / 2 > plotBottom - 4) {
214
- shouldRender = false;
215
+ shouldRender = forceVisible;
215
216
  }
216
217
  }
217
218
  tempText.remove();
package/dist/theme.d.ts CHANGED
@@ -1,11 +1,26 @@
1
1
  import { type ChartTheme, type ResponsiveConfig } from './types.js';
2
+ export declare const RUBY_COLOR_PALETTE: string[];
3
+ export declare const PEACOCK_COLOR_PALETTE: string[];
4
+ export declare const JADE_COLOR_PALETTE: string[];
5
+ export declare const LEMON_COLOR_PALETTE: string[];
6
+ export declare const OCEAN_COLOR_PALETTE: string[];
2
7
  export declare const DEFAULT_COLOR_PALETTE: string[];
3
8
  export declare const DEFAULT_CHART_WIDTH = 928;
4
9
  export declare const DEFAULT_CHART_HEIGHT = 600;
5
10
  export declare const defaultTheme: ChartTheme;
11
+ export declare const rubyTheme: ChartTheme;
12
+ export declare const peacockTheme: ChartTheme;
13
+ export declare const jadeTheme: ChartTheme;
14
+ export declare const lemonTheme: ChartTheme;
15
+ export declare const oceanTheme: ChartTheme;
6
16
  export declare const newspaperTheme: ChartTheme;
7
17
  export declare const defaultResponsiveConfig: ResponsiveConfig;
8
18
  export declare const themes: {
9
19
  default: ChartTheme;
20
+ ruby: ChartTheme;
21
+ peacock: ChartTheme;
22
+ jade: ChartTheme;
23
+ lemon: ChartTheme;
24
+ ocean: ChartTheme;
10
25
  newspaper: ChartTheme;
11
26
  };
package/dist/theme.js CHANGED
@@ -1,3 +1,39 @@
1
+ const SYSTEM_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
2
+ export const RUBY_COLOR_PALETTE = [
3
+ '#ff4069',
4
+ '#c51f46',
5
+ '#ff7f99',
6
+ '#ffb3c2',
7
+ '#7f102d',
8
+ ];
9
+ export const PEACOCK_COLOR_PALETTE = [
10
+ '#c27fec',
11
+ '#934bc5',
12
+ '#d5a4f3',
13
+ '#ead2fa',
14
+ '#5f287f',
15
+ ];
16
+ export const JADE_COLOR_PALETTE = [
17
+ '#55c7b4',
18
+ '#2f8f80',
19
+ '#88dacd',
20
+ '#c4eee8',
21
+ '#1b5f55',
22
+ ];
23
+ export const LEMON_COLOR_PALETTE = [
24
+ '#ffce2e',
25
+ '#c89200',
26
+ '#ffe07a',
27
+ '#fff0b8',
28
+ '#7a5900',
29
+ ];
30
+ export const OCEAN_COLOR_PALETTE = [
31
+ '#50b2fc',
32
+ '#147eca',
33
+ '#8bcbfd',
34
+ '#c6e6fe',
35
+ '#0f4f7f',
36
+ ];
1
37
  export const DEFAULT_COLOR_PALETTE = [
2
38
  '#50b2fc', // ocean
3
39
  '#ff4069', // ruby
@@ -11,7 +47,7 @@ export const DEFAULT_COLOR_PALETTE = [
11
47
  export const DEFAULT_CHART_WIDTH = 928;
12
48
  export const DEFAULT_CHART_HEIGHT = 600;
13
49
  export const defaultTheme = {
14
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
50
+ fontFamily: SYSTEM_FONT,
15
51
  margins: {
16
52
  top: 20,
17
53
  right: 20,
@@ -24,7 +60,7 @@ export const defaultTheme = {
24
60
  },
25
61
  colorPalette: [...DEFAULT_COLOR_PALETTE],
26
62
  axis: {
27
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
63
+ fontFamily: SYSTEM_FONT,
28
64
  fontSize: 14,
29
65
  fontWeight: 'normal',
30
66
  groupLabel: {
@@ -72,7 +108,7 @@ export const defaultTheme = {
72
108
  background: '#ffffff',
73
109
  border: '#dddddd',
74
110
  color: '#1f2a36',
75
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
111
+ fontFamily: SYSTEM_FONT,
76
112
  fontSize: 12,
77
113
  fontWeight: 'normal',
78
114
  },
@@ -86,7 +122,7 @@ export const defaultTheme = {
86
122
  },
87
123
  valueLabel: {
88
124
  fontSize: 12,
89
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
125
+ fontFamily: SYSTEM_FONT,
90
126
  fontWeight: '600',
91
127
  color: '#1f2a36',
92
128
  background: '#ffffff',
@@ -117,6 +153,51 @@ export const defaultTheme = {
117
153
  },
118
154
  },
119
155
  };
156
+ function cloneTheme(theme) {
157
+ return {
158
+ ...theme,
159
+ margins: { ...theme.margins },
160
+ grid: { ...theme.grid },
161
+ colorPalette: [...theme.colorPalette],
162
+ axis: {
163
+ ...theme.axis,
164
+ groupLabel: theme.axis.groupLabel
165
+ ? { ...theme.axis.groupLabel }
166
+ : undefined,
167
+ },
168
+ legend: { ...theme.legend },
169
+ text: {
170
+ variants: Object.fromEntries(Object.entries(theme.text.variants).map(([name, style]) => [
171
+ name,
172
+ { ...style },
173
+ ])),
174
+ },
175
+ tooltip: { ...theme.tooltip },
176
+ line: {
177
+ ...theme.line,
178
+ point: { ...theme.line.point },
179
+ },
180
+ valueLabel: { ...theme.valueLabel },
181
+ donut: {
182
+ ...theme.donut,
183
+ centerContent: {
184
+ mainValue: { ...theme.donut.centerContent.mainValue },
185
+ title: { ...theme.donut.centerContent.title },
186
+ subtitle: { ...theme.donut.centerContent.subtitle },
187
+ },
188
+ },
189
+ };
190
+ }
191
+ function createAccentTheme(colorPalette) {
192
+ const theme = cloneTheme(defaultTheme);
193
+ theme.colorPalette = [...colorPalette];
194
+ return theme;
195
+ }
196
+ export const rubyTheme = createAccentTheme(RUBY_COLOR_PALETTE);
197
+ export const peacockTheme = createAccentTheme(PEACOCK_COLOR_PALETTE);
198
+ export const jadeTheme = createAccentTheme(JADE_COLOR_PALETTE);
199
+ export const lemonTheme = createAccentTheme(LEMON_COLOR_PALETTE);
200
+ export const oceanTheme = createAccentTheme(OCEAN_COLOR_PALETTE);
120
201
  export const newspaperTheme = {
121
202
  fontFamily: 'Georgia, "Times New Roman", Times, serif',
122
203
  margins: {
@@ -261,5 +342,10 @@ export const defaultResponsiveConfig = {
261
342
  };
262
343
  export const themes = {
263
344
  default: defaultTheme,
345
+ ruby: rubyTheme,
346
+ peacock: peacockTheme,
347
+ jade: jadeTheme,
348
+ lemon: lemonTheme,
349
+ ocean: oceanTheme,
264
350
  newspaper: newspaperTheme,
265
351
  };
package/dist/tooltip.d.ts CHANGED
@@ -81,6 +81,10 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
81
81
  private countPlacedLayoutsOnEdge;
82
82
  private doSplitTooltipLayoutsOverlap;
83
83
  private resolveSplitTooltipPositions;
84
+ private resolveHorizontalChartSplitTooltipPositions;
85
+ private resolveVerticalChartSplitTooltipPositions;
86
+ private getSplitTooltipViewportBounds;
87
+ private groupSplitTooltipLayoutsByEdge;
84
88
  private resolveSideSplitTooltipPositions;
85
89
  private resolveHorizontalSideSplitTooltipPositions;
86
90
  private resolveHorizontalSideSplitTooltipCollisions;
@@ -91,6 +95,11 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
91
95
  private assignLayoutToHorizontalTooltipLane;
92
96
  private doHorizontalTooltipLanesOverlap;
93
97
  private doSplitTooltipLayoutsOverlapHorizontally;
94
- private resolveVerticalSplitTooltipPositions;
98
+ private resolveHorizontalChartAboveBelowSplitTooltipPositions;
99
+ private resolvePackedAboveBelowTooltipRow;
100
+ private resolveVerticalChartAboveBelowSplitTooltipPositions;
101
+ private resolveCollisionAwareAboveBelowTooltipPositions;
102
+ private resolveNonOverlappingAboveBelowTooltipLeft;
103
+ private doesSplitTooltipOverlapPlacedLayouts;
95
104
  }
96
105
  export {};
package/dist/tooltip.js CHANGED
@@ -466,10 +466,6 @@ export class Tooltip {
466
466
  if (layouts.length === 0) {
467
467
  return;
468
468
  }
469
- if (!isHorizontal) {
470
- this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
471
- this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
472
- }
473
469
  this.resolveSplitTooltipPositions(layouts, isHorizontal);
474
470
  layouts.forEach((layout) => {
475
471
  this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
@@ -1303,19 +1299,42 @@ export class Tooltip {
1303
1299
  a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
1304
1300
  }
1305
1301
  resolveSplitTooltipPositions(layouts, isHorizontal) {
1306
- if (isHorizontal && this.position === 'side') {
1307
- this.resolveHorizontalSideSplitTooltipPositions(layouts);
1302
+ if (isHorizontal) {
1303
+ this.resolveHorizontalChartSplitTooltipPositions(layouts);
1308
1304
  return;
1309
1305
  }
1306
+ this.resolveVerticalChartSplitTooltipPositions(layouts);
1307
+ }
1308
+ resolveHorizontalChartSplitTooltipPositions(layouts) {
1310
1309
  if (this.position === 'vertical') {
1311
- this.resolveVerticalSplitTooltipPositions(layouts);
1310
+ this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
1311
+ this.resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts);
1312
1312
  return;
1313
1313
  }
1314
+ this.resolveHorizontalSideSplitTooltipPositions(layouts);
1315
+ }
1316
+ resolveVerticalChartSplitTooltipPositions(layouts) {
1317
+ if (this.position === 'vertical') {
1318
+ this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
1319
+ this.resolveVerticalChartAboveBelowSplitTooltipPositions(layouts);
1320
+ return;
1321
+ }
1322
+ this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
1314
1323
  this.resolveSideSplitTooltipPositions(layouts);
1315
1324
  }
1316
- resolveSideSplitTooltipPositions(layouts) {
1317
- const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1318
- const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
1325
+ getSplitTooltipViewportBounds() {
1326
+ return {
1327
+ minLeft: window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX,
1328
+ maxRight: window.scrollX +
1329
+ window.innerWidth -
1330
+ TOOLTIP_VIEWPORT_PADDING_PX,
1331
+ minTop: window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX,
1332
+ maxBottom: window.scrollY +
1333
+ window.innerHeight -
1334
+ TOOLTIP_VIEWPORT_PADDING_PX,
1335
+ };
1336
+ }
1337
+ groupSplitTooltipLayoutsByEdge(layouts) {
1319
1338
  const tooltipsByEdge = {
1320
1339
  left: [],
1321
1340
  right: [],
@@ -1325,6 +1344,11 @@ export class Tooltip {
1325
1344
  layouts.forEach((layout) => {
1326
1345
  tooltipsByEdge[layout.arrowEdge].push(layout);
1327
1346
  });
1347
+ return tooltipsByEdge;
1348
+ }
1349
+ resolveSideSplitTooltipPositions(layouts) {
1350
+ const { minTop, maxBottom } = this.getSplitTooltipViewportBounds();
1351
+ const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1328
1352
  Object.values(tooltipsByEdge).forEach((edgeLayouts) => {
1329
1353
  if (edgeLayouts.length === 0) {
1330
1354
  return;
@@ -1463,65 +1487,98 @@ export class Tooltip {
1463
1487
  return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
1464
1488
  a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left);
1465
1489
  }
1466
- resolveVerticalSplitTooltipPositions(layouts) {
1467
- const minLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX;
1468
- const maxRight = window.scrollX + window.innerWidth - TOOLTIP_VIEWPORT_PADDING_PX;
1469
- const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1470
- const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
1471
- const tooltipsByEdge = {
1472
- left: [],
1473
- right: [],
1474
- top: [],
1475
- bottom: [],
1476
- };
1477
- layouts.forEach((layout) => {
1478
- tooltipsByEdge[layout.arrowEdge].push(layout);
1490
+ resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts) {
1491
+ const bounds = this.getSplitTooltipViewportBounds();
1492
+ const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1493
+ this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.top, bounds);
1494
+ this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.bottom, bounds);
1495
+ this.resolveSideSplitTooltipPositions([
1496
+ ...tooltipsByEdge.left,
1497
+ ...tooltipsByEdge.right,
1498
+ ]);
1499
+ }
1500
+ resolvePackedAboveBelowTooltipRow(layouts, bounds) {
1501
+ if (layouts.length === 0) {
1502
+ return;
1503
+ }
1504
+ const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.targetX - b.targetX);
1505
+ orderedLayouts.forEach((layout) => {
1506
+ const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
1507
+ layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
1479
1508
  });
1480
- [tooltipsByEdge.top, tooltipsByEdge.bottom].forEach((edgeLayouts) => {
1481
- if (edgeLayouts.length === 0) {
1482
- return;
1483
- }
1484
- edgeLayouts.sort((a, b) => a.left - b.left);
1485
- edgeLayouts[0].left = Math.max(minLeft, edgeLayouts[0].left);
1486
- for (let i = 1; i < edgeLayouts.length; i++) {
1487
- const previousLayout = edgeLayouts[i - 1];
1488
- const currentLayout = edgeLayouts[i];
1489
- const minAllowedLeft = previousLayout.left +
1490
- previousLayout.width +
1509
+ const firstLayout = orderedLayouts[0];
1510
+ const firstMaxLeft = Math.max(bounds.minLeft, bounds.maxRight - firstLayout.width);
1511
+ firstLayout.left = Math.max(bounds.minLeft, Math.min(firstLayout.left, firstMaxLeft));
1512
+ for (let i = 1; i < orderedLayouts.length; i++) {
1513
+ const previousLayout = orderedLayouts[i - 1];
1514
+ const currentLayout = orderedLayouts[i];
1515
+ const minAllowedLeft = previousLayout.left +
1516
+ previousLayout.width +
1517
+ SPLIT_TOOLTIP_GAP_PX;
1518
+ currentLayout.left = Math.max(currentLayout.left, minAllowedLeft);
1519
+ }
1520
+ const lastLayout = orderedLayouts[orderedLayouts.length - 1];
1521
+ const overflow = lastLayout.left + lastLayout.width - bounds.maxRight;
1522
+ if (overflow > 0) {
1523
+ lastLayout.left -= overflow;
1524
+ for (let i = orderedLayouts.length - 2; i >= 0; i--) {
1525
+ const currentLayout = orderedLayouts[i];
1526
+ const nextLayout = orderedLayouts[i + 1];
1527
+ const maxAllowedLeft = nextLayout.left -
1528
+ currentLayout.width -
1491
1529
  SPLIT_TOOLTIP_GAP_PX;
1492
- currentLayout.left = Math.max(currentLayout.left, minAllowedLeft);
1530
+ currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
1493
1531
  }
1494
- const lastLayout = edgeLayouts[edgeLayouts.length - 1];
1495
- const overflow = lastLayout.left + lastLayout.width - maxRight;
1496
- if (overflow > 0) {
1497
- lastLayout.left -= overflow;
1498
- for (let i = edgeLayouts.length - 2; i >= 0; i--) {
1499
- const currentLayout = edgeLayouts[i];
1500
- const nextLayout = edgeLayouts[i + 1];
1501
- const maxAllowedLeft = nextLayout.left -
1502
- currentLayout.width -
1503
- SPLIT_TOOLTIP_GAP_PX;
1504
- currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
1505
- }
1506
- const underflow = minLeft - edgeLayouts[0].left;
1507
- if (underflow > 0) {
1508
- edgeLayouts.forEach((layout) => {
1509
- layout.left += underflow;
1510
- });
1511
- }
1532
+ const underflow = bounds.minLeft - orderedLayouts[0].left;
1533
+ if (underflow > 0) {
1534
+ orderedLayouts.forEach((layout) => {
1535
+ layout.left += underflow;
1536
+ });
1512
1537
  }
1513
- edgeLayouts.forEach((layout) => {
1514
- const maxLeft = maxRight - layout.width;
1515
- const maxTop = maxBottom - layout.height;
1516
- layout.left = Math.max(minLeft, Math.min(layout.left, maxLeft));
1517
- layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
1518
- });
1538
+ }
1539
+ orderedLayouts.forEach((layout) => {
1540
+ const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
1541
+ layout.left = Math.max(bounds.minLeft, Math.min(layout.left, maxLeft));
1519
1542
  });
1543
+ }
1544
+ resolveVerticalChartAboveBelowSplitTooltipPositions(layouts) {
1545
+ const bounds = this.getSplitTooltipViewportBounds();
1546
+ const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1547
+ this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.top, bounds);
1548
+ this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.bottom, bounds);
1520
1549
  this.resolveSideSplitTooltipPositions([
1521
1550
  ...tooltipsByEdge.left,
1522
1551
  ...tooltipsByEdge.right,
1523
1552
  ]);
1524
1553
  }
1554
+ resolveCollisionAwareAboveBelowTooltipPositions(layouts, bounds) {
1555
+ const placedLayouts = [];
1556
+ const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.top - b.top);
1557
+ orderedLayouts.forEach((layout) => {
1558
+ const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
1559
+ const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
1560
+ layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
1561
+ layout.left = this.resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, bounds.minLeft, maxLeft);
1562
+ placedLayouts.push(layout);
1563
+ });
1564
+ }
1565
+ resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, minLeft, maxLeft) {
1566
+ const preferredLeft = Math.max(minLeft, Math.min(layout.left, maxLeft));
1567
+ if (!this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: preferredLeft }, placedLayouts)) {
1568
+ return preferredLeft;
1569
+ }
1570
+ const candidates = placedLayouts.flatMap((placedLayout) => [
1571
+ placedLayout.left + placedLayout.width + SPLIT_TOOLTIP_GAP_PX,
1572
+ placedLayout.left - layout.width - SPLIT_TOOLTIP_GAP_PX,
1573
+ ]);
1574
+ return (Array.from(new Set(candidates.map((candidate) => Math.round(Math.max(minLeft, Math.min(candidate, maxLeft))))))
1575
+ .sort((a, b) => Math.abs(a - preferredLeft) -
1576
+ Math.abs(b - preferredLeft))
1577
+ .find((candidate) => !this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: candidate }, placedLayouts)) ?? preferredLeft);
1578
+ }
1579
+ doesSplitTooltipOverlapPlacedLayouts(layout, placedLayouts) {
1580
+ return placedLayouts.some((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout));
1581
+ }
1525
1582
  }
1526
1583
  Object.defineProperty(Tooltip, "nextTooltipId", {
1527
1584
  enumerable: true,
package/dist/types.d.ts CHANGED
@@ -180,6 +180,7 @@ export type ValueLabelConfig = {
180
180
  borderRadius?: number;
181
181
  padding?: number;
182
182
  formatter?: SeriesValueFormatter;
183
+ forceVisible?: boolean;
183
184
  };
184
185
  export type LineValueLabelConfig = ValueLabelConfig & {
185
186
  show?: boolean;
@@ -1,8 +1,10 @@
1
1
  import { ChartValidationError, ChartValidator } from '../validation.js';
2
2
  import { easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, } from 'd3';
3
+ import { createCubicBezierEasing } from '../easing.js';
3
4
  const DEFAULT_ANIMATE = false;
4
5
  const DEFAULT_ANIMATION_DURATION_MS = 700;
5
6
  const DEFAULT_ANIMATION_EASING_PRESET = 'ease-in-out';
7
+ const easeSpringOut = createCubicBezierEasing(0.85, 0, 0.15, 1);
6
8
  const XY_ANIMATION_EASING_PRESETS = {
7
9
  linear: easeLinear,
8
10
  'ease-in': easeCubicIn,
@@ -10,6 +12,7 @@ const XY_ANIMATION_EASING_PRESETS = {
10
12
  'ease-in-out': easeCubicInOut,
11
13
  'bounce-out': easeBounceOut,
12
14
  'elastic-out': easeElasticOut,
15
+ 'spring-out': easeSpringOut,
13
16
  };
14
17
  export function normalizeXYAnimationConfig(config) {
15
18
  if (config === undefined) {
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from 'd3';
2
- export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
2
+ export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out' | 'spring-out';
3
3
  export type XYAnimationConfig = {
4
4
  show?: boolean;
5
5
  duration?: number;