@internetstiftelsen/charts 0.16.0 → 0.17.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,13 +1,15 @@
1
1
  import { pointer, select } from 'd3';
2
2
  import { getSeriesColor } from '../types.js';
3
- import { sanitizeForCSS } from '../utils.js';
4
- import { getAnchoredTooltipPosition, resolveSharedTooltipTarget, resolveSplitTooltipPositions, resolveSplitTooltipTarget, resolveTooltipArrowEdge, } from './geometry.js';
3
+ import { getContrastTextColor, sanitizeForCSS } from '../utils.js';
4
+ import { clipTooltipAnchorToBounds, getAnchoredTooltipPosition, getSplitTooltipViewportBounds, resolveSharedTooltipTarget, resolveSplitTooltipPositions, resolveSplitTooltipTarget, resolveTooltipArrowEdge, } from './geometry.js';
5
5
  export function attachXYTooltipArea(config) {
6
- const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, formatter, labelFormatter, customFormatter, dom, } = config;
6
+ const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, colorMode, formatter, labelFormatter, customFormatter, dom, } = config;
7
7
  const tooltip = dom.getRootTooltip();
8
8
  if (!tooltip || data.length === 0) {
9
9
  return null;
10
10
  }
11
+ const resolvedPosition = position === 'auto' ? (isHorizontal ? 'vertical' : 'side') : position;
12
+ const resolvedBarAnchorPosition = isHorizontal ? 'auto' : barAnchorPosition;
11
13
  const resolveSeriesValue = config.resolveSeriesValue ??
12
14
  ((targetSeries, dataPoint) => {
13
15
  const rawValue = dataPoint[targetSeries.dataKey];
@@ -24,6 +26,15 @@ export function attachXYTooltipArea(config) {
24
26
  const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
25
27
  return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
26
28
  };
29
+ const getTooltipValueSign = (value) => {
30
+ if (value < 0) {
31
+ return -1;
32
+ }
33
+ if (value > 0) {
34
+ return 1;
35
+ }
36
+ return 0;
37
+ };
27
38
  const buildTooltipLabel = (dataPoint) => {
28
39
  const labelValue = dataPoint[xKey];
29
40
  return labelFormatter
@@ -43,8 +54,26 @@ export function attachXYTooltipArea(config) {
43
54
  return value !== null && value !== undefined;
44
55
  });
45
56
  };
46
- const buildSharedTooltipContent = (dataPoint) => {
47
- const visibleSeries = getVisibleTooltipSeries(dataPoint);
57
+ const getSeriesTooltipStyle = (currentSeries, dataPoint, index) => {
58
+ if (colorMode !== 'series') {
59
+ return undefined;
60
+ }
61
+ const seriesColor = currentSeries.type === 'bar' && currentSeries.colorAdapter
62
+ ? currentSeries.colorAdapter(dataPoint, index)
63
+ : getSeriesColor(currentSeries);
64
+ return {
65
+ background: seriesColor,
66
+ border: seriesColor,
67
+ color: getContrastTextColor(seriesColor),
68
+ };
69
+ };
70
+ const getSharedTooltipStyle = (visibleSeries, dataPoint, index) => {
71
+ if (visibleSeries.length !== 1) {
72
+ return undefined;
73
+ }
74
+ return getSeriesTooltipStyle(visibleSeries[0], dataPoint, index);
75
+ };
76
+ const buildSharedTooltipContent = (dataPoint, visibleSeries) => {
48
77
  if (visibleSeries.length === 0) {
49
78
  return null;
50
79
  }
@@ -63,7 +92,7 @@ export function attachXYTooltipArea(config) {
63
92
  return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
64
93
  };
65
94
  const buildAccessibleLabel = (dataPoint) => {
66
- const content = buildSharedTooltipContent(dataPoint);
95
+ const content = buildSharedTooltipContent(dataPoint, getVisibleTooltipSeries(dataPoint));
67
96
  return content ? stripHtml(content) : buildTooltipLabel(dataPoint);
68
97
  };
69
98
  const dataPointPositions = data.map((dataPoint) => isHorizontal ? getYPosition(dataPoint) : getXPosition(dataPoint));
@@ -143,9 +172,11 @@ export function attachXYTooltipArea(config) {
143
172
  const showSharedTooltipAtIndex = (closestIndex) => {
144
173
  const dataPoint = data[closestIndex];
145
174
  const dataPointPosition = dataPointPositions[closestIndex];
175
+ const visibleSeries = getVisibleTooltipSeries(dataPoint);
146
176
  updateVisualStateAtIndex(closestIndex);
147
177
  dom.hideSplitTooltips();
148
- const content = buildSharedTooltipContent(dataPoint);
178
+ dom.applyRootTooltipStyles(theme, getSharedTooltipStyle(visibleSeries, dataPoint, closestIndex));
179
+ const content = buildSharedTooltipContent(dataPoint, visibleSeries);
149
180
  if (!content) {
150
181
  dom.hideTooltipSelection(tooltip);
151
182
  return;
@@ -171,17 +202,18 @@ export function attachXYTooltipArea(config) {
171
202
  resolveSeriesValue,
172
203
  closestIndex,
173
204
  });
174
- const target = resolveSharedTooltipTarget(sharedAnchor);
175
- const arrowEdge = resolveTooltipArrowEdge(position, sharedAnchor, target, measuredTooltip.width, measuredTooltip.height);
176
- const tooltipPosition = getAnchoredTooltipPosition(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
205
+ const placementBounds = getTooltipPlacementBounds(svgNode);
206
+ const visibleAnchor = clipTooltipAnchorToBounds(sharedAnchor, placementBounds);
207
+ const target = resolveSharedTooltipTarget(visibleAnchor);
208
+ const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, placementBounds);
209
+ const tooltipPosition = getAnchoredTooltipPosition(visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
177
210
  if (!tooltipPosition) {
178
211
  dom.hideTooltipSelection(tooltip);
179
212
  return;
180
213
  }
181
214
  dom.renderTooltipWithoutConnector(tooltip, tooltipPosition.left, tooltipPosition.top);
182
215
  };
183
- const getSeriesTooltipAnchor = (currentSeries, dataPoint, index) => {
184
- const value = resolveSeriesValue(currentSeries, dataPoint, index);
216
+ const getSeriesTooltipAnchor = (currentSeries, index, value) => {
185
217
  if (!Number.isFinite(value)) {
186
218
  return null;
187
219
  }
@@ -198,6 +230,12 @@ export function attachXYTooltipArea(config) {
198
230
  };
199
231
  const showSplitTooltipAtIndex = (closestIndex) => {
200
232
  const dataPoint = data[closestIndex];
233
+ const svgNode = svg.node();
234
+ if (!svgNode) {
235
+ dom.hideSplitTooltips();
236
+ return;
237
+ }
238
+ const placementBounds = getTooltipPlacementBounds(svgNode);
201
239
  updateVisualStateAtIndex(closestIndex);
202
240
  dom.hideTooltipSelection(tooltip);
203
241
  const layouts = [];
@@ -206,20 +244,26 @@ export function attachXYTooltipArea(config) {
206
244
  if (rawValue === null || rawValue === undefined) {
207
245
  return;
208
246
  }
209
- const anchor = getSeriesTooltipAnchor(currentSeries, dataPoint, closestIndex);
247
+ const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
248
+ const anchor = getSeriesTooltipAnchor(currentSeries, closestIndex, value);
210
249
  if (!anchor) {
211
250
  return;
212
251
  }
213
- const splitTooltip = dom.getSplitTooltip(seriesIndex, theme);
252
+ const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
253
+ const splitTooltip = dom.getSplitTooltip(seriesIndex, theme, getSeriesTooltipStyle(currentSeries, dataPoint, closestIndex));
214
254
  const content = buildSplitTooltipContent(dataPoint, currentSeries);
215
255
  const measuredTooltip = dom.measureTooltip(splitTooltip, content);
216
256
  if (!measuredTooltip) {
217
257
  return;
218
258
  }
219
- const target = resolveSplitTooltipTarget(currentSeries, anchor, barAnchorPosition);
259
+ const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
260
+ const targetMode = currentSeries.type === 'bar' &&
261
+ resolvedBarAnchorPosition === 'auto'
262
+ ? 'auto'
263
+ : 'fixed';
220
264
  layouts.push({
221
265
  div: splitTooltip,
222
- anchor,
266
+ anchor: visibleAnchor,
223
267
  width: measuredTooltip.width,
224
268
  height: measuredTooltip.height,
225
269
  left: 0,
@@ -228,39 +272,104 @@ export function attachXYTooltipArea(config) {
228
272
  targetX: target.x,
229
273
  targetY: target.y,
230
274
  order: seriesIndex,
275
+ targetMode,
276
+ valueSign: getTooltipValueSign(value),
231
277
  });
232
278
  });
233
279
  if (layouts.length === 0) {
234
280
  dom.hideSplitTooltips();
235
281
  return;
236
282
  }
237
- resolveSplitTooltipPositions(layouts, position);
283
+ resolveSplitTooltipPositions(layouts, resolvedPosition, placementBounds, isHorizontal);
238
284
  dom.hideUnusedSplitTooltips(layouts.map((layout) => layout.div));
239
285
  layouts.forEach((layout) => {
240
286
  dom.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
241
287
  });
242
288
  };
243
- let activeTooltipIndex = null;
289
+ const showSingleTooltip = (request) => {
290
+ const dataPoint = data[request.index];
291
+ const svgNode = svg.node();
292
+ if (!svgNode) {
293
+ dom.hideTooltipSelection(tooltip);
294
+ return null;
295
+ }
296
+ const placementBounds = getTooltipPlacementBounds(svgNode);
297
+ const candidates = [];
298
+ series.forEach((currentSeries, seriesIndex) => {
299
+ const rawValue = dataPoint[currentSeries.dataKey];
300
+ if (rawValue === null || rawValue === undefined) {
301
+ return;
302
+ }
303
+ const value = resolveSeriesValue(currentSeries, dataPoint, request.index);
304
+ const anchor = getSeriesTooltipAnchor(currentSeries, request.index, value);
305
+ if (!anchor) {
306
+ return;
307
+ }
308
+ const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
309
+ const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
310
+ candidates.push({
311
+ series: currentSeries,
312
+ seriesIndex,
313
+ anchor: visibleAnchor,
314
+ target,
315
+ });
316
+ });
317
+ const selectedCandidate = candidates.find((candidate) => candidate.seriesIndex === request.seriesIndex) ?? getClosestSingleTooltipCandidate(candidates, request);
318
+ updateVisualStateAtIndex(request.index);
319
+ dom.hideSplitTooltips();
320
+ if (!selectedCandidate) {
321
+ dom.hideTooltipSelection(tooltip);
322
+ return null;
323
+ }
324
+ dom.applyRootTooltipStyles(theme, getSeriesTooltipStyle(selectedCandidate.series, dataPoint, request.index));
325
+ const content = buildSplitTooltipContent(dataPoint, selectedCandidate.series);
326
+ const measuredTooltip = dom.measureTooltip(tooltip, content);
327
+ if (!measuredTooltip) {
328
+ dom.hideTooltipSelection(tooltip);
329
+ return null;
330
+ }
331
+ const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, placementBounds);
332
+ const tooltipPosition = getAnchoredTooltipPosition(selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
333
+ if (!tooltipPosition) {
334
+ dom.hideTooltipSelection(tooltip);
335
+ return null;
336
+ }
337
+ dom.renderTooltipWithConnector(tooltip, arrowEdge, tooltipPosition.left, tooltipPosition.top, measuredTooltip.width, measuredTooltip.height, selectedCandidate.target.x, selectedCandidate.target.y, selectedCandidate.anchor);
338
+ return {
339
+ index: request.index,
340
+ seriesIndex: selectedCandidate.seriesIndex,
341
+ };
342
+ };
343
+ let activeTooltipRequest = null;
244
344
  const hideTooltip = () => {
245
- activeTooltipIndex = null;
345
+ activeTooltipRequest = null;
246
346
  dom.hideTooltipSelection(tooltip);
247
347
  dom.hideSplitTooltips();
248
348
  clearVisualState();
249
349
  };
250
350
  const tooltipSvgNode = svg.node();
251
- const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (index) => {
252
- activeTooltipIndex = index;
351
+ const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (request) => {
352
+ if (mode === 'single') {
353
+ activeTooltipRequest = showSingleTooltip(request);
354
+ return;
355
+ }
356
+ activeTooltipRequest = { index: request.index };
253
357
  if (mode === 'split') {
254
- showSplitTooltipAtIndex(index);
358
+ showSplitTooltipAtIndex(request.index);
255
359
  return;
256
360
  }
257
- showSharedTooltipAtIndex(index);
361
+ showSharedTooltipAtIndex(request.index);
258
362
  });
259
363
  overlay
260
364
  .on('mousemove', (event) => {
261
365
  const [mouseX, mouseY] = pointer(event, svg.node());
262
366
  const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
263
- queuedRender.request(closestIndex);
367
+ const pointerPosition = getDocumentPointerPosition(event);
368
+ queuedRender.request({
369
+ index: closestIndex,
370
+ pointerX: pointerPosition.x,
371
+ pointerY: pointerPosition.y,
372
+ });
264
373
  })
265
374
  .on('mouseout', () => {
266
375
  if (isTooltipFocusTarget(document.activeElement)) {
@@ -296,7 +405,13 @@ export function attachXYTooltipArea(config) {
296
405
  }
297
406
  queuedRender.cancel();
298
407
  select(this).attr('stroke', '#111827').attr('stroke-width', 2);
299
- activeTooltipIndex = currentIndex;
408
+ if (mode === 'single') {
409
+ activeTooltipRequest = showSingleTooltip({
410
+ index: currentIndex,
411
+ });
412
+ return;
413
+ }
414
+ activeTooltipRequest = { index: currentIndex };
300
415
  if (mode === 'split') {
301
416
  showSplitTooltipAtIndex(currentIndex);
302
417
  return;
@@ -324,10 +439,10 @@ export function attachXYTooltipArea(config) {
324
439
  focusTargetNodes[nextIndex].focus();
325
440
  });
326
441
  return attachTooltipScrollListeners(tooltipSvgNode, () => {
327
- if (activeTooltipIndex === null) {
442
+ if (activeTooltipRequest === null) {
328
443
  return;
329
444
  }
330
- queuedRender.request(activeTooltipIndex);
445
+ queuedRender.request(activeTooltipRequest);
331
446
  });
332
447
  }
333
448
  function normalizeFormatterValue(value) {
@@ -376,6 +491,33 @@ function getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizo
376
491
  }
377
492
  return closestIndex;
378
493
  }
494
+ function getDocumentPointerPosition(event) {
495
+ return {
496
+ x: event.clientX + window.scrollX,
497
+ y: event.clientY + window.scrollY,
498
+ };
499
+ }
500
+ function getClosestSingleTooltipCandidate(candidates, request) {
501
+ if (candidates.length === 0) {
502
+ return null;
503
+ }
504
+ if (request.pointerX === undefined ||
505
+ request.pointerY === undefined ||
506
+ !Number.isFinite(request.pointerX) ||
507
+ !Number.isFinite(request.pointerY)) {
508
+ return candidates[0];
509
+ }
510
+ return candidates.reduce((closest, candidate) => {
511
+ const closestDistance = getSingleTooltipDistance(closest, request);
512
+ const candidateDistance = getSingleTooltipDistance(candidate, request);
513
+ return candidateDistance < closestDistance ? candidate : closest;
514
+ });
515
+ }
516
+ function getSingleTooltipDistance(candidate, request) {
517
+ const deltaX = (request.pointerX ?? 0) - candidate.target.x;
518
+ const deltaY = (request.pointerY ?? 0) - candidate.target.y;
519
+ return Math.hypot(deltaX, deltaY);
520
+ }
379
521
  function resolveSharedTooltipAnchor({ svgNode, dataPoint, dataPointPosition, series, x, y, isHorizontal, resolveSeriesValue, closestIndex, }) {
380
522
  const svgRect = svgNode.getBoundingClientRect();
381
523
  const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
@@ -467,25 +609,25 @@ function resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valu
467
609
  centerY: anchorY,
468
610
  };
469
611
  }
470
- function createQueuedTooltipRender(tooltipSvgNode, renderTooltipAtIndex) {
612
+ function createQueuedTooltipRender(tooltipSvgNode, renderTooltip) {
471
613
  let pendingTooltipFrame = null;
472
- let pendingTooltipIndex = null;
473
- const request = (index) => {
474
- pendingTooltipIndex = index;
614
+ let pendingTooltipRequest = null;
615
+ const request = (nextRequest) => {
616
+ pendingTooltipRequest = nextRequest;
475
617
  if (pendingTooltipFrame !== null) {
476
618
  return;
477
619
  }
478
620
  pendingTooltipFrame = requestFrame(() => {
479
- const nextIndex = pendingTooltipIndex;
621
+ const currentRequest = pendingTooltipRequest;
480
622
  pendingTooltipFrame = null;
481
- pendingTooltipIndex = null;
482
- if (nextIndex === null) {
623
+ pendingTooltipRequest = null;
624
+ if (currentRequest === null) {
483
625
  return;
484
626
  }
485
627
  if (!tooltipSvgNode?.isConnected) {
486
628
  return;
487
629
  }
488
- renderTooltipAtIndex(nextIndex);
630
+ renderTooltip(currentRequest);
489
631
  });
490
632
  };
491
633
  const cancel = () => {
@@ -494,7 +636,7 @@ function createQueuedTooltipRender(tooltipSvgNode, renderTooltipAtIndex) {
494
636
  }
495
637
  cancelFrame(pendingTooltipFrame);
496
638
  pendingTooltipFrame = null;
497
- pendingTooltipIndex = null;
639
+ pendingTooltipRequest = null;
498
640
  };
499
641
  return { request, cancel };
500
642
  }
@@ -577,6 +719,47 @@ function getNextFocusTargetIndex(currentIndex, key, focusTargetCount) {
577
719
  return -1;
578
720
  }
579
721
  }
722
+ function getTooltipPlacementBounds(svgNode) {
723
+ let bounds = getSplitTooltipViewportBounds();
724
+ let currentElement = svgNode.parentElement;
725
+ while (currentElement) {
726
+ if (isScrollableElement(currentElement)) {
727
+ bounds = intersectTooltipBounds(bounds, getElementTooltipBounds(currentElement));
728
+ }
729
+ currentElement = currentElement.parentElement;
730
+ }
731
+ return bounds;
732
+ }
733
+ function getElementTooltipBounds(element) {
734
+ const rect = element.getBoundingClientRect();
735
+ const left = rect.left + window.scrollX;
736
+ const top = rect.top + window.scrollY;
737
+ return {
738
+ minLeft: left,
739
+ maxRight: left + rect.width,
740
+ minTop: top,
741
+ maxBottom: top + rect.height,
742
+ };
743
+ }
744
+ function intersectTooltipBounds(bounds, nextBounds) {
745
+ if (nextBounds.maxRight <= nextBounds.minLeft ||
746
+ nextBounds.maxBottom <= nextBounds.minTop) {
747
+ return bounds;
748
+ }
749
+ const minLeft = Math.max(bounds.minLeft, nextBounds.minLeft);
750
+ const maxRight = Math.min(bounds.maxRight, nextBounds.maxRight);
751
+ const minTop = Math.max(bounds.minTop, nextBounds.minTop);
752
+ const maxBottom = Math.min(bounds.maxBottom, nextBounds.maxBottom);
753
+ if (maxRight <= minLeft || maxBottom <= minTop) {
754
+ return bounds;
755
+ }
756
+ return {
757
+ minLeft,
758
+ maxRight,
759
+ minTop,
760
+ maxBottom,
761
+ };
762
+ }
580
763
  function attachTooltipScrollListeners(svgNode, onScroll) {
581
764
  if (!svgNode) {
582
765
  return null;
package/dist/tooltip.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from 'd3';
2
- import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase, ScaleType, TooltipMode, TooltipPosition, TooltipBarAnchorPosition, TooltipTransitionConfig } from './types.js';
2
+ import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase, ScaleType, TooltipMode, TooltipPosition, TooltipBarAnchorPosition, TooltipTransitionConfig, TooltipColorMode } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  import type { PlotAreaBounds } from './layout-manager.js';
5
5
  import { type XYTooltipSeries } from './tooltip/types.js';
@@ -10,6 +10,7 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
10
10
  readonly mode: TooltipMode;
11
11
  readonly position: TooltipPosition;
12
12
  readonly barAnchorPosition: TooltipBarAnchorPosition;
13
+ readonly colorMode: TooltipColorMode;
13
14
  readonly maxWidth: number;
14
15
  readonly transition: Required<TooltipTransitionConfig>;
15
16
  readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
package/dist/tooltip.js CHANGED
@@ -34,6 +34,12 @@ export class Tooltip {
34
34
  writable: true,
35
35
  value: void 0
36
36
  });
37
+ Object.defineProperty(this, "colorMode", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: void 0
42
+ });
37
43
  Object.defineProperty(this, "maxWidth", {
38
44
  enumerable: true,
39
45
  configurable: true,
@@ -82,12 +88,13 @@ export class Tooltip {
82
88
  writable: true,
83
89
  value: null
84
90
  });
85
- const { mode = 'split', position = 'side', barAnchorPosition = 'middle', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
91
+ const { mode = 'split', position = 'auto', barAnchorPosition = 'auto', colorMode = 'theme', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
86
92
  const tooltipId = Tooltip.nextTooltipId++;
87
93
  this.id = `iisChartTooltip-${tooltipId}`;
88
94
  this.mode = mode;
89
95
  this.position = position;
90
96
  this.barAnchorPosition = barAnchorPosition;
97
+ this.colorMode = colorMode;
91
98
  this.maxWidth =
92
99
  maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
93
100
  ? maxWidth
@@ -112,6 +119,7 @@ export class Tooltip {
112
119
  mode: this.mode,
113
120
  position: this.position,
114
121
  barAnchorPosition: this.barAnchorPosition,
122
+ colorMode: this.colorMode,
115
123
  maxWidth: this.maxWidth,
116
124
  transition: this.transition,
117
125
  formatter: this.formatter,
@@ -153,6 +161,7 @@ export class Tooltip {
153
161
  mode: this.mode,
154
162
  position: this.position,
155
163
  barAnchorPosition: this.barAnchorPosition,
164
+ colorMode: this.colorMode,
156
165
  formatter: this.formatter,
157
166
  labelFormatter: this.labelFormatter,
158
167
  customFormatter: this.customFormatter,
package/dist/types.d.ts CHANGED
@@ -299,9 +299,10 @@ export type GridConfigBase = {
299
299
  export type GridConfig = GridConfigBase & {
300
300
  exportHooks?: ExportHooks<GridConfigBase>;
301
301
  };
302
- export type TooltipMode = 'shared' | 'split';
303
- export type TooltipPosition = 'side' | 'vertical';
304
- export type TooltipBarAnchorPosition = 'top' | 'middle';
302
+ export type TooltipMode = 'shared' | 'split' | 'single';
303
+ export type TooltipPosition = 'auto' | 'side' | 'vertical';
304
+ export type TooltipBarAnchorPosition = 'auto' | 'top' | 'middle';
305
+ export type TooltipColorMode = 'theme' | 'series';
305
306
  export type TooltipTransitionConfig = {
306
307
  show?: boolean;
307
308
  duration?: number;
@@ -311,6 +312,7 @@ export type TooltipConfigBase = {
311
312
  mode?: TooltipMode;
312
313
  position?: TooltipPosition;
313
314
  barAnchorPosition?: TooltipBarAnchorPosition;
315
+ colorMode?: TooltipColorMode;
314
316
  maxWidth?: number;
315
317
  transition?: TooltipTransitionConfig;
316
318
  formatter?: SeriesValueFormatter;
@@ -125,9 +125,10 @@ Renders interactive tooltips on hover and keyboard focus.
125
125
 
126
126
  ```typescript
127
127
  new Tooltip({
128
- mode?: 'shared' | 'split',
129
- position?: 'side' | 'vertical',
130
- barAnchorPosition?: 'top' | 'middle',
128
+ mode?: 'shared' | 'split' | 'single',
129
+ position?: 'auto' | 'side' | 'vertical',
130
+ barAnchorPosition?: 'auto' | 'top' | 'middle',
131
+ colorMode?: 'theme' | 'series',
131
132
  maxWidth?: number, // default: 280
132
133
  transition?: {
133
134
  show?: boolean,
@@ -146,18 +147,27 @@ The formatter receives:
146
147
 
147
148
  Tooltip modes:
148
149
 
149
- - `shared` - One tooltip per hovered category/value group
150
150
  - `split` - Default. One tooltip per visible series at the hovered category/value group
151
+ - `single` - One tooltip for the closest visible series at the hovered category/value group
152
+ - `shared` - One grouped tooltip per hovered category/value group
151
153
 
152
- Shared tooltips omit series whose value is `null` or `undefined` for the hovered
153
- data point. Shared `customFormatter` callbacks receive that filtered series list.
154
+ Single and shared tooltips omit series whose value is `null` or `undefined` for
155
+ the hovered data point. `customFormatter` receives the current series in
156
+ `split` and `single` modes, and the visible series list in `shared` mode.
154
157
 
155
- Use `position: 'side' | 'vertical'` for split tooltip placement. For bars,
156
- `barAnchorPosition` controls whether connectors point to the `top` or `middle`
157
- of each bar.
158
+ Use `position: 'auto' | 'side' | 'vertical'` for XY tooltip placement.
159
+ `auto` is the default. It uses above/below placement for horizontal XY charts
160
+ and side placement elsewhere. For bar tooltips, `barAnchorPosition` defaults to
161
+ `auto`, which aims arrows inside the visible bar segment when possible.
162
+
163
+ For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
164
+ `position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
165
+ legacy configs, but horizontal bars resolve bar anchoring automatically.
158
166
 
159
167
  Tooltips default to a `280px` max width. Set `transition.show: true` to fade and
160
- slide tooltips between hovered positions. Tooltip colors use `theme.tooltip`.
168
+ slide tooltips between hovered positions. Tooltip colors use `theme.tooltip` by
169
+ default. Set `colorMode: 'series'` to color-code XY tooltips from the series
170
+ color, with matching background and border colors plus automatic contrast text.
161
171
 
162
172
  `defaultResponsiveConfig` switches tooltip components to `mode: 'shared'` at
163
173
  the `sm` breakpoint so compact XY charts use one grouped tooltip by default.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.16.0",
2
+ "version": "0.17.0",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,