@gravity-ui/chartkit 4.9.3 → 4.10.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.
Files changed (31) hide show
  1. package/build/components/ChartKit.css +1 -0
  2. package/build/plugins/d3/examples/bar-x/DataLabels.d.ts +2 -0
  3. package/build/plugins/d3/examples/bar-x/DataLabels.js +46 -0
  4. package/build/plugins/d3/examples/line/DataLabels.d.ts +2 -0
  5. package/build/plugins/d3/examples/line/DataLabels.js +89 -0
  6. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +10 -2
  7. package/build/plugins/d3/renderer/hooks/useSeries/constants.js +1 -0
  8. package/build/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.js +4 -2
  9. package/build/plugins/d3/renderer/hooks/useSeries/prepare-line-series.js +1 -0
  10. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +3 -0
  11. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.d.ts +2 -2
  12. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.js +15 -15
  13. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.d.ts +1 -8
  14. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.js +26 -3
  15. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/types.d.ts +11 -0
  16. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/types.js +1 -0
  17. package/build/plugins/d3/renderer/hooks/useShapes/line/index.d.ts +1 -1
  18. package/build/plugins/d3/renderer/hooks/useShapes/line/index.js +44 -51
  19. package/build/plugins/d3/renderer/hooks/useShapes/line/prepare-data.js +40 -5
  20. package/build/plugins/d3/renderer/hooks/useShapes/line/types.d.ts +2 -0
  21. package/build/plugins/d3/renderer/hooks/useShapes/utils.d.ts +12 -2
  22. package/build/plugins/d3/renderer/hooks/useShapes/utils.js +15 -0
  23. package/build/plugins/d3/renderer/types/index.d.ts +16 -0
  24. package/build/plugins/d3/renderer/types/index.js +1 -0
  25. package/build/plugins/d3/renderer/utils/index.d.ts +1 -0
  26. package/build/plugins/d3/renderer/utils/index.js +1 -0
  27. package/build/plugins/d3/renderer/utils/labels.d.ts +3 -0
  28. package/build/plugins/d3/renderer/utils/labels.js +35 -0
  29. package/build/types/widget-data/base.d.ts +4 -0
  30. package/build/types/widget-data/series.d.ts +1 -1
  31. 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,2 @@
1
+ import React from 'react';
2
+ export declare const DataLabels: () => React.JSX.Element;
@@ -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,2 @@
1
+ import React from 'react';
2
+ export declare const DataLabels: () => React.JSX.Element;
@@ -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: get(yAxis1, 'type', 'linear'),
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: get(yAxis1, '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),
@@ -3,4 +3,5 @@ export const DEFAULT_DATALABELS_PADDING = 5;
3
3
  export const DEFAULT_DATALABELS_STYLE = {
4
4
  fontSize: '11px',
5
5
  fontWeight: 'bold',
6
+ fontColor: 'var(--d3-data-labels)',
6
7
  };
@@ -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 './prepare-data';
4
+ import type { PreparedBarXData } from './types';
5
5
  export { prepareBarXData } from './prepare-data';
6
- export type { PreparedBarXData } from './prepare-data';
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
- const DEFAULT_LABEL_PADDING = 7;
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
- const dataLabels = preparedData.filter((d) => d.series.dataLabels.enabled);
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('allLabels')
36
+ .selectAll('text')
32
37
  .data(dataLabels)
33
38
  .join('text')
34
- .text((d) => String(d.data.label || d.data.y))
39
+ .text((d) => d.text)
35
40
  .attr('class', b('label'))
36
- .attr('x', (d) => d.x + d.width / 2)
37
- .attr('y', (d) => {
38
- if (d.series.dataLabels.inside) {
39
- return d.y + d.height / 2;
40
- }
41
- return d.y - DEFAULT_LABEL_PADDING;
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
- export type PreparedBarXData = Omit<TooltipDataChunkBarX, 'series'> & {
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
- result.push({
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
+ };
@@ -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 { shapeKey } from '../utils';
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 selection = svgElement
23
+ const lineSelection = svgElement
22
24
  .selectAll('path')
23
- .data(preparedData, shapeKey)
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
- const dataLabels = preparedData.reduce((acc, d) => {
32
- if (d.series.dataLabels.enabled) {
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
- svgElement
43
- .selectAll('allLabels')
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) => String(d.data.label || d.data.y))
43
+ .text((d) => d.text)
47
44
  .attr('class', b('label'))
48
45
  .attr('x', (d) => d.x)
49
- .attr('y', (d) => {
50
- return d.y - d.series.dataLabels.padding;
51
- })
52
- .attr('text-anchor', 'middle')
53
- .style('font-size', (d) => d.series.dataLabels.style.fontSize)
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
- const updates = [];
62
- preparedData.forEach((p) => {
63
- const hovered = Boolean(hoverEnabled && p.id === selectedSeriesId);
64
- if (p.hovered !== hovered) {
65
- p.hovered = hovered;
66
- updates.push(p);
67
- }
68
- const active = Boolean(!inactiveEnabled || !selectedSeriesId || selectedSeriesId === p.id);
69
- if (p.active !== active) {
70
- p.active = active;
71
- updates.push(p);
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
- selection.data(updates, shapeKey).join('shape', (update) => {
75
- update
76
- .attr('stroke', (d) => {
77
- var _a;
78
- const initialColor = d.color || '';
79
- if (d.hovered) {
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
- return update;
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: s.data.map((d) => ({
8
- x: getXValue({ point: d, xAxis, xScale }),
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 { PreparedAxis } from '../useChartOptions/types';
2
- import { ChartScale } from '../useAxisScales';
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 {};
@@ -5,6 +5,7 @@ export * from './math';
5
5
  export * from './text';
6
6
  export * from './time';
7
7
  export * from './axis';
8
+ export * from './labels';
8
9
  export type AxisDirection = 'x' | 'y';
9
10
  export type NodeWithD3Data<T = unknown> = Element & {
10
11
  __data__: T;
@@ -10,6 +10,7 @@ export * from './math';
10
10
  export * from './text';
11
11
  export * from './time';
12
12
  export * from './axis';
13
+ export * from './labels';
13
14
  const CHARTS_WITHOUT_AXIS = ['pie'];
14
15
  /**
15
16
  * Checks whether the series should be drawn with axes.
@@ -0,0 +1,3 @@
1
+ import type { LabelData } from '../types';
2
+ export declare function getLeftPosition(label: LabelData): number;
3
+ export declare function filterOverlappingLabels(labels: LabelData[]): LabelData[];
@@ -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
+ }
@@ -17,6 +17,10 @@ export type BaseSeries = {
17
17
  * @default 5
18
18
  * */
19
19
  padding?: number;
20
+ /**
21
+ * @default false
22
+ * */
23
+ allowOverlap?: boolean;
20
24
  };
21
25
  };
22
26
  export type BaseSeriesData<T = any> = {
@@ -26,7 +26,7 @@ type BasicHoverState = {
26
26
  */
27
27
  brightness?: number;
28
28
  };
29
- type BasicInactiveState = {
29
+ export type BasicInactiveState = {
30
30
  /**
31
31
  * Enable separate styles for the inactive series.
32
32
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "4.9.3",
3
+ "version": "4.10.1",
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.2",
51
+ "@gravity-ui/yagr": "^3.11.4",
52
52
  "afterframe": "^1.0.2",
53
53
  "d3": "^7.8.5",
54
54
  "lodash": "^4.17.21",