@gravity-ui/chartkit 4.4.0 → 4.5.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/build/plugins/d3/renderer/components/AxisX.d.ts +2 -1
- package/build/plugins/d3/renderer/components/AxisX.js +28 -22
- package/build/plugins/d3/renderer/components/Chart.js +1 -1
- package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +16 -13
- package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +3 -7
- package/build/plugins/d3/renderer/hooks/useShapes/index.js +1 -3
- package/build/plugins/d3/renderer/hooks/useShapes/scatter.js +2 -5
- package/build/plugins/d3/renderer/utils/index.d.ts +1 -0
- package/build/plugins/d3/renderer/utils/index.js +1 -3
- package/build/plugins/d3/renderer/utils/text.d.ts +2 -0
- package/build/plugins/d3/renderer/utils/text.js +12 -0
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ type Props = {
|
|
|
5
5
|
width: number;
|
|
6
6
|
height: number;
|
|
7
7
|
scale: ChartScale;
|
|
8
|
+
chartWidth: number;
|
|
8
9
|
};
|
|
9
|
-
export declare const AxisX: ({ axis, width, height, scale }: Props) => React.JSX.Element;
|
|
10
|
+
export declare const AxisX: ({ axis, width, height, scale, chartWidth }: Props) => React.JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { axisBottom, select } from 'd3';
|
|
3
3
|
import { block } from '../../../../utils/cn';
|
|
4
|
-
import { formatAxisTickLabel, parseTransformStyle } from '../utils';
|
|
4
|
+
import { formatAxisTickLabel, parseTransformStyle, setEllipsisForOverflowText } from '../utils';
|
|
5
5
|
const b = block('d3-axis');
|
|
6
6
|
const EMPTY_SPACE_BETWEEN_LABELS = 10;
|
|
7
|
-
// Note: this method do not prepared for rotated labels
|
|
8
|
-
const removeOverlappingXTicks = (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.left < x) {
|
|
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.right + EMPTY_SPACE_BETWEEN_LABELS;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
7
|
// FIXME: add overflow ellipsis for the labels that out of boundaries
|
|
26
|
-
export const AxisX = ({ axis, width, height, scale }) => {
|
|
8
|
+
export const AxisX = ({ axis, width, height, scale, chartWidth }) => {
|
|
27
9
|
const ref = React.useRef(null);
|
|
28
10
|
React.useEffect(() => {
|
|
29
11
|
if (!ref.current) {
|
|
@@ -64,6 +46,30 @@ export const AxisX = ({ axis, width, height, scale }) => {
|
|
|
64
46
|
// Remove tick that has the same x coordinate like domain
|
|
65
47
|
svgElement.select('.tick').remove();
|
|
66
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
|
+
});
|
|
72
|
+
// add an axis header if necessary
|
|
67
73
|
if (axis.title.text) {
|
|
68
74
|
const textY = axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding;
|
|
69
75
|
svgElement
|
|
@@ -73,9 +79,9 @@ export const AxisX = ({ axis, width, height, scale }) => {
|
|
|
73
79
|
.attr('x', width / 2)
|
|
74
80
|
.attr('y', textY)
|
|
75
81
|
.attr('font-size', axis.title.style.fontSize)
|
|
76
|
-
.text(axis.title.text)
|
|
82
|
+
.text(axis.title.text)
|
|
83
|
+
.call(setEllipsisForOverflowText, width);
|
|
77
84
|
}
|
|
78
|
-
removeOverlappingXTicks(svgElement);
|
|
79
85
|
}, [axis, width, height, scale]);
|
|
80
86
|
return React.createElement("g", { ref: ref });
|
|
81
87
|
};
|
|
@@ -53,7 +53,7 @@ export const Chart = (props) => {
|
|
|
53
53
|
xScale && yScale && (React.createElement(React.Fragment, null,
|
|
54
54
|
React.createElement(AxisY, { axises: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale }),
|
|
55
55
|
React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
|
|
56
|
-
React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale })))),
|
|
56
|
+
React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, chartWidth: width })))),
|
|
57
57
|
shapes),
|
|
58
58
|
legend.enabled && (React.createElement(Legend, { width: boundsWidth, offsetWidth: chart.margin.left, height: legend.height, legend: legend, offsetHeight: height - legend.height / 2, chartSeries: preparedSeries, onItemClick: handleLegendItemClick }))),
|
|
59
59
|
React.createElement(Tooltip, { hovered: hovered, pointerPosition: pointerPosition, tooltip: tooltip, xAxis: xAxis, yAxis: yAxis[0] })));
|
|
@@ -7,15 +7,15 @@ const isNumericalArrayData = (data) => {
|
|
|
7
7
|
};
|
|
8
8
|
const filterCategoriesByVisibleSeries = (args) => {
|
|
9
9
|
const { axisDirection, categories, series } = args;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
10
|
+
const visibleCategories = new Set();
|
|
11
|
+
series.forEach((s) => {
|
|
12
|
+
if (isSeriesWithCategoryValues(s)) {
|
|
13
|
+
s.data.forEach((d) => {
|
|
14
|
+
visibleCategories.add(getDataCategoryValue({ axisDirection, categories, data: d }));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
18
17
|
});
|
|
18
|
+
return categories.filter((c) => visibleCategories.has(c));
|
|
19
19
|
};
|
|
20
20
|
const createScales = (args) => {
|
|
21
21
|
const { boundsWidth, boundsHeight, series, xAxis, yAxis } = args;
|
|
@@ -33,14 +33,15 @@ const createScales = (args) => {
|
|
|
33
33
|
visibleSeries = visibleSeries.length === 0 ? series : visibleSeries;
|
|
34
34
|
let xScale;
|
|
35
35
|
let yScale;
|
|
36
|
+
const xAxisMinPadding = boundsWidth * xAxis.maxPadding;
|
|
37
|
+
const xRange = [0, boundsWidth - xAxisMinPadding];
|
|
36
38
|
switch (xType) {
|
|
37
39
|
case 'linear': {
|
|
38
40
|
const domain = getDomainDataXBySeries(visibleSeries);
|
|
39
|
-
const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding];
|
|
40
41
|
if (isNumericalArrayData(domain)) {
|
|
41
42
|
const [domainXMin, xMax] = extent(domain);
|
|
42
43
|
const xMinValue = typeof xMin === 'number' ? xMin : domainXMin;
|
|
43
|
-
xScale = scaleLinear().domain([xMinValue, xMax]).range(
|
|
44
|
+
xScale = scaleLinear().domain([xMinValue, xMax]).range(xRange).nice();
|
|
44
45
|
}
|
|
45
46
|
break;
|
|
46
47
|
}
|
|
@@ -52,20 +53,22 @@ const createScales = (args) => {
|
|
|
52
53
|
series: visibleSeries,
|
|
53
54
|
});
|
|
54
55
|
xScale = scaleBand().domain(filteredCategories).range([0, boundsWidth]);
|
|
56
|
+
if (xScale.step() / 2 < xAxisMinPadding) {
|
|
57
|
+
xScale.range(xRange);
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
break;
|
|
57
61
|
}
|
|
58
62
|
case 'datetime': {
|
|
59
|
-
const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding];
|
|
60
63
|
if (xTimestamps) {
|
|
61
64
|
const [xMin, xMax] = extent(xTimestamps);
|
|
62
|
-
xScale = scaleUtc().domain([xMin, xMax]).range(
|
|
65
|
+
xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice();
|
|
63
66
|
}
|
|
64
67
|
else {
|
|
65
68
|
const domain = getDomainDataXBySeries(visibleSeries);
|
|
66
69
|
if (isNumericalArrayData(domain)) {
|
|
67
70
|
const [xMin, xMax] = extent(domain);
|
|
68
|
-
xScale = scaleUtc().domain([xMin, xMax]).range(
|
|
71
|
+
xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice();
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
break;
|
|
@@ -80,12 +80,8 @@ const getMarginLeft = (args) => {
|
|
|
80
80
|
return marginLeft;
|
|
81
81
|
};
|
|
82
82
|
const getMarginRight = (args) => {
|
|
83
|
-
const { chart
|
|
84
|
-
|
|
85
|
-
if (hasAxisRelatedSeries) {
|
|
86
|
-
marginRight += getAxisLabelMaxWidth({ axis: preparedXAxis, series: series.data }) / 2;
|
|
87
|
-
}
|
|
88
|
-
return marginRight;
|
|
83
|
+
const { chart } = args;
|
|
84
|
+
return get(chart, 'margin.right', 0);
|
|
89
85
|
};
|
|
90
86
|
export const getPreparedChart = (args) => {
|
|
91
87
|
const { chart, series, preparedLegend, preparedXAxis, preparedY1Axis, preparedTitle } = args;
|
|
@@ -98,7 +94,7 @@ export const getPreparedChart = (args) => {
|
|
|
98
94
|
preparedXAxis,
|
|
99
95
|
});
|
|
100
96
|
const marginLeft = getMarginLeft({ chart, hasAxisRelatedSeries, series, preparedY1Axis });
|
|
101
|
-
const marginRight = getMarginRight({ chart
|
|
97
|
+
const marginRight = getMarginRight({ chart });
|
|
102
98
|
return {
|
|
103
99
|
margin: {
|
|
104
100
|
top: marginTop,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { group } from 'd3';
|
|
3
|
-
import { getRandomCKId } from '../../../../../utils';
|
|
4
3
|
import { getOnlyVisibleSeries } from '../../utils';
|
|
5
4
|
import { BarXSeriesShapes } from './bar-x';
|
|
6
5
|
import { ScatterSeriesShape } from './scatter';
|
|
@@ -23,8 +22,7 @@ export const useShapes = (args) => {
|
|
|
23
22
|
case 'scatter': {
|
|
24
23
|
if (xScale && yScale) {
|
|
25
24
|
const scatterShapes = chartSeries.map((scatterSeries, i) => {
|
|
26
|
-
|
|
27
|
-
return (React.createElement(ScatterSeriesShape, { key: `${i}-${id}`, top: top, left: left, series: scatterSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
|
|
25
|
+
return (React.createElement(ScatterSeriesShape, { key: i, top: top, left: left, series: scatterSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
|
|
28
26
|
});
|
|
29
27
|
acc.push(...scatterShapes);
|
|
30
28
|
}
|
|
@@ -47,16 +47,13 @@ export function ScatterSeriesShape(props) {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const svgElement = select(ref.current);
|
|
50
|
-
svgElement.selectAll('*').remove();
|
|
51
50
|
const preparedData = xAxis.type === 'category' || ((_a = yAxis[0]) === null || _a === void 0 ? void 0 : _a.type) === 'category'
|
|
52
51
|
? series.data
|
|
53
52
|
: prepareLinearScatterData(series.data);
|
|
54
53
|
svgElement
|
|
55
|
-
.selectAll('
|
|
54
|
+
.selectAll('circle')
|
|
56
55
|
.data(preparedData)
|
|
57
|
-
.enter()
|
|
58
|
-
.append('circle')
|
|
59
|
-
.attr('class', b('point'))
|
|
56
|
+
.join((enter) => enter.append('circle').attr('class', b('point')), (update) => update, (exit) => exit.remove())
|
|
60
57
|
.attr('fill', (d) => d.color || series.color || '')
|
|
61
58
|
.attr('r', (d) => d.radius || DEFAULT_SCATTER_POINT_RADIUS)
|
|
62
59
|
.attr('cx', (d) => getCxAttr({ point: d, xAxis, xScale }))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AxisDomain } from 'd3';
|
|
2
2
|
import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData, ChartKitWidgetAxisType, ChartKitWidgetAxisLabels } from '../../../../types/widget-data';
|
|
3
3
|
export * from './math';
|
|
4
|
+
export * from './text';
|
|
4
5
|
export type AxisDirection = 'x' | 'y';
|
|
5
6
|
type UnknownSeries = {
|
|
6
7
|
type: ChartKitWidgetSeries['type'];
|
|
@@ -5,6 +5,7 @@ import { dateTime } from '@gravity-ui/date-utils';
|
|
|
5
5
|
import { formatNumber } from '../../../shared';
|
|
6
6
|
import { DEFAULT_AXIS_LABEL_FONT_SIZE } from '../constants';
|
|
7
7
|
export * from './math';
|
|
8
|
+
export * from './text';
|
|
8
9
|
const CHARTS_WITHOUT_AXIS = ['pie'];
|
|
9
10
|
/**
|
|
10
11
|
* Checks whether the series should be drawn with axes.
|
|
@@ -150,8 +151,5 @@ const extractCategoryValue = (args) => {
|
|
|
150
151
|
export const getDataCategoryValue = (args) => {
|
|
151
152
|
const { axisDirection, categories, data } = args;
|
|
152
153
|
const categoryValue = extractCategoryValue({ axisDirection, categories, data });
|
|
153
|
-
if (!categories.includes(categoryValue)) {
|
|
154
|
-
throw new Error('It seems you are trying to use category value that is not in categories array');
|
|
155
|
-
}
|
|
156
154
|
return categoryValue;
|
|
157
155
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function setEllipsisForOverflowText(selection, maxWidth) {
|
|
2
|
+
var _a, _b;
|
|
3
|
+
let text = selection.text();
|
|
4
|
+
selection.text(null).attr('text-anchor', 'left').append('title').text(text);
|
|
5
|
+
const tSpan = selection.append('tspan').text(text);
|
|
6
|
+
let textLength = ((_a = tSpan.node()) === null || _a === void 0 ? void 0 : _a.getComputedTextLength()) || 0;
|
|
7
|
+
while (textLength > maxWidth && text.length > 1) {
|
|
8
|
+
text = text.slice(0, -1);
|
|
9
|
+
tSpan.text(text + '…');
|
|
10
|
+
textLength = ((_b = tSpan.node()) === null || _b === void 0 ? void 0 : _b.getComputedTextLength()) || 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/chartkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "React component used to render charts based on any sources you need",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "git@github.com:gravity-ui/ChartKit.git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@bem-react/classname": "^1.6.0",
|
|
50
50
|
"@gravity-ui/date-utils": "^1.4.1",
|
|
51
|
-
"@gravity-ui/yagr": "^3.
|
|
51
|
+
"@gravity-ui/yagr": "^3.8.0",
|
|
52
52
|
"d3": "^7.8.5",
|
|
53
53
|
"lodash": "^4.17.21",
|
|
54
54
|
"react-split-pane": "^0.1.92"
|