@pushwoosh/kit-charts 0.0.23 → 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.
@@ -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
- $left: number;
68
+ $offset: number;
67
69
  $width: number;
68
70
  $height: number;
69
- $color: string;
70
- $below: boolean;
71
+ $side: TailSide;
71
72
  }
72
- /** The callout beak, drawn as a CSS triangle so it inherits the box colour. */
73
- 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>;
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 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
- $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"
@@ -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
- tailX: null,
22
- below: true
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 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);
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: limitX(flipX ? target.x - TOOLTIP_CURSOR_OFFSET - width : target.x + TOOLTIP_CURSOR_OFFSET),
39
- y: Math.max(VIEWPORT_MARGIN, flipY ? target.y - TOOLTIP_CURSOR_OFFSET - height : target.y + TOOLTIP_CURSOR_OFFSET),
40
- tailX: null,
41
- 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'
42
131
  });
43
132
  return;
44
133
  }
45
- // 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
46
135
  // point when there is no room. The box may be nudged sideways to stay on
47
- // screen, so the tail keeps its own offset and still points at the anchor.
48
- const reach = TOOLTIP_TAIL_HEIGHT + TOOLTIP_ANCHOR_GAP + markReach;
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 + 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)),
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": options?.borderRadius ?? TOOLTIP_RADIUS,
74
- "$borderColor": options?.borderColor ?? (tabular ? DEFAULTS.border : 'transparent'),
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.tailX === null ? null : _jsx(Tail, {
82
- "$left": placement.tailX,
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
- "$color": background,
86
- "$below": placement.below
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
  }
@@ -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: a wide, shallow triangle. */
40
- export declare const TOOLTIP_TAIL_WIDTH = 20;
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;
@@ -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: a wide, shallow triangle. */
40
- export const TOOLTIP_TAIL_WIDTH = 20;
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.23",
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": {