@gravity-ui/chartkit 4.5.0 → 4.6.1
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/build/plugins/d3/renderer/D3Widget.js +11 -1
- package/build/plugins/d3/renderer/components/AxisX.d.ts +1 -2
- package/build/plugins/d3/renderer/components/AxisX.js +39 -61
- package/build/plugins/d3/renderer/components/AxisY.js +28 -31
- package/build/plugins/d3/renderer/components/Chart.js +19 -7
- package/build/plugins/d3/renderer/components/Legend.d.ts +5 -6
- package/build/plugins/d3/renderer/components/Legend.js +139 -84
- package/build/plugins/d3/renderer/components/styles.css +27 -0
- package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +5 -0
- package/build/plugins/d3/renderer/constants/defaults/axis.js +5 -0
- package/build/plugins/d3/renderer/constants/defaults/index.d.ts +2 -0
- package/build/plugins/d3/renderer/constants/defaults/index.js +2 -0
- package/build/plugins/d3/renderer/constants/defaults/legend.d.ts +4 -0
- package/build/plugins/d3/renderer/constants/defaults/legend.js +8 -0
- package/build/plugins/d3/renderer/{constants.d.ts → constants/index.d.ts} +1 -1
- package/build/plugins/d3/renderer/{constants.js → constants/index.js} +1 -1
- package/build/plugins/d3/renderer/hooks/useAxisScales/index.d.ts +3 -1
- package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +64 -62
- package/build/plugins/d3/renderer/hooks/useChartDimensions/index.d.ts +7 -4
- package/build/plugins/d3/renderer/hooks/useChartDimensions/index.js +65 -7
- package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.d.ts +6 -0
- package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.js +7 -0
- package/build/plugins/d3/renderer/hooks/useChartOptions/chart.d.ts +1 -3
- package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +9 -68
- package/build/plugins/d3/renderer/hooks/useChartOptions/index.d.ts +3 -1
- package/build/plugins/d3/renderer/hooks/useChartOptions/index.js +3 -8
- package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +3 -6
- package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +4 -2
- package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +3 -2
- package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +31 -4
- package/build/plugins/d3/renderer/hooks/useSeries/constants.d.ts +1 -1
- package/build/plugins/d3/renderer/hooks/useSeries/constants.js +1 -1
- package/build/plugins/d3/renderer/hooks/useSeries/index.d.ts +19 -7
- package/build/plugins/d3/renderer/hooks/useSeries/index.js +26 -8
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.d.ts +27 -0
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.js +92 -0
- package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.d.ts +1 -2
- package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +26 -1
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x.js +2 -1
- package/build/plugins/d3/renderer/utils/axis-generators/bottom.d.ts +21 -0
- package/build/plugins/d3/renderer/utils/axis-generators/bottom.js +104 -0
- package/build/plugins/d3/renderer/utils/axis-generators/index.d.ts +1 -0
- package/build/plugins/d3/renderer/utils/axis-generators/index.js +1 -0
- package/build/plugins/d3/renderer/utils/axis.d.ts +22 -0
- package/build/plugins/d3/renderer/utils/axis.js +43 -0
- package/build/plugins/d3/renderer/utils/index.d.ts +7 -4
- package/build/plugins/d3/renderer/utils/index.js +16 -6
- package/build/plugins/d3/renderer/utils/text.d.ts +20 -2
- package/build/plugins/d3/renderer/utils/text.js +51 -1
- package/build/plugins/d3/renderer/utils/time.d.ts +3 -0
- package/build/plugins/d3/renderer/utils/time.js +34 -0
- package/build/plugins/highcharts/renderer/components/HighchartsComponent.js +3 -3
- package/build/plugins/shared/format-number/format-number.d.ts +1 -0
- package/build/plugins/shared/format-number/format-number.js +19 -20
- package/build/types/widget-data/axis.d.ts +13 -1
- package/build/types/widget-data/legend.d.ts +24 -7
- package/build/utils/common.d.ts +1 -0
- package/build/utils/common.js +1 -1
- package/build/utils/index.d.ts +1 -1
- package/build/utils/index.js +1 -1
- package/package.json +1 -1
- package/build/plugins/d3/renderer/hooks/useChartOptions/legend.d.ts +0 -6
- package/build/plugins/d3/renderer/hooks/useChartOptions/legend.js +0 -12
|
@@ -4,9 +4,19 @@ import debounce from 'lodash/debounce';
|
|
|
4
4
|
import { getRandomCKId } from '../../../utils';
|
|
5
5
|
import { Chart } from './components';
|
|
6
6
|
const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
|
|
7
|
+
const { data, onLoad, onRender } = props;
|
|
7
8
|
const ref = React.useRef(null);
|
|
8
9
|
const debounced = React.useRef();
|
|
9
10
|
const [dimensions, setDimensions] = React.useState();
|
|
11
|
+
//FIXME: add chartPerfomance data to callbacks;
|
|
12
|
+
React.useLayoutEffect(() => {
|
|
13
|
+
if (onLoad) {
|
|
14
|
+
onLoad({});
|
|
15
|
+
}
|
|
16
|
+
if (onRender) {
|
|
17
|
+
onRender({});
|
|
18
|
+
}
|
|
19
|
+
}, []);
|
|
10
20
|
const handleResize = React.useCallback(() => {
|
|
11
21
|
var _a;
|
|
12
22
|
const parentElement = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
@@ -44,6 +54,6 @@ const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
|
|
|
44
54
|
width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || '100%',
|
|
45
55
|
height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || '100%',
|
|
46
56
|
position: 'relative',
|
|
47
|
-
} }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (React.createElement(Chart, { top: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.top) || 0, left: dimensions.left || 0, width: dimensions.width, height: dimensions.height, data:
|
|
57
|
+
} }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (React.createElement(Chart, { top: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.top) || 0, left: dimensions.left || 0, width: dimensions.width, height: dimensions.height, data: data }))));
|
|
48
58
|
});
|
|
49
59
|
export default D3Widget;
|
|
@@ -5,7 +5,6 @@ type Props = {
|
|
|
5
5
|
width: number;
|
|
6
6
|
height: number;
|
|
7
7
|
scale: ChartScale;
|
|
8
|
-
chartWidth: number;
|
|
9
8
|
};
|
|
10
|
-
export declare const AxisX: ({ axis, width, height, scale
|
|
9
|
+
export declare const AxisX: React.MemoExoticComponent<({ axis, width, height, scale }: Props) => React.JSX.Element>;
|
|
11
10
|
export {};
|
|
@@ -1,74 +1,52 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { select } from 'd3';
|
|
3
3
|
import { block } from '../../../../utils/cn';
|
|
4
|
-
import { formatAxisTickLabel,
|
|
4
|
+
import { formatAxisTickLabel, getClosestPointsRange, setEllipsisForOverflowText, getTicksCount, getScaleTicks, getMaxTickCount, } from '../utils';
|
|
5
|
+
import { axisBottom } from '../utils/axis-generators';
|
|
5
6
|
const b = block('d3-axis');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function getLabelFormatter({ axis, scale }) {
|
|
8
|
+
const ticks = getScaleTicks(scale);
|
|
9
|
+
const tickStep = getClosestPointsRange(axis, ticks);
|
|
10
|
+
return (value) => {
|
|
11
|
+
if (!axis.labels.enabled) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
return formatAxisTickLabel({
|
|
15
|
+
axis,
|
|
16
|
+
value,
|
|
17
|
+
step: tickStep,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const AxisX = React.memo(({ axis, width, height, scale }) => {
|
|
9
22
|
const ref = React.useRef(null);
|
|
10
23
|
React.useEffect(() => {
|
|
11
24
|
if (!ref.current) {
|
|
12
25
|
return;
|
|
13
26
|
}
|
|
27
|
+
const xAxisGenerator = axisBottom({
|
|
28
|
+
scale: scale,
|
|
29
|
+
ticks: {
|
|
30
|
+
size: axis.grid.enabled ? height * -1 : 0,
|
|
31
|
+
labelFormat: getLabelFormatter({ axis, scale }),
|
|
32
|
+
labelsPaddings: axis.labels.padding,
|
|
33
|
+
labelsMargin: axis.labels.margin,
|
|
34
|
+
labelsStyle: axis.labels.style,
|
|
35
|
+
count: getTicksCount({ axis, range: width }),
|
|
36
|
+
maxTickCount: getMaxTickCount({ axis, width }),
|
|
37
|
+
autoRotation: axis.labels.autoRotation,
|
|
38
|
+
},
|
|
39
|
+
domain: {
|
|
40
|
+
size: width,
|
|
41
|
+
color: axis.lineColor,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
14
44
|
const svgElement = select(ref.current);
|
|
15
45
|
svgElement.selectAll('*').remove();
|
|
16
|
-
const tickSize = axis.grid.enabled ? height * -1 : 0;
|
|
17
|
-
let xAxisGenerator = axisBottom(scale)
|
|
18
|
-
.tickSize(tickSize)
|
|
19
|
-
.tickPadding(axis.labels.padding)
|
|
20
|
-
.tickFormat((value) => {
|
|
21
|
-
if (!axis.labels.enabled) {
|
|
22
|
-
return '';
|
|
23
|
-
}
|
|
24
|
-
return formatAxisTickLabel({
|
|
25
|
-
axisType: axis.type,
|
|
26
|
-
value,
|
|
27
|
-
dateFormat: axis.labels['dateFormat'],
|
|
28
|
-
numberFormat: axis.labels['numberFormat'],
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
if (axis.ticks.pixelInterval) {
|
|
32
|
-
const ticksCount = width / axis.ticks.pixelInterval;
|
|
33
|
-
xAxisGenerator = xAxisGenerator.ticks(ticksCount);
|
|
34
|
-
}
|
|
35
|
-
svgElement.call(xAxisGenerator).attr('class', b());
|
|
36
46
|
svgElement
|
|
37
|
-
.
|
|
38
|
-
.attr('
|
|
39
|
-
.style('
|
|
40
|
-
if (axis.labels.enabled) {
|
|
41
|
-
svgElement.selectAll('.tick text').style('font-size', axis.labels.style.fontSize);
|
|
42
|
-
}
|
|
43
|
-
const transformStyle = svgElement.select('.tick').attr('transform');
|
|
44
|
-
const { x } = parseTransformStyle(transformStyle);
|
|
45
|
-
if (x === 0) {
|
|
46
|
-
// Remove tick that has the same x coordinate like domain
|
|
47
|
-
svgElement.select('.tick').remove();
|
|
48
|
-
}
|
|
49
|
-
// remove overlapping labels
|
|
50
|
-
let elementX = 0;
|
|
51
|
-
svgElement
|
|
52
|
-
.selectAll('.tick')
|
|
53
|
-
.filter(function () {
|
|
54
|
-
const node = this;
|
|
55
|
-
const r = node.getBoundingClientRect();
|
|
56
|
-
if (r.left < elementX) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
elementX = r.right + EMPTY_SPACE_BETWEEN_LABELS;
|
|
60
|
-
return false;
|
|
61
|
-
})
|
|
62
|
-
.remove();
|
|
63
|
-
// add an ellipsis to the labels on the right that go beyond the boundaries of the chart
|
|
64
|
-
svgElement.selectAll('.tick text').each(function () {
|
|
65
|
-
const node = this;
|
|
66
|
-
const textRect = node.getBoundingClientRect();
|
|
67
|
-
if (textRect.right > chartWidth) {
|
|
68
|
-
const maxWidth = textRect.width - (textRect.right - chartWidth) * 2;
|
|
69
|
-
select(node).call(setEllipsisForOverflowText, maxWidth);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
47
|
+
.call(xAxisGenerator)
|
|
48
|
+
.attr('class', b())
|
|
49
|
+
.style('font-size', axis.labels.style.fontSize);
|
|
72
50
|
// add an axis header if necessary
|
|
73
51
|
if (axis.title.text) {
|
|
74
52
|
const textY = axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding;
|
|
@@ -84,4 +62,4 @@ export const AxisX = ({ axis, width, height, scale, chartWidth }) => {
|
|
|
84
62
|
}
|
|
85
63
|
}, [axis, width, height, scale]);
|
|
86
64
|
return React.createElement("g", { ref: ref });
|
|
87
|
-
};
|
|
65
|
+
});
|
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { axisLeft, select } from 'd3';
|
|
3
3
|
import { block } from '../../../../utils/cn';
|
|
4
|
-
import { formatAxisTickLabel, parseTransformStyle } from '../utils';
|
|
4
|
+
import { formatAxisTickLabel, getClosestPointsRange, parseTransformStyle, setEllipsisForOverflowText, setEllipsisForOverflowTexts, getTicksCount, getScaleTicks, } from '../utils';
|
|
5
5
|
const b = block('d3-axis');
|
|
6
|
-
const
|
|
7
|
-
// Note: this method do not prepared for rotated labels
|
|
8
|
-
const removeOverlappingYTicks = (axis) => {
|
|
9
|
-
var _a;
|
|
10
|
-
const a = axis.selectAll('g.tick').nodes();
|
|
11
|
-
if (a.length <= 1) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
for (let i = 0, x = 0; i < a.length; i++) {
|
|
15
|
-
const node = a[i];
|
|
16
|
-
const r = node.getBoundingClientRect();
|
|
17
|
-
if (r.bottom > x && i !== 0) {
|
|
18
|
-
(_a = node === null || node === void 0 ? void 0 : node.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(node);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
x = r.top - EMPTY_SPACE_BETWEEN_LABELS;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
// FIXME: add overflow ellipsis for the labels that out of boundaries
|
|
6
|
+
const MAX_WIDTH = 80;
|
|
26
7
|
export const AxisY = ({ axises, width, height, scale }) => {
|
|
27
8
|
const ref = React.useRef(null);
|
|
28
9
|
React.useEffect(() => {
|
|
@@ -33,22 +14,22 @@ export const AxisY = ({ axises, width, height, scale }) => {
|
|
|
33
14
|
const svgElement = select(ref.current);
|
|
34
15
|
svgElement.selectAll('*').remove();
|
|
35
16
|
const tickSize = axis.grid.enabled ? width * -1 : 0;
|
|
17
|
+
const step = getClosestPointsRange(axis, getScaleTicks(scale));
|
|
36
18
|
let yAxisGenerator = axisLeft(scale)
|
|
37
19
|
.tickSize(tickSize)
|
|
38
|
-
.tickPadding(axis.labels.
|
|
20
|
+
.tickPadding(axis.labels.margin)
|
|
39
21
|
.tickFormat((value) => {
|
|
40
22
|
if (!axis.labels.enabled) {
|
|
41
23
|
return '';
|
|
42
24
|
}
|
|
43
25
|
return formatAxisTickLabel({
|
|
44
|
-
|
|
26
|
+
axis,
|
|
45
27
|
value,
|
|
46
|
-
|
|
47
|
-
numberFormat: axis.labels['numberFormat'],
|
|
28
|
+
step,
|
|
48
29
|
});
|
|
49
30
|
});
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
const ticksCount = getTicksCount({ axis, range: height });
|
|
32
|
+
if (ticksCount) {
|
|
52
33
|
yAxisGenerator = yAxisGenerator.ticks(ticksCount);
|
|
53
34
|
}
|
|
54
35
|
svgElement.call(yAxisGenerator).attr('class', b());
|
|
@@ -57,10 +38,11 @@ export const AxisY = ({ axises, width, height, scale }) => {
|
|
|
57
38
|
.attr('d', `M0,${height}H0V0`)
|
|
58
39
|
.style('stroke', axis.lineColor || '');
|
|
59
40
|
if (axis.labels.enabled) {
|
|
60
|
-
svgElement
|
|
41
|
+
const tickTexts = svgElement
|
|
61
42
|
.selectAll('.tick text')
|
|
62
43
|
.style('font-size', axis.labels.style.fontSize)
|
|
63
44
|
.style('transform', 'translateY(-1px)');
|
|
45
|
+
tickTexts.call(setEllipsisForOverflowTexts, MAX_WIDTH);
|
|
64
46
|
}
|
|
65
47
|
const transformStyle = svgElement.select('.tick').attr('transform');
|
|
66
48
|
const { y } = parseTransformStyle(transformStyle);
|
|
@@ -68,8 +50,23 @@ export const AxisY = ({ axises, width, height, scale }) => {
|
|
|
68
50
|
// Remove stroke from tick that has the same y coordinate like domain
|
|
69
51
|
svgElement.select('.tick line').style('stroke', 'none');
|
|
70
52
|
}
|
|
53
|
+
// remove overlapping ticks
|
|
54
|
+
// Note: this method do not prepared for rotated labels
|
|
55
|
+
let elementY = 0;
|
|
56
|
+
svgElement
|
|
57
|
+
.selectAll('.tick')
|
|
58
|
+
.filter(function (_d, index) {
|
|
59
|
+
const node = this;
|
|
60
|
+
const r = node.getBoundingClientRect();
|
|
61
|
+
if (r.bottom > elementY && index !== 0) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
elementY = r.top - axis.labels.padding;
|
|
65
|
+
return false;
|
|
66
|
+
})
|
|
67
|
+
.remove();
|
|
71
68
|
if (axis.title.text) {
|
|
72
|
-
const textY = axis.title.height + axis.labels.
|
|
69
|
+
const textY = axis.title.height + axis.labels.margin;
|
|
73
70
|
svgElement
|
|
74
71
|
.append('text')
|
|
75
72
|
.attr('class', b('title'))
|
|
@@ -78,9 +75,9 @@ export const AxisY = ({ axises, width, height, scale }) => {
|
|
|
78
75
|
.attr('dx', -height / 2)
|
|
79
76
|
.attr('font-size', axis.title.style.fontSize)
|
|
80
77
|
.attr('transform', 'rotate(-90)')
|
|
81
|
-
.text(axis.title.text)
|
|
78
|
+
.text(axis.title.text)
|
|
79
|
+
.call(setEllipsisForOverflowText, height);
|
|
82
80
|
}
|
|
83
|
-
removeOverlappingYTicks(svgElement);
|
|
84
81
|
}, [axises, width, height, scale]);
|
|
85
82
|
return React.createElement("g", { ref: ref });
|
|
86
83
|
};
|
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { block } from '../../../../utils/cn';
|
|
3
|
+
import { useAxisScales, useChartDimensions, useChartEvents, useChartOptions, useSeries, useShapes, useTooltip, } from '../hooks';
|
|
3
4
|
import { AxisY } from './AxisY';
|
|
4
5
|
import { AxisX } from './AxisX';
|
|
5
6
|
import { Legend } from './Legend';
|
|
6
7
|
import { Title } from './Title';
|
|
7
8
|
import { Tooltip } from './Tooltip';
|
|
8
|
-
import { useChartDimensions, useChartEvents, useChartOptions, useAxisScales, useSeries, useShapes, useTooltip, } from '../hooks';
|
|
9
9
|
import './styles.css';
|
|
10
10
|
const b = block('d3');
|
|
11
11
|
export const Chart = (props) => {
|
|
12
|
-
const { top, left, width, height, data } = props;
|
|
13
12
|
// FIXME: add data validation
|
|
13
|
+
const { top, left, width, height, data } = props;
|
|
14
14
|
const svgRef = React.createRef();
|
|
15
15
|
const { chartHovered, handleMouseEnter, handleMouseLeave } = useChartEvents();
|
|
16
|
-
const { chart,
|
|
16
|
+
const { chart, title, tooltip, xAxis, yAxis } = useChartOptions({
|
|
17
|
+
data,
|
|
18
|
+
});
|
|
19
|
+
const { legendItems, legendConfig, preparedSeries, preparedLegend, handleLegendItemClick } = useSeries({
|
|
20
|
+
chartWidth: width,
|
|
21
|
+
chartHeight: height,
|
|
22
|
+
chartMargin: chart.margin,
|
|
23
|
+
series: data.series,
|
|
24
|
+
legend: data.legend,
|
|
25
|
+
preparedYAxis: yAxis,
|
|
26
|
+
});
|
|
17
27
|
const { boundsWidth, boundsHeight } = useChartDimensions({
|
|
18
28
|
width,
|
|
19
29
|
height,
|
|
20
30
|
margin: chart.margin,
|
|
21
|
-
|
|
31
|
+
preparedLegend,
|
|
32
|
+
preparedXAxis: xAxis,
|
|
33
|
+
preparedYAxis: yAxis,
|
|
34
|
+
preparedSeries: preparedSeries,
|
|
22
35
|
});
|
|
23
|
-
const { preparedSeries, handleLegendItemClick } = useSeries({ series: data.series, legend });
|
|
24
36
|
const { xScale, yScale } = useAxisScales({
|
|
25
37
|
boundsWidth,
|
|
26
38
|
boundsHeight,
|
|
@@ -53,8 +65,8 @@ export const Chart = (props) => {
|
|
|
53
65
|
xScale && yScale && (React.createElement(React.Fragment, null,
|
|
54
66
|
React.createElement(AxisY, { axises: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale }),
|
|
55
67
|
React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
|
|
56
|
-
React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale
|
|
68
|
+
React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale })))),
|
|
57
69
|
shapes),
|
|
58
|
-
|
|
70
|
+
preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick }))),
|
|
59
71
|
React.createElement(Tooltip, { hovered: hovered, pointerPosition: pointerPosition, tooltip: tooltip, xAxis: xAxis, yAxis: yAxis[0] })));
|
|
60
72
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { OnLegendItemClick, PreparedLegend, PreparedSeries } from '../hooks';
|
|
2
|
+
import type { OnLegendItemClick, PreparedLegend, PreparedSeries, LegendItem, LegendConfig } from '../hooks';
|
|
3
3
|
type Props = {
|
|
4
|
-
|
|
5
|
-
height: number;
|
|
6
|
-
legend: PreparedLegend;
|
|
7
|
-
offsetWidth: number;
|
|
8
|
-
offsetHeight: number;
|
|
4
|
+
boundsWidth: number;
|
|
9
5
|
chartSeries: PreparedSeries[];
|
|
6
|
+
legend: PreparedLegend;
|
|
7
|
+
items: LegendItem[][];
|
|
8
|
+
config: LegendConfig;
|
|
10
9
|
onItemClick: OnLegendItemClick;
|
|
11
10
|
};
|
|
12
11
|
export declare const Legend: (props: Props) => React.JSX.Element;
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { select
|
|
3
|
-
import get from 'lodash/get';
|
|
2
|
+
import { select } from 'd3';
|
|
4
3
|
import { block } from '../../../../utils/cn';
|
|
5
4
|
const b = block('d3-legend');
|
|
6
|
-
const
|
|
7
|
-
return series.reduce((acc, s) => {
|
|
8
|
-
const legendEnabled = get(s, 'legend.enabled', true);
|
|
9
|
-
if (legendEnabled) {
|
|
10
|
-
acc.push(Object.assign(Object.assign({}, s), { symbol: s.legend.symbol }));
|
|
11
|
-
}
|
|
12
|
-
return acc;
|
|
13
|
-
}, []);
|
|
14
|
-
};
|
|
15
|
-
function getLegendPosition(args) {
|
|
5
|
+
const getLegendPosition = (args) => {
|
|
16
6
|
const { align, offsetWidth, width, contentWidth } = args;
|
|
17
7
|
const top = 0;
|
|
18
8
|
if (align === 'left') {
|
|
@@ -22,85 +12,150 @@ function getLegendPosition(args) {
|
|
|
22
12
|
return { top, left: offsetWidth + width - contentWidth };
|
|
23
13
|
}
|
|
24
14
|
return { top, left: offsetWidth + width / 2 - contentWidth / 2 };
|
|
25
|
-
}
|
|
15
|
+
};
|
|
16
|
+
const appendPaginator = (args) => {
|
|
17
|
+
const { container, offset, maxPage, legend, transform, onArrowClick } = args;
|
|
18
|
+
const paginationLine = container.append('g').attr('class', b('pagination'));
|
|
19
|
+
let computedWidth = 0;
|
|
20
|
+
paginationLine
|
|
21
|
+
.append('text')
|
|
22
|
+
.text('▲')
|
|
23
|
+
.attr('class', function () {
|
|
24
|
+
return b('pagination-arrow', { inactive: offset === 0 });
|
|
25
|
+
})
|
|
26
|
+
.style('font-size', legend.itemStyle.fontSize)
|
|
27
|
+
.each(function () {
|
|
28
|
+
computedWidth += this.getComputedTextLength();
|
|
29
|
+
})
|
|
30
|
+
.on('click', function () {
|
|
31
|
+
if (offset - 1 >= 0) {
|
|
32
|
+
onArrowClick(offset - 1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
paginationLine
|
|
36
|
+
.append('text')
|
|
37
|
+
.text(`${offset + 1}/${maxPage}`)
|
|
38
|
+
.attr('class', b('pagination-counter'))
|
|
39
|
+
.attr('x', computedWidth)
|
|
40
|
+
.style('font-size', legend.itemStyle.fontSize)
|
|
41
|
+
.each(function () {
|
|
42
|
+
computedWidth += this.getComputedTextLength();
|
|
43
|
+
});
|
|
44
|
+
paginationLine
|
|
45
|
+
.append('text')
|
|
46
|
+
.text('▼')
|
|
47
|
+
.attr('class', function () {
|
|
48
|
+
return b('pagination-arrow', { inactive: offset === maxPage - 1 });
|
|
49
|
+
})
|
|
50
|
+
.attr('x', computedWidth)
|
|
51
|
+
.style('font-size', legend.itemStyle.fontSize)
|
|
52
|
+
.on('click', function () {
|
|
53
|
+
if (offset + 1 < maxPage) {
|
|
54
|
+
onArrowClick(offset + 1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
paginationLine.attr('transform', transform);
|
|
58
|
+
};
|
|
26
59
|
export const Legend = (props) => {
|
|
27
|
-
const {
|
|
60
|
+
const { boundsWidth, chartSeries, legend, items, config, onItemClick } = props;
|
|
28
61
|
const ref = React.useRef(null);
|
|
62
|
+
const [paginationOffset, setPaginationOffset] = React.useState(0);
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
setPaginationOffset(0);
|
|
65
|
+
}, [boundsWidth]);
|
|
29
66
|
React.useEffect(() => {
|
|
67
|
+
var _a;
|
|
30
68
|
if (!ref.current) {
|
|
31
69
|
return;
|
|
32
70
|
}
|
|
33
|
-
const legendItems = getLegendItems(chartSeries);
|
|
34
|
-
const textWidths = [0];
|
|
35
71
|
const svgElement = select(ref.current);
|
|
36
72
|
svgElement.selectAll('*').remove();
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
const limit = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.limit;
|
|
74
|
+
const pageItems = typeof limit === 'number'
|
|
75
|
+
? items.slice(paginationOffset * limit, paginationOffset * limit + limit)
|
|
76
|
+
: items;
|
|
77
|
+
pageItems.forEach((line, lineIndex) => {
|
|
78
|
+
var _a;
|
|
79
|
+
const textWidths = [];
|
|
80
|
+
const legendLine = svgElement.append('g').attr('class', b('line'));
|
|
81
|
+
const legendItemTemplate = legendLine
|
|
82
|
+
.selectAll('legend-history')
|
|
83
|
+
.data(line)
|
|
84
|
+
.enter()
|
|
85
|
+
.append('g')
|
|
86
|
+
.attr('class', b('item'))
|
|
87
|
+
.on('click', function (e, d) {
|
|
88
|
+
onItemClick({ name: d.name, metaKey: e.metaKey });
|
|
89
|
+
})
|
|
90
|
+
.each(function (d) {
|
|
91
|
+
textWidths.push(d.textWidth);
|
|
92
|
+
});
|
|
93
|
+
legendItemTemplate
|
|
94
|
+
.append('rect')
|
|
95
|
+
.attr('x', function (legendItem, i) {
|
|
96
|
+
return (i * legendItem.symbol.width +
|
|
97
|
+
i * legend.itemDistance +
|
|
98
|
+
i * legendItem.symbol.padding +
|
|
99
|
+
textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0));
|
|
100
|
+
})
|
|
101
|
+
.attr('y', (legendItem) => {
|
|
102
|
+
const lineOffset = legend.lineHeight * lineIndex;
|
|
103
|
+
return config.offset.top + lineOffset - legendItem.symbol.height / 2;
|
|
104
|
+
})
|
|
105
|
+
.attr('width', (legendItem) => {
|
|
106
|
+
return legendItem.symbol.width;
|
|
107
|
+
})
|
|
108
|
+
.attr('height', (legendItem) => legendItem.symbol.height)
|
|
109
|
+
.attr('rx', (legendItem) => legendItem.symbol.radius)
|
|
110
|
+
.attr('class', function (d) {
|
|
111
|
+
return b('item-shape', { unselected: !d.visible });
|
|
112
|
+
})
|
|
113
|
+
.style('fill', function (d) {
|
|
114
|
+
return d.visible ? d.color : '';
|
|
115
|
+
});
|
|
116
|
+
legendItemTemplate
|
|
117
|
+
.append('text')
|
|
118
|
+
.attr('x', function (legendItem, i) {
|
|
119
|
+
return (i * legendItem.symbol.width +
|
|
120
|
+
i * legend.itemDistance +
|
|
121
|
+
i * legendItem.symbol.padding +
|
|
122
|
+
legendItem.symbol.width +
|
|
123
|
+
legendItem.symbol.padding +
|
|
124
|
+
textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0));
|
|
125
|
+
})
|
|
126
|
+
.attr('y', config.offset.top + legend.lineHeight * lineIndex)
|
|
127
|
+
.attr('class', function (d) {
|
|
128
|
+
const mods = { selected: d.visible, unselected: !d.visible };
|
|
129
|
+
return b('item-text', mods);
|
|
130
|
+
})
|
|
131
|
+
.text(function (d) {
|
|
132
|
+
return ('name' in d && d.name);
|
|
133
|
+
})
|
|
134
|
+
.style('font-size', legend.itemStyle.fontSize)
|
|
135
|
+
.style('alignment-baseline', 'middle');
|
|
136
|
+
const contentWidth = ((_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
|
|
137
|
+
const { left } = getLegendPosition({
|
|
138
|
+
align: legend.align,
|
|
139
|
+
width: boundsWidth,
|
|
140
|
+
offsetWidth: config.offset.left,
|
|
141
|
+
contentWidth,
|
|
142
|
+
});
|
|
143
|
+
legendLine.attr('transform', `translate(${[left, 0].join(',')})`);
|
|
74
144
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return ('name' in d && d.name);
|
|
92
|
-
})
|
|
93
|
-
.style('alignment-baseline', 'middle');
|
|
94
|
-
const contentWidth = sum(textWidths) +
|
|
95
|
-
sum(legendItems, (item) => item.symbol.width + item.symbol.padding) +
|
|
96
|
-
legend.itemDistance * (legendItems.length - 1);
|
|
97
|
-
const { left } = getLegendPosition({
|
|
98
|
-
align: legend.align,
|
|
99
|
-
width,
|
|
100
|
-
offsetWidth,
|
|
101
|
-
contentWidth,
|
|
102
|
-
});
|
|
103
|
-
svgElement.attr('transform', `translate(${[left, 0].join(',')})`);
|
|
104
|
-
}, [width, offsetWidth, height, offsetHeight, chartSeries, onItemClick, legend]);
|
|
105
|
-
return React.createElement("g", { ref: ref, width: width, height: height });
|
|
145
|
+
if (config.pagination) {
|
|
146
|
+
const transform = `translate(${[
|
|
147
|
+
config.offset.left,
|
|
148
|
+
config.offset.top + legend.lineHeight * config.pagination.limit,
|
|
149
|
+
].join(',')})`;
|
|
150
|
+
appendPaginator({
|
|
151
|
+
container: svgElement,
|
|
152
|
+
offset: paginationOffset,
|
|
153
|
+
maxPage: config.pagination.maxPage,
|
|
154
|
+
legend,
|
|
155
|
+
transform,
|
|
156
|
+
onArrowClick: setPaginationOffset,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}, [boundsWidth, chartSeries, onItemClick, legend, items, config, paginationOffset]);
|
|
160
|
+
return React.createElement("g", { ref: ref, width: boundsWidth, height: legend.height });
|
|
106
161
|
};
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
fill: var(--g-color-base-misc-medium);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
.chartkit-d3-legend__item-shape_unselected {
|
|
27
|
+
fill: var(--g-color-text-hint);
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
.chartkit-d3-legend__item-text {
|
|
27
31
|
fill: var(--g-color-text-secondary);
|
|
28
32
|
}
|
|
@@ -35,6 +39,29 @@
|
|
|
35
39
|
fill: var(--g-color-text-complementary);
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
.chartkit-d3-legend__pagination {
|
|
43
|
+
fill: var(--g-color-text-primary);
|
|
44
|
+
user-select: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.chartkit-d3-legend__pagination-counter, .chartkit-d3-legend__pagination-arrow {
|
|
48
|
+
alignment-baseline: middle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.chartkit-d3-legend__pagination-arrow {
|
|
52
|
+
fill: var(--g-color-text-brand);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.chartkit-d3-legend__pagination-arrow_inactive {
|
|
57
|
+
fill: var(--g-color-base-generic-accent-disabled);
|
|
58
|
+
cursor: inherit;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.chartkit-d3-legend__pagination-arrow:hover:not(.chartkit-d3-legend__pagination-arrow_inactive) {
|
|
62
|
+
fill: var(--g-color-base-brand-hover);
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
.chartkit-d3-title {
|
|
39
66
|
font-size: var(--g-text-subheader-2-font-size);
|
|
40
67
|
font-weight: var(--g-text-subheader-font-weight);
|