@gravity-ui/charts 1.5.0 → 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 (47) 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 +74 -27
  18. package/dist/cjs/hooks/useShapes/pie/utils.d.ts +10 -0
  19. package/dist/cjs/hooks/useShapes/pie/utils.js +20 -1
  20. package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +23 -11
  21. package/dist/cjs/types/chart/pie.d.ts +8 -0
  22. package/dist/cjs/utils/chart/text.d.ts +1 -1
  23. package/dist/cjs/utils/chart/text.js +19 -8
  24. package/dist/esm/constants/defaults/data-labels.d.ts +2 -0
  25. package/dist/esm/constants/defaults/data-labels.js +5 -0
  26. package/dist/esm/constants/defaults/index.d.ts +1 -0
  27. package/dist/esm/constants/defaults/index.js +1 -0
  28. package/dist/esm/hooks/useSeries/constants.d.ts +1 -2
  29. package/dist/esm/hooks/useSeries/constants.js +0 -5
  30. package/dist/esm/hooks/useSeries/prepare-area.js +2 -1
  31. package/dist/esm/hooks/useSeries/prepare-bar-x.js +2 -1
  32. package/dist/esm/hooks/useSeries/prepare-bar-y.js +1 -1
  33. package/dist/esm/hooks/useSeries/prepare-line.js +2 -2
  34. package/dist/esm/hooks/useSeries/prepare-pie.js +3 -2
  35. package/dist/esm/hooks/useSeries/prepare-radar.js +2 -2
  36. package/dist/esm/hooks/useSeries/prepare-sankey.js +1 -1
  37. package/dist/esm/hooks/useSeries/prepare-treemap.js +2 -2
  38. package/dist/esm/hooks/useSeries/prepare-waterfall.js +2 -2
  39. package/dist/esm/hooks/useSeries/types.d.ts +1 -0
  40. package/dist/esm/hooks/useShapes/pie/prepare-data.js +74 -27
  41. package/dist/esm/hooks/useShapes/pie/utils.d.ts +10 -0
  42. package/dist/esm/hooks/useShapes/pie/utils.js +20 -1
  43. package/dist/esm/hooks/useShapes/treemap/prepare-data.js +23 -11
  44. package/dist/esm/types/chart/pie.d.ts +8 -0
  45. package/dist/esm/utils/chart/text.d.ts +1 -1
  46. package/dist/esm/utils/chart/text.js +19 -8
  47. 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,7 +1,9 @@
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
- import { getCurveFactory, pieGenerator } from './utils';
6
+ import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
5
7
  const FULL_CIRCLE = Math.PI * 2;
6
8
  const getCenter = (boundsWidth, boundsHeight, center) => {
7
9
  var _a, _b;
@@ -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);
@@ -103,22 +111,36 @@ export function preparePieData(args) {
103
111
  const labelArcGenerator = arc()
104
112
  .innerRadius((d) => d.data.radius + distance + connectorPadding)
105
113
  .outerRadius((d) => d.data.radius + distance + connectorPadding);
114
+ let shouldStopLabelPlacement = false;
115
+ // eslint-disable-next-line complexity
106
116
  series.forEach((d, index) => {
107
117
  const prevLabel = labels[labels.length - 1];
108
118
  const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
109
- const shouldUseHtml = dataLabels.html;
110
- const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
119
+ const labelSize = getLabelsSize({
120
+ labels: [text],
121
+ style: dataLabelsStyle,
122
+ html: shouldUseHtml,
123
+ });
111
124
  const labelWidth = labelSize.maxWidth;
112
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
+ */
113
138
  const getLabelPosition = (angle) => {
114
139
  let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
115
140
  if (shouldUseHtml) {
116
141
  x = x < 0 ? x - labelWidth : x;
117
- y = y - labelSize.maxHeight;
118
- }
119
- else {
120
- y = y < 0 ? y - labelHeight : y;
121
142
  }
143
+ y = y < 0 ? y - labelHeight : y;
122
144
  return [x, y];
123
145
  };
124
146
  const getConnectorPoints = (angle) => {
@@ -158,8 +180,13 @@ export function preparePieData(args) {
158
180
  let overlap = false;
159
181
  if (prevLabel) {
160
182
  overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
183
+ const startAngle = relatedSegment.startAngle +
184
+ (relatedSegment.endAngle - relatedSegment.startAngle) / 2;
161
185
  if (overlap) {
162
- let shouldAdjustAngle = true;
186
+ let shouldAdjustAngle = !shouldStopLabelPlacement;
187
+ const connectorPoints = getConnectorPoints(startAngle);
188
+ const pointA = connectorPoints[0];
189
+ const pointB = connectorPoints[connectorPoints.length - 1];
163
190
  const step = Math.PI / 180;
164
191
  while (shouldAdjustAngle) {
165
192
  const newAngle = label.angle + step;
@@ -167,10 +194,21 @@ export function preparePieData(args) {
167
194
  shouldAdjustAngle = false;
168
195
  }
169
196
  else {
170
- label.angle = newAngle;
171
197
  const [newX, newY] = getLabelPosition(newAngle);
198
+ label.angle = newAngle;
199
+ label.textAnchor = newAngle < Math.PI ? 'start' : 'end';
172
200
  label.x = newX;
173
201
  label.y = 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);
208
+ if (inscribedAngle > 90) {
209
+ shouldAdjustAngle = false;
210
+ shouldStopLabelPlacement = true;
211
+ }
174
212
  if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
175
213
  shouldAdjustAngle = false;
176
214
  overlap = false;
@@ -180,7 +218,8 @@ export function preparePieData(args) {
180
218
  }
181
219
  }
182
220
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
183
- if (!isLabelOverlapped && label.maxWidth > 0) {
221
+ if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
222
+ labels.push(label);
184
223
  if (shouldUseHtml) {
185
224
  htmlLabels.push({
186
225
  x: data.center[0] + label.x,
@@ -190,23 +229,21 @@ export function preparePieData(args) {
190
229
  style: label.style,
191
230
  });
192
231
  }
193
- else {
194
- labels.push(label);
195
- }
196
232
  const connector = {
197
- path: line(getConnectorPoints(midAngle)),
233
+ path: line(getConnectorPoints(label.angle)),
198
234
  color: relatedSegment.data.color,
199
235
  };
200
236
  connectors.push(connector);
201
237
  }
202
238
  });
203
239
  return {
204
- labels,
240
+ labels: shouldUseHtml ? [] : labels,
205
241
  htmlLabels,
206
242
  connectors,
207
243
  };
208
244
  };
209
245
  return Array.from(groupedPieSeries).map(([stackId, items]) => {
246
+ var _a;
210
247
  const data = prepareItem(stackId, items);
211
248
  const preparedLabels = prepareLabels({
212
249
  data,
@@ -240,7 +277,7 @@ export function preparePieData(args) {
240
277
  topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
241
278
  }
242
279
  if (preparedLabels.htmlLabels.length) {
243
- const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
280
+ const topHtmlLabel = Math.min(...preparedLabels.htmlLabels.map((l) => l.y));
244
281
  topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
245
282
  }
246
283
  let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
@@ -256,14 +293,16 @@ export function preparePieData(args) {
256
293
  const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
257
294
  if (topAdjustment && topAdjustment >= bottomAdjustment) {
258
295
  data.segments.forEach((s) => {
259
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
296
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
297
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
260
298
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
261
299
  });
262
300
  data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
263
301
  }
264
302
  else if (bottomAdjustment) {
265
303
  data.segments.forEach((s) => {
266
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
304
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
305
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
267
306
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
268
307
  });
269
308
  data.center[1] += (bottomAdjustment - topAdjustment) / 2;
@@ -274,6 +313,14 @@ export function preparePieData(args) {
274
313
  series: items,
275
314
  allowOverlow: false,
276
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
+ }
277
324
  data.labels = labels;
278
325
  data.htmlLabels = htmlLabels;
279
326
  data.connectors = connectors;
@@ -1,4 +1,14 @@
1
1
  import type { CurveFactory } from 'd3';
2
+ import type { PointPosition } from '../../../types';
2
3
  import type { PreparedPieData, SegmentData } from './types';
3
4
  export declare const pieGenerator: import("d3-shape").Pie<any, SegmentData>;
4
5
  export declare function getCurveFactory(data: PreparedPieData): CurveFactory | undefined;
6
+ /**
7
+ * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
8
+ *
9
+ * The order of B and C does not affect the result.
10
+ *
11
+ * @see: https://en.wikipedia.org/wiki/Inscribed_angle
12
+ * @returns The angle in degrees, in the range [0, 180].
13
+ */
14
+ export declare function getInscribedAngle(a: PointPosition, b: PointPosition, c: PointPosition): number;
@@ -10,6 +10,25 @@ export function getCurveFactory(data) {
10
10
  case 'linear': {
11
11
  return curveLinear;
12
12
  }
13
+ default:
14
+ return undefined;
13
15
  }
14
- return undefined;
16
+ }
17
+ /**
18
+ * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
19
+ *
20
+ * The order of B and C does not affect the result.
21
+ *
22
+ * @see: https://en.wikipedia.org/wiki/Inscribed_angle
23
+ * @returns The angle in degrees, in the range [0, 180].
24
+ */
25
+ export function getInscribedAngle(a, b, c) {
26
+ const ux = b[0] - a[0];
27
+ const uy = b[1] - a[1];
28
+ const vx = c[0] - a[0];
29
+ const vy = c[1] - a[1];
30
+ const dot = ux * vx + uy * vy;
31
+ const cross = ux * vy - uy * vx;
32
+ const radians = Math.atan2(Math.abs(cross), dot);
33
+ return (radians * 180) / Math.PI;
15
34
  }
@@ -4,21 +4,27 @@ import { getLabelsSize } from '../../../utils';
4
4
  import { getFormattedValue } from '../../../utils/chart/format';
5
5
  const DEFAULT_PADDING = 1;
6
6
  function getLabels(args) {
7
- const { data, options: { html, padding, align }, } = args;
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], 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,12 +41,16 @@ function getLabels(args) {
35
41
  break;
36
42
  }
37
43
  }
44
+ const bottom = y + labelMaxHeight;
45
+ if (!html && bottom > d.y1) {
46
+ return;
47
+ }
38
48
  const item = html
39
49
  ? {
40
50
  content: label,
41
51
  x,
42
52
  y,
43
- size: { width: labelWidth, height: lineHeight },
53
+ size: { width: labelWidth, height: labelHeight },
44
54
  }
45
55
  : {
46
56
  text: label,
@@ -50,6 +60,8 @@ function getLabels(args) {
50
60
  nodeData: d.data,
51
61
  };
52
62
  acc.push(item);
63
+ prevLabelsHeight += labelHeight;
64
+ availableSpaceHeight = Math.max(0, availableSpaceHeight - labelHeight);
53
65
  });
54
66
  return acc;
55
67
  }, []);
@@ -120,7 +132,7 @@ export function prepareTreemapData(args) {
120
132
  const { html, style: dataLabelsStyle } = series.dataLabels;
121
133
  const labels = getLabels({ data: leaves, options: series.dataLabels });
122
134
  if (html) {
123
- const htmlItems = labels.map((l) => (Object.assign({ style: dataLabelsStyle }, l)));
135
+ const htmlItems = labels.map((l) => (Object.assign({ style: Object.assign(Object.assign({}, dataLabelsStyle), { maxWidth: l.size.width, maxHeight: l.size.height, overflow: 'hidden' }) }, l)));
124
136
  htmlElements.push(...htmlItems);
125
137
  }
126
138
  else {
@@ -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,17 +69,23 @@ 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;
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
- labelWrapper = container.append('div').style('position', 'absolute').node();
80
+ labelWrapper = container
81
+ .append('div')
82
+ .style('position', 'absolute')
83
+ .style('display', 'inline-block')
84
+ .style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
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 : '')
88
+ .node();
78
89
  const { height, width } = labels.reduce((acc, l) => {
79
90
  var _a, _b;
80
91
  if (labelWrapper) {
@@ -97,9 +108,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
97
108
  .attr('text-anchor', rotation > 0 ? 'start' : 'end')
98
109
  .style('transform', `rotate(${rotation}deg)`);
99
110
  }
100
- const rect = (_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
101
- result.maxWidth = (_b = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _b !== void 0 ? _b : 0;
102
- result.maxHeight = (_c = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _c !== void 0 ? _c : 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;
103
114
  }
104
115
  container.remove();
105
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';