@gravity-ui/charts 0.4.1 → 0.6.0

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.
Files changed (79) hide show
  1. package/dist/cjs/components/ChartInner/index.d.ts +2 -8
  2. package/dist/cjs/components/ChartInner/index.js +22 -117
  3. package/dist/cjs/components/ChartInner/types.d.ts +6 -0
  4. package/dist/cjs/components/ChartInner/useChartInnerHandlers.d.ts +26 -0
  5. package/dist/cjs/components/ChartInner/useChartInnerHandlers.js +94 -0
  6. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +43 -0
  7. package/dist/cjs/components/ChartInner/useChartInnerProps.js +81 -0
  8. package/dist/cjs/components/ChartInner/useChartInnerState.d.ts +13 -0
  9. package/dist/cjs/components/ChartInner/useChartInnerState.js +34 -0
  10. package/dist/cjs/components/Legend/index.d.ts +1 -0
  11. package/dist/cjs/components/Legend/index.js +26 -6
  12. package/dist/cjs/components/Tooltip/index.d.ts +2 -0
  13. package/dist/cjs/components/Tooltip/index.js +9 -3
  14. package/dist/cjs/components/Tooltip/styles.css +3 -0
  15. package/dist/cjs/hooks/index.d.ts +2 -1
  16. package/dist/cjs/hooks/index.js +2 -1
  17. package/dist/cjs/hooks/usePrevious/index.d.ts +1 -0
  18. package/dist/cjs/hooks/usePrevious/index.js +8 -0
  19. package/dist/cjs/hooks/useSeries/prepare-legend.js +1 -0
  20. package/dist/cjs/hooks/useSeries/types.d.ts +1 -0
  21. package/dist/cjs/hooks/useShapes/area/index.js +8 -2
  22. package/dist/cjs/hooks/useShapes/bar-x/index.js +8 -2
  23. package/dist/cjs/hooks/useShapes/bar-y/index.js +8 -2
  24. package/dist/cjs/hooks/useShapes/line/index.js +8 -2
  25. package/dist/cjs/hooks/useShapes/scatter/index.js +8 -2
  26. package/dist/cjs/hooks/useShapes/treemap/index.js +8 -2
  27. package/dist/cjs/hooks/useShapes/utils.d.ts +2 -2
  28. package/dist/cjs/hooks/useShapes/utils.js +5 -3
  29. package/dist/cjs/hooks/useShapes/waterfall/index.js +8 -2
  30. package/dist/cjs/hooks/useTooltip/index.d.ts +2 -3
  31. package/dist/cjs/types/chart/chart.d.ts +2 -2
  32. package/dist/cjs/types/chart/legend.d.ts +2 -0
  33. package/dist/cjs/types/chart/series.d.ts +1 -2
  34. package/dist/cjs/types/chart/tooltip.d.ts +6 -6
  35. package/dist/cjs/types/misc.d.ts +1 -0
  36. package/dist/cjs/utils/misc.d.ts +10 -2
  37. package/dist/cjs/utils/misc.js +15 -3
  38. package/dist/esm/components/ChartInner/index.d.ts +2 -8
  39. package/dist/esm/components/ChartInner/index.js +22 -117
  40. package/dist/esm/components/ChartInner/types.d.ts +6 -0
  41. package/dist/esm/components/ChartInner/useChartInnerHandlers.d.ts +26 -0
  42. package/dist/esm/components/ChartInner/useChartInnerHandlers.js +94 -0
  43. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +43 -0
  44. package/dist/esm/components/ChartInner/useChartInnerProps.js +81 -0
  45. package/dist/esm/components/ChartInner/useChartInnerState.d.ts +13 -0
  46. package/dist/esm/components/ChartInner/useChartInnerState.js +34 -0
  47. package/dist/esm/components/Legend/index.d.ts +1 -0
  48. package/dist/esm/components/Legend/index.js +26 -6
  49. package/dist/esm/components/Tooltip/index.d.ts +2 -0
  50. package/dist/esm/components/Tooltip/index.js +9 -3
  51. package/dist/esm/components/Tooltip/styles.css +3 -0
  52. package/dist/esm/hooks/index.d.ts +2 -1
  53. package/dist/esm/hooks/index.js +2 -1
  54. package/dist/esm/hooks/usePrevious/index.d.ts +1 -0
  55. package/dist/esm/hooks/usePrevious/index.js +8 -0
  56. package/dist/esm/hooks/useSeries/prepare-legend.js +1 -0
  57. package/dist/esm/hooks/useSeries/types.d.ts +1 -0
  58. package/dist/esm/hooks/useShapes/area/index.js +8 -2
  59. package/dist/esm/hooks/useShapes/bar-x/index.js +8 -2
  60. package/dist/esm/hooks/useShapes/bar-y/index.js +8 -2
  61. package/dist/esm/hooks/useShapes/line/index.js +8 -2
  62. package/dist/esm/hooks/useShapes/scatter/index.js +8 -2
  63. package/dist/esm/hooks/useShapes/treemap/index.js +8 -2
  64. package/dist/esm/hooks/useShapes/utils.d.ts +2 -2
  65. package/dist/esm/hooks/useShapes/utils.js +5 -3
  66. package/dist/esm/hooks/useShapes/waterfall/index.js +8 -2
  67. package/dist/esm/hooks/useTooltip/index.d.ts +2 -3
  68. package/dist/esm/types/chart/chart.d.ts +2 -2
  69. package/dist/esm/types/chart/legend.d.ts +2 -0
  70. package/dist/esm/types/chart/series.d.ts +1 -2
  71. package/dist/esm/types/chart/tooltip.d.ts +6 -6
  72. package/dist/esm/types/misc.d.ts +1 -0
  73. package/dist/esm/utils/misc.d.ts +10 -2
  74. package/dist/esm/utils/misc.js +15 -3
  75. package/package.json +1 -1
  76. package/dist/cjs/hooks/useTooltip/types.d.ts +0 -1
  77. package/dist/esm/hooks/useTooltip/types.d.ts +0 -1
  78. /package/dist/cjs/{hooks/useTooltip → components/ChartInner}/types.js +0 -0
  79. /package/dist/esm/{hooks/useTooltip → components/ChartInner}/types.js +0 -0
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import type { PreparedAxis, ShapeData } from '../../hooks';
4
+ import type { useChartInnerState } from './useChartInnerState';
5
+ type ChartInnerState = ReturnType<typeof useChartInnerState>;
6
+ type Props = {
7
+ boundsHeight: number;
8
+ boundsOffsetLeft: number;
9
+ boundsOffsetTop: number;
10
+ boundsWidth: number;
11
+ dispatcher: Dispatch<object>;
12
+ shapesData: ShapeData[];
13
+ svgContainer: SVGSVGElement | null;
14
+ togglePinTooltip: ChartInnerState['togglePinTooltip'];
15
+ tooltipPinned: boolean;
16
+ unpinTooltip: ChartInnerState['unpinTooltip'];
17
+ xAxis: PreparedAxis;
18
+ yAxis: PreparedAxis[];
19
+ };
20
+ export declare function useChartInnerHandlers(props: Props): {
21
+ handleChartClick: (event: React.MouseEvent<SVGSVGElement>) => void;
22
+ handleMouseLeave: React.MouseEventHandler<SVGSVGElement>;
23
+ throttledHandleMouseMove: import("lodash").DebouncedFuncLeading<React.MouseEventHandler<SVGSVGElement>> | undefined;
24
+ throttledHandleTouchMove: import("lodash").DebouncedFuncLeading<React.TouchEventHandler<SVGSVGElement>> | undefined;
25
+ };
26
+ export {};
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { pointer } from 'd3';
3
+ import throttle from 'lodash/throttle';
4
+ import { IS_TOUCH_ENABLED } from '../../constants';
5
+ import { EventType } from '../../utils';
6
+ import { getClosestPoints } from '../../utils/chart/get-closest-data';
7
+ const THROTTLE_DELAY = 50;
8
+ export function useChartInnerHandlers(props) {
9
+ const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, dispatcher, shapesData, svgContainer, togglePinTooltip, tooltipPinned, unpinTooltip, xAxis, yAxis, } = props;
10
+ const isOutsideBounds = React.useCallback((x, y) => {
11
+ return x < 0 || x > boundsWidth || y < 0 || y > boundsHeight;
12
+ }, [boundsHeight, boundsWidth]);
13
+ const handleMove = ([pointerX, pointerY], event) => {
14
+ if (tooltipPinned) {
15
+ return;
16
+ }
17
+ const x = pointerX - boundsOffsetLeft;
18
+ const y = pointerY - boundsOffsetTop;
19
+ if (isOutsideBounds(x, y)) {
20
+ dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
21
+ dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
22
+ return;
23
+ }
24
+ const closest = getClosestPoints({
25
+ position: [x, y],
26
+ shapesData,
27
+ });
28
+ dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY]);
29
+ dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
30
+ hovered: closest,
31
+ xAxis,
32
+ yAxis: yAxis[0],
33
+ }, event);
34
+ };
35
+ const handleMouseMove = (event) => {
36
+ const [pointerX, pointerY] = pointer(event, svgContainer);
37
+ handleMove([pointerX, pointerY], event);
38
+ };
39
+ const throttledHandleMouseMove = IS_TOUCH_ENABLED
40
+ ? undefined
41
+ : throttle(handleMouseMove, THROTTLE_DELAY);
42
+ const handleMouseLeave = (event) => {
43
+ if (tooltipPinned) {
44
+ return;
45
+ }
46
+ throttledHandleMouseMove === null || throttledHandleMouseMove === void 0 ? void 0 : throttledHandleMouseMove.cancel();
47
+ dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
48
+ dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
49
+ };
50
+ const handleTouchMove = (event) => {
51
+ const touch = event.touches[0];
52
+ const [pointerX, pointerY] = pointer(touch, svgContainer);
53
+ handleMove([pointerX, pointerY], event);
54
+ };
55
+ const throttledHandleTouchMove = IS_TOUCH_ENABLED
56
+ ? throttle(handleTouchMove, THROTTLE_DELAY)
57
+ : undefined;
58
+ const handleChartClick = (event) => {
59
+ const [pointerX, pointerY] = pointer(event, svgContainer);
60
+ const x = pointerX - boundsOffsetLeft;
61
+ const y = pointerY - boundsOffsetTop;
62
+ if (isOutsideBounds(x, y)) {
63
+ return;
64
+ }
65
+ const items = getClosestPoints({
66
+ position: [x, y],
67
+ shapesData,
68
+ });
69
+ const selected = items === null || items === void 0 ? void 0 : items.find((item) => item.closest);
70
+ if (!selected) {
71
+ if (tooltipPinned) {
72
+ unpinTooltip === null || unpinTooltip === void 0 ? void 0 : unpinTooltip();
73
+ }
74
+ return;
75
+ }
76
+ dispatcher.call(EventType.CLICK_CHART, undefined, { point: selected.data, series: selected.series }, event);
77
+ const nextTooltipFixed = !tooltipPinned;
78
+ if (!nextTooltipFixed) {
79
+ dispatcher.call(EventType.HOVER_SHAPE, event.target, items, [pointerX, pointerY]);
80
+ dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
81
+ hovered: items,
82
+ xAxis,
83
+ yAxis: yAxis[0],
84
+ }, event);
85
+ }
86
+ togglePinTooltip === null || togglePinTooltip === void 0 ? void 0 : togglePinTooltip(nextTooltipFixed, event);
87
+ };
88
+ return {
89
+ handleChartClick,
90
+ handleMouseLeave,
91
+ throttledHandleMouseMove,
92
+ throttledHandleTouchMove,
93
+ };
94
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import type { ChartInnerProps } from './types';
4
+ type Props = ChartInnerProps & {
5
+ dispatcher: Dispatch<object>;
6
+ htmlLayout: HTMLElement | null;
7
+ };
8
+ export declare function useChartInnerProps(props: Props): {
9
+ boundsHeight: number;
10
+ boundsOffsetLeft: number;
11
+ boundsOffsetTop: number;
12
+ boundsWidth: number;
13
+ handleLegendItemClick: import("../../hooks").OnLegendItemClick;
14
+ legendConfig: {
15
+ offset: {
16
+ left: number;
17
+ top: number;
18
+ };
19
+ pagination: {
20
+ limit: number;
21
+ maxPage: number;
22
+ } | undefined;
23
+ };
24
+ legendItems: import("../../hooks").LegendItem[][];
25
+ preparedLegend: import("../../hooks").PreparedLegend;
26
+ preparedSeries: import("../../hooks").PreparedSeries[];
27
+ preparedSplit: import("../../hooks").PreparedSplit;
28
+ prevHeight: number | undefined;
29
+ prevWidth: number | undefined;
30
+ shapes: React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
31
+ shapesData: import("../../hooks").ShapeData[];
32
+ title: (import("../../types").ChartTitle & {
33
+ height: number;
34
+ }) | undefined;
35
+ tooltip: import("../../types").ChartTooltip<any> & {
36
+ enabled: boolean;
37
+ };
38
+ xAxis: import("../../hooks").PreparedAxis;
39
+ xScale: import("../../hooks").ChartScale | undefined;
40
+ yAxis: import("../../hooks").PreparedAxis[];
41
+ yScale: import("../../hooks").ChartScale[] | undefined;
42
+ };
43
+ export {};
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapes, useSplit, } from '../../hooks';
3
+ import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
+ import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
5
+ import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
6
+ export function useChartInnerProps(props) {
7
+ const { width, height, data, dispatcher, htmlLayout } = props;
8
+ const prevWidth = usePrevious(width);
9
+ const prevHeight = usePrevious(height);
10
+ const { chart, title, tooltip } = useChartOptions({ data });
11
+ const xAxis = React.useMemo(() => getPreparedXAxis({ xAxis: data.xAxis, width, series: data.series.data }), [data, width]);
12
+ const yAxis = React.useMemo(() => getPreparedYAxis({
13
+ series: data.series.data,
14
+ yAxis: data.yAxis,
15
+ height,
16
+ }), [data, height]);
17
+ const { legendItems, legendConfig, preparedSeries, preparedSeriesOptions, preparedLegend, handleLegendItemClick, } = useSeries({
18
+ chartWidth: width,
19
+ chartHeight: height,
20
+ chartMargin: chart.margin,
21
+ series: data.series,
22
+ legend: data.legend,
23
+ preparedYAxis: yAxis,
24
+ });
25
+ const { boundsWidth, boundsHeight } = useChartDimensions({
26
+ width,
27
+ height,
28
+ margin: chart.margin,
29
+ preparedLegend,
30
+ preparedXAxis: xAxis,
31
+ preparedYAxis: yAxis,
32
+ preparedSeries: preparedSeries,
33
+ });
34
+ const preparedSplit = useSplit({ split: data.split, boundsHeight, chartWidth: width });
35
+ const { xScale, yScale } = useAxisScales({
36
+ boundsWidth,
37
+ boundsHeight,
38
+ series: preparedSeries,
39
+ xAxis,
40
+ yAxis,
41
+ split: preparedSplit,
42
+ });
43
+ const { shapes, shapesData } = useShapes({
44
+ boundsWidth,
45
+ boundsHeight,
46
+ dispatcher,
47
+ series: preparedSeries,
48
+ seriesOptions: preparedSeriesOptions,
49
+ xAxis,
50
+ xScale,
51
+ yAxis,
52
+ yScale,
53
+ split: preparedSplit,
54
+ htmlLayout,
55
+ });
56
+ const boundsOffsetTop = chart.margin.top;
57
+ // We only need to consider the width of the first left axis
58
+ const boundsOffsetLeft = chart.margin.left + getYAxisWidth(yAxis[0]);
59
+ return {
60
+ boundsHeight,
61
+ boundsOffsetLeft,
62
+ boundsOffsetTop,
63
+ boundsWidth,
64
+ handleLegendItemClick,
65
+ legendConfig,
66
+ legendItems,
67
+ preparedLegend,
68
+ preparedSeries,
69
+ preparedSplit,
70
+ prevHeight,
71
+ prevWidth,
72
+ shapes,
73
+ shapesData,
74
+ title,
75
+ tooltip,
76
+ xAxis,
77
+ xScale,
78
+ yAxis,
79
+ yScale,
80
+ };
81
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import type { ChartTooltip } from '../../types';
4
+ type Props = {
5
+ dispatcher: Dispatch<object>;
6
+ tooltip?: ChartTooltip;
7
+ };
8
+ export declare function useChartInnerState(props: Props): {
9
+ tooltipPinned: boolean;
10
+ togglePinTooltip: ((value: boolean, event: React.MouseEvent) => void) | undefined;
11
+ unpinTooltip: (() => void) | undefined;
12
+ };
13
+ export {};
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { EventType, isMacintosh } from '../../utils';
3
+ export function useChartInnerState(props) {
4
+ var _a, _b;
5
+ const { dispatcher, tooltip } = props;
6
+ const [tooltipPinned, setTooltipPinned] = React.useState(false);
7
+ const tooltipEnabled = tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled;
8
+ const tooltipPinEnabled = (_a = tooltip === null || tooltip === void 0 ? void 0 : tooltip.pin) === null || _a === void 0 ? void 0 : _a.enabled;
9
+ const modifierKey = (_b = tooltip === null || tooltip === void 0 ? void 0 : tooltip.pin) === null || _b === void 0 ? void 0 : _b.modifierKey;
10
+ const togglePinTooltip = React.useCallback((value, event) => {
11
+ let resultValue = value;
12
+ if (value && modifierKey) {
13
+ switch (modifierKey) {
14
+ case 'altKey': {
15
+ resultValue = event.altKey;
16
+ break;
17
+ }
18
+ case 'metaKey': {
19
+ resultValue = isMacintosh() ? event.metaKey : event.ctrlKey;
20
+ }
21
+ }
22
+ }
23
+ setTooltipPinned(resultValue);
24
+ }, [modifierKey]);
25
+ const unpinTooltip = React.useCallback(() => {
26
+ setTooltipPinned(false);
27
+ dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
28
+ }, [dispatcher]);
29
+ return {
30
+ tooltipPinned,
31
+ togglePinTooltip: tooltipEnabled && tooltipPinEnabled ? togglePinTooltip : undefined,
32
+ unpinTooltip: tooltipEnabled && tooltipPinEnabled ? unpinTooltip : undefined,
33
+ };
34
+ }
@@ -8,6 +8,7 @@ type Props = {
8
8
  items: LegendItem[][];
9
9
  config: LegendConfig;
10
10
  onItemClick: OnLegendItemClick;
11
+ onUpdate?: () => void;
11
12
  };
12
13
  export declare const Legend: (props: Props) => React.JSX.Element;
13
14
  export {};
@@ -2,12 +2,13 @@ import React from 'react';
2
2
  import { line as lineGenerator, scaleLinear, select, symbol } from 'd3';
3
3
  import { CONTINUOUS_LEGEND_SIZE } from '../../constants';
4
4
  import { getLineDashArray } from '../../hooks/useShapes/utils';
5
+ import { formatNumber } from '../../libs';
5
6
  import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getSymbol, } from '../../utils';
6
7
  import { axisBottom } from '../../utils/chart/axis-generators';
7
8
  import './styles.css';
8
9
  const b = block('d3-legend');
9
10
  const getLegendPosition = (args) => {
10
- const { align, offsetWidth, width, contentWidth } = args;
11
+ const { align, offsetWidth = 0, width, contentWidth } = args;
11
12
  const top = 0;
12
13
  if (align === 'left') {
13
14
  return { top, left: offsetWidth };
@@ -133,7 +134,7 @@ function renderLegendSymbol(args) {
133
134
  });
134
135
  }
135
136
  export const Legend = (props) => {
136
- const { boundsWidth, chartSeries, legend, items, config, onItemClick } = props;
137
+ const { boundsWidth, chartSeries, legend, items, config, onItemClick, onUpdate } = props;
137
138
  const ref = React.useRef(null);
138
139
  const [paginationOffset, setPaginationOffset] = React.useState(0);
139
140
  React.useEffect(() => {
@@ -163,6 +164,7 @@ export const Legend = (props) => {
163
164
  .attr('class', b('item'))
164
165
  .on('click', function (e, d) {
165
166
  onItemClick({ name: d.name, metaKey: e.metaKey });
167
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate();
166
168
  });
167
169
  const getXPosition = (i) => {
168
170
  return line.slice(0, i).reduce((acc, legendItem) => {
@@ -229,14 +231,16 @@ export const Legend = (props) => {
229
231
  }),
230
232
  });
231
233
  // ticks
234
+ const scale = scaleLinear(domain, [0, legend.width]);
232
235
  const xAxisGenerator = axisBottom({
233
- scale: scaleLinear(domain, [0, legend.width]),
236
+ scale,
234
237
  ticks: {
235
238
  items: [[0, -rectHeight]],
236
239
  labelsMargin: legend.ticks.labelsMargin,
237
240
  labelsLineHeight: legend.ticks.labelsLineHeight,
238
241
  maxTickCount: 4,
239
242
  tickColor: '#fff',
243
+ labelFormat: (value) => formatNumber(value, { unit: 'auto' }),
240
244
  },
241
245
  domain: {
242
246
  size: legend.width,
@@ -251,15 +255,31 @@ export const Legend = (props) => {
251
255
  legendWidth = legend.width;
252
256
  }
253
257
  if (legend.title.enable) {
254
- const { maxWidth: labelWidth } = getLabelsSize({
258
+ const { maxWidth: titleWidth } = getLabelsSize({
255
259
  labels: [legend.title.text],
256
260
  style: legend.title.style,
257
261
  });
262
+ let dx = 0;
263
+ switch (legend.title.align) {
264
+ case 'center': {
265
+ dx = legend.width / 2 - titleWidth / 2;
266
+ break;
267
+ }
268
+ case 'right': {
269
+ dx = legend.width - titleWidth;
270
+ break;
271
+ }
272
+ case 'left':
273
+ default: {
274
+ dx = 0;
275
+ break;
276
+ }
277
+ }
258
278
  svgElement
259
279
  .append('g')
260
280
  .attr('class', b('title'))
261
281
  .append('text')
262
- .attr('dx', legend.width / 2 - labelWidth / 2)
282
+ .attr('dx', dx)
263
283
  .attr('font-weight', (_c = legend.title.style.fontWeight) !== null && _c !== void 0 ? _c : null)
264
284
  .attr('font-size', (_d = legend.title.style.fontSize) !== null && _d !== void 0 ? _d : null)
265
285
  .attr('fill', (_e = legend.title.style.fontColor) !== null && _e !== void 0 ? _e : null)
@@ -273,6 +293,6 @@ export const Legend = (props) => {
273
293
  contentWidth: legendWidth,
274
294
  });
275
295
  svgElement.attr('transform', `translate(${[left, config.offset.top].join(',')})`);
276
- }, [boundsWidth, chartSeries, onItemClick, legend, items, config, paginationOffset]);
296
+ }, [boundsWidth, chartSeries, onItemClick, onUpdate, legend, items, config, paginationOffset]);
277
297
  return React.createElement("g", { className: b(), ref: ref, width: boundsWidth, height: legend.height });
278
298
  };
@@ -8,6 +8,8 @@ type TooltipProps = {
8
8
  svgContainer: SVGSVGElement | null;
9
9
  xAxis: PreparedAxis;
10
10
  yAxis: PreparedAxis;
11
+ tooltipPinned: boolean;
12
+ onOutsideClick?: () => void;
11
13
  };
12
14
  export declare const Tooltip: (props: TooltipProps) => React.JSX.Element | null;
13
15
  export {};
@@ -6,16 +6,22 @@ import { ChartTooltipContent } from './ChartTooltipContent';
6
6
  import './styles.css';
7
7
  const b = block('d3-tooltip');
8
8
  export const Tooltip = (props) => {
9
- const { tooltip, xAxis, yAxis, svgContainer, dispatcher } = props;
9
+ const { tooltip, xAxis, yAxis, svgContainer, dispatcher, tooltipPinned, onOutsideClick } = props;
10
10
  const { hovered, pointerPosition } = useTooltip({ dispatcher, tooltip });
11
11
  const containerRect = (svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) || { left: 0, top: 0 };
12
12
  const left = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[0]) || 0) + containerRect.left;
13
13
  const top = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[1]) || 0) + containerRect.top;
14
- const anchorRef = useVirtualElementRef({ rect: { top, left } });
14
+ const anchorRef = useVirtualElementRef({ rect: { left, top } });
15
+ const handleOutsideClick = (e) => {
16
+ if (svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.contains(e.target)) {
17
+ return;
18
+ }
19
+ onOutsideClick === null || onOutsideClick === void 0 ? void 0 : onOutsideClick();
20
+ };
15
21
  React.useEffect(() => {
16
22
  window.dispatchEvent(new CustomEvent('scroll'));
17
23
  }, [left, top]);
18
- return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b(), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }] },
24
+ return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b({ pinned: tooltipPinned }), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }], onOutsideClick: tooltipPinned ? handleOutsideClick : undefined },
19
25
  React.createElement("div", { className: b('content') },
20
26
  React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer })))) : null;
21
27
  };
@@ -7,6 +7,9 @@
7
7
  animation-timing-function: unset;
8
8
  animation-fill-mode: unset;
9
9
  }
10
+ .gcharts-d3-tooltip[class].gcharts-d3-tooltip_pinned {
11
+ pointer-events: inherit;
12
+ }
10
13
  .gcharts-d3-tooltip__content {
11
14
  padding: 10px 14px;
12
15
  text-wrap: nowrap;
@@ -2,9 +2,10 @@ export * from './useChartDimensions';
2
2
  export * from './useChartOptions';
3
3
  export * from './useChartOptions/types';
4
4
  export * from './useAxisScales';
5
+ export * from './usePrevious';
5
6
  export * from './useSeries';
6
7
  export * from './useSeries/types';
7
8
  export * from './useShapes';
8
9
  export * from './useTooltip';
9
- export * from './useTooltip/types';
10
+ export * from './useSplit';
10
11
  export * from './useSplit/types';
@@ -2,9 +2,10 @@ export * from './useChartDimensions';
2
2
  export * from './useChartOptions';
3
3
  export * from './useChartOptions/types';
4
4
  export * from './useAxisScales';
5
+ export * from './usePrevious';
5
6
  export * from './useSeries';
6
7
  export * from './useSeries/types';
7
8
  export * from './useShapes';
8
9
  export * from './useTooltip';
9
- export * from './useTooltip/types';
10
+ export * from './useSplit';
10
11
  export * from './useSplit/types';
@@ -0,0 +1 @@
1
+ export declare function usePrevious<T>(value: T): T | undefined;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export function usePrevious(value) {
3
+ const ref = React.useRef();
4
+ React.useEffect(() => {
5
+ ref.current = value;
6
+ }, [value]);
7
+ return ref.current;
8
+ }
@@ -63,6 +63,7 @@ export const getPreparedLegend = (args) => {
63
63
  margin: titleMargin,
64
64
  style: titleStyle,
65
65
  height: titleHeight,
66
+ align: get(legend, 'title.align', 'left'),
66
67
  },
67
68
  width: legendWidth,
68
69
  ticks,
@@ -21,6 +21,7 @@ export type PreparedLegend = Required<Omit<ChartLegend, 'title' | 'colorScale'>>
21
21
  margin: number;
22
22
  style: BaseTextStyle;
23
23
  height: number;
24
+ align: Required<Required<ChartLegend>['title']>['align'];
24
25
  };
25
26
  ticks: {
26
27
  labelsMargin: number;
@@ -8,6 +8,7 @@ import { setActiveState } from '../utils';
8
8
  const b = block('d3-area');
9
9
  export const AreaSeriesShapes = (args) => {
10
10
  const { dispatcher, preparedData, seriesOptions, htmlLayout } = args;
11
+ const hoveredDataRef = React.useRef(null);
11
12
  const ref = React.useRef(null);
12
13
  React.useEffect(() => {
13
14
  var _a;
@@ -72,7 +73,8 @@ export const AreaSeriesShapes = (args) => {
72
73
  .call(renderMarker);
73
74
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
74
75
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
75
- dispatcher.on('hover-shape.area', (data) => {
76
+ function handleShapeHover(data) {
77
+ hoveredDataRef.current = data;
76
78
  const selected = (data === null || data === void 0 ? void 0 : data.filter((d) => d.series.type === 'area')) || [];
77
79
  const selectedDataItems = selected.map((d) => d.data);
78
80
  const selectedSeriesIds = selected.map((d) => { var _a; return (_a = d.series) === null || _a === void 0 ? void 0 : _a.id; });
@@ -132,7 +134,11 @@ export const AreaSeriesShapes = (args) => {
132
134
  }
133
135
  return d;
134
136
  });
135
- });
137
+ }
138
+ if (hoveredDataRef.current !== null) {
139
+ handleShapeHover(hoveredDataRef.current);
140
+ }
141
+ dispatcher.on('hover-shape.area', handleShapeHover);
136
142
  return () => {
137
143
  dispatcher.on('hover-shape.area', null);
138
144
  };
@@ -8,6 +8,7 @@ export * from './types';
8
8
  const b = block('d3-bar-x');
9
9
  export const BarXSeriesShapes = (args) => {
10
10
  const { dispatcher, preparedData, seriesOptions, htmlLayout } = args;
11
+ const hoveredDataRef = React.useRef(null);
11
12
  const ref = React.useRef(null);
12
13
  React.useEffect(() => {
13
14
  var _a;
@@ -46,7 +47,8 @@ export const BarXSeriesShapes = (args) => {
46
47
  .style('font-size', (d) => d.style.fontSize)
47
48
  .style('font-weight', (d) => d.style.fontWeight || null)
48
49
  .style('fill', (d) => d.style.fontColor || null);
49
- dispatcher.on('hover-shape.bar-x', (data) => {
50
+ function handleShapeHover(data) {
51
+ hoveredDataRef.current = data;
50
52
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
51
53
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
52
54
  if (!data) {
@@ -84,7 +86,11 @@ export const BarXSeriesShapes = (args) => {
84
86
  : (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
85
87
  });
86
88
  }
87
- });
89
+ }
90
+ if (hoveredDataRef.current !== null) {
91
+ handleShapeHover(hoveredDataRef.current);
92
+ }
93
+ dispatcher.on('hover-shape.bar-x', handleShapeHover);
88
94
  return () => {
89
95
  dispatcher.on('hover-shape.bar-x', null);
90
96
  };
@@ -7,6 +7,7 @@ export { prepareBarYData } from './prepare-data';
7
7
  const b = block('d3-bar-y');
8
8
  export const BarYSeriesShapes = (args) => {
9
9
  const { dispatcher, preparedData, seriesOptions, htmlLayout } = args;
10
+ const hoveredDataRef = React.useRef(null);
10
11
  const ref = React.useRef(null);
11
12
  React.useEffect(() => {
12
13
  if (!ref.current) {
@@ -46,7 +47,8 @@ export const BarYSeriesShapes = (args) => {
46
47
  .style('fill', (d) => d.style.fontColor || null);
47
48
  const hoverOptions = get(seriesOptions, 'bar-y.states.hover');
48
49
  const inactiveOptions = get(seriesOptions, 'bar-y.states.inactive');
49
- dispatcher.on('hover-shape.bar-y', (data) => {
50
+ function handleShapeHover(data) {
51
+ hoveredDataRef.current = data;
50
52
  if (hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled) {
51
53
  const hovered = data === null || data === void 0 ? void 0 : data.reduce((acc, d) => {
52
54
  acc.add(d.data.y);
@@ -73,7 +75,11 @@ export const BarYSeriesShapes = (args) => {
73
75
  rectSelection.attr('opacity', newOpacity);
74
76
  labelSelection.attr('opacity', newOpacity);
75
77
  }
76
- });
78
+ }
79
+ if (hoveredDataRef.current !== null) {
80
+ handleShapeHover(hoveredDataRef.current);
81
+ }
82
+ dispatcher.on('hover-shape.bar-y', handleShapeHover);
77
83
  return () => {
78
84
  dispatcher.on('hover-shape.bar-y', null);
79
85
  };
@@ -8,6 +8,7 @@ import { getLineDashArray, setActiveState } from '../utils';
8
8
  const b = block('d3-line');
9
9
  export const LineSeriesShapes = (args) => {
10
10
  const { dispatcher, preparedData, seriesOptions, htmlLayout } = args;
11
+ const hoveredDataRef = React.useRef(null);
11
12
  const ref = React.useRef(null);
12
13
  React.useEffect(() => {
13
14
  var _a;
@@ -60,7 +61,8 @@ export const LineSeriesShapes = (args) => {
60
61
  .call(renderMarker);
61
62
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
62
63
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
63
- dispatcher.on('hover-shape.line', (data) => {
64
+ function handleShapeHover(data) {
65
+ hoveredDataRef.current = data;
64
66
  const selected = (data === null || data === void 0 ? void 0 : data.filter((d) => d.series.type === 'line')) || [];
65
67
  const selectedDataItems = selected.map((d) => d.data);
66
68
  const selectedSeriesIds = selected.map((d) => { var _a; return (_a = d.series) === null || _a === void 0 ? void 0 : _a.id; });
@@ -119,7 +121,11 @@ export const LineSeriesShapes = (args) => {
119
121
  }
120
122
  return d;
121
123
  });
122
- });
124
+ }
125
+ if (hoveredDataRef.current !== null) {
126
+ handleShapeHover(hoveredDataRef.current);
127
+ }
128
+ dispatcher.on('hover-shape.line', handleShapeHover);
123
129
  return () => {
124
130
  dispatcher.on('hover-shape.line', null);
125
131
  };
@@ -9,6 +9,7 @@ export { prepareScatterData } from './prepare-data';
9
9
  const b = block('d3-scatter');
10
10
  export function ScatterSeriesShape(props) {
11
11
  const { dispatcher, preparedData, seriesOptions, htmlLayout } = props;
12
+ const hoveredDataRef = React.useRef(null);
12
13
  const ref = React.useRef(null);
13
14
  React.useEffect(() => {
14
15
  if (!ref.current) {
@@ -28,8 +29,9 @@ export function ScatterSeriesShape(props) {
28
29
  .attr('cursor', (d) => d.point.series.cursor);
29
30
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
30
31
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
31
- dispatcher.on('hover-shape.scatter', (data) => {
32
+ function handleShapeHover(data) {
32
33
  var _a;
34
+ hoveredDataRef.current = data;
33
35
  const selected = data === null || data === void 0 ? void 0 : data.find((d) => d.series.type === 'scatter');
34
36
  const selectedDataItem = selected === null || selected === void 0 ? void 0 : selected.data;
35
37
  const selectedSeriesId = (_a = selected === null || selected === void 0 ? void 0 : selected.series) === null || _a === void 0 ? void 0 : _a.id;
@@ -58,7 +60,11 @@ export function ScatterSeriesShape(props) {
58
60
  }
59
61
  return d;
60
62
  });
61
- });
63
+ }
64
+ if (hoveredDataRef.current !== null) {
65
+ handleShapeHover(hoveredDataRef.current);
66
+ }
67
+ dispatcher.on('hover-shape.scatter', handleShapeHover);
62
68
  return () => {
63
69
  dispatcher.on('hover-shape.scatter', null);
64
70
  };