@gravity-ui/charts 1.34.7 → 1.34.8

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.
@@ -141,14 +141,9 @@ export function createYScale(args) {
141
141
  }
142
142
  if (hasNumberAndNullValues) {
143
143
  const [yMinDomain, yMaxDomain] = extent(domain);
144
- const isPointDomain = hasOnlyMarkerSeries(series)
145
- ? checkIsPointDomain([yMinDomain, yMaxDomain])
146
- : false;
147
- const yMin = typeof yMinPropsOrState === 'number' && !isPointDomain
148
- ? yMinPropsOrState
149
- : yMinDomain;
144
+ const yMin = typeof yMinPropsOrState === 'number' ? yMinPropsOrState : yMinDomain;
150
145
  let yMax;
151
- if (typeof yMaxPropsOrState === 'number' && !isPointDomain) {
146
+ if (typeof yMaxPropsOrState === 'number') {
152
147
  yMax = yMaxPropsOrState;
153
148
  }
154
149
  else {
@@ -11,8 +11,10 @@ export const AreaSeriesShapes = (args) => {
11
11
  const hoveredDataRef = React.useRef(null);
12
12
  const plotRef = React.useRef(null);
13
13
  const markersRef = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!plotRef.current || !markersRef.current) {
17
19
  return () => { };
18
20
  }
@@ -55,7 +57,7 @@ export const AreaSeriesShapes = (args) => {
55
57
  let dataLabels = preparedData.reduce((acc, d) => {
56
58
  return acc.concat(d.labels);
57
59
  }, []);
58
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
60
+ if (!allowOverlapDataLabels) {
59
61
  dataLabels = filterOverlappingLabels(dataLabels);
60
62
  }
61
63
  const labelsSelection = plotSvgElement
@@ -147,9 +149,16 @@ export const AreaSeriesShapes = (args) => {
147
149
  return () => {
148
150
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.area', null);
149
151
  };
150
- }, [dispatcher, preparedData, seriesOptions]);
152
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
153
+ const htmlLayerData = React.useMemo(() => {
154
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
155
+ if (allowOverlapDataLabels) {
156
+ return { htmlElements: items };
157
+ }
158
+ return { htmlElements: filterOverlappingLabels(items) };
159
+ }, [allowOverlapDataLabels, preparedData]);
151
160
  return (React.createElement(React.Fragment, null,
152
161
  React.createElement("g", { ref: plotRef, className: b(), clipPath: `url(#${clipPathId})` }),
153
162
  React.createElement("g", { ref: markersRef }),
154
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
163
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
155
164
  };
@@ -27,14 +27,14 @@ function getXValues(series, xAxis, xScale) {
27
27
  }
28
28
  return Array.from(xValues);
29
29
  }
30
- async function prepareDataLabels({ series, points, xMax, yAxisTop, }) {
30
+ async function prepareDataLabels({ series, points, xMax, yAxisTop, isOutsideBounds, }) {
31
31
  var _a;
32
32
  const svgLabels = [];
33
33
  const htmlLabels = [];
34
34
  const getTextSize = getTextSizeFn({ style: series.dataLabels.style });
35
35
  for (let pointsIndex = 0; pointsIndex < points.length; pointsIndex++) {
36
36
  const point = points[pointsIndex];
37
- if (point.y === null) {
37
+ if (point.y === null || isOutsideBounds(point.x, point.y)) {
38
38
  continue;
39
39
  }
40
40
  const text = getFormattedValue(Object.assign({ value: (_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y }, series.dataLabels));
@@ -281,6 +281,7 @@ export const prepareAreaData = async (args) => {
281
281
  points: item.points,
282
282
  xMax,
283
283
  yAxisTop: itemYAxisTop,
284
+ isOutsideBounds,
284
285
  });
285
286
  item.labels.push(...labelsData.svgLabels);
286
287
  item.htmlElements.push(...labelsData.htmlLabels);
@@ -11,8 +11,10 @@ export const BarXSeriesShapes = (args) => {
11
11
  const { dispatcher, preparedData, seriesOptions, htmlLayout, clipPathId } = args;
12
12
  const hoveredDataRef = React.useRef(null);
13
13
  const ref = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!ref.current) {
17
19
  return () => { };
18
20
  }
@@ -46,7 +48,7 @@ export const BarXSeriesShapes = (args) => {
46
48
  .attr('opacity', (d) => d.opacity)
47
49
  .attr('cursor', (d) => d.series.cursor);
48
50
  let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
49
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
51
+ if (!allowOverlapDataLabels) {
50
52
  dataLabels = filterOverlappingLabels(dataLabels);
51
53
  }
52
54
  const labelSelection = svgElement
@@ -108,8 +110,15 @@ export const BarXSeriesShapes = (args) => {
108
110
  return () => {
109
111
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.bar-x', null);
110
112
  };
111
- }, [dispatcher, preparedData, seriesOptions]);
113
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
114
+ const htmlLayerData = React.useMemo(() => {
115
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
116
+ if (allowOverlapDataLabels) {
117
+ return { htmlElements: items };
118
+ }
119
+ return { htmlElements: filterOverlappingLabels(items) };
120
+ }, [allowOverlapDataLabels, preparedData]);
112
121
  return (React.createElement(React.Fragment, null,
113
122
  React.createElement("g", { ref: ref, className: b(), clipPath: `url(#${clipPathId})` }),
114
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
123
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
115
124
  };
@@ -5,7 +5,7 @@ import { getFormattedValue } from '../../../utils/chart/format';
5
5
  import { getSeriesStackId } from '../../useSeries/utils';
6
6
  import { getBarXLayout } from '../../utils/bar-x';
7
7
  const isSeriesDataValid = (d) => d.y !== null;
8
- async function getLabelData(d) {
8
+ async function getLabelData(d, xMax) {
9
9
  var _a;
10
10
  if (!d.series.dataLabels.enabled) {
11
11
  return undefined;
@@ -22,10 +22,10 @@ async function getLabelData(d) {
22
22
  if (d.series.dataLabels.inside) {
23
23
  y = d.y + d.height / 2;
24
24
  }
25
- const x = d.x + d.width / 2;
25
+ const centerX = Math.min(xMax - width / 2, Math.max(width / 2, d.x + d.width / 2));
26
26
  return {
27
27
  text,
28
- x: html ? x - width / 2 : x,
28
+ x: html ? centerX - width / 2 : centerX,
29
29
  y: html ? y - height : y,
30
30
  style,
31
31
  size: { width, height },
@@ -35,7 +35,7 @@ async function getLabelData(d) {
35
35
  }
36
36
  // eslint-disable-next-line complexity
37
37
  export const prepareBarXData = async (args) => {
38
- var _a, _b, _c, _d;
38
+ var _a, _b, _c, _d, _e;
39
39
  const { series, seriesOptions, xAxis, xScale, yAxis, yScale, boundsHeight: plotHeight, split, isRangeSlider, } = args;
40
40
  const stackGap = seriesOptions['bar-x'].stackGap;
41
41
  const categories = (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.categories) !== null && _a !== void 0 ? _a : [];
@@ -176,10 +176,19 @@ export const prepareBarXData = async (args) => {
176
176
  }
177
177
  }
178
178
  }
179
+ const [_xMin, xRangeMax] = xScale.range();
180
+ const xMax = xRangeMax;
179
181
  for (let i = 0; i < result.length; i++) {
180
182
  const barData = result[i];
181
- if (barData.series.dataLabels.enabled && !isRangeSlider) {
182
- const label = await getLabelData(barData);
183
+ const isBarOutsideBounds = barData.x + barData.width <= 0 ||
184
+ barData.x >= xMax ||
185
+ barData.y + barData.height <= 0 ||
186
+ barData.y >= plotHeight;
187
+ const isZeroValue = ((_e = barData.data.y) !== null && _e !== void 0 ? _e : 0) === 0;
188
+ if (barData.series.dataLabels.enabled &&
189
+ !isRangeSlider &&
190
+ (!isBarOutsideBounds || isZeroValue)) {
191
+ const label = await getLabelData(barData, xMax);
183
192
  if (barData.series.dataLabels.html && label) {
184
193
  barData.htmlElements.push({
185
194
  x: label.x,
@@ -11,8 +11,10 @@ export const LineSeriesShapes = (args) => {
11
11
  const hoveredDataRef = React.useRef(null);
12
12
  const plotRef = React.useRef(null);
13
13
  const markersRef = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!plotRef.current || !markersRef.current) {
17
19
  return () => { };
18
20
  }
@@ -42,7 +44,7 @@ export const LineSeriesShapes = (args) => {
42
44
  let dataLabels = preparedData.reduce((acc, d) => {
43
45
  return acc.concat(d.labels);
44
46
  }, []);
45
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
47
+ if (!allowOverlapDataLabels) {
46
48
  dataLabels = filterOverlappingLabels(dataLabels);
47
49
  }
48
50
  const labelsSelection = plotSvgElement
@@ -133,9 +135,16 @@ export const LineSeriesShapes = (args) => {
133
135
  return () => {
134
136
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.line', null);
135
137
  };
136
- }, [dispatcher, preparedData, seriesOptions]);
138
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
139
+ const htmlLayerData = React.useMemo(() => {
140
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
141
+ if (allowOverlapDataLabels) {
142
+ return { htmlElements: items };
143
+ }
144
+ return { htmlElements: filterOverlappingLabels(items) };
145
+ }, [allowOverlapDataLabels, preparedData]);
137
146
  return (React.createElement(React.Fragment, null,
138
147
  React.createElement("g", { ref: plotRef, className: b(), clipPath: `url(#${clipPathId})` }),
139
148
  React.createElement("g", { ref: markersRef }),
140
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
149
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
141
150
  };
@@ -49,7 +49,7 @@ export const prepareLineData = async (args) => {
49
49
  if (s.dataLabels.enabled && !isRangeSlider) {
50
50
  if (s.dataLabels.html) {
51
51
  const list = await Promise.all(points.reduce((result, p) => {
52
- if (p.y === null) {
52
+ if (p.y === null || p.x === null || isOutsideBounds(p.x, p.y)) {
53
53
  return result;
54
54
  }
55
55
  result.push(getHtmlLabel(p, s, xMax));
@@ -61,7 +61,9 @@ export const prepareLineData = async (args) => {
61
61
  const getTextSize = getTextSizeFn({ style: s.dataLabels.style });
62
62
  for (let index = 0; index < points.length; index++) {
63
63
  const point = points[index];
64
- if (point.y !== null && point.x !== null) {
64
+ if (point.y !== null &&
65
+ point.x !== null &&
66
+ !isOutsideBounds(point.x, point.y)) {
65
67
  const labelValue = (_b = point.data.label) !== null && _b !== void 0 ? _b : point.data.y;
66
68
  const text = getFormattedValue(Object.assign({ value: labelValue }, s.dataLabels));
67
69
  const labelSize = await getTextSize(text);
@@ -12,8 +12,10 @@ export const WaterfallSeriesShapes = (args) => {
12
12
  const hoveredDataRef = React.useRef(null);
13
13
  const ref = React.useRef(null);
14
14
  const connectorSelector = `.${b('connector')}`;
15
+ const allowOverlapDataLabels = React.useMemo(() => {
16
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
17
+ }, [preparedData]);
15
18
  React.useEffect(() => {
16
- var _a;
17
19
  if (!ref.current) {
18
20
  return () => { };
19
21
  }
@@ -34,7 +36,7 @@ export const WaterfallSeriesShapes = (args) => {
34
36
  .attr('opacity', (d) => d.opacity)
35
37
  .attr('cursor', (d) => d.series.cursor);
36
38
  let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
37
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
39
+ if (!allowOverlapDataLabels) {
38
40
  dataLabels = filterOverlappingLabels(dataLabels);
39
41
  }
40
42
  const labelSelection = svgElement
@@ -125,8 +127,15 @@ export const WaterfallSeriesShapes = (args) => {
125
127
  return () => {
126
128
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.waterfall', null);
127
129
  };
128
- }, [connectorSelector, dispatcher, preparedData, seriesOptions]);
130
+ }, [allowOverlapDataLabels, connectorSelector, dispatcher, preparedData, seriesOptions]);
131
+ const htmlLayerData = React.useMemo(() => {
132
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
133
+ if (allowOverlapDataLabels) {
134
+ return { htmlElements: items };
135
+ }
136
+ return { htmlElements: filterOverlappingLabels(items) };
137
+ }, [allowOverlapDataLabels, preparedData]);
129
138
  return (React.createElement(React.Fragment, null,
130
139
  React.createElement("g", { ref: ref, className: b(), clipPath: `url(#${clipPathId})` }),
131
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
140
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
132
141
  };
@@ -47,19 +47,53 @@ export function getClosestPoints(args) {
47
47
  const [pointerX, pointerY] = position;
48
48
  const result = [];
49
49
  const groups = groupBy(shapesData, getSeriesType);
50
+ const closestPointsByXValue = [];
50
51
  // eslint-disable-next-line complexity
51
52
  Object.entries(groups).forEach(([seriesType, list]) => {
52
53
  var _a, _b, _c, _d, _e;
53
54
  switch (seriesType) {
55
+ case 'line': {
56
+ const linePoints = list.reduce((acc, d) => {
57
+ acc.push(...d.points.reduce((accPoints, p) => {
58
+ if (p.y !== null && p.x !== null) {
59
+ accPoints.push({
60
+ data: p.data,
61
+ series: p.series,
62
+ x: p.x,
63
+ y0: p.y,
64
+ y1: p.y,
65
+ });
66
+ }
67
+ return accPoints;
68
+ }, []));
69
+ return acc;
70
+ }, []);
71
+ closestPointsByXValue.push(...linePoints);
72
+ break;
73
+ }
74
+ case 'area': {
75
+ const areaPoints = list.reduce((acc, d) => {
76
+ Array.prototype.push.apply(acc, d.points.map((p) => ({
77
+ data: p.data,
78
+ series: p.series,
79
+ x: p.x,
80
+ y0: p.y0,
81
+ y1: p.y,
82
+ })));
83
+ return acc;
84
+ }, []);
85
+ closestPointsByXValue.push(...areaPoints);
86
+ break;
87
+ }
54
88
  case 'bar-x': {
55
- const points = list.map((d) => ({
89
+ const barXPoints = list.map((d) => ({
56
90
  data: d.data,
57
91
  series: d.series,
58
92
  x: d.x + d.width / 2,
59
93
  y0: d.y,
60
94
  y1: d.y + d.height,
61
95
  }));
62
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
96
+ closestPointsByXValue.push(...barXPoints);
63
97
  break;
64
98
  }
65
99
  case 'waterfall': {
@@ -74,39 +108,6 @@ export function getClosestPoints(args) {
74
108
  result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
75
109
  break;
76
110
  }
77
- case 'area': {
78
- const points = list.reduce((acc, d) => {
79
- Array.prototype.push.apply(acc, d.points.map((p) => ({
80
- data: p.data,
81
- series: p.series,
82
- x: p.x,
83
- y0: p.y0,
84
- y1: p.y,
85
- })));
86
- return acc;
87
- }, []);
88
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
89
- break;
90
- }
91
- case 'line': {
92
- const points = list.reduce((acc, d) => {
93
- acc.push(...d.points.reduce((accPoints, p) => {
94
- if (p.y !== null && p.x !== null) {
95
- accPoints.push({
96
- data: p.data,
97
- series: p.series,
98
- x: p.x,
99
- y0: p.y,
100
- y1: p.y,
101
- });
102
- }
103
- return accPoints;
104
- }, []));
105
- return acc;
106
- }, []);
107
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
108
- break;
109
- }
110
111
  case 'bar-y': {
111
112
  const points = list;
112
113
  const sorted = sort(points, (p) => p.y);
@@ -267,6 +268,9 @@ export function getClosestPoints(args) {
267
268
  }
268
269
  }
269
270
  });
271
+ if (closestPointsByXValue.length) {
272
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, closestPointsByXValue));
273
+ }
270
274
  return result;
271
275
  }
272
276
  function isInsidePath(args) {
@@ -141,14 +141,9 @@ export function createYScale(args) {
141
141
  }
142
142
  if (hasNumberAndNullValues) {
143
143
  const [yMinDomain, yMaxDomain] = extent(domain);
144
- const isPointDomain = hasOnlyMarkerSeries(series)
145
- ? checkIsPointDomain([yMinDomain, yMaxDomain])
146
- : false;
147
- const yMin = typeof yMinPropsOrState === 'number' && !isPointDomain
148
- ? yMinPropsOrState
149
- : yMinDomain;
144
+ const yMin = typeof yMinPropsOrState === 'number' ? yMinPropsOrState : yMinDomain;
150
145
  let yMax;
151
- if (typeof yMaxPropsOrState === 'number' && !isPointDomain) {
146
+ if (typeof yMaxPropsOrState === 'number') {
152
147
  yMax = yMaxPropsOrState;
153
148
  }
154
149
  else {
@@ -11,8 +11,10 @@ export const AreaSeriesShapes = (args) => {
11
11
  const hoveredDataRef = React.useRef(null);
12
12
  const plotRef = React.useRef(null);
13
13
  const markersRef = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!plotRef.current || !markersRef.current) {
17
19
  return () => { };
18
20
  }
@@ -55,7 +57,7 @@ export const AreaSeriesShapes = (args) => {
55
57
  let dataLabels = preparedData.reduce((acc, d) => {
56
58
  return acc.concat(d.labels);
57
59
  }, []);
58
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
60
+ if (!allowOverlapDataLabels) {
59
61
  dataLabels = filterOverlappingLabels(dataLabels);
60
62
  }
61
63
  const labelsSelection = plotSvgElement
@@ -147,9 +149,16 @@ export const AreaSeriesShapes = (args) => {
147
149
  return () => {
148
150
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.area', null);
149
151
  };
150
- }, [dispatcher, preparedData, seriesOptions]);
152
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
153
+ const htmlLayerData = React.useMemo(() => {
154
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
155
+ if (allowOverlapDataLabels) {
156
+ return { htmlElements: items };
157
+ }
158
+ return { htmlElements: filterOverlappingLabels(items) };
159
+ }, [allowOverlapDataLabels, preparedData]);
151
160
  return (React.createElement(React.Fragment, null,
152
161
  React.createElement("g", { ref: plotRef, className: b(), clipPath: `url(#${clipPathId})` }),
153
162
  React.createElement("g", { ref: markersRef }),
154
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
163
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
155
164
  };
@@ -27,14 +27,14 @@ function getXValues(series, xAxis, xScale) {
27
27
  }
28
28
  return Array.from(xValues);
29
29
  }
30
- async function prepareDataLabels({ series, points, xMax, yAxisTop, }) {
30
+ async function prepareDataLabels({ series, points, xMax, yAxisTop, isOutsideBounds, }) {
31
31
  var _a;
32
32
  const svgLabels = [];
33
33
  const htmlLabels = [];
34
34
  const getTextSize = getTextSizeFn({ style: series.dataLabels.style });
35
35
  for (let pointsIndex = 0; pointsIndex < points.length; pointsIndex++) {
36
36
  const point = points[pointsIndex];
37
- if (point.y === null) {
37
+ if (point.y === null || isOutsideBounds(point.x, point.y)) {
38
38
  continue;
39
39
  }
40
40
  const text = getFormattedValue(Object.assign({ value: (_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y }, series.dataLabels));
@@ -281,6 +281,7 @@ export const prepareAreaData = async (args) => {
281
281
  points: item.points,
282
282
  xMax,
283
283
  yAxisTop: itemYAxisTop,
284
+ isOutsideBounds,
284
285
  });
285
286
  item.labels.push(...labelsData.svgLabels);
286
287
  item.htmlElements.push(...labelsData.htmlLabels);
@@ -11,8 +11,10 @@ export const BarXSeriesShapes = (args) => {
11
11
  const { dispatcher, preparedData, seriesOptions, htmlLayout, clipPathId } = args;
12
12
  const hoveredDataRef = React.useRef(null);
13
13
  const ref = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!ref.current) {
17
19
  return () => { };
18
20
  }
@@ -46,7 +48,7 @@ export const BarXSeriesShapes = (args) => {
46
48
  .attr('opacity', (d) => d.opacity)
47
49
  .attr('cursor', (d) => d.series.cursor);
48
50
  let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
49
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
51
+ if (!allowOverlapDataLabels) {
50
52
  dataLabels = filterOverlappingLabels(dataLabels);
51
53
  }
52
54
  const labelSelection = svgElement
@@ -108,8 +110,15 @@ export const BarXSeriesShapes = (args) => {
108
110
  return () => {
109
111
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.bar-x', null);
110
112
  };
111
- }, [dispatcher, preparedData, seriesOptions]);
113
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
114
+ const htmlLayerData = React.useMemo(() => {
115
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
116
+ if (allowOverlapDataLabels) {
117
+ return { htmlElements: items };
118
+ }
119
+ return { htmlElements: filterOverlappingLabels(items) };
120
+ }, [allowOverlapDataLabels, preparedData]);
112
121
  return (React.createElement(React.Fragment, null,
113
122
  React.createElement("g", { ref: ref, className: b(), clipPath: `url(#${clipPathId})` }),
114
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
123
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
115
124
  };
@@ -5,7 +5,7 @@ import { getFormattedValue } from '../../../utils/chart/format';
5
5
  import { getSeriesStackId } from '../../useSeries/utils';
6
6
  import { getBarXLayout } from '../../utils/bar-x';
7
7
  const isSeriesDataValid = (d) => d.y !== null;
8
- async function getLabelData(d) {
8
+ async function getLabelData(d, xMax) {
9
9
  var _a;
10
10
  if (!d.series.dataLabels.enabled) {
11
11
  return undefined;
@@ -22,10 +22,10 @@ async function getLabelData(d) {
22
22
  if (d.series.dataLabels.inside) {
23
23
  y = d.y + d.height / 2;
24
24
  }
25
- const x = d.x + d.width / 2;
25
+ const centerX = Math.min(xMax - width / 2, Math.max(width / 2, d.x + d.width / 2));
26
26
  return {
27
27
  text,
28
- x: html ? x - width / 2 : x,
28
+ x: html ? centerX - width / 2 : centerX,
29
29
  y: html ? y - height : y,
30
30
  style,
31
31
  size: { width, height },
@@ -35,7 +35,7 @@ async function getLabelData(d) {
35
35
  }
36
36
  // eslint-disable-next-line complexity
37
37
  export const prepareBarXData = async (args) => {
38
- var _a, _b, _c, _d;
38
+ var _a, _b, _c, _d, _e;
39
39
  const { series, seriesOptions, xAxis, xScale, yAxis, yScale, boundsHeight: plotHeight, split, isRangeSlider, } = args;
40
40
  const stackGap = seriesOptions['bar-x'].stackGap;
41
41
  const categories = (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.categories) !== null && _a !== void 0 ? _a : [];
@@ -176,10 +176,19 @@ export const prepareBarXData = async (args) => {
176
176
  }
177
177
  }
178
178
  }
179
+ const [_xMin, xRangeMax] = xScale.range();
180
+ const xMax = xRangeMax;
179
181
  for (let i = 0; i < result.length; i++) {
180
182
  const barData = result[i];
181
- if (barData.series.dataLabels.enabled && !isRangeSlider) {
182
- const label = await getLabelData(barData);
183
+ const isBarOutsideBounds = barData.x + barData.width <= 0 ||
184
+ barData.x >= xMax ||
185
+ barData.y + barData.height <= 0 ||
186
+ barData.y >= plotHeight;
187
+ const isZeroValue = ((_e = barData.data.y) !== null && _e !== void 0 ? _e : 0) === 0;
188
+ if (barData.series.dataLabels.enabled &&
189
+ !isRangeSlider &&
190
+ (!isBarOutsideBounds || isZeroValue)) {
191
+ const label = await getLabelData(barData, xMax);
183
192
  if (barData.series.dataLabels.html && label) {
184
193
  barData.htmlElements.push({
185
194
  x: label.x,
@@ -11,8 +11,10 @@ export const LineSeriesShapes = (args) => {
11
11
  const hoveredDataRef = React.useRef(null);
12
12
  const plotRef = React.useRef(null);
13
13
  const markersRef = React.useRef(null);
14
+ const allowOverlapDataLabels = React.useMemo(() => {
15
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
16
+ }, [preparedData]);
14
17
  React.useEffect(() => {
15
- var _a;
16
18
  if (!plotRef.current || !markersRef.current) {
17
19
  return () => { };
18
20
  }
@@ -42,7 +44,7 @@ export const LineSeriesShapes = (args) => {
42
44
  let dataLabels = preparedData.reduce((acc, d) => {
43
45
  return acc.concat(d.labels);
44
46
  }, []);
45
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
47
+ if (!allowOverlapDataLabels) {
46
48
  dataLabels = filterOverlappingLabels(dataLabels);
47
49
  }
48
50
  const labelsSelection = plotSvgElement
@@ -133,9 +135,16 @@ export const LineSeriesShapes = (args) => {
133
135
  return () => {
134
136
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.line', null);
135
137
  };
136
- }, [dispatcher, preparedData, seriesOptions]);
138
+ }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
139
+ const htmlLayerData = React.useMemo(() => {
140
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
141
+ if (allowOverlapDataLabels) {
142
+ return { htmlElements: items };
143
+ }
144
+ return { htmlElements: filterOverlappingLabels(items) };
145
+ }, [allowOverlapDataLabels, preparedData]);
137
146
  return (React.createElement(React.Fragment, null,
138
147
  React.createElement("g", { ref: plotRef, className: b(), clipPath: `url(#${clipPathId})` }),
139
148
  React.createElement("g", { ref: markersRef }),
140
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
149
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
141
150
  };
@@ -49,7 +49,7 @@ export const prepareLineData = async (args) => {
49
49
  if (s.dataLabels.enabled && !isRangeSlider) {
50
50
  if (s.dataLabels.html) {
51
51
  const list = await Promise.all(points.reduce((result, p) => {
52
- if (p.y === null) {
52
+ if (p.y === null || p.x === null || isOutsideBounds(p.x, p.y)) {
53
53
  return result;
54
54
  }
55
55
  result.push(getHtmlLabel(p, s, xMax));
@@ -61,7 +61,9 @@ export const prepareLineData = async (args) => {
61
61
  const getTextSize = getTextSizeFn({ style: s.dataLabels.style });
62
62
  for (let index = 0; index < points.length; index++) {
63
63
  const point = points[index];
64
- if (point.y !== null && point.x !== null) {
64
+ if (point.y !== null &&
65
+ point.x !== null &&
66
+ !isOutsideBounds(point.x, point.y)) {
65
67
  const labelValue = (_b = point.data.label) !== null && _b !== void 0 ? _b : point.data.y;
66
68
  const text = getFormattedValue(Object.assign({ value: labelValue }, s.dataLabels));
67
69
  const labelSize = await getTextSize(text);
@@ -12,8 +12,10 @@ export const WaterfallSeriesShapes = (args) => {
12
12
  const hoveredDataRef = React.useRef(null);
13
13
  const ref = React.useRef(null);
14
14
  const connectorSelector = `.${b('connector')}`;
15
+ const allowOverlapDataLabels = React.useMemo(() => {
16
+ return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
17
+ }, [preparedData]);
15
18
  React.useEffect(() => {
16
- var _a;
17
19
  if (!ref.current) {
18
20
  return () => { };
19
21
  }
@@ -34,7 +36,7 @@ export const WaterfallSeriesShapes = (args) => {
34
36
  .attr('opacity', (d) => d.opacity)
35
37
  .attr('cursor', (d) => d.series.cursor);
36
38
  let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
37
- if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
39
+ if (!allowOverlapDataLabels) {
38
40
  dataLabels = filterOverlappingLabels(dataLabels);
39
41
  }
40
42
  const labelSelection = svgElement
@@ -125,8 +127,15 @@ export const WaterfallSeriesShapes = (args) => {
125
127
  return () => {
126
128
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.waterfall', null);
127
129
  };
128
- }, [connectorSelector, dispatcher, preparedData, seriesOptions]);
130
+ }, [allowOverlapDataLabels, connectorSelector, dispatcher, preparedData, seriesOptions]);
131
+ const htmlLayerData = React.useMemo(() => {
132
+ const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlElements).flat();
133
+ if (allowOverlapDataLabels) {
134
+ return { htmlElements: items };
135
+ }
136
+ return { htmlElements: filterOverlappingLabels(items) };
137
+ }, [allowOverlapDataLabels, preparedData]);
129
138
  return (React.createElement(React.Fragment, null,
130
139
  React.createElement("g", { ref: ref, className: b(), clipPath: `url(#${clipPathId})` }),
131
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
140
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
132
141
  };
@@ -47,19 +47,53 @@ export function getClosestPoints(args) {
47
47
  const [pointerX, pointerY] = position;
48
48
  const result = [];
49
49
  const groups = groupBy(shapesData, getSeriesType);
50
+ const closestPointsByXValue = [];
50
51
  // eslint-disable-next-line complexity
51
52
  Object.entries(groups).forEach(([seriesType, list]) => {
52
53
  var _a, _b, _c, _d, _e;
53
54
  switch (seriesType) {
55
+ case 'line': {
56
+ const linePoints = list.reduce((acc, d) => {
57
+ acc.push(...d.points.reduce((accPoints, p) => {
58
+ if (p.y !== null && p.x !== null) {
59
+ accPoints.push({
60
+ data: p.data,
61
+ series: p.series,
62
+ x: p.x,
63
+ y0: p.y,
64
+ y1: p.y,
65
+ });
66
+ }
67
+ return accPoints;
68
+ }, []));
69
+ return acc;
70
+ }, []);
71
+ closestPointsByXValue.push(...linePoints);
72
+ break;
73
+ }
74
+ case 'area': {
75
+ const areaPoints = list.reduce((acc, d) => {
76
+ Array.prototype.push.apply(acc, d.points.map((p) => ({
77
+ data: p.data,
78
+ series: p.series,
79
+ x: p.x,
80
+ y0: p.y0,
81
+ y1: p.y,
82
+ })));
83
+ return acc;
84
+ }, []);
85
+ closestPointsByXValue.push(...areaPoints);
86
+ break;
87
+ }
54
88
  case 'bar-x': {
55
- const points = list.map((d) => ({
89
+ const barXPoints = list.map((d) => ({
56
90
  data: d.data,
57
91
  series: d.series,
58
92
  x: d.x + d.width / 2,
59
93
  y0: d.y,
60
94
  y1: d.y + d.height,
61
95
  }));
62
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
96
+ closestPointsByXValue.push(...barXPoints);
63
97
  break;
64
98
  }
65
99
  case 'waterfall': {
@@ -74,39 +108,6 @@ export function getClosestPoints(args) {
74
108
  result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
75
109
  break;
76
110
  }
77
- case 'area': {
78
- const points = list.reduce((acc, d) => {
79
- Array.prototype.push.apply(acc, d.points.map((p) => ({
80
- data: p.data,
81
- series: p.series,
82
- x: p.x,
83
- y0: p.y0,
84
- y1: p.y,
85
- })));
86
- return acc;
87
- }, []);
88
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
89
- break;
90
- }
91
- case 'line': {
92
- const points = list.reduce((acc, d) => {
93
- acc.push(...d.points.reduce((accPoints, p) => {
94
- if (p.y !== null && p.x !== null) {
95
- accPoints.push({
96
- data: p.data,
97
- series: p.series,
98
- x: p.x,
99
- y0: p.y,
100
- y1: p.y,
101
- });
102
- }
103
- return accPoints;
104
- }, []));
105
- return acc;
106
- }, []);
107
- result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
108
- break;
109
- }
110
111
  case 'bar-y': {
111
112
  const points = list;
112
113
  const sorted = sort(points, (p) => p.y);
@@ -267,6 +268,9 @@ export function getClosestPoints(args) {
267
268
  }
268
269
  }
269
270
  });
271
+ if (closestPointsByXValue.length) {
272
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, closestPointsByXValue));
273
+ }
270
274
  return result;
271
275
  }
272
276
  function isInsidePath(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.34.7",
3
+ "version": "1.34.8",
4
4
  "description": "A flexible JavaScript library for data visualization and chart rendering using React",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",