@gravity-ui/charts 1.5.1 → 1.6.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 (45) hide show
  1. package/dist/cjs/constants/defaults/data-labels.d.ts +2 -0
  2. package/dist/cjs/constants/defaults/data-labels.js +5 -0
  3. package/dist/cjs/constants/defaults/index.d.ts +1 -0
  4. package/dist/cjs/constants/defaults/index.js +1 -0
  5. package/dist/cjs/hooks/useSeries/constants.d.ts +1 -2
  6. package/dist/cjs/hooks/useSeries/constants.js +0 -5
  7. package/dist/cjs/hooks/useSeries/prepare-area.js +2 -1
  8. package/dist/cjs/hooks/useSeries/prepare-bar-x.js +2 -1
  9. package/dist/cjs/hooks/useSeries/prepare-bar-y.js +1 -1
  10. package/dist/cjs/hooks/useSeries/prepare-line.js +2 -2
  11. package/dist/cjs/hooks/useSeries/prepare-pie.js +3 -2
  12. package/dist/cjs/hooks/useSeries/prepare-radar.js +2 -2
  13. package/dist/cjs/hooks/useSeries/prepare-sankey.js +1 -1
  14. package/dist/cjs/hooks/useSeries/prepare-treemap.js +2 -2
  15. package/dist/cjs/hooks/useSeries/prepare-waterfall.js +2 -2
  16. package/dist/cjs/hooks/useSeries/types.d.ts +1 -0
  17. package/dist/cjs/hooks/useShapes/pie/prepare-data.js +60 -24
  18. package/dist/cjs/hooks/useShapes/pie/utils.js +2 -1
  19. package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +19 -11
  20. package/dist/cjs/types/chart/pie.d.ts +8 -0
  21. package/dist/cjs/utils/chart/text.d.ts +1 -1
  22. package/dist/cjs/utils/chart/text.js +13 -7
  23. package/dist/esm/constants/defaults/data-labels.d.ts +2 -0
  24. package/dist/esm/constants/defaults/data-labels.js +5 -0
  25. package/dist/esm/constants/defaults/index.d.ts +1 -0
  26. package/dist/esm/constants/defaults/index.js +1 -0
  27. package/dist/esm/hooks/useSeries/constants.d.ts +1 -2
  28. package/dist/esm/hooks/useSeries/constants.js +0 -5
  29. package/dist/esm/hooks/useSeries/prepare-area.js +2 -1
  30. package/dist/esm/hooks/useSeries/prepare-bar-x.js +2 -1
  31. package/dist/esm/hooks/useSeries/prepare-bar-y.js +1 -1
  32. package/dist/esm/hooks/useSeries/prepare-line.js +2 -2
  33. package/dist/esm/hooks/useSeries/prepare-pie.js +3 -2
  34. package/dist/esm/hooks/useSeries/prepare-radar.js +2 -2
  35. package/dist/esm/hooks/useSeries/prepare-sankey.js +1 -1
  36. package/dist/esm/hooks/useSeries/prepare-treemap.js +2 -2
  37. package/dist/esm/hooks/useSeries/prepare-waterfall.js +2 -2
  38. package/dist/esm/hooks/useSeries/types.d.ts +1 -0
  39. package/dist/esm/hooks/useShapes/pie/prepare-data.js +60 -24
  40. package/dist/esm/hooks/useShapes/pie/utils.js +2 -1
  41. package/dist/esm/hooks/useShapes/treemap/prepare-data.js +19 -11
  42. package/dist/esm/types/chart/pie.d.ts +8 -0
  43. package/dist/esm/utils/chart/text.d.ts +1 -1
  44. package/dist/esm/utils/chart/text.js +13 -7
  45. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import type { BaseTextStyle } from '../../types';
2
+ export declare const DEFAULT_DATALABELS_STYLE: BaseTextStyle;
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_DATALABELS_STYLE = {
2
+ fontSize: '11px',
3
+ fontWeight: 'bold',
4
+ fontColor: 'var(--gcharts-data-labels)',
5
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './axis';
2
+ export * from './data-labels';
2
3
  export * from './legend';
3
4
  export * from './series-options';
@@ -1,3 +1,4 @@
1
1
  export * from './axis';
2
+ export * from './data-labels';
2
3
  export * from './legend';
3
4
  export * from './series-options';
@@ -1,8 +1,7 @@
1
- import type { BaseTextStyle, Halo } from '../../types';
1
+ import type { Halo } from '../../types';
2
2
  import type { PointMarkerOptions } from '../../types/chart/marker';
3
3
  export declare const DEFAULT_LEGEND_SYMBOL_SIZE = 8;
4
4
  export declare const DEFAULT_LEGEND_SYMBOL_PADDING = 5;
5
5
  export declare const DEFAULT_DATALABELS_PADDING = 5;
6
- export declare const DEFAULT_DATALABELS_STYLE: BaseTextStyle;
7
6
  export declare const DEFAULT_HALO_OPTIONS: Required<Halo>;
8
7
  export declare const DEFAULT_POINT_MARKER_OPTIONS: Omit<Required<PointMarkerOptions>, 'enabled'>;
@@ -1,11 +1,6 @@
1
1
  export const DEFAULT_LEGEND_SYMBOL_SIZE = 8;
2
2
  export const DEFAULT_LEGEND_SYMBOL_PADDING = 5;
3
3
  export const DEFAULT_DATALABELS_PADDING = 5;
4
- export const DEFAULT_DATALABELS_STYLE = {
5
- fontSize: '11px',
6
- fontWeight: 'bold',
7
- fontColor: 'var(--gcharts-data-labels)',
8
- };
9
4
  export const DEFAULT_HALO_OPTIONS = {
10
5
  enabled: true,
11
6
  opacity: 0.25,
@@ -1,7 +1,8 @@
1
1
  import get from 'lodash/get';
2
2
  import merge from 'lodash/merge';
3
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
3
4
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
6
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
6
7
  export const DEFAULT_LINE_WIDTH = 1;
7
8
  export const DEFAULT_MARKER = Object.assign(Object.assign({}, DEFAULT_POINT_MARKER_OPTIONS), { enabled: false });
@@ -1,6 +1,7 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getUniqId } from '../../utils';
3
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
4
5
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
5
6
  export function prepareBarXSeries(args) {
6
7
  const { colorScale, series: seriesList, seriesOptions, legend } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getLabelsSize, getUniqId } from '../../utils';
3
4
  import { getFormattedValue } from '../../utils/chart/format';
4
- import { DEFAULT_DATALABELS_STYLE } from './constants';
5
5
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
6
6
  function prepareDataLabels(series) {
7
7
  var _a, _b;
@@ -1,8 +1,8 @@
1
1
  import get from 'lodash/get';
2
2
  import merge from 'lodash/merge';
3
- import { DashStyle, LineCap } from '../../constants';
3
+ import { DEFAULT_DATALABELS_STYLE, DashStyle, LineCap } from '../../constants';
4
4
  import { getUniqId } from '../../utils';
5
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_LEGEND_SYMBOL_PADDING, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_LEGEND_SYMBOL_PADDING, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
6
6
  export const DEFAULT_LEGEND_SYMBOL_SIZE = 16;
7
7
  export const DEFAULT_LINE_WIDTH = 1;
8
8
  export const DEFAULT_DASH_STYLE = DashStyle.Solid;
@@ -1,8 +1,8 @@
1
1
  import { scaleOrdinal } from 'd3';
2
2
  import get from 'lodash/get';
3
- import { DEFAULT_PALETTE } from '../../constants';
3
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
4
4
  import { getUniqId } from '../../utils';
5
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
6
6
  import { prepareLegendSymbol } from './utils';
7
7
  export function preparePieSeries(args) {
8
8
  const { series, seriesOptions, legend } = args;
@@ -43,6 +43,7 @@ export function preparePieSeries(args) {
43
43
  borderWidth: (_d = series.borderWidth) !== null && _d !== void 0 ? _d : 1,
44
44
  radius: (_f = (_e = dataItem.radius) !== null && _e !== void 0 ? _e : series.radius) !== null && _f !== void 0 ? _f : '100%',
45
45
  innerRadius: series.innerRadius || 0,
46
+ minRadius: series.minRadius,
46
47
  stackId,
47
48
  states: {
48
49
  hover: {
@@ -1,9 +1,9 @@
1
1
  import { scaleOrdinal } from 'd3';
2
2
  import get from 'lodash/get';
3
3
  import merge from 'lodash/merge';
4
- import { DEFAULT_PALETTE } from '../../constants';
4
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
5
5
  import { getUniqId } from '../../utils';
6
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
6
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
7
7
  import { prepareLegendSymbol } from './utils';
8
8
  export const DEFAULT_MARKER = Object.assign(Object.assign({}, DEFAULT_POINT_MARKER_OPTIONS), { enabled: true, radius: 2 });
9
9
  function prepareMarker(series, seriesOptions) {
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getUniqId } from '../../utils';
3
- import { DEFAULT_DATALABELS_STYLE } from './constants';
4
4
  import { prepareLegendSymbol } from './utils';
5
5
  export function prepareSankeySeries(args) {
6
6
  const { colorScale, legend, series } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
- import { LayoutAlgorithm } from '../../constants';
2
+ import { DEFAULT_DATALABELS_STYLE, LayoutAlgorithm } from '../../constants';
3
3
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
5
5
  import { prepareLegendSymbol } from './utils';
6
6
  export function prepareTreemap(args) {
7
7
  const { colorScale, legend, series } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
- import { DEFAULT_PALETTE } from '../../constants';
2
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
3
3
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
5
5
  import { prepareLegendSymbol } from './utils';
6
6
  export function prepareWaterfallSeries(args) {
7
7
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
@@ -137,6 +137,7 @@ export type PreparedPieSeries = {
137
137
  center?: [string | number | null, string | number | null];
138
138
  radius?: string | number;
139
139
  innerRadius?: string | number;
140
+ minRadius?: string | number;
140
141
  stackId: string;
141
142
  label?: PieSeriesData['label'];
142
143
  dataLabels: {
@@ -1,4 +1,6 @@
1
1
  import { arc, group, line as lineGenerator } from 'd3';
2
+ import merge from 'lodash/merge';
3
+ import { DEFAULT_DATALABELS_STYLE } from '../../../constants';
2
4
  import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
3
5
  import { getFormattedValue } from '../../../utils/chart/format';
4
6
  import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
@@ -16,17 +18,22 @@ const getCenter = (boundsWidth, boundsHeight, center) => {
16
18
  return [resultX, resultY];
17
19
  };
18
20
  export function preparePieData(args) {
21
+ var _a, _b;
19
22
  const { series: preparedSeries, boundsWidth, boundsHeight } = args;
20
23
  const haloSize = preparedSeries[0].states.hover.halo.enabled
21
24
  ? preparedSeries[0].states.hover.halo.size
22
25
  : 0;
23
26
  const maxRadius = Math.min(boundsWidth, boundsHeight) / 2 - haloSize;
24
- const minRadius = maxRadius * 0.3;
27
+ const propsMinRadius = calculateNumericProperty({
28
+ value: preparedSeries[0].minRadius,
29
+ base: maxRadius,
30
+ });
31
+ const minRadius = typeof propsMinRadius === 'number' ? propsMinRadius : maxRadius * 0.3;
25
32
  const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
33
+ const dataLabelsStyle = merge({}, DEFAULT_DATALABELS_STYLE, (_b = (_a = preparedSeries[0]) === null || _a === void 0 ? void 0 : _a.dataLabels) === null || _b === void 0 ? void 0 : _b.style);
26
34
  const prepareItem = (stackId, items) => {
27
- var _a;
28
35
  const series = items[0];
29
- const { center, borderWidth, borderColor, borderRadius, innerRadius: seriesInnerRadius, dataLabels, } = series;
36
+ const { center, borderWidth, borderColor, borderRadius, dataLabels } = series;
30
37
  const data = {
31
38
  id: stackId,
32
39
  center: getCenter(boundsWidth, boundsHeight, center),
@@ -48,9 +55,8 @@ export function preparePieData(args) {
48
55
  };
49
56
  const { maxHeight: labelHeight } = getLabelsSize({
50
57
  labels: ['Some Label'],
51
- style: dataLabels.style,
58
+ style: dataLabelsStyle,
52
59
  });
53
- let segmentMaxRadius = 0;
54
60
  const segments = items.map((item) => {
55
61
  var _a;
56
62
  let maxSegmentRadius = maxRadius;
@@ -58,7 +64,6 @@ export function preparePieData(args) {
58
64
  maxSegmentRadius -= dataLabels.distance + dataLabels.connectorPadding + labelHeight;
59
65
  }
60
66
  const segmentRadius = (_a = calculateNumericProperty({ value: item.radius, base: maxSegmentRadius })) !== null && _a !== void 0 ? _a : maxSegmentRadius;
61
- segmentMaxRadius = Math.max(segmentMaxRadius, segmentRadius);
62
67
  return {
63
68
  value: item.value,
64
69
  color: item.color,
@@ -71,8 +76,6 @@ export function preparePieData(args) {
71
76
  };
72
77
  });
73
78
  data.segments = pieGenerator(segments);
74
- data.innerRadius =
75
- (_a = calculateNumericProperty({ value: seriesInnerRadius, base: segmentMaxRadius })) !== null && _a !== void 0 ? _a : 0;
76
79
  return data;
77
80
  };
78
81
  const prepareLabels = (prepareLabelsArgs) => {
@@ -84,13 +87,18 @@ export function preparePieData(args) {
84
87
  if (!dataLabels.enabled) {
85
88
  return { labels, htmlLabels, connectors };
86
89
  }
90
+ const shouldUseHtml = dataLabels.html;
87
91
  let line = lineGenerator();
88
92
  const curveFactory = getCurveFactory(data);
89
93
  if (curveFactory) {
90
94
  line = line.curve(curveFactory);
91
95
  }
92
96
  const { style, connectorPadding, distance } = dataLabels;
93
- const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
97
+ const { maxHeight: labelHeight } = getLabelsSize({
98
+ labels: ['Some Label'],
99
+ style: dataLabelsStyle,
100
+ html: shouldUseHtml,
101
+ });
94
102
  const connectorStartPointGenerator = arc()
95
103
  .innerRadius((d) => d.data.radius)
96
104
  .outerRadius((d) => d.data.radius);
@@ -104,22 +112,35 @@ export function preparePieData(args) {
104
112
  .innerRadius((d) => d.data.radius + distance + connectorPadding)
105
113
  .outerRadius((d) => d.data.radius + distance + connectorPadding);
106
114
  let shouldStopLabelPlacement = false;
115
+ // eslint-disable-next-line complexity
107
116
  series.forEach((d, index) => {
108
117
  const prevLabel = labels[labels.length - 1];
109
118
  const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
110
- const shouldUseHtml = dataLabels.html;
111
- const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
119
+ const labelSize = getLabelsSize({
120
+ labels: [text],
121
+ style: dataLabelsStyle,
122
+ html: shouldUseHtml,
123
+ });
112
124
  const labelWidth = labelSize.maxWidth;
113
125
  const relatedSegment = data.segments[index];
126
+ /**
127
+ * Compute the label coordinates on the label arc for a given angle.
128
+ *
129
+ * For HTML labels, the function returns the top-left corner to account for
130
+ * element box positioning. It shifts left by the label width when the point is
131
+ * on the left side (x < 0) and shifts up by the label height when above the
132
+ * horizontal center (y < 0). For SVG text, only the vertical shift is applied
133
+ * to compensate for text baseline.
134
+ *
135
+ * @param {number} angle - Angle in radians at which the label should be placed.
136
+ * @returns {[number, number]} A tuple [x, y] relative to the pie center.
137
+ */
114
138
  const getLabelPosition = (angle) => {
115
139
  let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
116
140
  if (shouldUseHtml) {
117
141
  x = x < 0 ? x - labelWidth : x;
118
- y = y - labelSize.maxHeight;
119
- }
120
- else {
121
- y = y < 0 ? y - labelHeight : y;
122
142
  }
143
+ y = y < 0 ? y - labelHeight : y;
123
144
  return [x, y];
124
145
  };
125
146
  const getConnectorPoints = (angle) => {
@@ -173,11 +194,17 @@ export function preparePieData(args) {
173
194
  shouldAdjustAngle = false;
174
195
  }
175
196
  else {
176
- label.angle = newAngle;
177
197
  const [newX, newY] = getLabelPosition(newAngle);
198
+ label.angle = newAngle;
199
+ label.textAnchor = newAngle < Math.PI ? 'start' : 'end';
178
200
  label.x = newX;
179
201
  label.y = newY;
180
- const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
202
+ // See `getLabelPosition`: for HTML labels we return top-left,
203
+ // so shift x by labelWidth when textAnchor is 'end'.
204
+ const pointC = shouldUseHtml && label.textAnchor === 'end'
205
+ ? [newX + labelWidth, newY]
206
+ : [newX, newY];
207
+ const inscribedAngle = getInscribedAngle(pointA, pointB, pointC);
181
208
  if (inscribedAngle > 90) {
182
209
  shouldAdjustAngle = false;
183
210
  shouldStopLabelPlacement = true;
@@ -192,6 +219,7 @@ export function preparePieData(args) {
192
219
  }
193
220
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
194
221
  if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
222
+ labels.push(label);
195
223
  if (shouldUseHtml) {
196
224
  htmlLabels.push({
197
225
  x: data.center[0] + label.x,
@@ -201,9 +229,6 @@ export function preparePieData(args) {
201
229
  style: label.style,
202
230
  });
203
231
  }
204
- else {
205
- labels.push(label);
206
- }
207
232
  const connector = {
208
233
  path: line(getConnectorPoints(label.angle)),
209
234
  color: relatedSegment.data.color,
@@ -212,12 +237,13 @@ export function preparePieData(args) {
212
237
  }
213
238
  });
214
239
  return {
215
- labels,
240
+ labels: shouldUseHtml ? [] : labels,
216
241
  htmlLabels,
217
242
  connectors,
218
243
  };
219
244
  };
220
245
  return Array.from(groupedPieSeries).map(([stackId, items]) => {
246
+ var _a;
221
247
  const data = prepareItem(stackId, items);
222
248
  const preparedLabels = prepareLabels({
223
249
  data,
@@ -251,7 +277,7 @@ export function preparePieData(args) {
251
277
  topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
252
278
  }
253
279
  if (preparedLabels.htmlLabels.length) {
254
- const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
280
+ const topHtmlLabel = Math.min(...preparedLabels.htmlLabels.map((l) => l.y));
255
281
  topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
256
282
  }
257
283
  let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
@@ -267,14 +293,16 @@ export function preparePieData(args) {
267
293
  const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
268
294
  if (topAdjustment && topAdjustment >= bottomAdjustment) {
269
295
  data.segments.forEach((s) => {
270
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
296
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
297
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
271
298
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
272
299
  });
273
300
  data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
274
301
  }
275
302
  else if (bottomAdjustment) {
276
303
  data.segments.forEach((s) => {
277
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
304
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
305
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
278
306
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
279
307
  });
280
308
  data.center[1] += (bottomAdjustment - topAdjustment) / 2;
@@ -285,6 +313,14 @@ export function preparePieData(args) {
285
313
  series: items,
286
314
  allowOverlow: false,
287
315
  });
316
+ if (typeof ((_a = items[0]) === null || _a === void 0 ? void 0 : _a.innerRadius) !== 'undefined') {
317
+ const resultSegmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
318
+ const resultInnerRadius = calculateNumericProperty({
319
+ value: items[0].innerRadius,
320
+ base: resultSegmentMaxRadius,
321
+ }) || 0;
322
+ data.innerRadius = resultInnerRadius;
323
+ }
288
324
  data.labels = labels;
289
325
  data.htmlLabels = htmlLabels;
290
326
  data.connectors = connectors;
@@ -10,8 +10,9 @@ export function getCurveFactory(data) {
10
10
  case 'linear': {
11
11
  return curveLinear;
12
12
  }
13
+ default:
14
+ return undefined;
13
15
  }
14
- return undefined;
15
16
  }
16
17
  /**
17
18
  * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
@@ -7,18 +7,24 @@ function getLabels(args) {
7
7
  const { data, options: { html, padding, align, style }, } = args;
8
8
  return data.reduce((acc, d) => {
9
9
  const texts = Array.isArray(d.data.name) ? d.data.name : [d.data.name];
10
- texts.forEach((text, index) => {
10
+ const left = d.x0 + padding;
11
+ const right = d.x1 - padding;
12
+ const spaceWidth = Math.max(0, right - left);
13
+ let availableSpaceHeight = Math.max(0, d.y1 - d.y0 - padding);
14
+ let prevLabelsHeight = 0;
15
+ texts.forEach((text) => {
11
16
  var _a;
12
17
  const label = getFormattedValue(Object.assign({ value: text }, args.options));
13
- const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
14
- const left = d.x0 + padding;
15
- const right = d.x1 - padding;
16
- const spaceWidth = Math.max(0, right - left);
17
- const spaceHeight = Math.max(0, d.y1 - d.y0 - padding);
18
+ const { maxHeight: labelMaxHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({
19
+ labels: [label],
20
+ style: Object.assign(Object.assign({}, style), { maxWidth: `${spaceWidth}px`, maxHeight: `${availableSpaceHeight}px` }),
21
+ html,
22
+ })) !== null && _a !== void 0 ? _a : {};
18
23
  let x = left;
19
- const y = index * lineHeight + d.y0 + padding;
24
+ const y = prevLabelsHeight + d.y0 + padding;
20
25
  const labelWidth = Math.min(labelMaxWidth, spaceWidth);
21
- if (!labelWidth || lineHeight > spaceHeight) {
26
+ const labelHeight = Math.min(labelMaxHeight, availableSpaceHeight);
27
+ if (!labelWidth || y > d.y1) {
22
28
  return;
23
29
  }
24
30
  switch (align) {
@@ -35,8 +41,8 @@ function getLabels(args) {
35
41
  break;
36
42
  }
37
43
  }
38
- const bottom = y + lineHeight;
39
- if (bottom > d.y1) {
44
+ const bottom = y + labelMaxHeight;
45
+ if (!html && bottom > d.y1) {
40
46
  return;
41
47
  }
42
48
  const item = html
@@ -44,7 +50,7 @@ function getLabels(args) {
44
50
  content: label,
45
51
  x,
46
52
  y,
47
- size: { width: labelWidth, height: lineHeight },
53
+ size: { width: labelWidth, height: labelHeight },
48
54
  }
49
55
  : {
50
56
  text: label,
@@ -54,6 +60,8 @@ function getLabels(args) {
54
60
  nodeData: d.data,
55
61
  };
56
62
  acc.push(item);
63
+ prevLabelsHeight += labelHeight;
64
+ availableSpaceHeight = Math.max(0, availableSpaceHeight - labelHeight);
57
65
  });
58
66
  return acc;
59
67
  }, []);
@@ -46,6 +46,14 @@ export interface PieSeries<T = MeaningfulAny> extends BaseSeries {
46
46
  innerRadius?: string | number;
47
47
  /** The radius of the pie relative to the chart area. The default behaviour is to scale to the chart area. */
48
48
  radius?: string | number;
49
+ /**
50
+ * The minimum allowable radius of the pie.
51
+ *
52
+ * If specified as a percentage, the base for calculation is the height or width of the chart (the minimum value is taken) minus the halo effect.
53
+ *
54
+ * If not specified, the minimum radius is calculated as 30% of the height or width of the chart (the minimum value is taken) minus the halo effect.
55
+ */
56
+ minRadius?: string | number;
49
57
  /** Individual series legend options. Has higher priority than legend options in widget data */
50
58
  legend?: ChartLegend & {
51
59
  symbol?: RectLegendSymbolOptions;
@@ -11,7 +11,7 @@ export declare function hasOverlappingLabels({ width, labels, padding, style, }:
11
11
  }): boolean;
12
12
  export declare function getLabelsSize({ labels, style, rotation, html, }: {
13
13
  labels: string[];
14
- style?: BaseTextStyle;
14
+ style?: BaseTextStyle & React.CSSProperties;
15
15
  rotation?: number;
16
16
  html?: boolean;
17
17
  }): {
@@ -14,12 +14,17 @@ export function handleOverflowingText(tSpan, maxWidth) {
14
14
  revertRotation.setRotate(-angle, 0, 0);
15
15
  textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.appendItem(revertRotation);
16
16
  let text = tSpan.textContent || '';
17
- let textLength = ((_b = tSpan.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
17
+ // We believe that if the text goes beyond the boundaries of less than a pixel, it's not a big deal.
18
+ // Math.floor helps to solve the problem with the difference in rounding when comparing textLength with maxWidth.
19
+ let textLength = Math.floor(((_b = tSpan.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0);
18
20
  while (textLength > maxWidth && text.length > 1) {
19
21
  text = text.slice(0, -1);
20
22
  tSpan.textContent = text + '…';
21
23
  textLength = ((_c = tSpan.getBoundingClientRect()) === null || _c === void 0 ? void 0 : _c.width) || 0;
22
24
  }
25
+ if (textLength > maxWidth) {
26
+ tSpan.textContent = '';
27
+ }
23
28
  textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.removeItem((textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.length) - 1);
24
29
  }
25
30
  export function setEllipsisForOverflowText(selection, maxWidth) {
@@ -64,21 +69,22 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
64
69
  return text;
65
70
  }
66
71
  export function getLabelsSize({ labels, style, rotation, html, }) {
67
- var _a, _b, _c, _d, _e;
72
+ var _a, _b, _c, _d, _e, _f, _g;
68
73
  if (!labels.filter(Boolean).length) {
69
74
  return { maxHeight: 0, maxWidth: 0 };
70
75
  }
71
76
  const container = select(document.body).append('div');
72
- // TODO: Why do we need this styles?
73
- // .attr('class', 'chartkit chartkit-theme_common');
74
77
  const result = { maxHeight: 0, maxWidth: 0 };
75
78
  let labelWrapper;
76
79
  if (html) {
77
80
  labelWrapper = container
78
81
  .append('div')
79
82
  .style('position', 'absolute')
83
+ .style('display', 'inline-block')
80
84
  .style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
81
85
  .style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
86
+ .style('max-width', (_c = style === null || style === void 0 ? void 0 : style.maxWidth) !== null && _c !== void 0 ? _c : '')
87
+ .style('max-height', (_d = style === null || style === void 0 ? void 0 : style.maxHeight) !== null && _d !== void 0 ? _d : '')
82
88
  .node();
83
89
  const { height, width } = labels.reduce((acc, l) => {
84
90
  var _a, _b;
@@ -102,9 +108,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
102
108
  .attr('text-anchor', rotation > 0 ? 'start' : 'end')
103
109
  .style('transform', `rotate(${rotation}deg)`);
104
110
  }
105
- const rect = (_c = svg.select('g').node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
106
- result.maxWidth = (_d = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _d !== void 0 ? _d : 0;
107
- result.maxHeight = (_e = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _e !== void 0 ? _e : 0;
111
+ const rect = (_e = svg.select('g').node()) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();
112
+ result.maxWidth = (_f = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _f !== void 0 ? _f : 0;
113
+ result.maxHeight = (_g = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _g !== void 0 ? _g : 0;
108
114
  }
109
115
  container.remove();
110
116
  return result;
@@ -0,0 +1,2 @@
1
+ import type { BaseTextStyle } from '../../types';
2
+ export declare const DEFAULT_DATALABELS_STYLE: BaseTextStyle;
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_DATALABELS_STYLE = {
2
+ fontSize: '11px',
3
+ fontWeight: 'bold',
4
+ fontColor: 'var(--gcharts-data-labels)',
5
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './axis';
2
+ export * from './data-labels';
2
3
  export * from './legend';
3
4
  export * from './series-options';
@@ -1,3 +1,4 @@
1
1
  export * from './axis';
2
+ export * from './data-labels';
2
3
  export * from './legend';
3
4
  export * from './series-options';
@@ -1,8 +1,7 @@
1
- import type { BaseTextStyle, Halo } from '../../types';
1
+ import type { Halo } from '../../types';
2
2
  import type { PointMarkerOptions } from '../../types/chart/marker';
3
3
  export declare const DEFAULT_LEGEND_SYMBOL_SIZE = 8;
4
4
  export declare const DEFAULT_LEGEND_SYMBOL_PADDING = 5;
5
5
  export declare const DEFAULT_DATALABELS_PADDING = 5;
6
- export declare const DEFAULT_DATALABELS_STYLE: BaseTextStyle;
7
6
  export declare const DEFAULT_HALO_OPTIONS: Required<Halo>;
8
7
  export declare const DEFAULT_POINT_MARKER_OPTIONS: Omit<Required<PointMarkerOptions>, 'enabled'>;
@@ -1,11 +1,6 @@
1
1
  export const DEFAULT_LEGEND_SYMBOL_SIZE = 8;
2
2
  export const DEFAULT_LEGEND_SYMBOL_PADDING = 5;
3
3
  export const DEFAULT_DATALABELS_PADDING = 5;
4
- export const DEFAULT_DATALABELS_STYLE = {
5
- fontSize: '11px',
6
- fontWeight: 'bold',
7
- fontColor: 'var(--gcharts-data-labels)',
8
- };
9
4
  export const DEFAULT_HALO_OPTIONS = {
10
5
  enabled: true,
11
6
  opacity: 0.25,
@@ -1,7 +1,8 @@
1
1
  import get from 'lodash/get';
2
2
  import merge from 'lodash/merge';
3
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
3
4
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
6
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
6
7
  export const DEFAULT_LINE_WIDTH = 1;
7
8
  export const DEFAULT_MARKER = Object.assign(Object.assign({}, DEFAULT_POINT_MARKER_OPTIONS), { enabled: false });
@@ -1,6 +1,7 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getUniqId } from '../../utils';
3
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
4
5
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
5
6
  export function prepareBarXSeries(args) {
6
7
  const { colorScale, series: seriesList, seriesOptions, legend } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getLabelsSize, getUniqId } from '../../utils';
3
4
  import { getFormattedValue } from '../../utils/chart/format';
4
- import { DEFAULT_DATALABELS_STYLE } from './constants';
5
5
  import { getSeriesStackId, prepareLegendSymbol } from './utils';
6
6
  function prepareDataLabels(series) {
7
7
  var _a, _b;
@@ -1,8 +1,8 @@
1
1
  import get from 'lodash/get';
2
2
  import merge from 'lodash/merge';
3
- import { DashStyle, LineCap } from '../../constants';
3
+ import { DEFAULT_DATALABELS_STYLE, DashStyle, LineCap } from '../../constants';
4
4
  import { getUniqId } from '../../utils';
5
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_LEGEND_SYMBOL_PADDING, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_LEGEND_SYMBOL_PADDING, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
6
6
  export const DEFAULT_LEGEND_SYMBOL_SIZE = 16;
7
7
  export const DEFAULT_LINE_WIDTH = 1;
8
8
  export const DEFAULT_DASH_STYLE = DashStyle.Solid;
@@ -1,8 +1,8 @@
1
1
  import { scaleOrdinal } from 'd3';
2
2
  import get from 'lodash/get';
3
- import { DEFAULT_PALETTE } from '../../constants';
3
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
4
4
  import { getUniqId } from '../../utils';
5
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
5
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
6
6
  import { prepareLegendSymbol } from './utils';
7
7
  export function preparePieSeries(args) {
8
8
  const { series, seriesOptions, legend } = args;
@@ -43,6 +43,7 @@ export function preparePieSeries(args) {
43
43
  borderWidth: (_d = series.borderWidth) !== null && _d !== void 0 ? _d : 1,
44
44
  radius: (_f = (_e = dataItem.radius) !== null && _e !== void 0 ? _e : series.radius) !== null && _f !== void 0 ? _f : '100%',
45
45
  innerRadius: series.innerRadius || 0,
46
+ minRadius: series.minRadius,
46
47
  stackId,
47
48
  states: {
48
49
  hover: {
@@ -1,9 +1,9 @@
1
1
  import { scaleOrdinal } from 'd3';
2
2
  import get from 'lodash/get';
3
3
  import merge from 'lodash/merge';
4
- import { DEFAULT_PALETTE } from '../../constants';
4
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
5
5
  import { getUniqId } from '../../utils';
6
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
6
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
7
7
  import { prepareLegendSymbol } from './utils';
8
8
  export const DEFAULT_MARKER = Object.assign(Object.assign({}, DEFAULT_POINT_MARKER_OPTIONS), { enabled: true, radius: 2 });
9
9
  function prepareMarker(series, seriesOptions) {
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
+ import { DEFAULT_DATALABELS_STYLE } from '../../constants';
2
3
  import { getUniqId } from '../../utils';
3
- import { DEFAULT_DATALABELS_STYLE } from './constants';
4
4
  import { prepareLegendSymbol } from './utils';
5
5
  export function prepareSankeySeries(args) {
6
6
  const { colorScale, legend, series } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
- import { LayoutAlgorithm } from '../../constants';
2
+ import { DEFAULT_DATALABELS_STYLE, LayoutAlgorithm } from '../../constants';
3
3
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
5
5
  import { prepareLegendSymbol } from './utils';
6
6
  export function prepareTreemap(args) {
7
7
  const { colorScale, legend, series } = args;
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
- import { DEFAULT_PALETTE } from '../../constants';
2
+ import { DEFAULT_DATALABELS_STYLE, DEFAULT_PALETTE } from '../../constants';
3
3
  import { getUniqId } from '../../utils';
4
- import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
4
+ import { DEFAULT_DATALABELS_PADDING } from './constants';
5
5
  import { prepareLegendSymbol } from './utils';
6
6
  export function prepareWaterfallSeries(args) {
7
7
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
@@ -137,6 +137,7 @@ export type PreparedPieSeries = {
137
137
  center?: [string | number | null, string | number | null];
138
138
  radius?: string | number;
139
139
  innerRadius?: string | number;
140
+ minRadius?: string | number;
140
141
  stackId: string;
141
142
  label?: PieSeriesData['label'];
142
143
  dataLabels: {
@@ -1,4 +1,6 @@
1
1
  import { arc, group, line as lineGenerator } from 'd3';
2
+ import merge from 'lodash/merge';
3
+ import { DEFAULT_DATALABELS_STYLE } from '../../../constants';
2
4
  import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
3
5
  import { getFormattedValue } from '../../../utils/chart/format';
4
6
  import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
@@ -16,17 +18,22 @@ const getCenter = (boundsWidth, boundsHeight, center) => {
16
18
  return [resultX, resultY];
17
19
  };
18
20
  export function preparePieData(args) {
21
+ var _a, _b;
19
22
  const { series: preparedSeries, boundsWidth, boundsHeight } = args;
20
23
  const haloSize = preparedSeries[0].states.hover.halo.enabled
21
24
  ? preparedSeries[0].states.hover.halo.size
22
25
  : 0;
23
26
  const maxRadius = Math.min(boundsWidth, boundsHeight) / 2 - haloSize;
24
- const minRadius = maxRadius * 0.3;
27
+ const propsMinRadius = calculateNumericProperty({
28
+ value: preparedSeries[0].minRadius,
29
+ base: maxRadius,
30
+ });
31
+ const minRadius = typeof propsMinRadius === 'number' ? propsMinRadius : maxRadius * 0.3;
25
32
  const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
33
+ const dataLabelsStyle = merge({}, DEFAULT_DATALABELS_STYLE, (_b = (_a = preparedSeries[0]) === null || _a === void 0 ? void 0 : _a.dataLabels) === null || _b === void 0 ? void 0 : _b.style);
26
34
  const prepareItem = (stackId, items) => {
27
- var _a;
28
35
  const series = items[0];
29
- const { center, borderWidth, borderColor, borderRadius, innerRadius: seriesInnerRadius, dataLabels, } = series;
36
+ const { center, borderWidth, borderColor, borderRadius, dataLabels } = series;
30
37
  const data = {
31
38
  id: stackId,
32
39
  center: getCenter(boundsWidth, boundsHeight, center),
@@ -48,9 +55,8 @@ export function preparePieData(args) {
48
55
  };
49
56
  const { maxHeight: labelHeight } = getLabelsSize({
50
57
  labels: ['Some Label'],
51
- style: dataLabels.style,
58
+ style: dataLabelsStyle,
52
59
  });
53
- let segmentMaxRadius = 0;
54
60
  const segments = items.map((item) => {
55
61
  var _a;
56
62
  let maxSegmentRadius = maxRadius;
@@ -58,7 +64,6 @@ export function preparePieData(args) {
58
64
  maxSegmentRadius -= dataLabels.distance + dataLabels.connectorPadding + labelHeight;
59
65
  }
60
66
  const segmentRadius = (_a = calculateNumericProperty({ value: item.radius, base: maxSegmentRadius })) !== null && _a !== void 0 ? _a : maxSegmentRadius;
61
- segmentMaxRadius = Math.max(segmentMaxRadius, segmentRadius);
62
67
  return {
63
68
  value: item.value,
64
69
  color: item.color,
@@ -71,8 +76,6 @@ export function preparePieData(args) {
71
76
  };
72
77
  });
73
78
  data.segments = pieGenerator(segments);
74
- data.innerRadius =
75
- (_a = calculateNumericProperty({ value: seriesInnerRadius, base: segmentMaxRadius })) !== null && _a !== void 0 ? _a : 0;
76
79
  return data;
77
80
  };
78
81
  const prepareLabels = (prepareLabelsArgs) => {
@@ -84,13 +87,18 @@ export function preparePieData(args) {
84
87
  if (!dataLabels.enabled) {
85
88
  return { labels, htmlLabels, connectors };
86
89
  }
90
+ const shouldUseHtml = dataLabels.html;
87
91
  let line = lineGenerator();
88
92
  const curveFactory = getCurveFactory(data);
89
93
  if (curveFactory) {
90
94
  line = line.curve(curveFactory);
91
95
  }
92
96
  const { style, connectorPadding, distance } = dataLabels;
93
- const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
97
+ const { maxHeight: labelHeight } = getLabelsSize({
98
+ labels: ['Some Label'],
99
+ style: dataLabelsStyle,
100
+ html: shouldUseHtml,
101
+ });
94
102
  const connectorStartPointGenerator = arc()
95
103
  .innerRadius((d) => d.data.radius)
96
104
  .outerRadius((d) => d.data.radius);
@@ -104,22 +112,35 @@ export function preparePieData(args) {
104
112
  .innerRadius((d) => d.data.radius + distance + connectorPadding)
105
113
  .outerRadius((d) => d.data.radius + distance + connectorPadding);
106
114
  let shouldStopLabelPlacement = false;
115
+ // eslint-disable-next-line complexity
107
116
  series.forEach((d, index) => {
108
117
  const prevLabel = labels[labels.length - 1];
109
118
  const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
110
- const shouldUseHtml = dataLabels.html;
111
- const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
119
+ const labelSize = getLabelsSize({
120
+ labels: [text],
121
+ style: dataLabelsStyle,
122
+ html: shouldUseHtml,
123
+ });
112
124
  const labelWidth = labelSize.maxWidth;
113
125
  const relatedSegment = data.segments[index];
126
+ /**
127
+ * Compute the label coordinates on the label arc for a given angle.
128
+ *
129
+ * For HTML labels, the function returns the top-left corner to account for
130
+ * element box positioning. It shifts left by the label width when the point is
131
+ * on the left side (x < 0) and shifts up by the label height when above the
132
+ * horizontal center (y < 0). For SVG text, only the vertical shift is applied
133
+ * to compensate for text baseline.
134
+ *
135
+ * @param {number} angle - Angle in radians at which the label should be placed.
136
+ * @returns {[number, number]} A tuple [x, y] relative to the pie center.
137
+ */
114
138
  const getLabelPosition = (angle) => {
115
139
  let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
116
140
  if (shouldUseHtml) {
117
141
  x = x < 0 ? x - labelWidth : x;
118
- y = y - labelSize.maxHeight;
119
- }
120
- else {
121
- y = y < 0 ? y - labelHeight : y;
122
142
  }
143
+ y = y < 0 ? y - labelHeight : y;
123
144
  return [x, y];
124
145
  };
125
146
  const getConnectorPoints = (angle) => {
@@ -173,11 +194,17 @@ export function preparePieData(args) {
173
194
  shouldAdjustAngle = false;
174
195
  }
175
196
  else {
176
- label.angle = newAngle;
177
197
  const [newX, newY] = getLabelPosition(newAngle);
198
+ label.angle = newAngle;
199
+ label.textAnchor = newAngle < Math.PI ? 'start' : 'end';
178
200
  label.x = newX;
179
201
  label.y = newY;
180
- const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
202
+ // See `getLabelPosition`: for HTML labels we return top-left,
203
+ // so shift x by labelWidth when textAnchor is 'end'.
204
+ const pointC = shouldUseHtml && label.textAnchor === 'end'
205
+ ? [newX + labelWidth, newY]
206
+ : [newX, newY];
207
+ const inscribedAngle = getInscribedAngle(pointA, pointB, pointC);
181
208
  if (inscribedAngle > 90) {
182
209
  shouldAdjustAngle = false;
183
210
  shouldStopLabelPlacement = true;
@@ -192,6 +219,7 @@ export function preparePieData(args) {
192
219
  }
193
220
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
194
221
  if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
222
+ labels.push(label);
195
223
  if (shouldUseHtml) {
196
224
  htmlLabels.push({
197
225
  x: data.center[0] + label.x,
@@ -201,9 +229,6 @@ export function preparePieData(args) {
201
229
  style: label.style,
202
230
  });
203
231
  }
204
- else {
205
- labels.push(label);
206
- }
207
232
  const connector = {
208
233
  path: line(getConnectorPoints(label.angle)),
209
234
  color: relatedSegment.data.color,
@@ -212,12 +237,13 @@ export function preparePieData(args) {
212
237
  }
213
238
  });
214
239
  return {
215
- labels,
240
+ labels: shouldUseHtml ? [] : labels,
216
241
  htmlLabels,
217
242
  connectors,
218
243
  };
219
244
  };
220
245
  return Array.from(groupedPieSeries).map(([stackId, items]) => {
246
+ var _a;
221
247
  const data = prepareItem(stackId, items);
222
248
  const preparedLabels = prepareLabels({
223
249
  data,
@@ -251,7 +277,7 @@ export function preparePieData(args) {
251
277
  topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
252
278
  }
253
279
  if (preparedLabels.htmlLabels.length) {
254
- const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
280
+ const topHtmlLabel = Math.min(...preparedLabels.htmlLabels.map((l) => l.y));
255
281
  topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
256
282
  }
257
283
  let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
@@ -267,14 +293,16 @@ export function preparePieData(args) {
267
293
  const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
268
294
  if (topAdjustment && topAdjustment >= bottomAdjustment) {
269
295
  data.segments.forEach((s) => {
270
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
296
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
297
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
271
298
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
272
299
  });
273
300
  data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
274
301
  }
275
302
  else if (bottomAdjustment) {
276
303
  data.segments.forEach((s) => {
277
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
304
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
305
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
278
306
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
279
307
  });
280
308
  data.center[1] += (bottomAdjustment - topAdjustment) / 2;
@@ -285,6 +313,14 @@ export function preparePieData(args) {
285
313
  series: items,
286
314
  allowOverlow: false,
287
315
  });
316
+ if (typeof ((_a = items[0]) === null || _a === void 0 ? void 0 : _a.innerRadius) !== 'undefined') {
317
+ const resultSegmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
318
+ const resultInnerRadius = calculateNumericProperty({
319
+ value: items[0].innerRadius,
320
+ base: resultSegmentMaxRadius,
321
+ }) || 0;
322
+ data.innerRadius = resultInnerRadius;
323
+ }
288
324
  data.labels = labels;
289
325
  data.htmlLabels = htmlLabels;
290
326
  data.connectors = connectors;
@@ -10,8 +10,9 @@ export function getCurveFactory(data) {
10
10
  case 'linear': {
11
11
  return curveLinear;
12
12
  }
13
+ default:
14
+ return undefined;
13
15
  }
14
- return undefined;
15
16
  }
16
17
  /**
17
18
  * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
@@ -7,18 +7,24 @@ function getLabels(args) {
7
7
  const { data, options: { html, padding, align, style }, } = args;
8
8
  return data.reduce((acc, d) => {
9
9
  const texts = Array.isArray(d.data.name) ? d.data.name : [d.data.name];
10
- texts.forEach((text, index) => {
10
+ const left = d.x0 + padding;
11
+ const right = d.x1 - padding;
12
+ const spaceWidth = Math.max(0, right - left);
13
+ let availableSpaceHeight = Math.max(0, d.y1 - d.y0 - padding);
14
+ let prevLabelsHeight = 0;
15
+ texts.forEach((text) => {
11
16
  var _a;
12
17
  const label = getFormattedValue(Object.assign({ value: text }, args.options));
13
- const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
14
- const left = d.x0 + padding;
15
- const right = d.x1 - padding;
16
- const spaceWidth = Math.max(0, right - left);
17
- const spaceHeight = Math.max(0, d.y1 - d.y0 - padding);
18
+ const { maxHeight: labelMaxHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({
19
+ labels: [label],
20
+ style: Object.assign(Object.assign({}, style), { maxWidth: `${spaceWidth}px`, maxHeight: `${availableSpaceHeight}px` }),
21
+ html,
22
+ })) !== null && _a !== void 0 ? _a : {};
18
23
  let x = left;
19
- const y = index * lineHeight + d.y0 + padding;
24
+ const y = prevLabelsHeight + d.y0 + padding;
20
25
  const labelWidth = Math.min(labelMaxWidth, spaceWidth);
21
- if (!labelWidth || lineHeight > spaceHeight) {
26
+ const labelHeight = Math.min(labelMaxHeight, availableSpaceHeight);
27
+ if (!labelWidth || y > d.y1) {
22
28
  return;
23
29
  }
24
30
  switch (align) {
@@ -35,8 +41,8 @@ function getLabels(args) {
35
41
  break;
36
42
  }
37
43
  }
38
- const bottom = y + lineHeight;
39
- if (bottom > d.y1) {
44
+ const bottom = y + labelMaxHeight;
45
+ if (!html && bottom > d.y1) {
40
46
  return;
41
47
  }
42
48
  const item = html
@@ -44,7 +50,7 @@ function getLabels(args) {
44
50
  content: label,
45
51
  x,
46
52
  y,
47
- size: { width: labelWidth, height: lineHeight },
53
+ size: { width: labelWidth, height: labelHeight },
48
54
  }
49
55
  : {
50
56
  text: label,
@@ -54,6 +60,8 @@ function getLabels(args) {
54
60
  nodeData: d.data,
55
61
  };
56
62
  acc.push(item);
63
+ prevLabelsHeight += labelHeight;
64
+ availableSpaceHeight = Math.max(0, availableSpaceHeight - labelHeight);
57
65
  });
58
66
  return acc;
59
67
  }, []);
@@ -46,6 +46,14 @@ export interface PieSeries<T = MeaningfulAny> extends BaseSeries {
46
46
  innerRadius?: string | number;
47
47
  /** The radius of the pie relative to the chart area. The default behaviour is to scale to the chart area. */
48
48
  radius?: string | number;
49
+ /**
50
+ * The minimum allowable radius of the pie.
51
+ *
52
+ * If specified as a percentage, the base for calculation is the height or width of the chart (the minimum value is taken) minus the halo effect.
53
+ *
54
+ * If not specified, the minimum radius is calculated as 30% of the height or width of the chart (the minimum value is taken) minus the halo effect.
55
+ */
56
+ minRadius?: string | number;
49
57
  /** Individual series legend options. Has higher priority than legend options in widget data */
50
58
  legend?: ChartLegend & {
51
59
  symbol?: RectLegendSymbolOptions;
@@ -11,7 +11,7 @@ export declare function hasOverlappingLabels({ width, labels, padding, style, }:
11
11
  }): boolean;
12
12
  export declare function getLabelsSize({ labels, style, rotation, html, }: {
13
13
  labels: string[];
14
- style?: BaseTextStyle;
14
+ style?: BaseTextStyle & React.CSSProperties;
15
15
  rotation?: number;
16
16
  html?: boolean;
17
17
  }): {
@@ -14,12 +14,17 @@ export function handleOverflowingText(tSpan, maxWidth) {
14
14
  revertRotation.setRotate(-angle, 0, 0);
15
15
  textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.appendItem(revertRotation);
16
16
  let text = tSpan.textContent || '';
17
- let textLength = ((_b = tSpan.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
17
+ // We believe that if the text goes beyond the boundaries of less than a pixel, it's not a big deal.
18
+ // Math.floor helps to solve the problem with the difference in rounding when comparing textLength with maxWidth.
19
+ let textLength = Math.floor(((_b = tSpan.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0);
18
20
  while (textLength > maxWidth && text.length > 1) {
19
21
  text = text.slice(0, -1);
20
22
  tSpan.textContent = text + '…';
21
23
  textLength = ((_c = tSpan.getBoundingClientRect()) === null || _c === void 0 ? void 0 : _c.width) || 0;
22
24
  }
25
+ if (textLength > maxWidth) {
26
+ tSpan.textContent = '';
27
+ }
23
28
  textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.removeItem((textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.length) - 1);
24
29
  }
25
30
  export function setEllipsisForOverflowText(selection, maxWidth) {
@@ -64,21 +69,22 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
64
69
  return text;
65
70
  }
66
71
  export function getLabelsSize({ labels, style, rotation, html, }) {
67
- var _a, _b, _c, _d, _e;
72
+ var _a, _b, _c, _d, _e, _f, _g;
68
73
  if (!labels.filter(Boolean).length) {
69
74
  return { maxHeight: 0, maxWidth: 0 };
70
75
  }
71
76
  const container = select(document.body).append('div');
72
- // TODO: Why do we need this styles?
73
- // .attr('class', 'chartkit chartkit-theme_common');
74
77
  const result = { maxHeight: 0, maxWidth: 0 };
75
78
  let labelWrapper;
76
79
  if (html) {
77
80
  labelWrapper = container
78
81
  .append('div')
79
82
  .style('position', 'absolute')
83
+ .style('display', 'inline-block')
80
84
  .style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
81
85
  .style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
86
+ .style('max-width', (_c = style === null || style === void 0 ? void 0 : style.maxWidth) !== null && _c !== void 0 ? _c : '')
87
+ .style('max-height', (_d = style === null || style === void 0 ? void 0 : style.maxHeight) !== null && _d !== void 0 ? _d : '')
82
88
  .node();
83
89
  const { height, width } = labels.reduce((acc, l) => {
84
90
  var _a, _b;
@@ -102,9 +108,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
102
108
  .attr('text-anchor', rotation > 0 ? 'start' : 'end')
103
109
  .style('transform', `rotate(${rotation}deg)`);
104
110
  }
105
- const rect = (_c = svg.select('g').node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
106
- result.maxWidth = (_d = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _d !== void 0 ? _d : 0;
107
- result.maxHeight = (_e = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _e !== void 0 ? _e : 0;
111
+ const rect = (_e = svg.select('g').node()) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();
112
+ result.maxWidth = (_f = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _f !== void 0 ? _f : 0;
113
+ result.maxHeight = (_g = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _g !== void 0 ? _g : 0;
108
114
  }
109
115
  container.remove();
110
116
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",