@pushwoosh/kit-charts 0.0.22 → 0.0.23

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
@@ -64,7 +64,8 @@ export interface TooltipBoxProps {
64
64
  export declare const TooltipBox: import("styled-components").StyledComponent<"div", any, TooltipBoxProps, never>;
65
65
  export interface TailProps {
66
66
  $left: number;
67
- $size: number;
67
+ $width: number;
68
+ $height: number;
68
69
  $color: string;
69
70
  $below: boolean;
70
71
  }
@@ -133,19 +133,19 @@ export const Tail = styled.span.withConfig({
133
133
  componentId: "sc-1pit1b9-9"
134
134
  })(["position:absolute;", " left:", ";width:0;height:0;border-right:", " solid transparent;border-left:", " solid transparent;", ""], ({
135
135
  $below,
136
- $size
137
- }) => $below ? `top: -${$size}px;` : `bottom: -${$size}px;`, ({
136
+ $height
137
+ }) => $below ? `top: -${$height}px;` : `bottom: -${$height}px;`, ({
138
138
  $left,
139
- $size
140
- }) => `${$left - $size}px`, ({
141
- $size
142
- }) => `${$size}px`, ({
143
- $size
144
- }) => `${$size}px`, ({
139
+ $width
140
+ }) => `${$left - $width / 2}px`, ({
141
+ $width
142
+ }) => `${$width / 2}px`, ({
143
+ $width
144
+ }) => `${$width / 2}px`, ({
145
145
  $below,
146
- $size,
146
+ $height,
147
147
  $color
148
- }) => $below ? `border-bottom: ${$size}px solid ${$color};` : `border-top: ${$size}px solid ${$color};`);
148
+ }) => $below ? `border-bottom: ${$height}px solid ${$color};` : `border-top: ${$height}px solid ${$color};`);
149
149
  export const TooltipTitle = styled.div.withConfig({
150
150
  displayName: "TooltipTitle",
151
151
  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,11 +2,12 @@ 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
8
  export function Tooltip({
9
9
  at,
10
+ reach: markReach = 0,
10
11
  options,
11
12
  pointer,
12
13
  children
@@ -44,15 +45,16 @@ export function Tooltip({
44
45
  // Sit above the point with the tail pointing down at it, and flip under the
45
46
  // point when there is no room. The box may be nudged sideways to stay on
46
47
  // 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 reach = TOOLTIP_TAIL_HEIGHT + TOOLTIP_ANCHOR_GAP + markReach;
49
+ const below = target.y - height - reach < VIEWPORT_MARGIN;
48
50
  const x = limitX(target.x - width / 2);
49
51
  setPlacement({
50
52
  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
+ y: below ? target.y + reach : target.y - height - reach,
54
+ tailX: Math.min(Math.max(TOOLTIP_TAIL_WIDTH, target.x - x), Math.max(TOOLTIP_TAIL_WIDTH, width - TOOLTIP_TAIL_WIDTH)),
53
55
  below
54
56
  });
55
- }, [target.x, target.y, callout, children]);
57
+ }, [target.x, target.y, markReach, callout, children]);
56
58
  if (typeof document === 'undefined') {
57
59
  return null;
58
60
  }
@@ -78,7 +80,8 @@ export function Tooltip({
78
80
  "$shadow": shadow ? `${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 2}px ${shadow.width ?? 8}px ` + `${shadow.color ?? SHADOW_COLOR}` : DEFAULT_SHADOW,
79
81
  children: [children, placement.tailX === null ? null : _jsx(Tail, {
80
82
  "$left": placement.tailX,
81
- "$size": TOOLTIP_TAIL,
83
+ "$width": TOOLTIP_TAIL_WIDTH,
84
+ "$height": TOOLTIP_TAIL_HEIGHT,
82
85
  "$color": background,
83
86
  "$below": placement.below
84
87
  })]
@@ -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: a wide, shallow triangle. */
40
+ export declare const TOOLTIP_TAIL_WIDTH = 20;
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: a wide, shallow triangle. */
40
+ export const TOOLTIP_TAIL_WIDTH = 20;
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.23",
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
  }