@gravity-ui/charts 1.5.1 → 1.6.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 (55) hide show
  1. package/dist/cjs/components/ChartInner/index.js +3 -5
  2. package/dist/cjs/constants/defaults/data-labels.d.ts +2 -0
  3. package/dist/cjs/constants/defaults/data-labels.js +5 -0
  4. package/dist/cjs/constants/defaults/index.d.ts +1 -0
  5. package/dist/cjs/constants/defaults/index.js +1 -0
  6. package/dist/cjs/hooks/useSeries/constants.d.ts +1 -2
  7. package/dist/cjs/hooks/useSeries/constants.js +0 -5
  8. package/dist/cjs/hooks/useSeries/prepare-area.js +2 -1
  9. package/dist/cjs/hooks/useSeries/prepare-bar-x.js +2 -1
  10. package/dist/cjs/hooks/useSeries/prepare-bar-y.js +1 -1
  11. package/dist/cjs/hooks/useSeries/prepare-legend.js +1 -1
  12. package/dist/cjs/hooks/useSeries/prepare-line.js +2 -2
  13. package/dist/cjs/hooks/useSeries/prepare-pie.js +3 -2
  14. package/dist/cjs/hooks/useSeries/prepare-radar.js +2 -2
  15. package/dist/cjs/hooks/useSeries/prepare-sankey.js +1 -1
  16. package/dist/cjs/hooks/useSeries/prepare-treemap.js +2 -2
  17. package/dist/cjs/hooks/useSeries/prepare-waterfall.js +2 -2
  18. package/dist/cjs/hooks/useSeries/types.d.ts +1 -0
  19. package/dist/cjs/hooks/useShapes/HtmlLayer.js +2 -1
  20. package/dist/cjs/hooks/useShapes/pie/prepare-data.js +84 -30
  21. package/dist/cjs/hooks/useShapes/pie/utils.js +2 -1
  22. package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +19 -11
  23. package/dist/cjs/types/chart/pie.d.ts +8 -0
  24. package/dist/cjs/types/chart-ui.d.ts +1 -1
  25. package/dist/cjs/utils/chart/text.d.ts +1 -1
  26. package/dist/cjs/utils/chart/text.js +13 -7
  27. package/dist/cjs/utils/chart-ui/pie-center-text.js +5 -1
  28. package/dist/esm/components/ChartInner/index.js +3 -5
  29. package/dist/esm/constants/defaults/data-labels.d.ts +2 -0
  30. package/dist/esm/constants/defaults/data-labels.js +5 -0
  31. package/dist/esm/constants/defaults/index.d.ts +1 -0
  32. package/dist/esm/constants/defaults/index.js +1 -0
  33. package/dist/esm/hooks/useSeries/constants.d.ts +1 -2
  34. package/dist/esm/hooks/useSeries/constants.js +0 -5
  35. package/dist/esm/hooks/useSeries/prepare-area.js +2 -1
  36. package/dist/esm/hooks/useSeries/prepare-bar-x.js +2 -1
  37. package/dist/esm/hooks/useSeries/prepare-bar-y.js +1 -1
  38. package/dist/esm/hooks/useSeries/prepare-legend.js +1 -1
  39. package/dist/esm/hooks/useSeries/prepare-line.js +2 -2
  40. package/dist/esm/hooks/useSeries/prepare-pie.js +3 -2
  41. package/dist/esm/hooks/useSeries/prepare-radar.js +2 -2
  42. package/dist/esm/hooks/useSeries/prepare-sankey.js +1 -1
  43. package/dist/esm/hooks/useSeries/prepare-treemap.js +2 -2
  44. package/dist/esm/hooks/useSeries/prepare-waterfall.js +2 -2
  45. package/dist/esm/hooks/useSeries/types.d.ts +1 -0
  46. package/dist/esm/hooks/useShapes/HtmlLayer.js +2 -1
  47. package/dist/esm/hooks/useShapes/pie/prepare-data.js +84 -30
  48. package/dist/esm/hooks/useShapes/pie/utils.js +2 -1
  49. package/dist/esm/hooks/useShapes/treemap/prepare-data.js +19 -11
  50. package/dist/esm/types/chart/pie.d.ts +8 -0
  51. package/dist/esm/types/chart-ui.d.ts +1 -1
  52. package/dist/esm/utils/chart/text.d.ts +1 -1
  53. package/dist/esm/utils/chart/text.js +13 -7
  54. package/dist/esm/utils/chart-ui/pie-center-text.js +5 -1
  55. package/package.json +1 -1
@@ -59,8 +59,8 @@ export const ChartInner = (props) => {
59
59
  unpinTooltip === null || unpinTooltip === void 0 ? void 0 : unpinTooltip();
60
60
  }
61
61
  }, [prevWidth, width, prevHeight, height, tooltipPinned, unpinTooltip]);
62
- return (React.createElement(React.Fragment, null,
63
- React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick },
62
+ return (React.createElement("div", { className: b() },
63
+ React.createElement("svg", { ref: svgRef, width: width, height: height, onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick },
64
64
  title && React.createElement(Title, Object.assign({}, title, { chartWidth: width })),
65
65
  React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit.plots.map((plot, index) => {
66
66
  return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
@@ -72,8 +72,6 @@ export const ChartInner = (props) => {
72
72
  React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotRef: plotRef })))),
73
73
  shapes),
74
74
  preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip }))),
75
- React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef, style: {
76
- transform: `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
77
- } }),
75
+ React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef }),
78
76
  React.createElement(Tooltip, { dispatcher: dispatcher, tooltip: tooltip, svgContainer: svgRef.current, xAxis: xAxis, yAxis: yAxis[0], onOutsideClick: unpinTooltip, tooltipPinned: tooltipPinned })));
79
77
  };
@@ -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;
@@ -133,7 +133,7 @@ export const getLegendComponents = (args) => {
133
133
  const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight) - 1;
134
134
  const maxPage = Math.ceil(items.length / limit);
135
135
  pagination = { limit, maxPage };
136
- legendHeight = maxLegendHeight;
136
+ legendHeight = preparedLegend.lineHeight * (limit + 1);
137
137
  }
138
138
  preparedLegend.height = legendHeight;
139
139
  }
@@ -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: {
@@ -17,7 +17,8 @@ export const HtmlLayer = (props) => {
17
17
  return null;
18
18
  }
19
19
  return (React.createElement(Portal, { container: htmlLayout }, items.map((item, index) => {
20
- const style = Object.assign(Object.assign({}, item.style), { position: 'absolute', left: item.x, top: item.y });
20
+ var _a, _b, _c;
21
+ const style = Object.assign(Object.assign({}, item.style), { color: (_b = (_a = item.style) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : (_c = item.style) === null || _c === void 0 ? void 0 : _c.fontColor, position: 'absolute', left: item.x, top: item.y });
21
22
  return (React.createElement("div", { key: index, dangerouslySetInnerHTML: { __html: item.content }, style: style }));
22
23
  })));
23
24
  };
@@ -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) => {
@@ -148,10 +169,14 @@ export function preparePieData(args) {
148
169
  angle: midAngle,
149
170
  };
150
171
  if (!allowOverlow) {
151
- const labelLeftPosition = getLeftPosition(label);
152
- const newMaxWidth = labelLeftPosition > 0
153
- ? Math.min(boundsWidth / 2 - labelLeftPosition, labelWidth)
154
- : Math.min(labelWidth - (-labelLeftPosition - boundsWidth / 2), labelWidth);
172
+ const labelLeftPosition = shouldUseHtml ? label.x : getLeftPosition(label);
173
+ let newMaxWidth;
174
+ if (label.x > 0) {
175
+ newMaxWidth = Math.min(boundsWidth - data.center[0] - labelLeftPosition, labelWidth);
176
+ }
177
+ else {
178
+ newMaxWidth = Math.min(data.center[0] + labelLeftPosition + label.size.width, labelWidth);
179
+ }
155
180
  if (newMaxWidth !== label.maxWidth) {
156
181
  label.maxWidth = Math.max(0, newMaxWidth);
157
182
  }
@@ -173,11 +198,17 @@ export function preparePieData(args) {
173
198
  shouldAdjustAngle = false;
174
199
  }
175
200
  else {
176
- label.angle = newAngle;
177
201
  const [newX, newY] = getLabelPosition(newAngle);
202
+ label.angle = newAngle;
203
+ label.textAnchor = newAngle < Math.PI ? 'start' : 'end';
178
204
  label.x = newX;
179
205
  label.y = newY;
180
- const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
206
+ // See `getLabelPosition`: for HTML labels we return top-left,
207
+ // so shift x by labelWidth when textAnchor is 'end'.
208
+ const pointC = shouldUseHtml && label.textAnchor === 'end'
209
+ ? [newX + labelWidth, newY]
210
+ : [newX, newY];
211
+ const inscribedAngle = getInscribedAngle(pointA, pointB, pointC);
181
212
  if (inscribedAngle > 90) {
182
213
  shouldAdjustAngle = false;
183
214
  shouldStopLabelPlacement = true;
@@ -192,18 +223,19 @@ export function preparePieData(args) {
192
223
  }
193
224
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
194
225
  if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
226
+ labels.push(label);
195
227
  if (shouldUseHtml) {
228
+ const htmlLabelX = data.center[0] + label.x;
196
229
  htmlLabels.push({
197
- x: data.center[0] + label.x,
230
+ x: Math.max(0, htmlLabelX),
198
231
  y: Math.max(0, data.center[1] + label.y),
199
232
  content: label.text,
200
233
  size: label.size,
201
- style: label.style,
234
+ style: Object.assign(Object.assign({}, label.style), { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', maxWidth: Math.min(label.x > 0
235
+ ? boundsWidth - htmlLabelX
236
+ : htmlLabelX + label.size.width, label.size.width) }),
202
237
  });
203
238
  }
204
- else {
205
- labels.push(label);
206
- }
207
239
  const connector = {
208
240
  path: line(getConnectorPoints(label.angle)),
209
241
  color: relatedSegment.data.color,
@@ -212,12 +244,13 @@ export function preparePieData(args) {
212
244
  }
213
245
  });
214
246
  return {
215
- labels,
247
+ labels: shouldUseHtml ? [] : labels,
216
248
  htmlLabels,
217
249
  connectors,
218
250
  };
219
251
  };
220
252
  return Array.from(groupedPieSeries).map(([stackId, items]) => {
253
+ var _a;
221
254
  const data = prepareItem(stackId, items);
222
255
  const preparedLabels = prepareLabels({
223
256
  data,
@@ -237,6 +270,17 @@ export function preparePieData(args) {
237
270
  maxLeftRightFreeSpace = Math.max(0, Math.min(maxLeftRightFreeSpace, freeSpace));
238
271
  labelsOverflow = freeSpace < 0 ? Math.max(labelsOverflow, -freeSpace) : labelsOverflow;
239
272
  });
273
+ preparedLabels.htmlLabels.forEach((label) => {
274
+ let freeSpace = 0;
275
+ if (label.x < data.center[0]) {
276
+ freeSpace = Math.max(label.x, 0);
277
+ }
278
+ else {
279
+ freeSpace = boundsWidth - label.x - label.size.width;
280
+ }
281
+ maxLeftRightFreeSpace = Math.max(0, Math.min(maxLeftRightFreeSpace, freeSpace));
282
+ labelsOverflow = freeSpace < 0 ? Math.max(labelsOverflow, -freeSpace) : labelsOverflow;
283
+ });
240
284
  const segmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
241
285
  if (labelsOverflow) {
242
286
  data.segments.forEach((s) => {
@@ -251,7 +295,7 @@ export function preparePieData(args) {
251
295
  topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
252
296
  }
253
297
  if (preparedLabels.htmlLabels.length) {
254
- const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
298
+ const topHtmlLabel = Math.min(...preparedLabels.htmlLabels.map((l) => l.y));
255
299
  topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
256
300
  }
257
301
  let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
@@ -267,14 +311,16 @@ export function preparePieData(args) {
267
311
  const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
268
312
  if (topAdjustment && topAdjustment >= bottomAdjustment) {
269
313
  data.segments.forEach((s) => {
270
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
314
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
315
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
271
316
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
272
317
  });
273
318
  data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
274
319
  }
275
320
  else if (bottomAdjustment) {
276
321
  data.segments.forEach((s) => {
277
- const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
322
+ let nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
323
+ nextPossibleRadius = Math.max(nextPossibleRadius, minRadius);
278
324
  s.data.radius = Math.min(nextPossibleRadius, maxRadius);
279
325
  });
280
326
  data.center[1] += (bottomAdjustment - topAdjustment) / 2;
@@ -285,6 +331,14 @@ export function preparePieData(args) {
285
331
  series: items,
286
332
  allowOverlow: false,
287
333
  });
334
+ if (typeof ((_a = items[0]) === null || _a === void 0 ? void 0 : _a.innerRadius) !== 'undefined') {
335
+ const resultSegmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
336
+ const resultInnerRadius = calculateNumericProperty({
337
+ value: items[0].innerRadius,
338
+ base: resultSegmentMaxRadius,
339
+ }) || 0;
340
+ data.innerRadius = resultInnerRadius;
341
+ }
288
342
  data.labels = labels;
289
343
  data.htmlLabels = htmlLabels;
290
344
  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;
@@ -22,7 +22,7 @@ export interface HtmlItem {
22
22
  width: number;
23
23
  height: number;
24
24
  };
25
- style?: BaseTextStyle;
25
+ style?: BaseTextStyle & React.CSSProperties;
26
26
  }
27
27
  export interface ShapeDataWithHtmlItems {
28
28
  htmlElements: HtmlItem[];
@@ -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;