@pushwoosh/kit-charts 0.0.22 → 0.0.24

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.
@@ -325,7 +325,8 @@ function ChartBody({
325
325
  anchor: {
326
326
  x: bounds.left + highest.anchor.x,
327
327
  y: bounds.top + highest.anchor.y
328
- }
328
+ },
329
+ reach: highest.reach ?? 0
329
330
  });
330
331
  }, [model, hitsAt, options.tooltip, drag]);
331
332
  const handlePointerLeave = useCallback(() => setHover(null), []);
@@ -578,6 +579,13 @@ function ChartBody({
578
579
  }), model.axisless ? null : _jsx(XAxisLine, {
579
580
  plot: model.plot,
580
581
  axis: model.xAxis
582
+ }), model.series.map(series => {
583
+ const ctx = series.hidden ? null : contextFor(series);
584
+ const overlay = ctx ? series.renderer.renderOverlay?.(ctx) : null;
585
+ return overlay ? _jsx("g", {
586
+ "data-series-index": series.index,
587
+ children: overlay
588
+ }, `overlay-${series.index}`) : null;
581
589
  }), decorations('over'), drag ? _jsx(ZoomBand, {
582
590
  x: Math.min(drag.from, drag.to),
583
591
  y: model.plot.y,
@@ -627,6 +635,7 @@ function ChartBody({
627
635
  children: options.chart?.resetZoomText ?? 'Reset zoom'
628
636
  }) : null, hover && tooltipContent ? _jsx(Tooltip, {
629
637
  at: hover.anchor,
638
+ reach: hover.reach,
630
639
  pointer: hover.pointer,
631
640
  options: options.tooltip,
632
641
  children: tooltipContent
@@ -62,14 +62,16 @@ export interface TooltipBoxProps {
62
62
  $shadow: string;
63
63
  }
64
64
  export declare const TooltipBox: import("styled-components").StyledComponent<"div", any, TooltipBoxProps, never>;
65
+ /** Edge of the box the beak sits on, named after that edge. */
66
+ export type TailSide = 'top' | 'right' | 'bottom' | 'left';
65
67
  export interface TailProps {
66
- $left: number;
67
- $size: number;
68
- $color: string;
69
- $below: boolean;
68
+ $offset: number;
69
+ $width: number;
70
+ $height: number;
71
+ $side: TailSide;
70
72
  }
71
- /** The callout beak, drawn as a CSS triangle so it inherits the box colour. */
72
- export declare const Tail: import("styled-components").StyledComponent<"span", any, TailProps, never>;
73
+ /** The callout beak; an SVG, so it can carry the box border and cut it at the base. */
74
+ export declare const Tail: import("styled-components").StyledComponent<"svg", any, TailProps, never>;
73
75
  export declare const TooltipTitle: import("styled-components").StyledComponent<"div", any, {}, never>;
74
76
  export declare const TooltipRow: import("styled-components").StyledComponent<"div", any, {}, never>;
75
77
  export declare const TooltipSwatch: import("styled-components").StyledComponent<"span", any, {
@@ -127,25 +127,21 @@ export const TooltipBox = styled.div.withConfig({
127
127
  }) => $color, ({
128
128
  $whiteSpace
129
129
  }) => $whiteSpace);
130
- /** The callout beak, drawn as a CSS triangle so it inherits the box colour. */
131
- export const Tail = styled.span.withConfig({
130
+ /** The side names the CSS edge the beak hangs off; the other axis carries its offset. */
131
+ function tailAnchor({
132
+ $side,
133
+ $offset,
134
+ $width,
135
+ $height
136
+ }) {
137
+ const along = $side === 'top' || $side === 'bottom' ? 'left' : 'top';
138
+ return `${$side}: -${$height}px; ${along}: ${$offset - $width / 2}px;`;
139
+ }
140
+ /** The callout beak; an SVG, so it can carry the box border and cut it at the base. */
141
+ export const Tail = styled.svg.withConfig({
132
142
  displayName: "Tail",
133
143
  componentId: "sc-1pit1b9-9"
134
- })(["position:absolute;", " left:", ";width:0;height:0;border-right:", " solid transparent;border-left:", " solid transparent;", ""], ({
135
- $below,
136
- $size
137
- }) => $below ? `top: -${$size}px;` : `bottom: -${$size}px;`, ({
138
- $left,
139
- $size
140
- }) => `${$left - $size}px`, ({
141
- $size
142
- }) => `${$size}px`, ({
143
- $size
144
- }) => `${$size}px`, ({
145
- $below,
146
- $size,
147
- $color
148
- }) => $below ? `border-bottom: ${$size}px solid ${$color};` : `border-top: ${$size}px solid ${$color};`);
144
+ })(["position:absolute;", ";overflow:visible;"], tailAnchor);
149
145
  export const TooltipTitle = styled.div.withConfig({
150
146
  displayName: "TooltipTitle",
151
147
  componentId: "sc-1pit1b9-10"
@@ -4,10 +4,12 @@ import { type TooltipOptions } from '../types/options';
4
4
  interface TooltipProps {
5
5
  /** Anchor the callout points at, in viewport coordinates. */
6
6
  at: Point;
7
+ /** How far the hovered mark reaches past the anchor, so the beak clears it. */
8
+ reach?: number;
7
9
  /** Pointer position, used when the tooltip follows the cursor instead. */
8
10
  pointer: Point;
9
11
  options: TooltipOptions | undefined;
10
12
  children: ReactNode;
11
13
  }
12
- export declare function Tooltip({ at, options, pointer, children, }: TooltipProps): ReactElement | null;
14
+ export declare function Tooltip({ at, reach: markReach, options, pointer, children, }: TooltipProps): ReactElement | null;
13
15
  export {};
@@ -2,23 +2,93 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { DEFAULTS, Tail, TooltipBox } from './Chart.styles';
5
- import { TOOLTIP_CURSOR_OFFSET, TOOLTIP_PADDING, TOOLTIP_RADIUS, TOOLTIP_TAIL, VIEWPORT_MARGIN } from '../layout/metrics';
5
+ import { TOOLTIP_ANCHOR_GAP, TOOLTIP_CURSOR_OFFSET, TOOLTIP_PADDING, TOOLTIP_RADIUS, TOOLTIP_TAIL_HEIGHT, TOOLTIP_TAIL_WIDTH, VIEWPORT_MARGIN } from '../layout/metrics';
6
6
  const SHADOW_COLOR = 'rgba(0, 0, 0, 0.12)';
7
7
  const DEFAULT_SHADOW = `0 2px 8px ${SHADOW_COLOR}`;
8
+ function clamp(value, min, max) {
9
+ return Math.min(Math.max(value, min), Math.max(min, max));
10
+ }
11
+ /** Keeps a box of `size` inside `viewport`, margins included. */
12
+ function fit(value, size, viewport) {
13
+ return clamp(value, VIEWPORT_MARGIN, viewport - size - VIEWPORT_MARGIN);
14
+ }
15
+ /** Two slopes only: left open so the fill closes it without a stroke along the base. */
16
+ function beakPath(side) {
17
+ const long = TOOLTIP_TAIL_WIDTH;
18
+ const deep = TOOLTIP_TAIL_HEIGHT;
19
+ const half = long / 2;
20
+ switch (side) {
21
+ case 'top':
22
+ return `M0 ${deep} L${half} 0 L${long} ${deep}`;
23
+ case 'left':
24
+ return `M${deep} 0 L0 ${half} L${deep} ${long}`;
25
+ case 'right':
26
+ return `M0 0 L${deep} ${half} L0 ${long}`;
27
+ default:
28
+ return `M0 0 L${half} ${deep} L${long} 0`;
29
+ }
30
+ }
31
+ /** The beak viewport: deep across the edge it leaves, long along it. */
32
+ function beakBox(side) {
33
+ return side === 'left' || side === 'right' ? {
34
+ width: TOOLTIP_TAIL_HEIGHT,
35
+ height: TOOLTIP_TAIL_WIDTH
36
+ } : {
37
+ width: TOOLTIP_TAIL_WIDTH,
38
+ height: TOOLTIP_TAIL_HEIGHT
39
+ };
40
+ }
41
+ /** The 1px of box border the beak covers where it leaves the edge. */
42
+ function beakSeam(side) {
43
+ const long = TOOLTIP_TAIL_WIDTH - 1;
44
+ const deep = TOOLTIP_TAIL_HEIGHT;
45
+ switch (side) {
46
+ case 'top':
47
+ return {
48
+ x: 0.5,
49
+ y: deep,
50
+ width: long,
51
+ height: 1
52
+ };
53
+ case 'left':
54
+ return {
55
+ x: deep,
56
+ y: 0.5,
57
+ width: 1,
58
+ height: long
59
+ };
60
+ case 'right':
61
+ return {
62
+ x: -1,
63
+ y: 0.5,
64
+ width: 1,
65
+ height: long
66
+ };
67
+ default:
68
+ return {
69
+ x: 0.5,
70
+ y: -1,
71
+ width: long,
72
+ height: 1
73
+ };
74
+ }
75
+ }
8
76
  export function Tooltip({
9
77
  at,
78
+ reach: markReach = 0,
10
79
  options,
11
80
  pointer,
12
81
  children
13
82
  }) {
14
83
  const ref = useRef(null);
15
84
  const callout = (options?.shape ?? 'callout') === 'callout' && !options?.followPointer;
85
+ const radius = options?.borderRadius ?? TOOLTIP_RADIUS;
16
86
  const target = options?.followPointer ? pointer : at;
17
87
  const [placement, setPlacement] = useState({
18
88
  x: target.x + TOOLTIP_CURSOR_OFFSET,
19
89
  y: target.y + TOOLTIP_CURSOR_OFFSET,
20
- tailX: null,
21
- below: true
90
+ tail: null,
91
+ side: 'top'
22
92
  });
23
93
  useLayoutEffect(() => {
24
94
  const node = ref.current;
@@ -29,30 +99,49 @@ export function Tooltip({
29
99
  width,
30
100
  height
31
101
  } = node.getBoundingClientRect();
32
- const limitX = value => Math.min(Math.max(VIEWPORT_MARGIN, value), Math.max(VIEWPORT_MARGIN, window.innerWidth - width - VIEWPORT_MARGIN));
102
+ const fitX = value => fit(value, width, window.innerWidth);
103
+ const fitY = value => fit(value, height, window.innerHeight);
33
104
  if (!callout) {
34
105
  const flipX = target.x + TOOLTIP_CURSOR_OFFSET + width > window.innerWidth - VIEWPORT_MARGIN;
35
106
  const flipY = target.y + TOOLTIP_CURSOR_OFFSET + height > window.innerHeight - VIEWPORT_MARGIN;
36
107
  setPlacement({
37
- x: limitX(flipX ? target.x - TOOLTIP_CURSOR_OFFSET - width : target.x + TOOLTIP_CURSOR_OFFSET),
38
- y: Math.max(VIEWPORT_MARGIN, flipY ? target.y - TOOLTIP_CURSOR_OFFSET - height : target.y + TOOLTIP_CURSOR_OFFSET),
39
- tailX: null,
40
- below: !flipY
108
+ x: fitX(flipX ? target.x - TOOLTIP_CURSOR_OFFSET - width : target.x + TOOLTIP_CURSOR_OFFSET),
109
+ y: fitY(flipY ? target.y - TOOLTIP_CURSOR_OFFSET - height : target.y + TOOLTIP_CURSOR_OFFSET),
110
+ tail: null,
111
+ side: flipY ? 'bottom' : 'top'
112
+ });
113
+ return;
114
+ }
115
+ // What the beak needs of an edge: its own half plus the rounded corner beside it.
116
+ const beakReach = TOOLTIP_TAIL_WIDTH / 2 + radius;
117
+ const standoff = TOOLTIP_TAIL_HEIGHT + TOOLTIP_ANCHOR_GAP + markReach;
118
+ const centred = target.x - width / 2;
119
+ const x = fitX(centred);
120
+ // Only the viewport can push the box off centre; if that pushed the anchor past
121
+ // the edge the beak reaches, the box stands beside the point instead of over it.
122
+ const beside = x !== centred && (target.x < x + beakReach || target.x > x + width - beakReach);
123
+ if (beside) {
124
+ const onLeft = target.x > x + width - beakReach;
125
+ const y = fitY(target.y - height / 2);
126
+ setPlacement({
127
+ x: fitX(onLeft ? target.x - standoff - width : target.x + standoff),
128
+ y,
129
+ tail: clamp(target.y - y, beakReach, height - beakReach),
130
+ side: onLeft ? 'right' : 'left'
41
131
  });
42
132
  return;
43
133
  }
44
- // Sit above the point with the tail pointing down at it, and flip under the
134
+ // Sit above the point with the beak pointing down at it, and flip under the
45
135
  // point when there is no room. The box may be nudged sideways to stay on
46
- // screen, so the tail keeps its own offset and still points at the anchor.
47
- const below = target.y - height - TOOLTIP_TAIL < VIEWPORT_MARGIN;
48
- const x = limitX(target.x - width / 2);
136
+ // screen, so the beak keeps its own offset and still points at the anchor.
137
+ const below = target.y - height - standoff < VIEWPORT_MARGIN;
49
138
  setPlacement({
50
139
  x,
51
- y: below ? target.y + TOOLTIP_TAIL : target.y - height - TOOLTIP_TAIL,
52
- tailX: Math.min(Math.max(TOOLTIP_TAIL * 2, target.x - x), Math.max(TOOLTIP_TAIL * 2, width - TOOLTIP_TAIL * 2)),
53
- below
140
+ y: below ? target.y + standoff : target.y - height - standoff,
141
+ tail: clamp(target.x - x, beakReach, width - beakReach),
142
+ side: below ? 'top' : 'bottom'
54
143
  });
55
- }, [target.x, target.y, callout, children]);
144
+ }, [target.x, target.y, markReach, callout, radius, children]);
56
145
  if (typeof document === 'undefined') {
57
146
  return null;
58
147
  }
@@ -61,26 +150,42 @@ export function Tooltip({
61
150
  // so the tabular tooltip is a light card unless the caller says otherwise.
62
151
  const tabular = Boolean(options?.table);
63
152
  const background = options?.backgroundColor ?? (tabular ? DEFAULTS.surface : DEFAULTS.tooltipSurface);
153
+ const borderColor = options?.borderColor ?? (tabular ? DEFAULTS.border : 'transparent');
64
154
  const color = options?.style?.color ?? (tabular ? DEFAULTS.text : DEFAULTS.tooltipText);
155
+ const beak = beakBox(placement.side);
65
156
  return createPortal(_jsxs(TooltipBox, {
66
157
  ref: ref,
67
158
  role: "tooltip",
68
159
  "$x": placement.x,
69
160
  "$y": placement.y,
70
161
  "$padding": options?.padding ?? TOOLTIP_PADDING,
71
- "$radius": options?.borderRadius ?? TOOLTIP_RADIUS,
72
- "$borderColor": options?.borderColor ?? (tabular ? DEFAULTS.border : 'transparent'),
162
+ "$radius": radius,
163
+ "$borderColor": borderColor,
73
164
  "$background": background,
74
165
  "$color": color,
75
166
  "$fontFamily": options?.style?.fontFamily ?? DEFAULTS.fontFamily,
76
167
  "$fontSize": options?.style?.fontSize ?? DEFAULTS.fontSize,
77
168
  "$whiteSpace": options?.style?.whiteSpace ?? 'nowrap',
78
169
  "$shadow": shadow ? `${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 2}px ${shadow.width ?? 8}px ` + `${shadow.color ?? SHADOW_COLOR}` : DEFAULT_SHADOW,
79
- children: [children, placement.tailX === null ? null : _jsx(Tail, {
80
- "$left": placement.tailX,
81
- "$size": TOOLTIP_TAIL,
82
- "$color": background,
83
- "$below": placement.below
170
+ children: [children, placement.tail === null ? null : _jsxs(Tail, {
171
+ "$offset": placement.tail,
172
+ "$width": TOOLTIP_TAIL_WIDTH,
173
+ "$height": TOOLTIP_TAIL_HEIGHT,
174
+ "$side": placement.side,
175
+ width: beak.width,
176
+ height: beak.height,
177
+ viewBox: `0 0 ${beak.width} ${beak.height}`,
178
+ "aria-hidden": true,
179
+ children: [_jsx("path", {
180
+ d: beakPath(placement.side),
181
+ fill: background,
182
+ stroke: borderColor,
183
+ strokeWidth: 1,
184
+ strokeLinejoin: "round"
185
+ }), _jsx("rect", {
186
+ ...beakSeam(placement.side),
187
+ fill: background
188
+ })]
84
189
  })]
85
190
  }), document.body);
86
191
  }
@@ -36,8 +36,11 @@ export declare const HINT_ICON_GAP = 6;
36
36
  export declare const HINT_PADDING = 12;
37
37
  /** Between the title block and the plot below it. */
38
38
  export declare const TITLE_BLOCK_GAP = 10;
39
- /** Tooltip beak: both its height and half its width. */
40
- export declare const TOOLTIP_TAIL = 7;
39
+ /** Tooltip beak, as the design system draws it. */
40
+ export declare const TOOLTIP_TAIL_WIDTH = 14;
41
+ export declare const TOOLTIP_TAIL_HEIGHT = 4;
42
+ /** Air between the beak tip and the edge of the mark it points at. */
43
+ export declare const TOOLTIP_ANCHOR_GAP = 8;
41
44
  /** Tooltip to the cursor, when it follows the pointer instead of a point. */
42
45
  export declare const TOOLTIP_CURSOR_OFFSET = 12;
43
46
  /** Keep a floating element this far inside the viewport. */
@@ -36,8 +36,11 @@ export const HINT_ICON_GAP = 6;
36
36
  export const HINT_PADDING = 12;
37
37
  /** Between the title block and the plot below it. */
38
38
  export const TITLE_BLOCK_GAP = 10;
39
- /** Tooltip beak: both its height and half its width. */
40
- export const TOOLTIP_TAIL = 7;
39
+ /** Tooltip beak, as the design system draws it. */
40
+ export const TOOLTIP_TAIL_WIDTH = 14;
41
+ export const TOOLTIP_TAIL_HEIGHT = 4;
42
+ /** Air between the beak tip and the edge of the mark it points at. */
43
+ export const TOOLTIP_ANCHOR_GAP = 8;
41
44
  /** Tooltip to the cursor, when it follows the pointer instead of a point. */
42
45
  export const TOOLTIP_CURSOR_OFFSET = 12;
43
46
  /** Keep a floating element this far inside the viewport. */
@@ -61,6 +61,8 @@ export interface PointHit {
61
61
  y: number | null;
62
62
  value: number | null;
63
63
  anchor: Point;
64
+ /** How far the hovered mark reaches past `anchor`; 0 when it draws none. */
65
+ reach?: number;
64
66
  distance: number;
65
67
  }
66
68
  export interface LegendItem {
@@ -95,6 +97,8 @@ export interface SeriesRenderer<TSeries extends SeriesOptionsBase = SeriesOption
95
97
  readonly animatesGeometry?: boolean;
96
98
  extendDomain(series: TSeries, acc: DomainAcc): DomainAcc;
97
99
  render(ctx: RenderContext<TSeries>): ReactNode;
100
+ /** Drawn above every series and the axis line — the hovered point marker lives here. */
101
+ renderOverlay?(ctx: RenderContext<TSeries>): ReactNode;
98
102
  hitTest(ctx: RenderContext<TSeries>, pointer: Point): PointHit[] | null;
99
103
  legendItems(series: TSeries, seriesIndex: number, colors: readonly string[]): LegendItem[];
100
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/kit-charts",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Small React chart library on SVG, with a Highcharts-shaped configuration model",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "directory": "dist"
27
27
  },
28
28
  "dependencies": {
29
- "@pushwoosh/kit-constants": "^2.0.0"
29
+ "@pushwoosh/kit-constants": "^2.1.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@pushwoosh/frontend-builder-engine": "^3.1.0",
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { drawable, projectSeries, toAreaPath, toLinePath } from '../../core/geometry/path';
3
- import { showsHoverMarker } from '../../core/options/normalize';
4
3
  import { includeZero } from '../shared/domain';
5
4
  import { nearestByX } from '../shared/hit';
6
5
  import { seriesLegend } from '../shared/legend';
7
- import { Markers } from '../shared/Markers';
6
+ import { hoverMarkerFor, Markers } from '../shared/Markers';
8
7
  const DEFAULT_FILL_OPACITY = 0.75;
9
8
  export const area = {
10
9
  type: 'area',
@@ -21,10 +20,6 @@ export const area = {
21
20
  }
22
21
  const color = ctx.color;
23
22
  const lineWidth = ctx.series.lineWidth ?? 2;
24
- const hoveredIndex = ctx.hovered?.pointIndex;
25
- const hovered = hoveredIndex === undefined ? undefined : projected.find(({
26
- point
27
- }) => point.index === hoveredIndex);
28
23
  return _jsxs("g", {
29
24
  children: [_jsx("path", {
30
25
  d: toAreaPath(slots),
@@ -42,16 +37,10 @@ export const area = {
42
37
  marker: ctx.series.marker,
43
38
  projected: projected,
44
39
  color: color
45
- }), hovered && showsHoverMarker(ctx.series.marker) ? _jsx("circle", {
46
- cx: hovered.px,
47
- cy: hovered.py,
48
- r: ctx.series.marker?.states?.hover?.radius ?? 4,
49
- fill: color,
50
- stroke: "#FFFFFF",
51
- strokeWidth: 2
52
- }) : null]
40
+ })]
53
41
  });
54
42
  },
43
+ renderOverlay: ctx => hoverMarkerFor(ctx),
55
44
  hitTest: (ctx, pointer) => nearestByX(ctx, pointer),
56
45
  legendItems: (series, seriesIndex) => seriesLegend(series, seriesIndex, 'square')
57
46
  };
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { drawable, projectSeries, toLinePath } from '../../core/geometry/path';
3
- import { showsHoverMarker } from '../../core/options/normalize';
4
3
  import { nearestByX } from '../shared/hit';
5
4
  import { seriesLegend } from '../shared/legend';
6
- import { Markers } from '../shared/Markers';
5
+ import { hoverMarkerFor, Markers } from '../shared/Markers';
7
6
  export const line = {
8
7
  type: 'line',
9
8
  scales: {
@@ -18,10 +17,6 @@ export const line = {
18
17
  return null;
19
18
  }
20
19
  const color = ctx.color;
21
- const hoveredIndex = ctx.hovered?.pointIndex;
22
- const hovered = hoveredIndex === undefined ? undefined : projected.find(({
23
- point
24
- }) => point.index === hoveredIndex);
25
20
  return _jsxs("g", {
26
21
  children: [_jsx("path", {
27
22
  d: toLinePath(slots),
@@ -34,16 +29,10 @@ export const line = {
34
29
  marker: ctx.series.marker,
35
30
  projected: projected,
36
31
  color: color
37
- }), hovered && showsHoverMarker(ctx.series.marker) ? _jsx("circle", {
38
- cx: hovered.px,
39
- cy: hovered.py,
40
- r: ctx.series.marker?.states?.hover?.radius ?? 4,
41
- fill: color,
42
- stroke: "#FFFFFF",
43
- strokeWidth: 2
44
- }) : null]
32
+ })]
45
33
  });
46
34
  },
35
+ renderOverlay: ctx => hoverMarkerFor(ctx),
47
36
  hitTest: (ctx, pointer) => nearestByX(ctx, pointer),
48
37
  legendItems: (series, seriesIndex) => seriesLegend(series, seriesIndex, 'line')
49
38
  };
@@ -1,6 +1,11 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type Projected } from '../../core/geometry/path';
3
3
  import { type MarkerOptions } from '../../core/types/options';
4
+ import { type RenderContext } from '../../core/types/renderer';
5
+ /** How far the hover mark reaches past its point, so a tooltip can clear it. */
6
+ export declare const hoverReachOf: (marker: MarkerOptions | undefined) => number;
7
+ /** The hovered point of a series, or nothing when it has none. */
8
+ export declare function hoverMarkerFor(ctx: RenderContext): ReactNode;
4
9
  export interface MarkersProps {
5
10
  marker: MarkerOptions | undefined;
6
11
  projected: readonly Projected[];
@@ -1,6 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { showsMarkers } from '../../core/options/normalize';
2
+ import { drawable, projectSeries } from '../../core/geometry/path';
3
+ import { DEFAULT_SURFACE_COLOR } from '../../core/model/palette';
4
+ import { showsHoverMarker, showsMarkers } from '../../core/options/normalize';
5
+ /** Hovered point per the design system: a ring in the series colour on the surface. */
6
+ const HOVER_RADIUS = 2.5;
7
+ const HOVER_RING_WIDTH = 2;
3
8
  const markerRadius = marker => marker?.radius ?? (marker?.width ? marker.width / 2 : 3);
9
+ /** How far the hover mark reaches past its point, so a tooltip can clear it. */
10
+ export const hoverReachOf = marker => (marker?.states?.hover?.radius ?? HOVER_RADIUS) + HOVER_RING_WIDTH / 2;
11
+ /** The hovered point of a series, or nothing when it has none. */
12
+ export function hoverMarkerFor(ctx) {
13
+ const {
14
+ marker
15
+ } = ctx.series;
16
+ const hoveredIndex = ctx.hovered?.pointIndex;
17
+ if (hoveredIndex === undefined || !showsHoverMarker(marker)) {
18
+ return null;
19
+ }
20
+ const at = drawable(projectSeries(ctx)).find(({
21
+ point
22
+ }) => point.index === hoveredIndex);
23
+ return at ? _jsx("circle", {
24
+ cx: at.px,
25
+ cy: at.py,
26
+ r: marker?.states?.hover?.radius ?? HOVER_RADIUS,
27
+ fill: DEFAULT_SURFACE_COLOR,
28
+ stroke: ctx.color,
29
+ strokeWidth: HOVER_RING_WIDTH
30
+ }) : null;
31
+ }
4
32
  export function Markers({
5
33
  marker,
6
34
  projected,
@@ -1,5 +1,7 @@
1
+ import { hoverReachOf } from './Markers';
1
2
  import { pointKey } from '../../core/data/points';
2
3
  import { drawable, projectSeries } from '../../core/geometry/path';
4
+ import { showsHoverMarker } from '../../core/options/normalize';
3
5
  const HIT_RADIUS = 40;
4
6
  export function nearestByX(ctx, pointer, radius = HIT_RADIUS) {
5
7
  const projected = drawable(projectSeries(ctx));
@@ -28,6 +30,7 @@ export function nearestByX(ctx, pointer, radius = HIT_RADIUS) {
28
30
  x: best.px,
29
31
  y: best.py
30
32
  },
33
+ reach: showsHoverMarker(ctx.series.marker) ? hoverReachOf(ctx.series.marker) : 0,
31
34
  distance: bestDistance
32
35
  }];
33
36
  }