@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.
- package/core/Chart/Chart.js +10 -1
- package/core/Chart/Chart.styles.d.ts +8 -6
- package/core/Chart/Chart.styles.js +13 -17
- package/core/Chart/Tooltip.d.ts +3 -1
- package/core/Chart/Tooltip.js +128 -23
- package/core/layout/metrics.d.ts +5 -2
- package/core/layout/metrics.js +5 -2
- package/core/types/renderer.d.ts +4 -0
- package/package.json +2 -2
- package/series/area/area.js +3 -14
- package/series/line/line.js +3 -14
- package/series/shared/Markers.d.ts +5 -0
- package/series/shared/Markers.js +29 -1
- package/series/shared/hit.js +3 -0
package/core/Chart/Chart.js
CHANGED
|
@@ -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
|
-
$
|
|
67
|
-
$
|
|
68
|
-
$
|
|
69
|
-
$
|
|
68
|
+
$offset: number;
|
|
69
|
+
$width: number;
|
|
70
|
+
$height: number;
|
|
71
|
+
$side: TailSide;
|
|
70
72
|
}
|
|
71
|
-
/** The callout beak,
|
|
72
|
-
export declare const Tail: import("styled-components").StyledComponent<"
|
|
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
|
|
131
|
-
|
|
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;", "
|
|
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"
|
package/core/Chart/Tooltip.d.ts
CHANGED
|
@@ -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 {};
|
package/core/Chart/Tooltip.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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:
|
|
38
|
-
y:
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
47
|
-
const below = target.y - height -
|
|
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 +
|
|
52
|
-
|
|
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":
|
|
72
|
-
"$borderColor":
|
|
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.
|
|
80
|
-
"$
|
|
81
|
-
"$
|
|
82
|
-
"$
|
|
83
|
-
"$
|
|
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
|
}
|
package/core/layout/metrics.d.ts
CHANGED
|
@@ -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
|
|
40
|
-
export declare const
|
|
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. */
|
package/core/layout/metrics.js
CHANGED
|
@@ -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
|
|
40
|
-
export const
|
|
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. */
|
package/core/types/renderer.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
29
|
+
"@pushwoosh/kit-constants": "^2.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@pushwoosh/frontend-builder-engine": "^3.1.0",
|
package/series/area/area.js
CHANGED
|
@@ -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
|
-
})
|
|
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
|
};
|
package/series/line/line.js
CHANGED
|
@@ -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
|
-
})
|
|
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[];
|
package/series/shared/Markers.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
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,
|
package/series/shared/hit.js
CHANGED
|
@@ -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
|
}
|