@gravity-ui/charts 1.47.0 → 1.48.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 (53) 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/core/utils/ticks/datetime.js +7 -0
  23. package/dist/cjs/hooks/useShapes/index.js +5 -3
  24. package/dist/cjs/hooks/useShapes/scatter/index.d.ts +2 -2
  25. package/dist/cjs/hooks/useShapes/scatter/index.js +4 -1
  26. package/dist/cjs/hooks/useShapes/styles.css +8 -25
  27. package/dist/esm/components/AxisX/prepare-axis-data.js +9 -6
  28. package/dist/esm/components/AxisY/prepare-axis-data.js +11 -4
  29. package/dist/esm/core/axes/types.d.ts +4 -2
  30. package/dist/esm/core/axes/x-axis.js +2 -0
  31. package/dist/esm/core/axes/y-axis.js +2 -0
  32. package/dist/esm/core/series/prepare-scatter.js +11 -3
  33. package/dist/esm/core/series/types.d.ts +8 -0
  34. package/dist/esm/core/shapes/area/prepare-data.js +2 -49
  35. package/dist/esm/core/shapes/bar-x/prepare-data.js +49 -35
  36. package/dist/esm/core/shapes/line/prepare-data.js +13 -58
  37. package/dist/esm/core/shapes/scatter/prepare-data.d.ts +5 -2
  38. package/dist/esm/core/shapes/scatter/prepare-data.js +43 -4
  39. package/dist/esm/core/shapes/scatter/renderer.d.ts +2 -2
  40. package/dist/esm/core/shapes/scatter/renderer.js +9 -1
  41. package/dist/esm/core/shapes/scatter/types.d.ts +6 -1
  42. package/dist/esm/core/types/chart/axis.d.ts +20 -0
  43. package/dist/esm/core/types/chart/scatter.d.ts +2 -0
  44. package/dist/esm/core/utils/data-labels.d.ts +46 -0
  45. package/dist/esm/core/utils/data-labels.js +64 -0
  46. package/dist/esm/core/utils/index.d.ts +1 -0
  47. package/dist/esm/core/utils/index.js +1 -0
  48. package/dist/esm/core/utils/ticks/datetime.js +7 -0
  49. package/dist/esm/hooks/useShapes/index.js +5 -3
  50. package/dist/esm/hooks/useShapes/scatter/index.d.ts +2 -2
  51. package/dist/esm/hooks/useShapes/scatter/index.js +4 -1
  52. package/dist/esm/hooks/useShapes/styles.css +8 -25
  53. package/package.json +4 -1
@@ -0,0 +1,46 @@
1
+ import type { HtmlItem, LabelData } from '../../types';
2
+ import type { BaseTextStyle, ValueFormat } from '../types/chart/base';
3
+ type PointLabelSeries = {
4
+ id: string;
5
+ dataLabels: {
6
+ style: BaseTextStyle;
7
+ html: boolean;
8
+ padding: number;
9
+ format?: ValueFormat;
10
+ };
11
+ };
12
+ type LabelPoint = {
13
+ x: number | null;
14
+ y: number | null;
15
+ data: {
16
+ label?: string | number | null;
17
+ y?: string | number | null;
18
+ };
19
+ };
20
+ /**
21
+ * Shared "above-point" dataLabels algorithm used by line, area, and scatter series.
22
+ *
23
+ * For each visible point it:
24
+ * 1. Formats the value via getFormattedValue
25
+ * 2. Measures the label size (HTML → getLabelsSize, SVG → getTextSizeFn)
26
+ * 3. Positions the label centered above the point, clamped to chart bounds
27
+ *
28
+ * `anchorYOffset` shifts the vertical anchor from the point center upward by the given
29
+ * number of pixels (e.g. marker radius for scatter), so padding is measured from the
30
+ * marker edge rather than its center. The top-boundary clamp also respects this offset
31
+ * so the label never drops below the anchor.
32
+ *
33
+ * Overlap filtering is intentionally left to the caller.
34
+ */
35
+ export declare function preparePointDataLabels<S extends PointLabelSeries, P extends LabelPoint>({ series, points, xMax, yAxisTop, isOutsideBounds, anchorYOffset, }: {
36
+ series: S;
37
+ points: P[];
38
+ xMax: number;
39
+ yAxisTop: number;
40
+ isOutsideBounds: (x: number, y: number) => boolean;
41
+ anchorYOffset?: number;
42
+ }): Promise<{
43
+ svgLabels: LabelData[];
44
+ htmlLabels: HtmlItem[];
45
+ }>;
46
+ export {};
@@ -0,0 +1,64 @@
1
+ import { getFormattedValue } from './format';
2
+ import { getLabelsSize, getTextSizeFn } from './text';
3
+ /**
4
+ * Shared "above-point" dataLabels algorithm used by line, area, and scatter series.
5
+ *
6
+ * For each visible point it:
7
+ * 1. Formats the value via getFormattedValue
8
+ * 2. Measures the label size (HTML → getLabelsSize, SVG → getTextSizeFn)
9
+ * 3. Positions the label centered above the point, clamped to chart bounds
10
+ *
11
+ * `anchorYOffset` shifts the vertical anchor from the point center upward by the given
12
+ * number of pixels (e.g. marker radius for scatter), so padding is measured from the
13
+ * marker edge rather than its center. The top-boundary clamp also respects this offset
14
+ * so the label never drops below the anchor.
15
+ *
16
+ * Overlap filtering is intentionally left to the caller.
17
+ */
18
+ export async function preparePointDataLabels({ series, points, xMax, yAxisTop, isOutsideBounds, anchorYOffset = 0, }) {
19
+ var _a;
20
+ const svgLabels = [];
21
+ const htmlLabels = [];
22
+ const getTextSize = getTextSizeFn({ style: series.dataLabels.style });
23
+ for (let i = 0; i < points.length; i++) {
24
+ const point = points[i];
25
+ if (point.y === null || point.x === null || isOutsideBounds(point.x, point.y)) {
26
+ continue;
27
+ }
28
+ const text = getFormattedValue(Object.assign({ value: (_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y }, series.dataLabels));
29
+ const anchorY = point.y - anchorYOffset;
30
+ if (series.dataLabels.html) {
31
+ const size = await getLabelsSize({
32
+ labels: [text],
33
+ style: series.dataLabels.style,
34
+ html: true,
35
+ });
36
+ const width = size.maxWidth;
37
+ const height = size.maxHeight;
38
+ htmlLabels.push({
39
+ x: Math.min(xMax - width, Math.max(0, point.x - width / 2)),
40
+ y: Math.max(yAxisTop, anchorY - series.dataLabels.padding - height),
41
+ content: text,
42
+ size: { width, height },
43
+ style: series.dataLabels.style,
44
+ });
45
+ }
46
+ else {
47
+ const labelSize = await getTextSize(text);
48
+ svgLabels.push({
49
+ text,
50
+ x: Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2)),
51
+ y: Math.max(yAxisTop, anchorY -
52
+ series.dataLabels.padding -
53
+ labelSize.height +
54
+ labelSize.hangingOffset),
55
+ style: series.dataLabels.style,
56
+ size: labelSize,
57
+ textAnchor: 'start',
58
+ series,
59
+ active: true,
60
+ });
61
+ }
62
+ }
63
+ return { svgLabels, htmlLabels };
64
+ }
@@ -15,3 +15,4 @@ export * from '../layout/chart-dimensions';
15
15
  export * from './common';
16
16
  export * from './misc';
17
17
  export * from './dispatcher';
18
+ export * from './data-labels';
@@ -15,3 +15,4 @@ export * from '../layout/chart-dimensions';
15
15
  export * from './common';
16
16
  export * from './misc';
17
17
  export * from './dispatcher';
18
+ export * from './data-labels';
@@ -21,6 +21,10 @@ const tickIntervals = [
21
21
  [utcMonth, 3, 3 * MONTH],
22
22
  [utcYear, 1, YEAR],
23
23
  ];
24
+ // utcDay.every(2) resets its day counter at the start of each month (field = getUTCDate() - 1),
25
+ // so in a 31-day month the last tick lands on day 31 and the next tick is day 1 of the following
26
+ // month — only 1 day apart. Filtering by absolute Unix day number avoids the monthly reset.
27
+ const utcEvery2Days = utcDay.filter((d) => Math.floor(d.getTime() / DAY) % 2 === 0);
24
28
  function getDateTimeTickInterval(start, stop, count) {
25
29
  const target = Math.abs(stop - start) / count;
26
30
  const i = bisector(([, , step]) => step).right(tickIntervals, target);
@@ -31,6 +35,9 @@ function getDateTimeTickInterval(start, stop, count) {
31
35
  return utcMillisecond.every(Math.max(tickStep(start, stop, count), 1));
32
36
  }
33
37
  const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
38
+ if (t === utcDay && step === 2) {
39
+ return utcEvery2Days;
40
+ }
34
41
  return t.every(step);
35
42
  }
36
43
  /**
@@ -146,18 +146,20 @@ export async function getShapes(args) {
146
146
  }
147
147
  case SERIES_TYPE.Scatter: {
148
148
  if (xAxis && xScale && (yScale === null || yScale === void 0 ? void 0 : yScale.length)) {
149
- const preparedData = prepareScatterData({
149
+ const scatterShapeData = await prepareScatterData({
150
150
  series: chartSeries,
151
151
  xAxis,
152
152
  xScale,
153
153
  yAxis,
154
154
  yScale,
155
+ split,
155
156
  isOutsideBounds,
157
+ isRangeSlider,
156
158
  });
157
159
  shapes[index] = (React.createElement(ScatterSeriesShape, { key: SERIES_TYPE.Scatter, clipPathId: shouldUseClipPathId(SERIES_TYPE.Scatter, clipPathBySeriesType)
158
160
  ? clipPathId
159
- : undefined, dispatcher: dispatcher, preparedData: preparedData, seriesOptions: seriesOptions, htmlLayout: htmlLayout }));
160
- shapesData.splice(index, 0, ...preparedData);
161
+ : undefined, dispatcher: dispatcher, preparedData: scatterShapeData, seriesOptions: seriesOptions, htmlLayout: htmlLayout }));
162
+ shapesData.splice(index, 0, ...scatterShapeData.markers);
161
163
  }
162
164
  break;
163
165
  }
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { Dispatch } from 'd3-dispatch';
3
3
  import type { PreparedSeriesOptions } from '../../../core/series/types';
4
- import type { PreparedScatterData } from '../../../core/shapes/scatter/types';
4
+ import type { PreparedScatterShapeData } from '../../../core/shapes/scatter/types';
5
5
  export { prepareScatterData } from '../../../core/shapes/scatter/prepare-data';
6
6
  type ScatterSeriesShapeProps = {
7
7
  htmlLayout: HTMLElement | null;
8
- preparedData: PreparedScatterData[];
8
+ preparedData: PreparedScatterShapeData;
9
9
  seriesOptions: PreparedSeriesOptions;
10
10
  clipPathId?: string;
11
11
  dispatcher?: Dispatch<object>;
@@ -13,7 +13,10 @@ export function ScatterSeriesShape(props) {
13
13
  }
14
14
  return renderScatter({ plot: ref.current }, preparedData, seriesOptions, dispatcher);
15
15
  }, [dispatcher, preparedData, seriesOptions]);
16
+ const htmlLayerData = React.useMemo(() => {
17
+ return { htmlElements: preparedData.htmlLabels };
18
+ }, [preparedData]);
16
19
  return (React.createElement(React.Fragment, null,
17
20
  React.createElement("g", { ref: ref, className: b(), clipPath: clipPathId ? `url(#${clipPathId})` : undefined }),
18
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
21
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
19
22
  }
@@ -1,8 +1,15 @@
1
1
  .gcharts-line__label,
2
2
  .gcharts-area__label,
3
3
  .gcharts-radar__label,
4
+ .gcharts-bar-x__label,
5
+ .gcharts-bar-y__label,
4
6
  .gcharts-heatmap__label,
5
- .gcharts-funnel__label {
7
+ .gcharts-funnel__label,
8
+ .gcharts-treemap__label,
9
+ .gcharts-pie__label,
10
+ .gcharts-scatter__label {
11
+ user-select: none;
12
+ pointer-events: none;
6
13
  dominant-baseline: hanging;
7
14
  }
8
15
 
@@ -13,30 +20,6 @@
13
20
  .gcharts-pie__segment {
14
21
  stroke: var(--g-color-base-background);
15
22
  }
16
- .gcharts-pie__label {
17
- font-size: 11px;
18
- font-weight: bold;
19
- fill: var(--g-color-text-complementary);
20
- dominant-baseline: hanging;
21
- }
22
-
23
- .gcharts-bar-x__label {
24
- user-select: none;
25
- fill: var(--g-color-text-complementary);
26
- }
27
-
28
- .gcharts-bar-y__label {
29
- user-select: none;
30
- fill: var(--g-color-text-complementary);
31
- dominant-baseline: hanging;
32
- }
33
-
34
- .gcharts-treemap__label {
35
- user-select: none;
36
- pointer-events: none;
37
- fill: var(--g-color-text-complementary);
38
- dominant-baseline: hanging;
39
- }
40
23
 
41
24
  .gcharts-waterfall__connector {
42
25
  stroke: var(--g-color-line-generic-active);
@@ -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
  }