@pushwoosh/kit-charts 0.0.23 → 0.0.25
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/ai-documentation.md +4 -3
- package/core/Chart/Chart.styles.d.ts +6 -5
- package/core/Chart/Chart.styles.js +13 -17
- package/core/Chart/Tooltip.js +124 -22
- package/core/Chart/useSize.d.ts +2 -1
- package/core/layout/metrics.d.ts +2 -2
- package/core/layout/metrics.js +2 -2
- package/package.json +4 -7
package/ai-documentation.md
CHANGED
|
@@ -269,9 +269,10 @@ actually reads is here: `tsconfig.json`, `playground/tsconfig.json`,
|
|
|
269
269
|
`pushwoosh.config.js`, and `jest.config.js`.
|
|
270
270
|
|
|
271
271
|
`jest.config.js` is hand-written because the engine has no test runner. The
|
|
272
|
-
`playground` script
|
|
273
|
-
engine's `
|
|
274
|
-
|
|
272
|
+
`playground` script runs the command `lib:command:playground` prints (backticks, like the
|
|
273
|
+
engine's `app:command:*`): since engine 3.1.3 it names webpack-dev-server and the playground
|
|
274
|
+
config by absolute paths inside the engine, so the package needs no webpack of its own.
|
|
275
|
+
`pushwoosh.config.js` still points `resolveLoader` at the engine's dependencies for the loaders.
|
|
275
276
|
|
|
276
277
|
## Testing
|
|
277
278
|
|
|
@@ -62,15 +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
|
-
$
|
|
68
|
+
$offset: number;
|
|
67
69
|
$width: number;
|
|
68
70
|
$height: number;
|
|
69
|
-
$
|
|
70
|
-
$below: boolean;
|
|
71
|
+
$side: TailSide;
|
|
71
72
|
}
|
|
72
|
-
/** The callout beak,
|
|
73
|
-
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>;
|
|
74
75
|
export declare const TooltipTitle: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
75
76
|
export declare const TooltipRow: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
76
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
|
-
$height
|
|
137
|
-
}) => $below ? `top: -${$height}px;` : `bottom: -${$height}px;`, ({
|
|
138
|
-
$left,
|
|
139
|
-
$width
|
|
140
|
-
}) => `${$left - $width / 2}px`, ({
|
|
141
|
-
$width
|
|
142
|
-
}) => `${$width / 2}px`, ({
|
|
143
|
-
$width
|
|
144
|
-
}) => `${$width / 2}px`, ({
|
|
145
|
-
$below,
|
|
146
|
-
$height,
|
|
147
|
-
$color
|
|
148
|
-
}) => $below ? `border-bottom: ${$height}px solid ${$color};` : `border-top: ${$height}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.js
CHANGED
|
@@ -5,6 +5,74 @@ import { DEFAULTS, Tail, TooltipBox } from './Chart.styles';
|
|
|
5
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,
|
|
10
78
|
reach: markReach = 0,
|
|
@@ -14,12 +82,13 @@ export function Tooltip({
|
|
|
14
82
|
}) {
|
|
15
83
|
const ref = useRef(null);
|
|
16
84
|
const callout = (options?.shape ?? 'callout') === 'callout' && !options?.followPointer;
|
|
85
|
+
const radius = options?.borderRadius ?? TOOLTIP_RADIUS;
|
|
17
86
|
const target = options?.followPointer ? pointer : at;
|
|
18
87
|
const [placement, setPlacement] = useState({
|
|
19
88
|
x: target.x + TOOLTIP_CURSOR_OFFSET,
|
|
20
89
|
y: target.y + TOOLTIP_CURSOR_OFFSET,
|
|
21
|
-
|
|
22
|
-
|
|
90
|
+
tail: null,
|
|
91
|
+
side: 'top'
|
|
23
92
|
});
|
|
24
93
|
useLayoutEffect(() => {
|
|
25
94
|
const node = ref.current;
|
|
@@ -30,31 +99,49 @@ export function Tooltip({
|
|
|
30
99
|
width,
|
|
31
100
|
height
|
|
32
101
|
} = node.getBoundingClientRect();
|
|
33
|
-
const
|
|
102
|
+
const fitX = value => fit(value, width, window.innerWidth);
|
|
103
|
+
const fitY = value => fit(value, height, window.innerHeight);
|
|
34
104
|
if (!callout) {
|
|
35
105
|
const flipX = target.x + TOOLTIP_CURSOR_OFFSET + width > window.innerWidth - VIEWPORT_MARGIN;
|
|
36
106
|
const flipY = target.y + TOOLTIP_CURSOR_OFFSET + height > window.innerHeight - VIEWPORT_MARGIN;
|
|
37
107
|
setPlacement({
|
|
38
|
-
x:
|
|
39
|
-
y:
|
|
40
|
-
|
|
41
|
-
|
|
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'
|
|
42
131
|
});
|
|
43
132
|
return;
|
|
44
133
|
}
|
|
45
|
-
// Sit above the point with the
|
|
134
|
+
// Sit above the point with the beak pointing down at it, and flip under the
|
|
46
135
|
// point when there is no room. The box may be nudged sideways to stay on
|
|
47
|
-
// screen, so the
|
|
48
|
-
const
|
|
49
|
-
const below = target.y - height - reach < VIEWPORT_MARGIN;
|
|
50
|
-
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;
|
|
51
138
|
setPlacement({
|
|
52
139
|
x,
|
|
53
|
-
y: below ? target.y +
|
|
54
|
-
|
|
55
|
-
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'
|
|
56
143
|
});
|
|
57
|
-
}, [target.x, target.y, markReach, callout, children]);
|
|
144
|
+
}, [target.x, target.y, markReach, callout, radius, children]);
|
|
58
145
|
if (typeof document === 'undefined') {
|
|
59
146
|
return null;
|
|
60
147
|
}
|
|
@@ -63,27 +150,42 @@ export function Tooltip({
|
|
|
63
150
|
// so the tabular tooltip is a light card unless the caller says otherwise.
|
|
64
151
|
const tabular = Boolean(options?.table);
|
|
65
152
|
const background = options?.backgroundColor ?? (tabular ? DEFAULTS.surface : DEFAULTS.tooltipSurface);
|
|
153
|
+
const borderColor = options?.borderColor ?? (tabular ? DEFAULTS.border : 'transparent');
|
|
66
154
|
const color = options?.style?.color ?? (tabular ? DEFAULTS.text : DEFAULTS.tooltipText);
|
|
155
|
+
const beak = beakBox(placement.side);
|
|
67
156
|
return createPortal(_jsxs(TooltipBox, {
|
|
68
157
|
ref: ref,
|
|
69
158
|
role: "tooltip",
|
|
70
159
|
"$x": placement.x,
|
|
71
160
|
"$y": placement.y,
|
|
72
161
|
"$padding": options?.padding ?? TOOLTIP_PADDING,
|
|
73
|
-
"$radius":
|
|
74
|
-
"$borderColor":
|
|
162
|
+
"$radius": radius,
|
|
163
|
+
"$borderColor": borderColor,
|
|
75
164
|
"$background": background,
|
|
76
165
|
"$color": color,
|
|
77
166
|
"$fontFamily": options?.style?.fontFamily ?? DEFAULTS.fontFamily,
|
|
78
167
|
"$fontSize": options?.style?.fontSize ?? DEFAULTS.fontSize,
|
|
79
168
|
"$whiteSpace": options?.style?.whiteSpace ?? 'nowrap',
|
|
80
169
|
"$shadow": shadow ? `${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 2}px ${shadow.width ?? 8}px ` + `${shadow.color ?? SHADOW_COLOR}` : DEFAULT_SHADOW,
|
|
81
|
-
children: [children, placement.
|
|
82
|
-
"$
|
|
170
|
+
children: [children, placement.tail === null ? null : _jsxs(Tail, {
|
|
171
|
+
"$offset": placement.tail,
|
|
83
172
|
"$width": TOOLTIP_TAIL_WIDTH,
|
|
84
173
|
"$height": TOOLTIP_TAIL_HEIGHT,
|
|
85
|
-
"$
|
|
86
|
-
|
|
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
|
+
})]
|
|
87
189
|
})]
|
|
88
190
|
}), document.body);
|
|
89
191
|
}
|
package/core/Chart/useSize.d.ts
CHANGED
package/core/layout/metrics.d.ts
CHANGED
|
@@ -36,8 +36,8 @@ 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, as the design system draws it
|
|
40
|
-
export declare const TOOLTIP_TAIL_WIDTH =
|
|
39
|
+
/** Tooltip beak, as the design system draws it. */
|
|
40
|
+
export declare const TOOLTIP_TAIL_WIDTH = 14;
|
|
41
41
|
export declare const TOOLTIP_TAIL_HEIGHT = 4;
|
|
42
42
|
/** Air between the beak tip and the edge of the mark it points at. */
|
|
43
43
|
export declare const TOOLTIP_ANCHOR_GAP = 8;
|
package/core/layout/metrics.js
CHANGED
|
@@ -36,8 +36,8 @@ 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, as the design system draws it
|
|
40
|
-
export const TOOLTIP_TAIL_WIDTH =
|
|
39
|
+
/** Tooltip beak, as the design system draws it. */
|
|
40
|
+
export const TOOLTIP_TAIL_WIDTH = 14;
|
|
41
41
|
export const TOOLTIP_TAIL_HEIGHT = 4;
|
|
42
42
|
/** Air between the beak tip and the edge of the mark it points at. */
|
|
43
43
|
export const TOOLTIP_ANCHOR_GAP = 8;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushwoosh/kit-charts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "Small React chart library on SVG, with a Highcharts-shaped configuration model",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@pushwoosh/kit-constants": "^2.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@pushwoosh/frontend-builder-engine": "^3.1.
|
|
32
|
+
"@pushwoosh/frontend-builder-engine": "^3.1.3",
|
|
33
33
|
"@testing-library/dom": "^10.4.2",
|
|
34
34
|
"@testing-library/jest-dom": "^6.9.1",
|
|
35
35
|
"@testing-library/react": "^16.3.0",
|
|
@@ -44,10 +44,7 @@
|
|
|
44
44
|
"react-dom": "^18.3.1",
|
|
45
45
|
"styled-components": "^5.3.11",
|
|
46
46
|
"ts-jest": "^29.4.6",
|
|
47
|
-
"typescript": "^6.0.3"
|
|
48
|
-
"webpack": "^5.94.0",
|
|
49
|
-
"webpack-cli": "^6.0.1",
|
|
50
|
-
"webpack-dev-server": "^5.0.4"
|
|
47
|
+
"typescript": "^6.0.3"
|
|
51
48
|
},
|
|
52
49
|
"peerDependencies": {
|
|
53
50
|
"react": ">=18.0.0",
|
|
@@ -59,7 +56,7 @@
|
|
|
59
56
|
"scripts": {
|
|
60
57
|
"start": "pnpm run playground",
|
|
61
58
|
"build": "pushwoosh-engine lib:build && cp ai-documentation.md LICENSE dist/",
|
|
62
|
-
"playground": "
|
|
59
|
+
"playground": "`pushwoosh-engine lib:command:playground`",
|
|
63
60
|
"check": "pnpm run check:lint && pnpm run check:types",
|
|
64
61
|
"check:lint": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src ./playground ./tests",
|
|
65
62
|
"check:lint-fix": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src ./playground ./tests --fix",
|