@gravity-ui/chartkit 4.9.2 → 4.10.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/components/ChartKit.css +1 -0
- package/build/plugins/d3/examples/bar-x/DataLabels.d.ts +2 -0
- package/build/plugins/d3/examples/bar-x/DataLabels.js +46 -0
- package/build/plugins/d3/examples/line/DataLabels.d.ts +2 -0
- package/build/plugins/d3/examples/line/DataLabels.js +89 -0
- package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +10 -2
- package/build/plugins/d3/renderer/hooks/useSeries/constants.js +1 -0
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.js +4 -2
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-line-series.js +1 -0
- package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +3 -0
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.d.ts +2 -2
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.js +15 -15
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.d.ts +1 -8
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.js +26 -3
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/types.d.ts +11 -0
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x/types.js +1 -0
- package/build/plugins/d3/renderer/hooks/useShapes/line/index.d.ts +1 -1
- package/build/plugins/d3/renderer/hooks/useShapes/line/index.js +44 -51
- package/build/plugins/d3/renderer/hooks/useShapes/line/prepare-data.js +40 -5
- package/build/plugins/d3/renderer/hooks/useShapes/line/types.d.ts +2 -0
- package/build/plugins/d3/renderer/hooks/useShapes/utils.d.ts +12 -2
- package/build/plugins/d3/renderer/hooks/useShapes/utils.js +15 -0
- package/build/plugins/d3/renderer/types/index.d.ts +16 -0
- package/build/plugins/d3/renderer/types/index.js +1 -0
- package/build/plugins/d3/renderer/utils/index.d.ts +1 -0
- package/build/plugins/d3/renderer/utils/index.js +1 -0
- package/build/plugins/d3/renderer/utils/labels.d.ts +3 -0
- package/build/plugins/d3/renderer/utils/labels.js +35 -0
- package/build/types/widget-data/base.d.ts +4 -0
- package/build/types/widget-data/series.d.ts +1 -1
- package/package.json +2 -2
|
@@ -28,4 +28,5 @@
|
|
|
28
28
|
--highcharts-tooltip-alternate-bg: var(--yc-color-base-generic);
|
|
29
29
|
--highcharts-tooltip-text-complementary: var(--yc-color-text-complementary);
|
|
30
30
|
--highcharts-holiday-band: var(--yc-color-base-generic);
|
|
31
|
+
--d3-data-labels: var(--yc-color-text-complementary);
|
|
31
32
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChartKit } from '../../../../components/ChartKit';
|
|
3
|
+
import nintendoGames from '../nintendoGames';
|
|
4
|
+
import { groups, sort } from 'd3';
|
|
5
|
+
const years = Array.from(new Set(nintendoGames.map((g) => g.date ? new Date(g.date).getFullYear().toString() : 'unknown')));
|
|
6
|
+
function prepareData() {
|
|
7
|
+
const games = sort(nintendoGames.filter((d) => {
|
|
8
|
+
return d.date && d.user_score;
|
|
9
|
+
}), (d) => d.date);
|
|
10
|
+
const groupByYear = (d) => (d.date ? new Date(d.date).getFullYear() : 'unknown');
|
|
11
|
+
const byGenre = (genre) => {
|
|
12
|
+
const data = groups(games.filter((d) => d.genres.includes(genre)), groupByYear).map(([year, items]) => {
|
|
13
|
+
return {
|
|
14
|
+
x: years.indexOf(String(year)),
|
|
15
|
+
y: items.length,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
type: 'bar-x',
|
|
20
|
+
name: genre,
|
|
21
|
+
dataLabels: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
},
|
|
24
|
+
stacking: 'normal',
|
|
25
|
+
data,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
return [byGenre('Strategy'), byGenre('Shooter'), byGenre('Puzzle'), byGenre('Action')];
|
|
29
|
+
}
|
|
30
|
+
export const DataLabels = () => {
|
|
31
|
+
const series = prepareData();
|
|
32
|
+
const widgetData = {
|
|
33
|
+
series: {
|
|
34
|
+
data: series,
|
|
35
|
+
},
|
|
36
|
+
xAxis: {
|
|
37
|
+
categories: years,
|
|
38
|
+
type: 'category',
|
|
39
|
+
title: {
|
|
40
|
+
text: 'Release year',
|
|
41
|
+
},
|
|
42
|
+
ticks: { pixelInterval: 200 },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return React.createElement(ChartKit, { type: "d3", data: widgetData });
|
|
46
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChartKit } from '../../../../components/ChartKit';
|
|
3
|
+
import nintendoGames from '../nintendoGames';
|
|
4
|
+
import { dateTime } from '@gravity-ui/date-utils';
|
|
5
|
+
function prepareData() {
|
|
6
|
+
const games = nintendoGames.filter((d) => {
|
|
7
|
+
return d.date && d.user_score;
|
|
8
|
+
});
|
|
9
|
+
const byGenre = (genre) => {
|
|
10
|
+
return games
|
|
11
|
+
.filter((d) => d.genres.includes(genre))
|
|
12
|
+
.map((d) => {
|
|
13
|
+
return {
|
|
14
|
+
x: d.date,
|
|
15
|
+
y: d.user_score,
|
|
16
|
+
label: `${d.title} (${d.user_score})`,
|
|
17
|
+
custom: d,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
name: 'Strategy',
|
|
24
|
+
type: 'line',
|
|
25
|
+
data: byGenre('Strategy'),
|
|
26
|
+
dataLabels: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Shooter',
|
|
32
|
+
type: 'line',
|
|
33
|
+
data: byGenre('Shooter'),
|
|
34
|
+
dataLabels: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
export const DataLabels = () => {
|
|
41
|
+
const series = prepareData();
|
|
42
|
+
const widgetData = {
|
|
43
|
+
series: {
|
|
44
|
+
data: series.map((s) => ({
|
|
45
|
+
type: 'line',
|
|
46
|
+
data: s.data.filter((d) => d.x),
|
|
47
|
+
name: s.name,
|
|
48
|
+
dataLabels: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
},
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
yAxis: [
|
|
54
|
+
{
|
|
55
|
+
title: {
|
|
56
|
+
text: 'User score',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
xAxis: {
|
|
61
|
+
type: 'datetime',
|
|
62
|
+
title: {
|
|
63
|
+
text: 'Release dates',
|
|
64
|
+
},
|
|
65
|
+
ticks: { pixelInterval: 200 },
|
|
66
|
+
},
|
|
67
|
+
tooltip: {
|
|
68
|
+
renderer: (d) => {
|
|
69
|
+
var _a;
|
|
70
|
+
const point = (_a = d.hovered[0]) === null || _a === void 0 ? void 0 : _a.data;
|
|
71
|
+
if (!point) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const title = point.custom.title;
|
|
75
|
+
const score = point.custom.user_score;
|
|
76
|
+
const date = dateTime({ input: point.custom.date }).format('DD MMM YYYY');
|
|
77
|
+
return (React.createElement(React.Fragment, null,
|
|
78
|
+
React.createElement("b", null, title),
|
|
79
|
+
React.createElement("br", null),
|
|
80
|
+
"Release date: ",
|
|
81
|
+
date,
|
|
82
|
+
React.createElement("br", null),
|
|
83
|
+
"User score: ",
|
|
84
|
+
score));
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
return React.createElement(ChartKit, { type: "d3", data: widgetData });
|
|
89
|
+
};
|
|
@@ -25,6 +25,13 @@ const getAxisLabelMaxWidth = (args) => {
|
|
|
25
25
|
rotation: axis.labels.rotation,
|
|
26
26
|
}).maxWidth;
|
|
27
27
|
};
|
|
28
|
+
function getAxisMin(axis, series) {
|
|
29
|
+
const min = axis === null || axis === void 0 ? void 0 : axis.min;
|
|
30
|
+
if (typeof min === 'undefined' && (series === null || series === void 0 ? void 0 : series.some((s) => s.type === 'bar-x'))) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
return min;
|
|
34
|
+
}
|
|
28
35
|
export const getPreparedYAxis = ({ series, yAxis, }) => {
|
|
29
36
|
// FIXME: add support for n axises
|
|
30
37
|
const yAxis1 = yAxis === null || yAxis === void 0 ? void 0 : yAxis[0];
|
|
@@ -36,8 +43,9 @@ export const getPreparedYAxis = ({ series, yAxis, }) => {
|
|
|
36
43
|
const y1TitleStyle = {
|
|
37
44
|
fontSize: get(yAxis1, 'title.style.fontSize', yAxisTitleDefaults.fontSize),
|
|
38
45
|
};
|
|
46
|
+
const axisType = get(yAxis1, 'type', 'linear');
|
|
39
47
|
const preparedY1Axis = {
|
|
40
|
-
type:
|
|
48
|
+
type: axisType,
|
|
41
49
|
labels: {
|
|
42
50
|
enabled: labelsEnabled,
|
|
43
51
|
margin: labelsEnabled ? get(yAxis1, 'labels.margin', axisLabelsDefaults.margin) : 0,
|
|
@@ -62,7 +70,7 @@ export const getPreparedYAxis = ({ series, yAxis, }) => {
|
|
|
62
70
|
? getHorisontalSvgTextHeight({ text: y1TitleText, style: y1TitleStyle })
|
|
63
71
|
: 0,
|
|
64
72
|
},
|
|
65
|
-
min:
|
|
73
|
+
min: getAxisMin(yAxis1, series),
|
|
66
74
|
maxPadding: get(yAxis1, 'maxPadding', 0.05),
|
|
67
75
|
grid: {
|
|
68
76
|
enabled: get(yAxis1, 'grid.enabled', true),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import get from 'lodash/get';
|
|
2
2
|
import { getRandomCKId } from '../../../../../utils';
|
|
3
3
|
import { prepareLegendSymbol } from './utils';
|
|
4
|
-
import { DEFAULT_DATALABELS_STYLE } from './constants';
|
|
4
|
+
import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
|
|
5
5
|
export function prepareBarXSeries(args) {
|
|
6
6
|
const { colorScale, series: seriesList, legend } = args;
|
|
7
7
|
const commonStackId = getRandomCKId();
|
|
8
8
|
return seriesList.map((series) => {
|
|
9
|
-
var _a, _b, _c, _d;
|
|
9
|
+
var _a, _b, _c, _d, _e;
|
|
10
10
|
const name = series.name || '';
|
|
11
11
|
const color = series.color || colorScale(name);
|
|
12
12
|
let stackId = series.stackId;
|
|
@@ -32,6 +32,8 @@ export function prepareBarXSeries(args) {
|
|
|
32
32
|
? (_c = series.dataLabels) === null || _c === void 0 ? void 0 : _c.inside
|
|
33
33
|
: false,
|
|
34
34
|
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_d = series.dataLabels) === null || _d === void 0 ? void 0 : _d.style),
|
|
35
|
+
allowOverlap: ((_e = series.dataLabels) === null || _e === void 0 ? void 0 : _e.allowOverlap) || false,
|
|
36
|
+
padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
}, []);
|
|
@@ -26,6 +26,7 @@ export function prepareLineSeries(args) {
|
|
|
26
26
|
enabled: ((_a = series.dataLabels) === null || _a === void 0 ? void 0 : _a.enabled) || false,
|
|
27
27
|
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_b = series.dataLabels) === null || _b === void 0 ? void 0 : _b.style),
|
|
28
28
|
padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
|
|
29
|
+
allowOverlap: get(series, 'dataLabels.allowOverlap', false),
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
}, []);
|
|
@@ -51,6 +51,8 @@ export type PreparedBarXSeries = {
|
|
|
51
51
|
enabled: boolean;
|
|
52
52
|
inside: boolean;
|
|
53
53
|
style: BaseTextStyle;
|
|
54
|
+
allowOverlap: boolean;
|
|
55
|
+
padding: number;
|
|
54
56
|
};
|
|
55
57
|
} & BasePreparedSeries;
|
|
56
58
|
export type PreparedBarYSeries = {
|
|
@@ -79,6 +81,7 @@ export type PreparedLineSeries = {
|
|
|
79
81
|
enabled: boolean;
|
|
80
82
|
style: BaseTextStyle;
|
|
81
83
|
padding: number;
|
|
84
|
+
allowOverlap: boolean;
|
|
82
85
|
};
|
|
83
86
|
} & BasePreparedSeries;
|
|
84
87
|
export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Dispatch } from 'd3';
|
|
3
3
|
import type { PreparedSeriesOptions } from '../../useSeries/types';
|
|
4
|
-
import type { PreparedBarXData } from './
|
|
4
|
+
import type { PreparedBarXData } from './types';
|
|
5
5
|
export { prepareBarXData } from './prepare-data';
|
|
6
|
-
export
|
|
6
|
+
export * from './types';
|
|
7
7
|
type Args = {
|
|
8
8
|
dispatcher: Dispatch<object>;
|
|
9
9
|
preparedData: PreparedBarXData[];
|
|
@@ -2,13 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import get from 'lodash/get';
|
|
3
3
|
import { color, select } from 'd3';
|
|
4
4
|
import { block } from '../../../../../../utils/cn';
|
|
5
|
+
import { filterOverlappingLabels } from '../../../utils';
|
|
5
6
|
export { prepareBarXData } from './prepare-data';
|
|
6
|
-
|
|
7
|
+
export * from './types';
|
|
7
8
|
const b = block('d3-bar-x');
|
|
8
9
|
export const BarXSeriesShapes = (args) => {
|
|
9
10
|
const { dispatcher, preparedData, seriesOptions } = args;
|
|
10
11
|
const ref = React.useRef(null);
|
|
11
12
|
React.useEffect(() => {
|
|
13
|
+
var _a;
|
|
12
14
|
if (!ref.current) {
|
|
13
15
|
return () => { };
|
|
14
16
|
}
|
|
@@ -26,24 +28,22 @@ export const BarXSeriesShapes = (args) => {
|
|
|
26
28
|
.attr('height', (d) => d.height)
|
|
27
29
|
.attr('width', (d) => d.width)
|
|
28
30
|
.attr('fill', (d) => d.data.color || d.series.color);
|
|
29
|
-
|
|
31
|
+
let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
|
|
32
|
+
if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
|
|
33
|
+
dataLabels = filterOverlappingLabels(dataLabels);
|
|
34
|
+
}
|
|
30
35
|
const labelSelection = svgElement
|
|
31
|
-
.selectAll('
|
|
36
|
+
.selectAll('text')
|
|
32
37
|
.data(dataLabels)
|
|
33
38
|
.join('text')
|
|
34
|
-
.text((d) =>
|
|
39
|
+
.text((d) => d.text)
|
|
35
40
|
.attr('class', b('label'))
|
|
36
|
-
.attr('x', (d) => d.x
|
|
37
|
-
.attr('y', (d) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
43
|
-
.attr('text-anchor', 'middle')
|
|
44
|
-
.style('font-size', (d) => d.series.dataLabels.style.fontSize)
|
|
45
|
-
.style('font-weight', (d) => d.series.dataLabels.style.fontWeight || null)
|
|
46
|
-
.style('fill', (d) => d.series.dataLabels.style.fontColor || null);
|
|
41
|
+
.attr('x', (d) => d.x)
|
|
42
|
+
.attr('y', (d) => d.y)
|
|
43
|
+
.attr('text-anchor', (d) => d.textAnchor)
|
|
44
|
+
.style('font-size', (d) => d.style.fontSize)
|
|
45
|
+
.style('font-weight', (d) => d.style.fontWeight || null)
|
|
46
|
+
.style('fill', (d) => d.style.fontColor || null);
|
|
47
47
|
dispatcher.on('hover-shape.bar-x', (data) => {
|
|
48
48
|
const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
|
|
49
49
|
const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import type { TooltipDataChunkBarX } from '../../../../../../types';
|
|
2
1
|
import type { ChartScale } from '../../useAxisScales';
|
|
3
2
|
import type { PreparedAxis } from '../../useChartOptions/types';
|
|
4
3
|
import type { PreparedBarXSeries, PreparedSeriesOptions } from '../../useSeries/types';
|
|
5
|
-
|
|
6
|
-
x: number;
|
|
7
|
-
y: number;
|
|
8
|
-
width: number;
|
|
9
|
-
height: number;
|
|
10
|
-
series: PreparedBarXSeries;
|
|
11
|
-
};
|
|
4
|
+
import { PreparedBarXData } from './types';
|
|
12
5
|
export declare const prepareBarXData: (args: {
|
|
13
6
|
series: PreparedBarXSeries[];
|
|
14
7
|
seriesOptions: PreparedSeriesOptions;
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { ascending, descending, max, sort } from 'd3';
|
|
2
2
|
import get from 'lodash/get';
|
|
3
|
-
import { getDataCategoryValue } from '../../../utils';
|
|
3
|
+
import { getDataCategoryValue, getLabelsSize } from '../../../utils';
|
|
4
4
|
import { MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH } from '../constants';
|
|
5
|
+
function getLabelData(d) {
|
|
6
|
+
if (!d.series.dataLabels.enabled) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const text = String(d.data.label || d.data.y);
|
|
10
|
+
const style = d.series.dataLabels.style;
|
|
11
|
+
const { maxHeight: height, maxWidth: width } = getLabelsSize({ labels: [text], style });
|
|
12
|
+
let y = Math.max(height, d.y - d.series.dataLabels.padding);
|
|
13
|
+
if (d.series.dataLabels.inside) {
|
|
14
|
+
y = d.y + d.height / 2;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
text,
|
|
18
|
+
x: d.x + d.width / 2,
|
|
19
|
+
y,
|
|
20
|
+
style,
|
|
21
|
+
size: { width, height },
|
|
22
|
+
textAnchor: 'middle',
|
|
23
|
+
series: d.series,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
5
26
|
export const prepareBarXData = (args) => {
|
|
6
27
|
const { series, seriesOptions, xAxis, xScale, yScale } = args;
|
|
7
28
|
const categories = get(xAxis, 'categories', []);
|
|
@@ -89,14 +110,16 @@ export const prepareBarXData = (args) => {
|
|
|
89
110
|
const yLinearScale = yScale;
|
|
90
111
|
const y = yLinearScale(yValue.data.y);
|
|
91
112
|
const height = yLinearScale(yLinearScale.domain()[0]) - y;
|
|
92
|
-
|
|
113
|
+
const barData = {
|
|
93
114
|
x,
|
|
94
115
|
y: y - stackHeight,
|
|
95
116
|
width: rectWidth,
|
|
96
117
|
height,
|
|
97
118
|
data: yValue.data,
|
|
98
119
|
series: yValue.series,
|
|
99
|
-
}
|
|
120
|
+
};
|
|
121
|
+
barData.label = getLabelData(barData);
|
|
122
|
+
result.push(barData);
|
|
100
123
|
stackHeight += height + 1;
|
|
101
124
|
});
|
|
102
125
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TooltipDataChunkBarX } from '../../../../../../types';
|
|
2
|
+
import { PreparedBarXSeries } from '../../useSeries/types';
|
|
3
|
+
import { LabelData } from '../../../types';
|
|
4
|
+
export type PreparedBarXData = Omit<TooltipDataChunkBarX, 'series'> & {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
series: PreparedBarXSeries;
|
|
10
|
+
label?: LabelData;
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Dispatch } from 'd3';
|
|
3
3
|
import type { PreparedSeriesOptions } from '../../useSeries/types';
|
|
4
|
-
import { PreparedLineData } from './types';
|
|
4
|
+
import type { PreparedLineData } from './types';
|
|
5
5
|
type Args = {
|
|
6
6
|
dispatcher: Dispatch<object>;
|
|
7
7
|
preparedData: PreparedLineData[];
|
|
@@ -2,12 +2,14 @@ import React from 'react';
|
|
|
2
2
|
import { select, line as lineGenerator, color } from 'd3';
|
|
3
3
|
import get from 'lodash/get';
|
|
4
4
|
import { block } from '../../../../../../utils/cn';
|
|
5
|
-
import {
|
|
5
|
+
import { filterOverlappingLabels } from '../../../utils';
|
|
6
|
+
import { setActiveState } from '../utils';
|
|
6
7
|
const b = block('d3-line');
|
|
7
8
|
export const LineSeriesShapes = (args) => {
|
|
8
9
|
const { dispatcher, preparedData, seriesOptions } = args;
|
|
9
10
|
const ref = React.useRef(null);
|
|
10
11
|
React.useEffect(() => {
|
|
12
|
+
var _a;
|
|
11
13
|
if (!ref.current) {
|
|
12
14
|
return () => { };
|
|
13
15
|
}
|
|
@@ -18,9 +20,9 @@ export const LineSeriesShapes = (args) => {
|
|
|
18
20
|
.x((d) => d.x)
|
|
19
21
|
.y((d) => d.y);
|
|
20
22
|
svgElement.selectAll('*').remove();
|
|
21
|
-
const
|
|
23
|
+
const lineSelection = svgElement
|
|
22
24
|
.selectAll('path')
|
|
23
|
-
.data(preparedData
|
|
25
|
+
.data(preparedData)
|
|
24
26
|
.join('path')
|
|
25
27
|
.attr('d', (d) => line(d.points))
|
|
26
28
|
.attr('fill', 'none')
|
|
@@ -28,67 +30,58 @@ export const LineSeriesShapes = (args) => {
|
|
|
28
30
|
.attr('stroke-width', (d) => d.width)
|
|
29
31
|
.attr('stroke-linejoin', 'round')
|
|
30
32
|
.attr('stroke-linecap', 'round');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
acc.push(...d.points.map((p) => ({
|
|
34
|
-
x: p.x,
|
|
35
|
-
y: p.y,
|
|
36
|
-
data: p.data,
|
|
37
|
-
series: d.series,
|
|
38
|
-
})));
|
|
39
|
-
}
|
|
40
|
-
return acc;
|
|
33
|
+
let dataLabels = preparedData.reduce((acc, d) => {
|
|
34
|
+
return acc.concat(d.labels);
|
|
41
35
|
}, []);
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
|
|
37
|
+
dataLabels = filterOverlappingLabels(dataLabels);
|
|
38
|
+
}
|
|
39
|
+
const labelsSelection = svgElement
|
|
40
|
+
.selectAll('text')
|
|
44
41
|
.data(dataLabels)
|
|
45
42
|
.join('text')
|
|
46
|
-
.text((d) =>
|
|
43
|
+
.text((d) => d.text)
|
|
47
44
|
.attr('class', b('label'))
|
|
48
45
|
.attr('x', (d) => d.x)
|
|
49
|
-
.attr('y', (d) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
.style('
|
|
54
|
-
.style('font-weight', (d) => d.series.dataLabels.style.fontWeight || null)
|
|
55
|
-
.style('fill', (d) => d.series.dataLabels.style.fontColor || null);
|
|
46
|
+
.attr('y', (d) => d.y)
|
|
47
|
+
.attr('text-anchor', (d) => d.textAnchor)
|
|
48
|
+
.style('font-size', (d) => d.style.fontSize)
|
|
49
|
+
.style('font-weight', (d) => d.style.fontWeight || null)
|
|
50
|
+
.style('fill', (d) => d.style.fontColor || null);
|
|
56
51
|
const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
|
|
57
52
|
const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
|
|
58
53
|
dispatcher.on('hover-shape.line', (data) => {
|
|
59
54
|
var _a, _b;
|
|
60
55
|
const selectedSeriesId = (_b = (_a = data === null || data === void 0 ? void 0 : data.find((d) => d.series.type === 'line')) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.id;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const hovered = Boolean(hoverEnabled &&
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
lineSelection.datum((d, index, list) => {
|
|
57
|
+
const elementSelection = select(list[index]);
|
|
58
|
+
const hovered = Boolean(hoverEnabled && d.id === selectedSeriesId);
|
|
59
|
+
if (d.hovered !== hovered) {
|
|
60
|
+
d.hovered = hovered;
|
|
61
|
+
elementSelection.attr('stroke', (d) => {
|
|
62
|
+
var _a;
|
|
63
|
+
const initialColor = d.color || '';
|
|
64
|
+
if (d.hovered) {
|
|
65
|
+
return (((_a = color(initialColor)) === null || _a === void 0 ? void 0 : _a.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) || initialColor);
|
|
66
|
+
}
|
|
67
|
+
return initialColor;
|
|
68
|
+
});
|
|
72
69
|
}
|
|
70
|
+
return setActiveState({
|
|
71
|
+
element: list[index],
|
|
72
|
+
state: inactiveOptions,
|
|
73
|
+
active: Boolean(!inactiveEnabled || !selectedSeriesId || selectedSeriesId === d.id),
|
|
74
|
+
datum: d,
|
|
75
|
+
});
|
|
73
76
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return (((_a = color(initialColor)) === null || _a === void 0 ? void 0 : _a.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) || initialColor);
|
|
81
|
-
}
|
|
82
|
-
return initialColor;
|
|
83
|
-
})
|
|
84
|
-
.attr('opacity', function (d) {
|
|
85
|
-
if (!d.active) {
|
|
86
|
-
return (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
77
|
+
labelsSelection.datum((d, index, list) => {
|
|
78
|
+
return setActiveState({
|
|
79
|
+
element: list[index],
|
|
80
|
+
state: inactiveOptions,
|
|
81
|
+
active: Boolean(!inactiveEnabled || !selectedSeriesId || selectedSeriesId === d.series.id),
|
|
82
|
+
datum: d,
|
|
89
83
|
});
|
|
90
|
-
|
|
91
|
-
}, (exit) => exit);
|
|
84
|
+
});
|
|
92
85
|
});
|
|
93
86
|
return () => {
|
|
94
87
|
dispatcher.on('hover-shape.line', null);
|
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
import { getXValue, getYValue } from '../utils';
|
|
2
|
+
import { getLabelsSize, getLeftPosition } from '../../../utils';
|
|
3
|
+
function getLabelData(point, series, xMax) {
|
|
4
|
+
const text = String(point.data.label || point.data.y);
|
|
5
|
+
const style = series.dataLabels.style;
|
|
6
|
+
const size = getLabelsSize({ labels: [text], style });
|
|
7
|
+
const labelData = {
|
|
8
|
+
text,
|
|
9
|
+
x: point.x,
|
|
10
|
+
y: point.y - series.dataLabels.padding,
|
|
11
|
+
style,
|
|
12
|
+
size: { width: size.maxWidth, height: size.maxHeight },
|
|
13
|
+
textAnchor: 'middle',
|
|
14
|
+
series: series,
|
|
15
|
+
active: true,
|
|
16
|
+
};
|
|
17
|
+
const left = getLeftPosition(labelData);
|
|
18
|
+
if (left < 0) {
|
|
19
|
+
labelData.x = labelData.x + Math.abs(left);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const right = left + labelData.size.width;
|
|
23
|
+
if (right > xMax) {
|
|
24
|
+
labelData.x = labelData.x - xMax - right;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return labelData;
|
|
28
|
+
}
|
|
2
29
|
export const prepareLineData = (args) => {
|
|
3
30
|
const { series, xAxis, xScale, yScale } = args;
|
|
4
31
|
const yAxis = args.yAxis[0];
|
|
32
|
+
const [_xMin, xRangeMax] = xScale.range();
|
|
33
|
+
const xMax = xRangeMax / (1 - xAxis.maxPadding);
|
|
5
34
|
return series.reduce((acc, s) => {
|
|
35
|
+
const points = s.data.map((d) => ({
|
|
36
|
+
x: getXValue({ point: d, xAxis, xScale }),
|
|
37
|
+
y: getYValue({ point: d, yAxis, yScale }),
|
|
38
|
+
data: d,
|
|
39
|
+
}));
|
|
40
|
+
let labels = [];
|
|
41
|
+
if (s.dataLabels.enabled) {
|
|
42
|
+
labels = points.map((p) => getLabelData(p, s, xMax));
|
|
43
|
+
}
|
|
6
44
|
acc.push({
|
|
7
|
-
points
|
|
8
|
-
|
|
9
|
-
y: getYValue({ point: d, yAxis, yScale }),
|
|
10
|
-
data: d,
|
|
11
|
-
})),
|
|
45
|
+
points,
|
|
46
|
+
labels,
|
|
12
47
|
color: s.color,
|
|
13
48
|
width: s.lineWidth,
|
|
14
49
|
series: s,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PreparedLineSeries } from '../../useSeries/types';
|
|
2
2
|
import { LineSeriesData } from '../../../../../../types';
|
|
3
|
+
import { LabelData } from '../../../types';
|
|
3
4
|
export type PointData = {
|
|
4
5
|
x: number;
|
|
5
6
|
y: number;
|
|
@@ -13,4 +14,5 @@ export type PreparedLineData = {
|
|
|
13
14
|
series: PreparedLineSeries;
|
|
14
15
|
hovered: boolean;
|
|
15
16
|
active: boolean;
|
|
17
|
+
labels: LabelData[];
|
|
16
18
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { BaseType } from 'd3';
|
|
2
|
+
import type { BasicInactiveState } from '../../../../../types';
|
|
3
|
+
import type { ChartScale } from '../useAxisScales';
|
|
4
|
+
import type { PreparedAxis } from '../useChartOptions/types';
|
|
3
5
|
export declare function getXValue(args: {
|
|
4
6
|
point: {
|
|
5
7
|
x?: number | string;
|
|
@@ -15,3 +17,11 @@ export declare function getYValue(args: {
|
|
|
15
17
|
yScale: ChartScale;
|
|
16
18
|
}): number;
|
|
17
19
|
export declare const shapeKey: (d: unknown) => string | number;
|
|
20
|
+
export declare function setActiveState<T extends {
|
|
21
|
+
active?: boolean;
|
|
22
|
+
}>(args: {
|
|
23
|
+
element: BaseType;
|
|
24
|
+
datum: T;
|
|
25
|
+
state: BasicInactiveState | undefined;
|
|
26
|
+
active: boolean;
|
|
27
|
+
}): T;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
1
2
|
import get from 'lodash/get';
|
|
2
3
|
import { getDataCategoryValue } from '../../utils';
|
|
3
4
|
export function getXValue(args) {
|
|
@@ -23,3 +24,17 @@ export function getYValue(args) {
|
|
|
23
24
|
return yLinearScale(point.y);
|
|
24
25
|
}
|
|
25
26
|
export const shapeKey = (d) => d.id || -1;
|
|
27
|
+
export function setActiveState(args) {
|
|
28
|
+
const { element, datum, state, active } = args;
|
|
29
|
+
const elementSelection = select(element);
|
|
30
|
+
if (datum.active !== active) {
|
|
31
|
+
datum.active = active;
|
|
32
|
+
elementSelection.attr('opacity', function (d) {
|
|
33
|
+
if (!d.active) {
|
|
34
|
+
return (state === null || state === void 0 ? void 0 : state.opacity) || null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return datum;
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseTextStyle } from '../../../../types';
|
|
2
|
+
export type LabelData = {
|
|
3
|
+
text: string;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
style: BaseTextStyle;
|
|
7
|
+
size: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
textAnchor: 'middle';
|
|
12
|
+
series: {
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
active?: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import sortBy from 'lodash/sortBy';
|
|
2
|
+
export function getLeftPosition(label) {
|
|
3
|
+
switch (label.textAnchor) {
|
|
4
|
+
case 'middle': {
|
|
5
|
+
return label.x - label.size.width / 2;
|
|
6
|
+
}
|
|
7
|
+
default: {
|
|
8
|
+
return label.x;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function hasOverlappingByX(rect1, rect2) {
|
|
13
|
+
const left1 = getLeftPosition(rect1);
|
|
14
|
+
const right1 = left1 + rect1.size.width;
|
|
15
|
+
const left2 = getLeftPosition(rect2);
|
|
16
|
+
const right2 = left2 + rect2.size.width;
|
|
17
|
+
return Math.max(0, Math.min(right1, right2) - Math.max(left1, left2)) > 0;
|
|
18
|
+
}
|
|
19
|
+
function hasOverlappingByY(rect1, rect2) {
|
|
20
|
+
const top1 = rect1.y - rect1.size.height;
|
|
21
|
+
const bottom1 = rect1.y;
|
|
22
|
+
const top2 = rect2.y - rect2.size.height;
|
|
23
|
+
const bottom2 = rect2.y;
|
|
24
|
+
return Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2)) > 0;
|
|
25
|
+
}
|
|
26
|
+
export function filterOverlappingLabels(labels) {
|
|
27
|
+
const result = [];
|
|
28
|
+
const sorted = sortBy(labels, (d) => d.y, getLeftPosition);
|
|
29
|
+
sorted.forEach((label) => {
|
|
30
|
+
if (!result.some((l) => hasOverlappingByX(l, label) && hasOverlappingByY(l, label))) {
|
|
31
|
+
result.push(label);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return result;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/chartkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.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.11.
|
|
51
|
+
"@gravity-ui/yagr": "^3.11.3",
|
|
52
52
|
"afterframe": "^1.0.2",
|
|
53
53
|
"d3": "^7.8.5",
|
|
54
54
|
"lodash": "^4.17.21",
|