@gravity-ui/charts 0.2.0 → 0.4.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.
- package/dist/cjs/components/ChartInner/index.js +43 -14
- package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +9 -0
- package/dist/cjs/components/Tooltip/ChartTooltipContent.js +11 -0
- package/dist/cjs/components/Tooltip/DefaultContent.d.ts +3 -4
- package/dist/cjs/components/Tooltip/DefaultContent.js +10 -10
- package/dist/cjs/components/Tooltip/index.js +3 -11
- package/dist/cjs/components/index.d.ts +1 -1
- package/dist/cjs/components/index.js +1 -0
- package/dist/cjs/constants/index.d.ts +1 -0
- package/dist/cjs/constants/index.js +1 -0
- package/dist/cjs/constants/misc.d.ts +1 -0
- package/dist/cjs/constants/misc.js +7 -0
- package/dist/cjs/types/chart/chart.d.ts +5 -2
- package/dist/cjs/types/chart/tooltip.d.ts +8 -0
- package/dist/cjs/utils/d3-dispatcher.d.ts +5 -0
- package/dist/cjs/utils/d3-dispatcher.js +6 -1
- package/dist/esm/components/ChartInner/index.js +43 -14
- package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +9 -0
- package/dist/esm/components/Tooltip/ChartTooltipContent.js +11 -0
- package/dist/esm/components/Tooltip/DefaultContent.d.ts +3 -4
- package/dist/esm/components/Tooltip/DefaultContent.js +10 -10
- package/dist/esm/components/Tooltip/index.js +3 -11
- package/dist/esm/components/index.d.ts +1 -1
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/constants/index.d.ts +1 -0
- package/dist/esm/constants/index.js +1 -0
- package/dist/esm/constants/misc.d.ts +1 -0
- package/dist/esm/constants/misc.js +7 -0
- package/dist/esm/types/chart/chart.d.ts +5 -2
- package/dist/esm/types/chart/tooltip.d.ts +8 -0
- package/dist/esm/utils/d3-dispatcher.d.ts +5 -0
- package/dist/esm/utils/d3-dispatcher.js +6 -1
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { pointer } from 'd3';
|
|
3
3
|
import throttle from 'lodash/throttle';
|
|
4
|
+
import { IS_TOUCH_ENABLED } from '../../constants';
|
|
4
5
|
import { useAxisScales, useChartDimensions, useChartOptions, useSeries, useShapes, } from '../../hooks';
|
|
5
6
|
import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
|
|
6
7
|
import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
|
|
7
8
|
import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
|
|
8
9
|
import { useSplit } from '../../hooks/useSplit';
|
|
9
|
-
import { block, getD3Dispatcher } from '../../utils';
|
|
10
|
+
import { EventType, block, getD3Dispatcher } from '../../utils';
|
|
10
11
|
import { getClosestPoints } from '../../utils/chart/get-closest-data';
|
|
11
12
|
import { AxisX, AxisY } from '../Axis';
|
|
12
13
|
import { Legend } from '../Legend';
|
|
@@ -17,7 +18,7 @@ import './styles.css';
|
|
|
17
18
|
const b = block('d3');
|
|
18
19
|
const THROTTLE_DELAY = 50;
|
|
19
20
|
export const ChartInner = (props) => {
|
|
20
|
-
var _a, _b;
|
|
21
|
+
var _a, _b, _c, _d;
|
|
21
22
|
const { width, height, data } = props;
|
|
22
23
|
const svgRef = React.useRef(null);
|
|
23
24
|
const htmlLayerRef = React.useRef(null);
|
|
@@ -73,39 +74,67 @@ export const ChartInner = (props) => {
|
|
|
73
74
|
htmlLayout: htmlLayerRef.current,
|
|
74
75
|
});
|
|
75
76
|
const clickHandler = (_b = (_a = data.chart) === null || _a === void 0 ? void 0 : _a.events) === null || _b === void 0 ? void 0 : _b.click;
|
|
77
|
+
const pointerMoveHandler = (_d = (_c = data.chart) === null || _c === void 0 ? void 0 : _c.events) === null || _d === void 0 ? void 0 : _d.pointermove;
|
|
76
78
|
React.useEffect(() => {
|
|
77
79
|
if (clickHandler) {
|
|
78
|
-
dispatcher.on(
|
|
80
|
+
dispatcher.on(EventType.CLICK_CHART, clickHandler);
|
|
81
|
+
}
|
|
82
|
+
if (pointerMoveHandler) {
|
|
83
|
+
dispatcher.on(EventType.POINTERMOVE_CHART, (...args) => {
|
|
84
|
+
const [handlerData, event] = args;
|
|
85
|
+
pointerMoveHandler(handlerData, event);
|
|
86
|
+
});
|
|
79
87
|
}
|
|
80
88
|
return () => {
|
|
81
|
-
dispatcher.on(
|
|
89
|
+
dispatcher.on(EventType.CLICK_CHART, null);
|
|
90
|
+
dispatcher.on(EventType.POINTERMOVE_CHART, null);
|
|
82
91
|
};
|
|
83
|
-
}, [dispatcher, clickHandler]);
|
|
92
|
+
}, [dispatcher, clickHandler, pointerMoveHandler]);
|
|
84
93
|
const boundsOffsetTop = chart.margin.top;
|
|
85
94
|
// We only need to consider the width of the first left axis
|
|
86
95
|
const boundsOffsetLeft = chart.margin.left + getYAxisWidth(yAxis[0]);
|
|
87
96
|
const isOutsideBounds = React.useCallback((x, y) => {
|
|
88
97
|
return x < 0 || x > boundsWidth || y < 0 || y > boundsHeight;
|
|
89
98
|
}, [boundsHeight, boundsWidth]);
|
|
90
|
-
const
|
|
91
|
-
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
99
|
+
const handleMove = ([pointerX, pointerY], event) => {
|
|
92
100
|
const x = pointerX - boundsOffsetLeft;
|
|
93
101
|
const y = pointerY - boundsOffsetTop;
|
|
94
102
|
if (isOutsideBounds(x, y)) {
|
|
95
|
-
dispatcher.call(
|
|
103
|
+
dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
|
|
104
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
98
107
|
const closest = getClosestPoints({
|
|
99
108
|
position: [x, y],
|
|
100
109
|
shapesData,
|
|
101
110
|
});
|
|
102
|
-
dispatcher.call(
|
|
111
|
+
dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY]);
|
|
112
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
|
|
113
|
+
hovered: closest,
|
|
114
|
+
xAxis,
|
|
115
|
+
yAxis: yAxis[0],
|
|
116
|
+
}, event);
|
|
117
|
+
};
|
|
118
|
+
const handleMouseMove = (event) => {
|
|
119
|
+
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
120
|
+
handleMove([pointerX, pointerY], event);
|
|
121
|
+
};
|
|
122
|
+
const throttledHandleMouseMove = IS_TOUCH_ENABLED
|
|
123
|
+
? undefined
|
|
124
|
+
: throttle(handleMouseMove, THROTTLE_DELAY);
|
|
125
|
+
const handleMouseLeave = (event) => {
|
|
126
|
+
throttledHandleMouseMove === null || throttledHandleMouseMove === void 0 ? void 0 : throttledHandleMouseMove.cancel();
|
|
127
|
+
dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
|
|
128
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
|
|
103
129
|
};
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
const handleTouchMove = (event) => {
|
|
131
|
+
const touch = event.touches[0];
|
|
132
|
+
const [pointerX, pointerY] = pointer(touch, svgRef.current);
|
|
133
|
+
handleMove([pointerX, pointerY], event);
|
|
108
134
|
};
|
|
135
|
+
const throttledHandleTouchMove = IS_TOUCH_ENABLED
|
|
136
|
+
? throttle(handleTouchMove, THROTTLE_DELAY)
|
|
137
|
+
: undefined;
|
|
109
138
|
const handleChartClick = React.useCallback((event) => {
|
|
110
139
|
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
111
140
|
const x = pointerX - boundsOffsetLeft;
|
|
@@ -124,7 +153,7 @@ export const ChartInner = (props) => {
|
|
|
124
153
|
dispatcher.call('click-chart', undefined, { point: selected.data, series: selected.series }, event);
|
|
125
154
|
}, [boundsOffsetLeft, boundsOffsetTop, dispatcher, isOutsideBounds, shapesData]);
|
|
126
155
|
return (React.createElement(React.Fragment, null,
|
|
127
|
-
React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleChartClick },
|
|
156
|
+
React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick },
|
|
128
157
|
title && React.createElement(Title, Object.assign({}, title, { chartWidth: width })),
|
|
129
158
|
React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit.plots.map((plot, index) => {
|
|
130
159
|
return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ChartTooltip, ChartXAxis, ChartYAxis, TooltipDataChunk } from '../../types';
|
|
3
|
+
export type ChartTooltipContentProps = {
|
|
4
|
+
hovered?: TooltipDataChunk[];
|
|
5
|
+
xAxis?: ChartXAxis;
|
|
6
|
+
yAxis?: ChartYAxis;
|
|
7
|
+
renderer?: ChartTooltip['renderer'];
|
|
8
|
+
};
|
|
9
|
+
export declare const ChartTooltipContent: (props: ChartTooltipContentProps) => React.JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import isNil from 'lodash/isNil';
|
|
3
|
+
import { DefaultContent } from './DefaultContent';
|
|
4
|
+
export const ChartTooltipContent = (props) => {
|
|
5
|
+
const { hovered, xAxis, yAxis, renderer } = props;
|
|
6
|
+
if (!hovered) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const customTooltip = renderer === null || renderer === void 0 ? void 0 : renderer({ hovered, xAxis, yAxis });
|
|
10
|
+
return isNil(customTooltip) ? (React.createElement(DefaultContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis })) : (customTooltip);
|
|
11
|
+
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import type { TooltipDataChunk } from '../../types';
|
|
2
|
+
import type { ChartXAxis, ChartYAxis, TooltipDataChunk } from '../../types';
|
|
4
3
|
type Props = {
|
|
5
4
|
hovered: TooltipDataChunk[];
|
|
6
|
-
xAxis
|
|
7
|
-
yAxis
|
|
5
|
+
xAxis?: ChartXAxis;
|
|
6
|
+
yAxis?: ChartYAxis;
|
|
8
7
|
};
|
|
9
8
|
export declare const DefaultContent: ({ hovered, xAxis, yAxis }: Props) => React.JSX.Element;
|
|
10
9
|
export {};
|
|
@@ -5,8 +5,8 @@ import { formatNumber } from '../../libs';
|
|
|
5
5
|
import { block, getDataCategoryValue, getWaterfallPointSubtotal } from '../../utils';
|
|
6
6
|
const b = block('d3-tooltip');
|
|
7
7
|
const DEFAULT_DATE_FORMAT = 'DD.MM.YY';
|
|
8
|
-
const getRowData = (fieldName,
|
|
9
|
-
switch (axis.type) {
|
|
8
|
+
const getRowData = (fieldName, data, axis) => {
|
|
9
|
+
switch (axis === null || axis === void 0 ? void 0 : axis.type) {
|
|
10
10
|
case 'category': {
|
|
11
11
|
const categories = get(axis, 'categories', []);
|
|
12
12
|
return getDataCategoryValue({ axisDirection: fieldName, categories, data });
|
|
@@ -25,17 +25,17 @@ const getRowData = (fieldName, axis, data) => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
const getXRowData = (
|
|
29
|
-
const getYRowData = (
|
|
28
|
+
const getXRowData = (data, xAxis) => getRowData('x', data, xAxis);
|
|
29
|
+
const getYRowData = (data, yAxis) => getRowData('y', data, yAxis);
|
|
30
30
|
const getMeasureValue = (data, xAxis, yAxis) => {
|
|
31
31
|
var _a, _b;
|
|
32
32
|
if (data.every((item) => ['pie', 'treemap', 'waterfall'].includes(item.series.type))) {
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
if (data.some((item) => item.series.type === 'bar-y')) {
|
|
36
|
-
return getYRowData(
|
|
36
|
+
return getYRowData((_a = data[0]) === null || _a === void 0 ? void 0 : _a.data, yAxis);
|
|
37
37
|
}
|
|
38
|
-
return getXRowData(
|
|
38
|
+
return getXRowData((_b = data[0]) === null || _b === void 0 ? void 0 : _b.data, xAxis);
|
|
39
39
|
};
|
|
40
40
|
export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
41
41
|
const measureValue = getMeasureValue(hovered, xAxis, yAxis);
|
|
@@ -52,7 +52,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
52
52
|
const value = (React.createElement(React.Fragment, null,
|
|
53
53
|
series.name,
|
|
54
54
|
": ",
|
|
55
|
-
getYRowData(
|
|
55
|
+
getYRowData(data, yAxis)));
|
|
56
56
|
return (React.createElement("div", { key: id, className: b('content-row') },
|
|
57
57
|
React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
|
|
58
58
|
React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
|
|
@@ -63,12 +63,12 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
63
63
|
return (React.createElement("div", { key: `${id}_${get(data, 'x')}` },
|
|
64
64
|
!isTotal && (React.createElement(React.Fragment, null,
|
|
65
65
|
React.createElement("div", { key: id, className: b('content-row') },
|
|
66
|
-
React.createElement("b", null, getXRowData(
|
|
66
|
+
React.createElement("b", null, getXRowData(data, xAxis))),
|
|
67
67
|
React.createElement("div", { className: b('content-row') },
|
|
68
68
|
React.createElement("span", null,
|
|
69
69
|
series.name,
|
|
70
70
|
"\u00A0"),
|
|
71
|
-
React.createElement("span", null, getYRowData(
|
|
71
|
+
React.createElement("span", null, getYRowData(data, yAxis))))),
|
|
72
72
|
React.createElement("div", { key: id, className: b('content-row') },
|
|
73
73
|
isTotal ? 'Total' : 'Subtotal',
|
|
74
74
|
": ",
|
|
@@ -78,7 +78,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
78
78
|
const value = (React.createElement(React.Fragment, null,
|
|
79
79
|
series.name,
|
|
80
80
|
": ",
|
|
81
|
-
getXRowData(
|
|
81
|
+
getXRowData(data, xAxis)));
|
|
82
82
|
return (React.createElement("div", { key: id, className: b('content-row') },
|
|
83
83
|
React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
|
|
84
84
|
React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Popup, useVirtualElementRef } from '@gravity-ui/uikit';
|
|
3
|
-
import isNil from 'lodash/isNil';
|
|
4
3
|
import { useTooltip } from '../../hooks';
|
|
5
4
|
import { block } from '../../utils';
|
|
6
|
-
import {
|
|
5
|
+
import { ChartTooltipContent } from './ChartTooltipContent';
|
|
7
6
|
import './styles.css';
|
|
8
7
|
const b = block('d3-tooltip');
|
|
9
8
|
export const Tooltip = (props) => {
|
|
@@ -13,17 +12,10 @@ export const Tooltip = (props) => {
|
|
|
13
12
|
const left = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[0]) || 0) + containerRect.left;
|
|
14
13
|
const top = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[1]) || 0) + containerRect.top;
|
|
15
14
|
const anchorRef = useVirtualElementRef({ rect: { top, left } });
|
|
16
|
-
const content = React.useMemo(() => {
|
|
17
|
-
var _a;
|
|
18
|
-
if (!hovered) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const customTooltip = (_a = tooltip.renderer) === null || _a === void 0 ? void 0 : _a.call(tooltip, { hovered });
|
|
22
|
-
return isNil(customTooltip) ? (React.createElement(DefaultContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis })) : (customTooltip);
|
|
23
|
-
}, [hovered, tooltip, xAxis, yAxis]);
|
|
24
15
|
React.useEffect(() => {
|
|
25
16
|
window.dispatchEvent(new CustomEvent('scroll'));
|
|
26
17
|
}, [left, top]);
|
|
27
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 } }] },
|
|
28
|
-
React.createElement("div", { className: b('content') },
|
|
19
|
+
React.createElement("div", { className: b('content') },
|
|
20
|
+
React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer })))) : null;
|
|
29
21
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ChartData } from '../types';
|
|
3
|
+
export * from './Tooltip/ChartTooltipContent';
|
|
3
4
|
export type ChartRef = {
|
|
4
5
|
reflow: () => void;
|
|
5
6
|
};
|
|
@@ -15,4 +16,3 @@ export type ChartProps = {
|
|
|
15
16
|
}) => void;
|
|
16
17
|
};
|
|
17
18
|
export declare const Chart: React.ForwardRefExoticComponent<ChartProps & React.RefAttributes<ChartRef>>;
|
|
18
|
-
export {};
|
|
@@ -5,6 +5,7 @@ import { i18nFactory } from '../i18n';
|
|
|
5
5
|
import { getUniqId } from '../utils';
|
|
6
6
|
import { validateData } from '../validation';
|
|
7
7
|
import { ChartInner } from './ChartInner';
|
|
8
|
+
export * from './Tooltip/ChartTooltipContent';
|
|
8
9
|
export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
|
|
9
10
|
const { data, lang, onResize } = props;
|
|
10
11
|
const validatedData = React.useRef();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const IS_TOUCH_ENABLED: boolean;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MeaningfulAny } from '../misc';
|
|
2
|
+
import type { ChartTooltipRendererData } from './tooltip';
|
|
1
3
|
export type ChartMargin = {
|
|
2
4
|
top: number;
|
|
3
5
|
right: number;
|
|
@@ -8,8 +10,9 @@ export type ChartOptions = {
|
|
|
8
10
|
margin?: Partial<ChartMargin>;
|
|
9
11
|
events?: {
|
|
10
12
|
click?: (data: {
|
|
11
|
-
point:
|
|
12
|
-
series:
|
|
13
|
+
point: MeaningfulAny;
|
|
14
|
+
series: MeaningfulAny;
|
|
13
15
|
}, event: PointerEvent) => void;
|
|
16
|
+
pointermove?: (data: ChartTooltipRendererData | undefined, event: PointerEvent) => void;
|
|
14
17
|
};
|
|
15
18
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MeaningfulAny } from '../misc';
|
|
2
2
|
import type { AreaSeries, AreaSeriesData } from './area';
|
|
3
|
+
import type { ChartXAxis, ChartYAxis } from './axis';
|
|
3
4
|
import type { BarXSeries, BarXSeriesData } from './bar-x';
|
|
4
5
|
import type { BarYSeries, BarYSeriesData } from './bar-y';
|
|
5
6
|
import type { LineSeries, LineSeriesData } from './line';
|
|
@@ -58,10 +59,17 @@ export type TooltipDataChunkWaterfall<T = MeaningfulAny> = {
|
|
|
58
59
|
export type TooltipDataChunk<T = MeaningfulAny> = (TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T> | TooltipDataChunkTreemap<T> | TooltipDataChunkWaterfall<T>) & {
|
|
59
60
|
closest?: boolean;
|
|
60
61
|
};
|
|
62
|
+
export type ChartTooltipRendererData<T = MeaningfulAny> = {
|
|
63
|
+
hovered: TooltipDataChunk<T>[];
|
|
64
|
+
xAxis?: ChartXAxis;
|
|
65
|
+
yAxis?: ChartYAxis;
|
|
66
|
+
};
|
|
61
67
|
export type ChartTooltip<T = MeaningfulAny> = {
|
|
62
68
|
enabled?: boolean;
|
|
63
69
|
/** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
|
|
64
70
|
renderer?: (args: {
|
|
65
71
|
hovered: TooltipDataChunk<T>[];
|
|
72
|
+
xAxis?: ChartXAxis;
|
|
73
|
+
yAxis?: ChartYAxis;
|
|
66
74
|
}) => React.ReactElement | null;
|
|
67
75
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { dispatch } from 'd3';
|
|
2
|
+
export const EventType = {
|
|
3
|
+
CLICK_CHART: 'click-chart',
|
|
4
|
+
HOVER_SHAPE: 'hover-shape',
|
|
5
|
+
POINTERMOVE_CHART: 'pointermove-chart',
|
|
6
|
+
};
|
|
2
7
|
export const getD3Dispatcher = () => {
|
|
3
|
-
return dispatch(
|
|
8
|
+
return dispatch(EventType.CLICK_CHART, EventType.HOVER_SHAPE, EventType.POINTERMOVE_CHART);
|
|
4
9
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { pointer } from 'd3';
|
|
3
3
|
import throttle from 'lodash/throttle';
|
|
4
|
+
import { IS_TOUCH_ENABLED } from '../../constants';
|
|
4
5
|
import { useAxisScales, useChartDimensions, useChartOptions, useSeries, useShapes, } from '../../hooks';
|
|
5
6
|
import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
|
|
6
7
|
import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
|
|
7
8
|
import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
|
|
8
9
|
import { useSplit } from '../../hooks/useSplit';
|
|
9
|
-
import { block, getD3Dispatcher } from '../../utils';
|
|
10
|
+
import { EventType, block, getD3Dispatcher } from '../../utils';
|
|
10
11
|
import { getClosestPoints } from '../../utils/chart/get-closest-data';
|
|
11
12
|
import { AxisX, AxisY } from '../Axis';
|
|
12
13
|
import { Legend } from '../Legend';
|
|
@@ -17,7 +18,7 @@ import './styles.css';
|
|
|
17
18
|
const b = block('d3');
|
|
18
19
|
const THROTTLE_DELAY = 50;
|
|
19
20
|
export const ChartInner = (props) => {
|
|
20
|
-
var _a, _b;
|
|
21
|
+
var _a, _b, _c, _d;
|
|
21
22
|
const { width, height, data } = props;
|
|
22
23
|
const svgRef = React.useRef(null);
|
|
23
24
|
const htmlLayerRef = React.useRef(null);
|
|
@@ -73,39 +74,67 @@ export const ChartInner = (props) => {
|
|
|
73
74
|
htmlLayout: htmlLayerRef.current,
|
|
74
75
|
});
|
|
75
76
|
const clickHandler = (_b = (_a = data.chart) === null || _a === void 0 ? void 0 : _a.events) === null || _b === void 0 ? void 0 : _b.click;
|
|
77
|
+
const pointerMoveHandler = (_d = (_c = data.chart) === null || _c === void 0 ? void 0 : _c.events) === null || _d === void 0 ? void 0 : _d.pointermove;
|
|
76
78
|
React.useEffect(() => {
|
|
77
79
|
if (clickHandler) {
|
|
78
|
-
dispatcher.on(
|
|
80
|
+
dispatcher.on(EventType.CLICK_CHART, clickHandler);
|
|
81
|
+
}
|
|
82
|
+
if (pointerMoveHandler) {
|
|
83
|
+
dispatcher.on(EventType.POINTERMOVE_CHART, (...args) => {
|
|
84
|
+
const [handlerData, event] = args;
|
|
85
|
+
pointerMoveHandler(handlerData, event);
|
|
86
|
+
});
|
|
79
87
|
}
|
|
80
88
|
return () => {
|
|
81
|
-
dispatcher.on(
|
|
89
|
+
dispatcher.on(EventType.CLICK_CHART, null);
|
|
90
|
+
dispatcher.on(EventType.POINTERMOVE_CHART, null);
|
|
82
91
|
};
|
|
83
|
-
}, [dispatcher, clickHandler]);
|
|
92
|
+
}, [dispatcher, clickHandler, pointerMoveHandler]);
|
|
84
93
|
const boundsOffsetTop = chart.margin.top;
|
|
85
94
|
// We only need to consider the width of the first left axis
|
|
86
95
|
const boundsOffsetLeft = chart.margin.left + getYAxisWidth(yAxis[0]);
|
|
87
96
|
const isOutsideBounds = React.useCallback((x, y) => {
|
|
88
97
|
return x < 0 || x > boundsWidth || y < 0 || y > boundsHeight;
|
|
89
98
|
}, [boundsHeight, boundsWidth]);
|
|
90
|
-
const
|
|
91
|
-
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
99
|
+
const handleMove = ([pointerX, pointerY], event) => {
|
|
92
100
|
const x = pointerX - boundsOffsetLeft;
|
|
93
101
|
const y = pointerY - boundsOffsetTop;
|
|
94
102
|
if (isOutsideBounds(x, y)) {
|
|
95
|
-
dispatcher.call(
|
|
103
|
+
dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
|
|
104
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
98
107
|
const closest = getClosestPoints({
|
|
99
108
|
position: [x, y],
|
|
100
109
|
shapesData,
|
|
101
110
|
});
|
|
102
|
-
dispatcher.call(
|
|
111
|
+
dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY]);
|
|
112
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
|
|
113
|
+
hovered: closest,
|
|
114
|
+
xAxis,
|
|
115
|
+
yAxis: yAxis[0],
|
|
116
|
+
}, event);
|
|
117
|
+
};
|
|
118
|
+
const handleMouseMove = (event) => {
|
|
119
|
+
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
120
|
+
handleMove([pointerX, pointerY], event);
|
|
121
|
+
};
|
|
122
|
+
const throttledHandleMouseMove = IS_TOUCH_ENABLED
|
|
123
|
+
? undefined
|
|
124
|
+
: throttle(handleMouseMove, THROTTLE_DELAY);
|
|
125
|
+
const handleMouseLeave = (event) => {
|
|
126
|
+
throttledHandleMouseMove === null || throttledHandleMouseMove === void 0 ? void 0 : throttledHandleMouseMove.cancel();
|
|
127
|
+
dispatcher.call(EventType.HOVER_SHAPE, {}, undefined);
|
|
128
|
+
dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
|
|
103
129
|
};
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
const handleTouchMove = (event) => {
|
|
131
|
+
const touch = event.touches[0];
|
|
132
|
+
const [pointerX, pointerY] = pointer(touch, svgRef.current);
|
|
133
|
+
handleMove([pointerX, pointerY], event);
|
|
108
134
|
};
|
|
135
|
+
const throttledHandleTouchMove = IS_TOUCH_ENABLED
|
|
136
|
+
? throttle(handleTouchMove, THROTTLE_DELAY)
|
|
137
|
+
: undefined;
|
|
109
138
|
const handleChartClick = React.useCallback((event) => {
|
|
110
139
|
const [pointerX, pointerY] = pointer(event, svgRef.current);
|
|
111
140
|
const x = pointerX - boundsOffsetLeft;
|
|
@@ -124,7 +153,7 @@ export const ChartInner = (props) => {
|
|
|
124
153
|
dispatcher.call('click-chart', undefined, { point: selected.data, series: selected.series }, event);
|
|
125
154
|
}, [boundsOffsetLeft, boundsOffsetTop, dispatcher, isOutsideBounds, shapesData]);
|
|
126
155
|
return (React.createElement(React.Fragment, null,
|
|
127
|
-
React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleChartClick },
|
|
156
|
+
React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick },
|
|
128
157
|
title && React.createElement(Title, Object.assign({}, title, { chartWidth: width })),
|
|
129
158
|
React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit.plots.map((plot, index) => {
|
|
130
159
|
return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ChartTooltip, ChartXAxis, ChartYAxis, TooltipDataChunk } from '../../types';
|
|
3
|
+
export type ChartTooltipContentProps = {
|
|
4
|
+
hovered?: TooltipDataChunk[];
|
|
5
|
+
xAxis?: ChartXAxis;
|
|
6
|
+
yAxis?: ChartYAxis;
|
|
7
|
+
renderer?: ChartTooltip['renderer'];
|
|
8
|
+
};
|
|
9
|
+
export declare const ChartTooltipContent: (props: ChartTooltipContentProps) => React.JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import isNil from 'lodash/isNil';
|
|
3
|
+
import { DefaultContent } from './DefaultContent';
|
|
4
|
+
export const ChartTooltipContent = (props) => {
|
|
5
|
+
const { hovered, xAxis, yAxis, renderer } = props;
|
|
6
|
+
if (!hovered) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const customTooltip = renderer === null || renderer === void 0 ? void 0 : renderer({ hovered, xAxis, yAxis });
|
|
10
|
+
return isNil(customTooltip) ? (React.createElement(DefaultContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis })) : (customTooltip);
|
|
11
|
+
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import type { TooltipDataChunk } from '../../types';
|
|
2
|
+
import type { ChartXAxis, ChartYAxis, TooltipDataChunk } from '../../types';
|
|
4
3
|
type Props = {
|
|
5
4
|
hovered: TooltipDataChunk[];
|
|
6
|
-
xAxis
|
|
7
|
-
yAxis
|
|
5
|
+
xAxis?: ChartXAxis;
|
|
6
|
+
yAxis?: ChartYAxis;
|
|
8
7
|
};
|
|
9
8
|
export declare const DefaultContent: ({ hovered, xAxis, yAxis }: Props) => React.JSX.Element;
|
|
10
9
|
export {};
|
|
@@ -5,8 +5,8 @@ import { formatNumber } from '../../libs';
|
|
|
5
5
|
import { block, getDataCategoryValue, getWaterfallPointSubtotal } from '../../utils';
|
|
6
6
|
const b = block('d3-tooltip');
|
|
7
7
|
const DEFAULT_DATE_FORMAT = 'DD.MM.YY';
|
|
8
|
-
const getRowData = (fieldName,
|
|
9
|
-
switch (axis.type) {
|
|
8
|
+
const getRowData = (fieldName, data, axis) => {
|
|
9
|
+
switch (axis === null || axis === void 0 ? void 0 : axis.type) {
|
|
10
10
|
case 'category': {
|
|
11
11
|
const categories = get(axis, 'categories', []);
|
|
12
12
|
return getDataCategoryValue({ axisDirection: fieldName, categories, data });
|
|
@@ -25,17 +25,17 @@ const getRowData = (fieldName, axis, data) => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
const getXRowData = (
|
|
29
|
-
const getYRowData = (
|
|
28
|
+
const getXRowData = (data, xAxis) => getRowData('x', data, xAxis);
|
|
29
|
+
const getYRowData = (data, yAxis) => getRowData('y', data, yAxis);
|
|
30
30
|
const getMeasureValue = (data, xAxis, yAxis) => {
|
|
31
31
|
var _a, _b;
|
|
32
32
|
if (data.every((item) => ['pie', 'treemap', 'waterfall'].includes(item.series.type))) {
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
if (data.some((item) => item.series.type === 'bar-y')) {
|
|
36
|
-
return getYRowData(
|
|
36
|
+
return getYRowData((_a = data[0]) === null || _a === void 0 ? void 0 : _a.data, yAxis);
|
|
37
37
|
}
|
|
38
|
-
return getXRowData(
|
|
38
|
+
return getXRowData((_b = data[0]) === null || _b === void 0 ? void 0 : _b.data, xAxis);
|
|
39
39
|
};
|
|
40
40
|
export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
41
41
|
const measureValue = getMeasureValue(hovered, xAxis, yAxis);
|
|
@@ -52,7 +52,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
52
52
|
const value = (React.createElement(React.Fragment, null,
|
|
53
53
|
series.name,
|
|
54
54
|
": ",
|
|
55
|
-
getYRowData(
|
|
55
|
+
getYRowData(data, yAxis)));
|
|
56
56
|
return (React.createElement("div", { key: id, className: b('content-row') },
|
|
57
57
|
React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
|
|
58
58
|
React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
|
|
@@ -63,12 +63,12 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
63
63
|
return (React.createElement("div", { key: `${id}_${get(data, 'x')}` },
|
|
64
64
|
!isTotal && (React.createElement(React.Fragment, null,
|
|
65
65
|
React.createElement("div", { key: id, className: b('content-row') },
|
|
66
|
-
React.createElement("b", null, getXRowData(
|
|
66
|
+
React.createElement("b", null, getXRowData(data, xAxis))),
|
|
67
67
|
React.createElement("div", { className: b('content-row') },
|
|
68
68
|
React.createElement("span", null,
|
|
69
69
|
series.name,
|
|
70
70
|
"\u00A0"),
|
|
71
|
-
React.createElement("span", null, getYRowData(
|
|
71
|
+
React.createElement("span", null, getYRowData(data, yAxis))))),
|
|
72
72
|
React.createElement("div", { key: id, className: b('content-row') },
|
|
73
73
|
isTotal ? 'Total' : 'Subtotal',
|
|
74
74
|
": ",
|
|
@@ -78,7 +78,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
78
78
|
const value = (React.createElement(React.Fragment, null,
|
|
79
79
|
series.name,
|
|
80
80
|
": ",
|
|
81
|
-
getXRowData(
|
|
81
|
+
getXRowData(data, xAxis)));
|
|
82
82
|
return (React.createElement("div", { key: id, className: b('content-row') },
|
|
83
83
|
React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
|
|
84
84
|
React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Popup, useVirtualElementRef } from '@gravity-ui/uikit';
|
|
3
|
-
import isNil from 'lodash/isNil';
|
|
4
3
|
import { useTooltip } from '../../hooks';
|
|
5
4
|
import { block } from '../../utils';
|
|
6
|
-
import {
|
|
5
|
+
import { ChartTooltipContent } from './ChartTooltipContent';
|
|
7
6
|
import './styles.css';
|
|
8
7
|
const b = block('d3-tooltip');
|
|
9
8
|
export const Tooltip = (props) => {
|
|
@@ -13,17 +12,10 @@ export const Tooltip = (props) => {
|
|
|
13
12
|
const left = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[0]) || 0) + containerRect.left;
|
|
14
13
|
const top = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[1]) || 0) + containerRect.top;
|
|
15
14
|
const anchorRef = useVirtualElementRef({ rect: { top, left } });
|
|
16
|
-
const content = React.useMemo(() => {
|
|
17
|
-
var _a;
|
|
18
|
-
if (!hovered) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const customTooltip = (_a = tooltip.renderer) === null || _a === void 0 ? void 0 : _a.call(tooltip, { hovered });
|
|
22
|
-
return isNil(customTooltip) ? (React.createElement(DefaultContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis })) : (customTooltip);
|
|
23
|
-
}, [hovered, tooltip, xAxis, yAxis]);
|
|
24
15
|
React.useEffect(() => {
|
|
25
16
|
window.dispatchEvent(new CustomEvent('scroll'));
|
|
26
17
|
}, [left, top]);
|
|
27
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 } }] },
|
|
28
|
-
React.createElement("div", { className: b('content') },
|
|
19
|
+
React.createElement("div", { className: b('content') },
|
|
20
|
+
React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer })))) : null;
|
|
29
21
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ChartData } from '../types';
|
|
3
|
+
export * from './Tooltip/ChartTooltipContent';
|
|
3
4
|
export type ChartRef = {
|
|
4
5
|
reflow: () => void;
|
|
5
6
|
};
|
|
@@ -15,4 +16,3 @@ export type ChartProps = {
|
|
|
15
16
|
}) => void;
|
|
16
17
|
};
|
|
17
18
|
export declare const Chart: React.ForwardRefExoticComponent<ChartProps & React.RefAttributes<ChartRef>>;
|
|
18
|
-
export {};
|
|
@@ -5,6 +5,7 @@ import { i18nFactory } from '../i18n';
|
|
|
5
5
|
import { getUniqId } from '../utils';
|
|
6
6
|
import { validateData } from '../validation';
|
|
7
7
|
import { ChartInner } from './ChartInner';
|
|
8
|
+
export * from './Tooltip/ChartTooltipContent';
|
|
8
9
|
export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
|
|
9
10
|
const { data, lang, onResize } = props;
|
|
10
11
|
const validatedData = React.useRef();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const IS_TOUCH_ENABLED: boolean;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MeaningfulAny } from '../misc';
|
|
2
|
+
import type { ChartTooltipRendererData } from './tooltip';
|
|
1
3
|
export type ChartMargin = {
|
|
2
4
|
top: number;
|
|
3
5
|
right: number;
|
|
@@ -8,8 +10,9 @@ export type ChartOptions = {
|
|
|
8
10
|
margin?: Partial<ChartMargin>;
|
|
9
11
|
events?: {
|
|
10
12
|
click?: (data: {
|
|
11
|
-
point:
|
|
12
|
-
series:
|
|
13
|
+
point: MeaningfulAny;
|
|
14
|
+
series: MeaningfulAny;
|
|
13
15
|
}, event: PointerEvent) => void;
|
|
16
|
+
pointermove?: (data: ChartTooltipRendererData | undefined, event: PointerEvent) => void;
|
|
14
17
|
};
|
|
15
18
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MeaningfulAny } from '../misc';
|
|
2
2
|
import type { AreaSeries, AreaSeriesData } from './area';
|
|
3
|
+
import type { ChartXAxis, ChartYAxis } from './axis';
|
|
3
4
|
import type { BarXSeries, BarXSeriesData } from './bar-x';
|
|
4
5
|
import type { BarYSeries, BarYSeriesData } from './bar-y';
|
|
5
6
|
import type { LineSeries, LineSeriesData } from './line';
|
|
@@ -58,10 +59,17 @@ export type TooltipDataChunkWaterfall<T = MeaningfulAny> = {
|
|
|
58
59
|
export type TooltipDataChunk<T = MeaningfulAny> = (TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T> | TooltipDataChunkTreemap<T> | TooltipDataChunkWaterfall<T>) & {
|
|
59
60
|
closest?: boolean;
|
|
60
61
|
};
|
|
62
|
+
export type ChartTooltipRendererData<T = MeaningfulAny> = {
|
|
63
|
+
hovered: TooltipDataChunk<T>[];
|
|
64
|
+
xAxis?: ChartXAxis;
|
|
65
|
+
yAxis?: ChartYAxis;
|
|
66
|
+
};
|
|
61
67
|
export type ChartTooltip<T = MeaningfulAny> = {
|
|
62
68
|
enabled?: boolean;
|
|
63
69
|
/** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
|
|
64
70
|
renderer?: (args: {
|
|
65
71
|
hovered: TooltipDataChunk<T>[];
|
|
72
|
+
xAxis?: ChartXAxis;
|
|
73
|
+
yAxis?: ChartYAxis;
|
|
66
74
|
}) => React.ReactElement | null;
|
|
67
75
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { dispatch } from 'd3';
|
|
2
|
+
export const EventType = {
|
|
3
|
+
CLICK_CHART: 'click-chart',
|
|
4
|
+
HOVER_SHAPE: 'hover-shape',
|
|
5
|
+
POINTERMOVE_CHART: 'pointermove-chart',
|
|
6
|
+
};
|
|
2
7
|
export const getD3Dispatcher = () => {
|
|
3
|
-
return dispatch(
|
|
8
|
+
return dispatch(EventType.CLICK_CHART, EventType.HOVER_SHAPE, EventType.POINTERMOVE_CHART);
|
|
4
9
|
};
|