@gravity-ui/charts 1.47.0 → 1.48.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.
Files changed (51) hide show
  1. package/dist/cjs/components/AxisX/prepare-axis-data.js +9 -6
  2. package/dist/cjs/components/AxisY/prepare-axis-data.js +11 -4
  3. package/dist/cjs/core/axes/types.d.ts +4 -2
  4. package/dist/cjs/core/axes/x-axis.js +2 -0
  5. package/dist/cjs/core/axes/y-axis.js +2 -0
  6. package/dist/cjs/core/series/prepare-scatter.js +11 -3
  7. package/dist/cjs/core/series/types.d.ts +8 -0
  8. package/dist/cjs/core/shapes/area/prepare-data.js +2 -49
  9. package/dist/cjs/core/shapes/bar-x/prepare-data.js +49 -35
  10. package/dist/cjs/core/shapes/line/prepare-data.js +13 -58
  11. package/dist/cjs/core/shapes/scatter/prepare-data.d.ts +5 -2
  12. package/dist/cjs/core/shapes/scatter/prepare-data.js +43 -4
  13. package/dist/cjs/core/shapes/scatter/renderer.d.ts +2 -2
  14. package/dist/cjs/core/shapes/scatter/renderer.js +9 -1
  15. package/dist/cjs/core/shapes/scatter/types.d.ts +6 -1
  16. package/dist/cjs/core/types/chart/axis.d.ts +20 -0
  17. package/dist/cjs/core/types/chart/scatter.d.ts +2 -0
  18. package/dist/cjs/core/utils/data-labels.d.ts +46 -0
  19. package/dist/cjs/core/utils/data-labels.js +64 -0
  20. package/dist/cjs/core/utils/index.d.ts +1 -0
  21. package/dist/cjs/core/utils/index.js +1 -0
  22. package/dist/cjs/hooks/useShapes/index.js +5 -3
  23. package/dist/cjs/hooks/useShapes/scatter/index.d.ts +2 -2
  24. package/dist/cjs/hooks/useShapes/scatter/index.js +4 -1
  25. package/dist/cjs/hooks/useShapes/styles.css +8 -25
  26. package/dist/esm/components/AxisX/prepare-axis-data.js +9 -6
  27. package/dist/esm/components/AxisY/prepare-axis-data.js +11 -4
  28. package/dist/esm/core/axes/types.d.ts +4 -2
  29. package/dist/esm/core/axes/x-axis.js +2 -0
  30. package/dist/esm/core/axes/y-axis.js +2 -0
  31. package/dist/esm/core/series/prepare-scatter.js +11 -3
  32. package/dist/esm/core/series/types.d.ts +8 -0
  33. package/dist/esm/core/shapes/area/prepare-data.js +2 -49
  34. package/dist/esm/core/shapes/bar-x/prepare-data.js +49 -35
  35. package/dist/esm/core/shapes/line/prepare-data.js +13 -58
  36. package/dist/esm/core/shapes/scatter/prepare-data.d.ts +5 -2
  37. package/dist/esm/core/shapes/scatter/prepare-data.js +43 -4
  38. package/dist/esm/core/shapes/scatter/renderer.d.ts +2 -2
  39. package/dist/esm/core/shapes/scatter/renderer.js +9 -1
  40. package/dist/esm/core/shapes/scatter/types.d.ts +6 -1
  41. package/dist/esm/core/types/chart/axis.d.ts +20 -0
  42. package/dist/esm/core/types/chart/scatter.d.ts +2 -0
  43. package/dist/esm/core/utils/data-labels.d.ts +46 -0
  44. package/dist/esm/core/utils/data-labels.js +64 -0
  45. package/dist/esm/core/utils/index.d.ts +1 -0
  46. package/dist/esm/core/utils/index.js +1 -0
  47. package/dist/esm/hooks/useShapes/index.js +5 -3
  48. package/dist/esm/hooks/useShapes/scatter/index.d.ts +2 -2
  49. package/dist/esm/hooks/useShapes/scatter/index.js +4 -1
  50. package/dist/esm/hooks/useShapes/styles.css +8 -25
  51. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { getUniqId } from '@gravity-ui/uikit';
2
2
  import { select } from 'd3-selection';
3
- import { calculateCos, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
3
+ import { calculateCos, calculateNumericProperty, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
4
4
  import { getXAxisTickValues } from '../../core/utils/axis/x-axis';
5
5
  import { getMultilineTitleContentRows } from '../utils/axis-title';
6
6
  async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, }) {
@@ -72,7 +72,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
72
72
  return svgLabel;
73
73
  }
74
74
  export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }) {
75
- var _a, _b, _c, _d, _e, _f, _g, _h;
75
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
76
76
  const xAxisItems = [];
77
77
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
78
78
  for (let plotIndex = 0; plotIndex < splitPlots.length; plotIndex++) {
@@ -267,6 +267,9 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
267
267
  if (plotBandWidth < 0) {
268
268
  continue;
269
269
  }
270
+ const perpExtent = (_g = calculateNumericProperty({ value: plotBand.size, base: axisHeight })) !== null && _g !== void 0 ? _g : axisHeight;
271
+ // X axis is positioned at the bottom of the plot area, so 'start' = bottom edge.
272
+ const bandY = plotBand.align === 'end' ? axisTop : axisTop + axisHeight - perpExtent;
270
273
  const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
271
274
  const labelSize = plotBand.label.text
272
275
  ? await getPlotLabelSize(plotBand.label.text)
@@ -274,17 +277,17 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
274
277
  plotBands.push({
275
278
  layerPlacement: plotBand.layerPlacement,
276
279
  x: Math.max(0, startPos),
277
- y: axisTop,
280
+ y: bandY,
278
281
  width: plotBandWidth,
279
- height: axisHeight,
282
+ height: perpExtent,
280
283
  color: plotBand.color,
281
284
  opacity: plotBand.opacity,
282
285
  label: plotBand.label.text
283
286
  ? {
284
287
  text: plotBand.label.text,
285
288
  style: plotBand.label.style,
286
- x: plotBand.label.padding + ((_g = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _g !== void 0 ? _g : 0),
287
- y: plotBand.label.padding + ((_h = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _h !== void 0 ? _h : 0),
289
+ x: plotBand.label.padding + ((_h = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _h !== void 0 ? _h : 0),
290
+ y: plotBand.label.padding + ((_j = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _j !== void 0 ? _j : 0),
288
291
  rotate: -90,
289
292
  qa: plotBand.label.qa,
290
293
  }
@@ -1,6 +1,6 @@
1
1
  import { getUniqId } from '@gravity-ui/uikit';
2
2
  import { select } from 'd3-selection';
3
- import { calculateCos, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../core/utils';
3
+ import { calculateCos, calculateNumericProperty, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../core/utils';
4
4
  import { prepareHtmlYAxisTitle, prepareSvgYAxisTitle } from './prepare-axis-title';
5
5
  import { getTickValues } from './utils';
6
6
  async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHeight, topOffset, }) {
@@ -113,7 +113,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHei
113
113
  return svgLabel;
114
114
  }
115
115
  export async function prepareYAxisData({ axis, split, scale, top: topOffset, width, height, series, }) {
116
- var _a, _b, _c, _d, _e;
116
+ var _a, _b, _c, _d, _e, _f;
117
117
  const axisPlotTopPosition = ((_a = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
118
118
  const axisHeight = ((_b = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _b === void 0 ? void 0 : _b.height) || height;
119
119
  const domainX = axis.position === 'left' ? 0 : width;
@@ -231,11 +231,18 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
231
231
  if (plotBandHeight < 0) {
232
232
  continue;
233
233
  }
234
+ const perpExtent = (_f = calculateNumericProperty({ value: plotBand.size, base: width })) !== null && _f !== void 0 ? _f : width;
235
+ // 'start' = at the main Y axis line. For a left axis that's x=0;
236
+ // for a right axis that's x = width - perpExtent. 'end' is mirrored.
237
+ const isLeftAxis = axis.position === 'left';
238
+ const atMainAxis = isLeftAxis ? 0 : width - perpExtent;
239
+ const atOpposite = isLeftAxis ? width - perpExtent : 0;
240
+ const bandX = plotBand.align === 'end' ? atOpposite : atMainAxis;
234
241
  const plotBandItem = {
235
242
  layerPlacement: plotBand.layerPlacement,
236
- x: 0,
243
+ x: bandX,
237
244
  y: axisPlotTopPosition + top,
238
- width,
245
+ width: perpExtent,
239
246
  height: plotBandHeight,
240
247
  color: plotBand.color,
241
248
  opacity: plotBand.opacity,
@@ -1,4 +1,4 @@
1
- import type { AxisCrosshair, AxisPlotBand, AxisPlotShape, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisRangeSlider, ChartAxisTitleAlignment, ChartAxisTitleRotation, ChartAxisType, DeepRequired, MeaningfulAny, PlotLayerPlacement } from '../../types';
1
+ import type { AxisCrosshair, AxisPlotBand, AxisPlotShape, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisRangeSlider, ChartAxisTitleAlignment, ChartAxisTitleRotation, ChartAxisType, DeepRequired, MeaningfulAny, PlotBandAlign, PlotLayerPlacement } from '../../types';
2
2
  import type { DashStyle } from '../constants';
3
3
  type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation' | 'html'>> & {
4
4
  style: BaseTextStyle;
@@ -8,7 +8,8 @@ type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style'
8
8
  lineHeight: number;
9
9
  maxWidth: number;
10
10
  };
11
- export type PreparedAxisPlotBand = Required<AxisPlotBand> & {
11
+ export type PreparedAxisPlotBand = Required<Omit<AxisPlotBand, 'size' | 'align'>> & {
12
+ align: PlotBandAlign;
12
13
  custom?: MeaningfulAny;
13
14
  label: {
14
15
  text: string;
@@ -16,6 +17,7 @@ export type PreparedAxisPlotBand = Required<AxisPlotBand> & {
16
17
  padding: number;
17
18
  qa?: string;
18
19
  };
20
+ size?: number | string;
19
21
  };
20
22
  type PreparedAxisCrosshair = Required<AxisCrosshair>;
21
23
  export type PreparedAxisPlotLine = {
@@ -166,6 +166,8 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
166
166
  opacity: get(d, 'opacity', 1),
167
167
  from: get(d, 'from', 0),
168
168
  to: get(d, 'to', 0),
169
+ align: get(d, 'align', 'start'),
170
+ size: d.size,
169
171
  layerPlacement: get(d, 'layerPlacement', 'before'),
170
172
  custom: d.custom,
171
173
  label: prepareAxisPlotLabel(d),
@@ -181,6 +181,8 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
181
181
  opacity: get(d, 'opacity', 1),
182
182
  from: get(d, 'from', 0),
183
183
  to: get(d, 'to', 0),
184
+ align: get(d, 'align', 'start'),
185
+ size: d.size,
184
186
  layerPlacement: get(d, 'layerPlacement', 'before'),
185
187
  custom: d.custom,
186
188
  label: prepareAxisPlotLabel(d),
@@ -1,8 +1,8 @@
1
1
  import get from 'lodash/get';
2
2
  import merge from 'lodash/merge';
3
- import { seriesRangeSliderOptionsDefaults } from '../constants';
3
+ import { DEFAULT_DATALABELS_STYLE, seriesRangeSliderOptionsDefaults } from '../constants';
4
4
  import { getSymbolType, getUniqId } from '../utils';
5
- import { DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
6
6
  import { prepareLegendSymbol } from './utils';
7
7
  function prepareMarker(series, seriesOptions, index) {
8
8
  const seriesHoverState = get(seriesOptions, 'scatter.states.hover');
@@ -39,7 +39,7 @@ function prepareSeriesData(series) {
39
39
  export function prepareScatterSeries(args) {
40
40
  const { colorScale, series, seriesOptions, legend } = args;
41
41
  return series.map((s, index) => {
42
- var _a, _b, _c, _d, _e, _f;
42
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
43
43
  const id = getUniqId();
44
44
  const name = 'name' in s && s.name ? s.name : '';
45
45
  const symbolType = s.symbolType || getSymbolType(index);
@@ -56,6 +56,14 @@ export function prepareScatterSeries(args) {
56
56
  itemText: (_f = (_e = s.legend) === null || _e === void 0 ? void 0 : _e.itemText) !== null && _f !== void 0 ? _f : name,
57
57
  },
58
58
  data: prepareSeriesData(s),
59
+ dataLabels: {
60
+ enabled: ((_g = s.dataLabels) === null || _g === void 0 ? void 0 : _g.enabled) || false,
61
+ style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_h = s.dataLabels) === null || _h === void 0 ? void 0 : _h.style),
62
+ padding: get(s, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
63
+ allowOverlap: get(s, 'dataLabels.allowOverlap', false),
64
+ html: get(s, 'dataLabels.html', false),
65
+ format: (_j = s.dataLabels) === null || _j === void 0 ? void 0 : _j.format,
66
+ },
59
67
  marker: prepareMarker(s, seriesOptions, index),
60
68
  cursor: get(s, 'cursor', null),
61
69
  yAxis: get(s, 'yAxis', 0),
@@ -101,6 +101,14 @@ type BasePreparedAxisRelatedSeries = {
101
101
  export type PreparedScatterSeries = {
102
102
  type: ScatterSeries['type'];
103
103
  data: ScatterSeriesData[];
104
+ dataLabels: {
105
+ enabled: boolean;
106
+ style: BaseTextStyle;
107
+ padding: number;
108
+ allowOverlap: boolean;
109
+ html: boolean;
110
+ format?: ValueFormat;
111
+ };
104
112
  marker: {
105
113
  states: {
106
114
  normal: {
@@ -3,8 +3,7 @@ import isNil from 'lodash/isNil';
3
3
  import round from 'lodash/round';
4
4
  import { prepareAnnotation } from '../../series/prepare-annotation';
5
5
  import { getXValue, getYValue, markHiddenPointsOutOfYRange } from '../../shapes/utils';
6
- import { getDataCategoryValue, getLabelsSize, getTextSizeFn } from '../../utils';
7
- import { getFormattedValue } from '../../utils/format';
6
+ import { getDataCategoryValue, preparePointDataLabels } from '../../utils';
8
7
  function getXValues(series, xAxis, xScale) {
9
8
  const categories = xAxis.categories || [];
10
9
  const xValues = series.reduce((acc, s) => {
@@ -30,52 +29,6 @@ function getXValues(series, xAxis, xScale) {
30
29
  }
31
30
  return sort(Array.from(xValues), (d) => d[1]);
32
31
  }
33
- async function prepareDataLabels({ series, points, xMax, yAxisTop, isOutsideBounds, }) {
34
- var _a;
35
- const svgLabels = [];
36
- const htmlLabels = [];
37
- const getTextSize = getTextSizeFn({ style: series.dataLabels.style });
38
- for (let pointsIndex = 0; pointsIndex < points.length; pointsIndex++) {
39
- const point = points[pointsIndex];
40
- if (point.y === null || isOutsideBounds(point.x, point.y)) {
41
- continue;
42
- }
43
- const text = getFormattedValue(Object.assign({ value: (_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y }, series.dataLabels));
44
- if (series.dataLabels.html) {
45
- const size = await getLabelsSize({
46
- labels: [text],
47
- style: series.dataLabels.style,
48
- html: series.dataLabels.html,
49
- });
50
- const labelSize = { width: size.maxWidth, height: size.maxHeight };
51
- const x = Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2));
52
- const y = Math.max(yAxisTop, point.y - series.dataLabels.padding - labelSize.height);
53
- htmlLabels.push({
54
- x,
55
- y,
56
- content: text,
57
- size: labelSize,
58
- style: series.dataLabels.style,
59
- });
60
- }
61
- else {
62
- const labelSize = await getTextSize(text);
63
- const x = Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2));
64
- const y = Math.max(yAxisTop, point.y - series.dataLabels.padding - labelSize.height + labelSize.hangingOffset);
65
- svgLabels.push({
66
- text,
67
- x,
68
- y,
69
- style: series.dataLabels.style,
70
- size: labelSize,
71
- textAnchor: 'start',
72
- series,
73
- active: true,
74
- });
75
- }
76
- }
77
- return { svgLabels, htmlLabels };
78
- }
79
32
  export const prepareAreaData = async (args) => {
80
33
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
81
34
  const { series, seriesOptions, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider, } = args;
@@ -340,7 +293,7 @@ export const prepareAreaData = async (args) => {
340
293
  const currentYAxis = yAxis[item.series.yAxis];
341
294
  const itemYAxisTop = ((_p = split.plots[currentYAxis.plotIndex]) === null || _p === void 0 ? void 0 : _p.top) || 0;
342
295
  if (item.series.dataLabels.enabled && !isRangeSlider) {
343
- const labelsData = await prepareDataLabels({
296
+ const labelsData = await preparePointDataLabels({
344
297
  series: item.series,
345
298
  points: item.points,
346
299
  xMax,
@@ -3,37 +3,59 @@ import get from 'lodash/get';
3
3
  import { prepareAnnotation } from '../../series/prepare-annotation';
4
4
  import { getSeriesStackId } from '../../series/utils';
5
5
  import { MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH } from '../../shapes/bar-constants';
6
- import { getDataCategoryValue, getLabelsSize } from '../../utils';
6
+ import { getDataCategoryValue, getLabelsSize, getTextSizeFn } from '../../utils';
7
7
  import { getBandSize } from '../../utils/band-size';
8
8
  import { getFormattedValue } from '../../utils/format';
9
9
  const isSeriesDataValid = (d) => d.y !== null;
10
10
  async function getLabelData(d, xMax) {
11
11
  var _a;
12
12
  if (!d.series.dataLabels.enabled) {
13
- return undefined;
13
+ return {};
14
14
  }
15
15
  const text = getFormattedValue(Object.assign({ value: (_a = d.data.label) !== null && _a !== void 0 ? _a : d.data.y }, d.series.dataLabels));
16
16
  const style = d.series.dataLabels.style;
17
- const html = d.series.dataLabels.html;
18
- const { maxHeight: height, maxWidth: width } = await getLabelsSize({
19
- labels: [text],
20
- style,
21
- html,
22
- });
23
- let y = Math.max(height, d.y - d.series.dataLabels.padding);
24
- if (d.series.dataLabels.inside) {
25
- y = d.y + d.height / 2;
17
+ if (d.series.dataLabels.html) {
18
+ const { maxHeight: height, maxWidth: width } = await getLabelsSize({
19
+ labels: [text],
20
+ style,
21
+ html: true,
22
+ });
23
+ let y = Math.max(height, d.y - d.series.dataLabels.padding);
24
+ if (d.series.dataLabels.inside) {
25
+ y = d.y + d.height / 2;
26
+ }
27
+ const centerX = Math.min(xMax - width / 2, Math.max(width / 2, d.x + d.width / 2));
28
+ return {
29
+ htmlLabel: {
30
+ content: text,
31
+ x: centerX - width / 2,
32
+ y: y - height,
33
+ size: { width, height },
34
+ style,
35
+ },
36
+ };
37
+ }
38
+ else {
39
+ const getTextSize = getTextSizeFn({ style });
40
+ const { width, height, hangingOffset } = await getTextSize(text);
41
+ let y = Math.max(hangingOffset, d.y - height + hangingOffset - d.series.dataLabels.padding);
42
+ if (d.series.dataLabels.inside) {
43
+ const centerY = d.y + d.height / 2;
44
+ y = Math.min(d.y + d.height - height + hangingOffset, centerY - height / 2 + hangingOffset);
45
+ }
46
+ const centerX = Math.min(xMax - width / 2, Math.max(width / 2, d.x + d.width / 2));
47
+ return {
48
+ svgLabel: {
49
+ text,
50
+ x: centerX,
51
+ y,
52
+ style,
53
+ size: { width, height, hangingOffset },
54
+ textAnchor: 'middle',
55
+ series: d.series,
56
+ },
57
+ };
26
58
  }
27
- const centerX = Math.min(xMax - width / 2, Math.max(width / 2, d.x + d.width / 2));
28
- return {
29
- text,
30
- x: html ? centerX - width / 2 : centerX,
31
- y: html ? y - height : y,
32
- style,
33
- size: { width, height },
34
- textAnchor: 'middle',
35
- series: d.series,
36
- };
37
59
  }
38
60
  export const prepareBarXData = async (args) => {
39
61
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
@@ -232,20 +254,12 @@ export const prepareBarXData = async (args) => {
232
254
  if (barData.series.dataLabels.enabled &&
233
255
  !isRangeSlider &&
234
256
  (!isBarOutsideBounds || isZeroValue)) {
235
- const label = await getLabelData(barData, xMax);
236
- if (label) {
237
- if (barData.series.dataLabels.html) {
238
- barData.htmlLabels.push({
239
- x: label.x,
240
- y: label.y,
241
- content: label.text,
242
- size: label.size,
243
- style: label.style,
244
- });
245
- }
246
- else {
247
- barData.svgLabels.push(label);
248
- }
257
+ const { svgLabel, htmlLabel } = await getLabelData(barData, xMax);
258
+ if (svgLabel) {
259
+ barData.svgLabels.push(svgLabel);
260
+ }
261
+ if (htmlLabel) {
262
+ barData.htmlLabels.push(htmlLabel);
249
263
  }
250
264
  }
251
265
  }
@@ -1,22 +1,8 @@
1
1
  import { prepareAnnotation } from '../../series/prepare-annotation';
2
- import { filterOverlappingLabels, getLabelsSize, getTextSizeFn } from '../../utils';
3
- import { getFormattedValue } from '../../utils/format';
2
+ import { filterOverlappingLabels, preparePointDataLabels } from '../../utils';
4
3
  import { getXValue, getYValue, markHiddenPointsOutOfYRange } from '../utils';
5
- async function getHtmlLabel(point, series, xMax) {
6
- var _a;
7
- const content = String((_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y);
8
- const size = await getLabelsSize({ labels: [content], html: true });
9
- const width = size.maxWidth;
10
- return {
11
- x: Math.min(xMax - size.maxWidth, Math.max(0, point.x - width / 2)),
12
- y: Math.max(0, point.y - series.dataLabels.padding - size.maxHeight),
13
- content,
14
- size: { width, height: size.maxHeight },
15
- style: series.dataLabels.style,
16
- };
17
- }
18
4
  export const prepareLineData = async (args) => {
19
- var _a, _b, _c, _d, _e, _f, _g;
5
+ var _a, _b, _c, _d, _e, _f;
20
6
  const { series, seriesOptions, xAxis, yAxis, xScale, yScale, split, isOutsideBounds, isRangeSlider, otherLayers, } = args;
21
7
  const [_xMin, xRangeMax] = xScale.range();
22
8
  const xMax = xRangeMax;
@@ -58,46 +44,15 @@ export const prepareLineData = async (args) => {
58
44
  let htmlElements = [];
59
45
  let svgLabels = [];
60
46
  if (s.dataLabels.enabled && !isRangeSlider) {
61
- if (s.dataLabels.html) {
62
- const list = await Promise.all(points.reduce((result, p) => {
63
- if (p.y === null || p.x === null || isOutsideBounds(p.x, p.y)) {
64
- return result;
65
- }
66
- result.push(getHtmlLabel(p, s, xMax));
67
- return result;
68
- }, []));
69
- htmlElements.push(...list);
70
- }
71
- else {
72
- const getTextSize = getTextSizeFn({ style: s.dataLabels.style });
73
- for (let index = 0; index < points.length; index++) {
74
- const point = points[index];
75
- if (point.y !== null &&
76
- point.x !== null &&
77
- !isOutsideBounds(point.x, point.y)) {
78
- const labelValue = (_e = point.data.label) !== null && _e !== void 0 ? _e : point.data.y;
79
- const text = getFormattedValue(Object.assign({ value: labelValue }, s.dataLabels));
80
- const labelSize = await getTextSize(text);
81
- const style = s.dataLabels.style;
82
- const y = Math.max(yAxisTop, point.y -
83
- s.dataLabels.padding -
84
- labelSize.height +
85
- labelSize.hangingOffset);
86
- const x = Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2));
87
- const labelData = {
88
- text,
89
- x,
90
- y,
91
- style,
92
- size: labelSize,
93
- textAnchor: 'start',
94
- series: s,
95
- active: true,
96
- };
97
- svgLabels.push(labelData);
98
- }
99
- }
100
- }
47
+ const labelsData = await preparePointDataLabels({
48
+ series: s,
49
+ points,
50
+ xMax,
51
+ yAxisTop,
52
+ isOutsideBounds,
53
+ });
54
+ svgLabels = labelsData.svgLabels;
55
+ htmlElements = labelsData.htmlLabels;
101
56
  }
102
57
  if (!s.dataLabels.allowOverlap) {
103
58
  svgLabels = filterOverlappingLabels(svgLabels, otherLayers.map((l) => l.svgLabels).flat());
@@ -148,11 +103,11 @@ export const prepareLineData = async (args) => {
148
103
  id: s.id,
149
104
  htmlLabels: htmlElements,
150
105
  color: s.color,
151
- lineWidth: (_f = (isRangeSlider ? s.rangeSlider.lineWidth : undefined)) !== null && _f !== void 0 ? _f : s.lineWidth,
106
+ lineWidth: (_e = (isRangeSlider ? s.rangeSlider.lineWidth : undefined)) !== null && _e !== void 0 ? _e : s.lineWidth,
152
107
  dashStyle: s.dashStyle,
153
108
  linecap: s.linecap,
154
109
  linejoin: s.linejoin,
155
- opacity: (_g = (isRangeSlider ? s.rangeSlider.opacity : undefined)) !== null && _g !== void 0 ? _g : s.opacity,
110
+ opacity: (_f = (isRangeSlider ? s.rangeSlider.opacity : undefined)) !== null && _f !== void 0 ? _f : s.opacity,
156
111
  };
157
112
  acc.push(result);
158
113
  }
@@ -1,12 +1,15 @@
1
1
  import type { PreparedXAxis, PreparedYAxis } from '../../axes/types';
2
+ import type { PreparedSplit } from '../../layout/split-types';
2
3
  import type { ChartScale } from '../../scales/types';
3
4
  import type { PreparedScatterSeries } from '../../series/types';
4
- import type { PreparedScatterData } from './types';
5
+ import type { PreparedScatterShapeData } from './types';
5
6
  export declare function prepareScatterData(args: {
6
7
  series: PreparedScatterSeries[];
7
8
  xAxis: PreparedXAxis;
8
9
  xScale: ChartScale;
9
10
  yAxis: PreparedYAxis[];
10
11
  yScale: (ChartScale | undefined)[];
12
+ split: PreparedSplit;
11
13
  isOutsideBounds: (x: number, y: number) => boolean;
12
- }): PreparedScatterData[];
14
+ isRangeSlider?: boolean;
15
+ }): Promise<PreparedScatterShapeData>;
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { getXValue, getYValue } from '../../shapes/utils';
3
- import { getDataCategoryValue } from '../../utils';
3
+ import { filterOverlappingLabels, getDataCategoryValue, preparePointDataLabels } from '../../utils';
4
4
  function getFilteredLinearScatterData(data) {
5
5
  return data.filter((d) => typeof d.x === 'number' && typeof d.y === 'number');
6
6
  }
@@ -32,9 +32,12 @@ function getFilteredCategoryScatterData(args) {
32
32
  return xInRange && yInRange;
33
33
  });
34
34
  }
35
- export function prepareScatterData(args) {
36
- const { series, xAxis, xScale, yAxis, yScale, isOutsideBounds } = args;
37
- return series.reduce((acc, s) => {
35
+ export async function prepareScatterData(args) {
36
+ var _a;
37
+ const { series, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider } = args;
38
+ const [_xMin, xRangeMax] = xScale.range();
39
+ const xMax = xRangeMax;
40
+ const markers = series.reduce((acc, s) => {
38
41
  const yAxisIndex = get(s, 'yAxis', 0);
39
42
  const seriesYAxis = yAxis[yAxisIndex];
40
43
  const seriesYScale = yScale[yAxisIndex];
@@ -74,4 +77,40 @@ export function prepareScatterData(args) {
74
77
  });
75
78
  return acc;
76
79
  }, []);
80
+ const allSvgLabels = [];
81
+ const allHtmlLabels = [];
82
+ if (!isRangeSlider) {
83
+ for (const s of series) {
84
+ if (!s.dataLabels.enabled) {
85
+ continue;
86
+ }
87
+ const yAxisIndex = get(s, 'yAxis', 0);
88
+ const seriesYAxis = yAxis[yAxisIndex];
89
+ const seriesYScale = yScale[yAxisIndex];
90
+ if (!seriesYScale) {
91
+ continue;
92
+ }
93
+ const yAxisTop = ((_a = split.plots[seriesYAxis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
94
+ const seriesPoints = markers
95
+ .filter((m) => m.point.series.id === s.id && !m.clipped)
96
+ .map((m) => m.point);
97
+ const { svgLabels, htmlLabels } = await preparePointDataLabels({
98
+ series: s,
99
+ points: seriesPoints,
100
+ xMax,
101
+ yAxisTop,
102
+ isOutsideBounds,
103
+ anchorYOffset: s.marker.states.normal.radius,
104
+ });
105
+ if (s.dataLabels.allowOverlap) {
106
+ allSvgLabels.push(...svgLabels);
107
+ allHtmlLabels.push(...htmlLabels);
108
+ }
109
+ else {
110
+ allSvgLabels.push(...filterOverlappingLabels(svgLabels, allSvgLabels));
111
+ allHtmlLabels.push(...filterOverlappingLabels(htmlLabels, allHtmlLabels));
112
+ }
113
+ }
114
+ }
115
+ return { markers, svgLabels: allSvgLabels, htmlLabels: allHtmlLabels };
77
116
  }
@@ -1,6 +1,6 @@
1
1
  import type { Dispatch } from 'd3-dispatch';
2
2
  import type { PreparedSeriesOptions } from '../../series/types';
3
- import type { PreparedScatterData } from './types';
3
+ import type { PreparedScatterShapeData } from './types';
4
4
  export declare function renderScatter(elements: {
5
5
  plot: SVGGElement;
6
- }, preparedData: PreparedScatterData[], seriesOptions: PreparedSeriesOptions, dispatcher?: Dispatch<object>): () => void;
6
+ }, preparedData: PreparedScatterShapeData, seriesOptions: PreparedSeriesOptions, dispatcher?: Dispatch<object>): () => void;
@@ -1,7 +1,10 @@
1
1
  import { select } from 'd3-selection';
2
2
  import get from 'lodash/get';
3
+ import { block } from '../../../utils';
3
4
  import { getMarkerHaloVisibility, renderMarker, selectMarkerHalo, selectMarkerSymbol, setMarker, } from '../../shapes/marker';
4
5
  import { setActiveState, shapeKey } from '../../shapes/utils';
6
+ import { renderDataLabels } from '../data-labels';
7
+ const b = block('scatter');
5
8
  export function renderScatter(elements, preparedData, seriesOptions, dispatcher) {
6
9
  const svgElement = select(elements.plot);
7
10
  const hoverOptions = get(seriesOptions, 'scatter.states.hover');
@@ -9,11 +12,16 @@ export function renderScatter(elements, preparedData, seriesOptions, dispatcher)
9
12
  svgElement.selectAll('*').remove();
10
13
  const selection = svgElement
11
14
  .selectAll('path')
12
- .data(preparedData, shapeKey)
15
+ .data(preparedData.markers, shapeKey)
13
16
  .join('g')
14
17
  .call(renderMarker)
15
18
  .attr('opacity', (d) => d.point.opacity)
16
19
  .attr('cursor', (d) => d.point.series.cursor);
20
+ renderDataLabels({
21
+ container: svgElement,
22
+ data: preparedData.svgLabels,
23
+ className: b('label'),
24
+ });
17
25
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
18
26
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
19
27
  function handleShapeHover(data) {
@@ -1,4 +1,4 @@
1
- import type { HtmlItem, ScatterSeriesData } from '../../../types';
1
+ import type { HtmlItem, LabelData, ScatterSeriesData } from '../../../types';
2
2
  import type { PreparedScatterSeries } from '../../series/types';
3
3
  type PointData = {
4
4
  x: number;
@@ -16,4 +16,9 @@ export type MarkerData = {
16
16
  clipped: boolean;
17
17
  };
18
18
  export type PreparedScatterData = MarkerData;
19
+ export type PreparedScatterShapeData = {
20
+ markers: PreparedScatterData[];
21
+ svgLabels: LabelData[];
22
+ htmlLabels: HtmlItem[];
23
+ };
19
24
  export {};
@@ -317,6 +317,7 @@ export interface AxisPlotShape extends AxisPlot {
317
317
  plotHeight: number;
318
318
  }) => string;
319
319
  }
320
+ export type PlotBandAlign = 'start' | 'end';
320
321
  export interface AxisPlotBand extends AxisPlot {
321
322
  /**
322
323
  * The start position of the plot band in axis units.
@@ -336,6 +337,25 @@ export interface AxisPlotBand extends AxisPlot {
336
337
  * If the value is `Infinity` or `null`, it will be treated as the end of the axis.
337
338
  */
338
339
  to: number | string | null;
340
+ /**
341
+ * Anchor side on the perpendicular axis when `size` is set.
342
+ *
343
+ * - `'start'` — the band sticks to the main axis line (bottom for an X axis,
344
+ * left for a left Y axis, right for a right Y axis).
345
+ * - `'end'` — the band sticks to the opposite side of the plot area.
346
+ *
347
+ * Has no effect without `size`.
348
+ * @default 'start'
349
+ */
350
+ align?: PlotBandAlign;
351
+ /**
352
+ * Perpendicular extent of the band.
353
+ *
354
+ * Accepts a pixel number (`40`), a pixel string (`"40px"`), or a percentage of
355
+ * the perpendicular plot extent (`"25%"`). When omitted, the band spans the
356
+ * full perpendicular extent of the plot area (default behavior).
357
+ */
358
+ size?: number | string;
339
359
  }
340
360
  export interface AxisCrosshair extends Pick<AxisPlotLine, 'color' | 'dashStyle' | 'opacity' | 'layerPlacement' | 'width'> {
341
361
  /**
@@ -23,6 +23,8 @@ export interface ScatterSeriesData<T = MeaningfulAny> extends BaseSeriesData<T>
23
23
  * @deprecated use `x` or `y` instead
24
24
  */
25
25
  category?: string;
26
+ /** Data label value of the point. If not specified, the y value is used. */
27
+ label?: string | number;
26
28
  /** Individual radius for the point. */
27
29
  radius?: number;
28
30
  /** Individual opacity for the point. */