@gravity-ui/charts 0.6.0 → 0.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.
@@ -4,7 +4,7 @@ import { useTooltip } from '../../hooks';
4
4
  import { block } from '../../utils';
5
5
  import { ChartTooltipContent } from './ChartTooltipContent';
6
6
  import './styles.css';
7
- const b = block('d3-tooltip');
7
+ const b = block('tooltip');
8
8
  export const Tooltip = (props) => {
9
9
  const { tooltip, xAxis, yAxis, svgContainer, dispatcher, tooltipPinned, onOutsideClick } = props;
10
10
  const { hovered, pointerPosition } = useTooltip({ dispatcher, tooltip });
@@ -21,7 +21,7 @@ export const Tooltip = (props) => {
21
21
  React.useEffect(() => {
22
22
  window.dispatchEvent(new CustomEvent('scroll'));
23
23
  }, [left, top]);
24
- return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b({ pinned: tooltipPinned }), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }], onOutsideClick: tooltipPinned ? handleOutsideClick : undefined },
24
+ return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b({ pinned: tooltipPinned }), contentClassName: b('popup-content'), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }], onOutsideClick: tooltipPinned ? handleOutsideClick : undefined },
25
25
  React.createElement("div", { className: b('content') },
26
26
  React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer })))) : null;
27
27
  };
@@ -1,28 +1,30 @@
1
- .gcharts-d3-tooltip[class] {
1
+ .gcharts-tooltip[class] {
2
2
  --g-popup-border-width: 0;
3
3
  pointer-events: none;
4
4
  }
5
- .gcharts-d3-tooltip[class] > div {
5
+ .gcharts-tooltip[class] > div {
6
6
  animation-duration: unset;
7
7
  animation-timing-function: unset;
8
8
  animation-fill-mode: unset;
9
9
  }
10
- .gcharts-d3-tooltip[class].gcharts-d3-tooltip_pinned {
10
+ .gcharts-tooltip[class].gcharts-tooltip_pinned {
11
11
  pointer-events: inherit;
12
12
  }
13
- .gcharts-d3-tooltip__content {
14
- padding: 10px 14px;
13
+ .gcharts-tooltip__popup-content {
14
+ box-shadow: none;
15
+ }
16
+ .gcharts-tooltip__content {
17
+ padding: 8px 14px;
15
18
  text-wrap: nowrap;
16
- border: 1px solid var(--g-color-line-generic);
17
- border-radius: 3px;
19
+ border-radius: 4px;
18
20
  background-color: var(--g-color-infographics-tooltip-bg);
19
21
  box-shadow: 0 2px 12px var(--g-color-sfx-shadow);
20
22
  }
21
- .gcharts-d3-tooltip__content-row {
23
+ .gcharts-tooltip__content-row {
22
24
  display: flex;
23
25
  align-items: center;
24
26
  }
25
- .gcharts-d3-tooltip__color {
27
+ .gcharts-tooltip__color {
26
28
  display: inline-block;
27
29
  width: 16px;
28
30
  height: 8px;
@@ -102,10 +102,14 @@ export const prepareAreaData = (args) => {
102
102
  const labelItems = points.map((p) => getLabelData(p, s, xMax));
103
103
  if (s.dataLabels.html) {
104
104
  const htmlLabels = labelItems.map((l) => {
105
+ var _a;
106
+ const style = (_a = l.style) !== null && _a !== void 0 ? _a : s.dataLabels.style;
107
+ const labelSize = getLabelsSize({ labels: [l.text], style, html: true });
105
108
  return {
106
109
  x: l.x - l.size.width / 2,
107
110
  y: l.y,
108
111
  content: l.text,
112
+ size: { width: labelSize.maxWidth, height: labelSize.maxHeight },
109
113
  };
110
114
  });
111
115
  htmlElements.push(...htmlLabels);
@@ -136,6 +136,7 @@ export const prepareBarXData = (args) => {
136
136
  x: label.x,
137
137
  y: label.y,
138
138
  content: label.text,
139
+ size: label.size,
139
140
  });
140
141
  }
141
142
  else {
@@ -69,6 +69,7 @@ function setLabel(prepared) {
69
69
  x,
70
70
  y: y - height / 2,
71
71
  content,
72
+ size: { width, height },
72
73
  });
73
74
  }
74
75
  else {
@@ -33,6 +33,7 @@ function getHtmlLabel(point, series, xMax) {
33
33
  x: Math.min(xMax - size.maxWidth, Math.max(0, point.x)),
34
34
  y: Math.max(0, point.y - series.dataLabels.padding - size.maxHeight),
35
35
  content,
36
+ size: { width: size.maxWidth, height: size.maxHeight },
36
37
  };
37
38
  }
38
39
  export const prepareLineData = (args) => {
@@ -165,7 +165,8 @@ export function PieSeriesShapes(args) {
165
165
  dispatcher.on(eventName, null);
166
166
  };
167
167
  }, [dispatcher, preparedData, seriesOptions]);
168
+ const htmlElements = preparedData.map((d) => d.htmlLabels).flat();
168
169
  return (React.createElement(React.Fragment, null,
169
170
  React.createElement("g", { ref: ref, className: b(), style: { zIndex: 9 } }),
170
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
171
+ React.createElement(HtmlLayer, { preparedData: { htmlElements }, htmlLayout: htmlLayout })));
171
172
  }
@@ -18,7 +18,7 @@ export function preparePieData(args) {
18
18
  const { series: preparedSeries, boundsWidth, boundsHeight } = args;
19
19
  const maxRadius = Math.min(boundsWidth, boundsHeight) / 2;
20
20
  const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
21
- return Array.from(groupedPieSeries).map(([stackId, items]) => {
21
+ const prepareItem = (stackId, items) => {
22
22
  var _a, _b, _c;
23
23
  const series = items[0];
24
24
  const { center, borderWidth, borderColor, borderRadius, radius: seriesRadius, innerRadius: seriesInnerRadius, dataLabels, } = series;
@@ -30,6 +30,7 @@ export function preparePieData(args) {
30
30
  radius,
31
31
  segments: [],
32
32
  labels: [],
33
+ htmlLabels: [],
33
34
  connectors: [],
34
35
  borderColor,
35
36
  borderWidth,
@@ -41,7 +42,6 @@ export function preparePieData(args) {
41
42
  opacity: series.states.hover.halo.opacity,
42
43
  size: series.states.hover.halo.size,
43
44
  },
44
- htmlElements: [],
45
45
  };
46
46
  const segments = items.map((item) => {
47
47
  return {
@@ -55,132 +55,180 @@ export function preparePieData(args) {
55
55
  };
56
56
  });
57
57
  data.segments = pieGenerator(segments);
58
- let line = lineGenerator();
59
- const curveFactory = getCurveFactory(data);
60
- if (curveFactory) {
61
- line = line.curve(curveFactory);
62
- }
63
58
  if (dataLabels.enabled) {
64
59
  const { style, connectorPadding, distance } = dataLabels;
65
60
  const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
66
- const minSegmentRadius = maxRadius - connectorPadding - distance - labelHeight;
61
+ const minSegmentRadius = maxRadius - distance - connectorPadding - labelHeight;
67
62
  if (data.radius > minSegmentRadius) {
68
63
  data.radius = minSegmentRadius;
69
64
  data.innerRadius =
70
65
  (_c = calculateNumericProperty({ value: seriesInnerRadius, base: data.radius })) !== null && _c !== void 0 ? _c : 0;
71
66
  }
72
- const connectorStartPointGenerator = arc()
73
- .innerRadius(data.radius)
74
- .outerRadius(data.radius);
75
- const connectorMidPointRadius = data.radius + distance / 2;
76
- const connectorMidPointGenerator = arc()
77
- .innerRadius(connectorMidPointRadius)
78
- .outerRadius(connectorMidPointRadius);
79
- const connectorArcRadius = data.radius + distance;
80
- const connectorEndPointGenerator = arc()
81
- .innerRadius(connectorArcRadius)
82
- .outerRadius(connectorArcRadius);
83
- const labelArcRadius = connectorArcRadius + connectorPadding;
84
- const labelArcGenerator = arc()
85
- .innerRadius(labelArcRadius)
86
- .outerRadius(labelArcRadius);
87
- const labels = [];
88
- items.forEach((d, index) => {
89
- const prevLabel = labels[labels.length - 1];
90
- const text = String(d.data.label || d.data.value);
91
- const shouldUseHtml = dataLabels.html;
92
- const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
93
- const labelWidth = labelSize.maxWidth;
94
- const relatedSegment = data.segments[index];
95
- const getLabelPosition = (angle) => {
96
- let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
67
+ }
68
+ return data;
69
+ };
70
+ const prepareLabels = (prepareLabelsArgs) => {
71
+ const { data, series } = prepareLabelsArgs;
72
+ const { dataLabels } = series[0];
73
+ const labels = [];
74
+ const htmlLabels = [];
75
+ const connectors = [];
76
+ if (!dataLabels.enabled) {
77
+ return { labels, htmlLabels, connectors };
78
+ }
79
+ let line = lineGenerator();
80
+ const curveFactory = getCurveFactory(data);
81
+ if (curveFactory) {
82
+ line = line.curve(curveFactory);
83
+ }
84
+ const { style, connectorPadding, distance } = dataLabels;
85
+ const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
86
+ const connectorStartPointGenerator = arc()
87
+ .innerRadius(data.radius)
88
+ .outerRadius(data.radius);
89
+ const connectorMidPointRadius = data.radius + distance / 2;
90
+ const connectorMidPointGenerator = arc()
91
+ .innerRadius(connectorMidPointRadius)
92
+ .outerRadius(connectorMidPointRadius);
93
+ const connectorArcRadius = data.radius + distance;
94
+ const connectorEndPointGenerator = arc()
95
+ .innerRadius(connectorArcRadius)
96
+ .outerRadius(connectorArcRadius);
97
+ const labelArcRadius = connectorArcRadius + connectorPadding;
98
+ const labelArcGenerator = arc()
99
+ .innerRadius(labelArcRadius)
100
+ .outerRadius(labelArcRadius);
101
+ series.forEach((d, index) => {
102
+ const prevLabel = labels[labels.length - 1];
103
+ const text = String(d.data.label || d.data.value);
104
+ const shouldUseHtml = dataLabels.html;
105
+ const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
106
+ const labelWidth = labelSize.maxWidth;
107
+ const relatedSegment = data.segments[index];
108
+ const getLabelPosition = (angle) => {
109
+ let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
110
+ if (shouldUseHtml) {
111
+ x = x < 0 ? x - labelWidth : x;
112
+ y = y - labelSize.maxHeight;
113
+ }
114
+ else {
97
115
  y = y < 0 ? y - labelHeight : y;
98
- if (shouldUseHtml) {
99
- x = x < 0 ? x - labelWidth : x;
100
- }
101
- x = Math.max(-boundsWidth / 2, x);
102
- return [x, y];
103
- };
104
- const getConnectorPoints = (angle) => {
105
- const connectorStartPoint = connectorStartPointGenerator.centroid(relatedSegment);
106
- const connectorEndPoint = connectorEndPointGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
107
- if (dataLabels.connectorShape === 'straight-line') {
108
- return [connectorStartPoint, connectorEndPoint];
109
- }
110
- const connectorMidPoint = connectorMidPointGenerator.centroid(relatedSegment);
111
- return [connectorStartPoint, connectorMidPoint, connectorEndPoint];
112
- };
113
- const midAngle = Math.max((prevLabel === null || prevLabel === void 0 ? void 0 : prevLabel.angle) || 0, relatedSegment.startAngle +
114
- (relatedSegment.endAngle - relatedSegment.startAngle) / 2);
115
- const [x, y] = getLabelPosition(midAngle);
116
- const label = {
117
- text,
118
- x,
119
- y,
120
- style,
121
- size: { width: labelWidth, height: labelHeight },
122
- maxWidth: labelWidth,
123
- textAnchor: midAngle < Math.PI ? 'start' : 'end',
124
- series: { id: d.id },
125
- active: true,
126
- segment: relatedSegment.data,
127
- angle: midAngle,
128
- };
129
- let overlap = false;
130
- if (prevLabel) {
131
- overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
132
- if (overlap) {
133
- let shouldAdjustAngle = true;
134
- const step = Math.PI / 180;
135
- while (shouldAdjustAngle) {
136
- const newAngle = label.angle + step;
137
- if (newAngle > FULL_CIRCLE &&
138
- newAngle % FULL_CIRCLE > labels[0].angle) {
116
+ }
117
+ x = Math.max(-boundsWidth / 2, x);
118
+ return [x, y];
119
+ };
120
+ const getConnectorPoints = (angle) => {
121
+ const connectorStartPoint = connectorStartPointGenerator.centroid(relatedSegment);
122
+ const connectorEndPoint = connectorEndPointGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
123
+ if (dataLabels.connectorShape === 'straight-line') {
124
+ return [connectorStartPoint, connectorEndPoint];
125
+ }
126
+ const connectorMidPoint = connectorMidPointGenerator.centroid(relatedSegment);
127
+ return [connectorStartPoint, connectorMidPoint, connectorEndPoint];
128
+ };
129
+ const midAngle = Math.max((prevLabel === null || prevLabel === void 0 ? void 0 : prevLabel.angle) || 0, relatedSegment.startAngle +
130
+ (relatedSegment.endAngle - relatedSegment.startAngle) / 2);
131
+ const [x, y] = getLabelPosition(midAngle);
132
+ const label = {
133
+ text,
134
+ x,
135
+ y,
136
+ style,
137
+ size: { width: labelWidth, height: labelHeight },
138
+ maxWidth: labelWidth,
139
+ textAnchor: midAngle < Math.PI ? 'start' : 'end',
140
+ series: { id: d.id },
141
+ active: true,
142
+ segment: relatedSegment.data,
143
+ angle: midAngle,
144
+ };
145
+ let overlap = false;
146
+ if (prevLabel) {
147
+ overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
148
+ if (overlap) {
149
+ let shouldAdjustAngle = true;
150
+ const step = Math.PI / 180;
151
+ while (shouldAdjustAngle) {
152
+ const newAngle = label.angle + step;
153
+ if (newAngle > FULL_CIRCLE && newAngle % FULL_CIRCLE > labels[0].angle) {
154
+ shouldAdjustAngle = false;
155
+ }
156
+ else {
157
+ label.angle = newAngle;
158
+ const [newX, newY] = getLabelPosition(newAngle);
159
+ label.x = newX;
160
+ label.y = newY;
161
+ if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
139
162
  shouldAdjustAngle = false;
140
- }
141
- else {
142
- label.angle = newAngle;
143
- const [newX, newY] = getLabelPosition(newAngle);
144
- label.x = newX;
145
- label.y = newY;
146
- if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
147
- shouldAdjustAngle = false;
148
- overlap = false;
149
- }
163
+ overlap = false;
150
164
  }
151
165
  }
152
166
  }
153
167
  }
154
- if (dataLabels.allowOverlap || !overlap) {
155
- const left = getLeftPosition(label);
156
- if (Math.abs(left) > boundsWidth / 2) {
157
- label.maxWidth = label.size.width - (Math.abs(left) - boundsWidth / 2);
158
- }
159
- else {
160
- const right = left + label.size.width;
161
- if (right > boundsWidth / 2) {
162
- label.maxWidth = label.size.width - (right - boundsWidth / 2);
163
- }
164
- }
165
- if (shouldUseHtml) {
166
- data.htmlElements.push({
167
- x: boundsWidth / 2 + label.x,
168
- y: boundsHeight / 2 + label.y,
169
- content: label.text,
170
- });
171
- }
172
- else {
173
- labels.push(label);
168
+ }
169
+ if (dataLabels.allowOverlap || !overlap) {
170
+ const left = getLeftPosition(label);
171
+ if (Math.abs(left) > boundsWidth / 2) {
172
+ label.maxWidth = label.size.width - (Math.abs(left) - boundsWidth / 2);
173
+ }
174
+ else {
175
+ const right = left + label.size.width;
176
+ if (right > boundsWidth / 2) {
177
+ label.maxWidth = label.size.width - (right - boundsWidth / 2);
174
178
  }
175
- const connector = {
176
- path: line(getConnectorPoints(midAngle)),
177
- color: relatedSegment.data.color,
178
- };
179
- data.connectors.push(connector);
180
179
  }
181
- });
182
- data.labels = labels;
180
+ if (shouldUseHtml) {
181
+ htmlLabels.push({
182
+ x: boundsWidth / 2 + label.x,
183
+ y: boundsHeight / 2 + label.y,
184
+ content: label.text,
185
+ size: label.size,
186
+ });
187
+ }
188
+ else {
189
+ labels.push(label);
190
+ }
191
+ const connector = {
192
+ path: line(getConnectorPoints(midAngle)),
193
+ color: relatedSegment.data.color,
194
+ };
195
+ connectors.push(connector);
196
+ }
197
+ });
198
+ return {
199
+ labels,
200
+ htmlLabels,
201
+ connectors,
202
+ };
203
+ };
204
+ return Array.from(groupedPieSeries).map(([stackId, items]) => {
205
+ const data = prepareItem(stackId, items);
206
+ const preparedLabels = prepareLabels({
207
+ data,
208
+ series: items,
209
+ });
210
+ const allPreparedLabels = [...preparedLabels.labels, ...preparedLabels.htmlLabels];
211
+ const top = Math.min(data.center[1] - data.radius, ...allPreparedLabels.map((l) => l.y + data.center[1]));
212
+ const bottom = Math.max(data.center[1] + data.radius, ...allPreparedLabels.map((l) => data.center[1] + l.y + l.size.height));
213
+ const topAdjustment = Math.floor(top - data.halo.size);
214
+ if (topAdjustment > 0) {
215
+ // should adjust top position and height
216
+ data.radius += topAdjustment / 2;
217
+ data.center[1] -= topAdjustment / 2;
183
218
  }
219
+ const bottomAdjustment = Math.floor(boundsHeight - bottom - data.halo.size);
220
+ if (bottomAdjustment > 0) {
221
+ // should adjust position and radius
222
+ data.radius += bottomAdjustment / 2;
223
+ data.center[1] += bottomAdjustment / 2;
224
+ }
225
+ const { labels, htmlLabels, connectors } = prepareLabels({
226
+ data,
227
+ series: items,
228
+ });
229
+ data.labels = labels;
230
+ data.htmlLabels = htmlLabels;
231
+ data.connectors = connectors;
184
232
  return data;
185
233
  });
186
234
  }
@@ -37,5 +37,5 @@ export type PreparedPieData = {
37
37
  opacity: number;
38
38
  size: number;
39
39
  };
40
- htmlElements: HtmlItem[];
40
+ htmlLabels: HtmlItem[];
41
41
  };
@@ -33,6 +33,7 @@ function getLabels(args) {
33
33
  content: text,
34
34
  x,
35
35
  y,
36
+ size: { width, height: lineHeight },
36
37
  }
37
38
  : {
38
39
  text,
@@ -18,6 +18,10 @@ export type HtmlItem = {
18
18
  x: number;
19
19
  y: number;
20
20
  content: string;
21
+ size: {
22
+ width: number;
23
+ height: number;
24
+ };
21
25
  };
22
26
  export type ShapeDataWithHtmlItems = {
23
27
  htmlElements: HtmlItem[];
@@ -4,7 +4,7 @@ import { useTooltip } from '../../hooks';
4
4
  import { block } from '../../utils';
5
5
  import { ChartTooltipContent } from './ChartTooltipContent';
6
6
  import './styles.css';
7
- const b = block('d3-tooltip');
7
+ const b = block('tooltip');
8
8
  export const Tooltip = (props) => {
9
9
  const { tooltip, xAxis, yAxis, svgContainer, dispatcher, tooltipPinned, onOutsideClick } = props;
10
10
  const { hovered, pointerPosition } = useTooltip({ dispatcher, tooltip });
@@ -21,7 +21,7 @@ export const Tooltip = (props) => {
21
21
  React.useEffect(() => {
22
22
  window.dispatchEvent(new CustomEvent('scroll'));
23
23
  }, [left, top]);
24
- return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b({ pinned: tooltipPinned }), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }], onOutsideClick: tooltipPinned ? handleOutsideClick : undefined },
24
+ return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { className: b({ pinned: tooltipPinned }), contentClassName: b('popup-content'), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }], onOutsideClick: tooltipPinned ? handleOutsideClick : undefined },
25
25
  React.createElement("div", { className: b('content') },
26
26
  React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer })))) : null;
27
27
  };
@@ -1,28 +1,30 @@
1
- .gcharts-d3-tooltip[class] {
1
+ .gcharts-tooltip[class] {
2
2
  --g-popup-border-width: 0;
3
3
  pointer-events: none;
4
4
  }
5
- .gcharts-d3-tooltip[class] > div {
5
+ .gcharts-tooltip[class] > div {
6
6
  animation-duration: unset;
7
7
  animation-timing-function: unset;
8
8
  animation-fill-mode: unset;
9
9
  }
10
- .gcharts-d3-tooltip[class].gcharts-d3-tooltip_pinned {
10
+ .gcharts-tooltip[class].gcharts-tooltip_pinned {
11
11
  pointer-events: inherit;
12
12
  }
13
- .gcharts-d3-tooltip__content {
14
- padding: 10px 14px;
13
+ .gcharts-tooltip__popup-content {
14
+ box-shadow: none;
15
+ }
16
+ .gcharts-tooltip__content {
17
+ padding: 8px 14px;
15
18
  text-wrap: nowrap;
16
- border: 1px solid var(--g-color-line-generic);
17
- border-radius: 3px;
19
+ border-radius: 4px;
18
20
  background-color: var(--g-color-infographics-tooltip-bg);
19
21
  box-shadow: 0 2px 12px var(--g-color-sfx-shadow);
20
22
  }
21
- .gcharts-d3-tooltip__content-row {
23
+ .gcharts-tooltip__content-row {
22
24
  display: flex;
23
25
  align-items: center;
24
26
  }
25
- .gcharts-d3-tooltip__color {
27
+ .gcharts-tooltip__color {
26
28
  display: inline-block;
27
29
  width: 16px;
28
30
  height: 8px;
@@ -102,10 +102,14 @@ export const prepareAreaData = (args) => {
102
102
  const labelItems = points.map((p) => getLabelData(p, s, xMax));
103
103
  if (s.dataLabels.html) {
104
104
  const htmlLabels = labelItems.map((l) => {
105
+ var _a;
106
+ const style = (_a = l.style) !== null && _a !== void 0 ? _a : s.dataLabels.style;
107
+ const labelSize = getLabelsSize({ labels: [l.text], style, html: true });
105
108
  return {
106
109
  x: l.x - l.size.width / 2,
107
110
  y: l.y,
108
111
  content: l.text,
112
+ size: { width: labelSize.maxWidth, height: labelSize.maxHeight },
109
113
  };
110
114
  });
111
115
  htmlElements.push(...htmlLabels);
@@ -136,6 +136,7 @@ export const prepareBarXData = (args) => {
136
136
  x: label.x,
137
137
  y: label.y,
138
138
  content: label.text,
139
+ size: label.size,
139
140
  });
140
141
  }
141
142
  else {
@@ -69,6 +69,7 @@ function setLabel(prepared) {
69
69
  x,
70
70
  y: y - height / 2,
71
71
  content,
72
+ size: { width, height },
72
73
  });
73
74
  }
74
75
  else {
@@ -33,6 +33,7 @@ function getHtmlLabel(point, series, xMax) {
33
33
  x: Math.min(xMax - size.maxWidth, Math.max(0, point.x)),
34
34
  y: Math.max(0, point.y - series.dataLabels.padding - size.maxHeight),
35
35
  content,
36
+ size: { width: size.maxWidth, height: size.maxHeight },
36
37
  };
37
38
  }
38
39
  export const prepareLineData = (args) => {
@@ -165,7 +165,8 @@ export function PieSeriesShapes(args) {
165
165
  dispatcher.on(eventName, null);
166
166
  };
167
167
  }, [dispatcher, preparedData, seriesOptions]);
168
+ const htmlElements = preparedData.map((d) => d.htmlLabels).flat();
168
169
  return (React.createElement(React.Fragment, null,
169
170
  React.createElement("g", { ref: ref, className: b(), style: { zIndex: 9 } }),
170
- React.createElement(HtmlLayer, { preparedData: preparedData, htmlLayout: htmlLayout })));
171
+ React.createElement(HtmlLayer, { preparedData: { htmlElements }, htmlLayout: htmlLayout })));
171
172
  }
@@ -18,7 +18,7 @@ export function preparePieData(args) {
18
18
  const { series: preparedSeries, boundsWidth, boundsHeight } = args;
19
19
  const maxRadius = Math.min(boundsWidth, boundsHeight) / 2;
20
20
  const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
21
- return Array.from(groupedPieSeries).map(([stackId, items]) => {
21
+ const prepareItem = (stackId, items) => {
22
22
  var _a, _b, _c;
23
23
  const series = items[0];
24
24
  const { center, borderWidth, borderColor, borderRadius, radius: seriesRadius, innerRadius: seriesInnerRadius, dataLabels, } = series;
@@ -30,6 +30,7 @@ export function preparePieData(args) {
30
30
  radius,
31
31
  segments: [],
32
32
  labels: [],
33
+ htmlLabels: [],
33
34
  connectors: [],
34
35
  borderColor,
35
36
  borderWidth,
@@ -41,7 +42,6 @@ export function preparePieData(args) {
41
42
  opacity: series.states.hover.halo.opacity,
42
43
  size: series.states.hover.halo.size,
43
44
  },
44
- htmlElements: [],
45
45
  };
46
46
  const segments = items.map((item) => {
47
47
  return {
@@ -55,132 +55,180 @@ export function preparePieData(args) {
55
55
  };
56
56
  });
57
57
  data.segments = pieGenerator(segments);
58
- let line = lineGenerator();
59
- const curveFactory = getCurveFactory(data);
60
- if (curveFactory) {
61
- line = line.curve(curveFactory);
62
- }
63
58
  if (dataLabels.enabled) {
64
59
  const { style, connectorPadding, distance } = dataLabels;
65
60
  const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
66
- const minSegmentRadius = maxRadius - connectorPadding - distance - labelHeight;
61
+ const minSegmentRadius = maxRadius - distance - connectorPadding - labelHeight;
67
62
  if (data.radius > minSegmentRadius) {
68
63
  data.radius = minSegmentRadius;
69
64
  data.innerRadius =
70
65
  (_c = calculateNumericProperty({ value: seriesInnerRadius, base: data.radius })) !== null && _c !== void 0 ? _c : 0;
71
66
  }
72
- const connectorStartPointGenerator = arc()
73
- .innerRadius(data.radius)
74
- .outerRadius(data.radius);
75
- const connectorMidPointRadius = data.radius + distance / 2;
76
- const connectorMidPointGenerator = arc()
77
- .innerRadius(connectorMidPointRadius)
78
- .outerRadius(connectorMidPointRadius);
79
- const connectorArcRadius = data.radius + distance;
80
- const connectorEndPointGenerator = arc()
81
- .innerRadius(connectorArcRadius)
82
- .outerRadius(connectorArcRadius);
83
- const labelArcRadius = connectorArcRadius + connectorPadding;
84
- const labelArcGenerator = arc()
85
- .innerRadius(labelArcRadius)
86
- .outerRadius(labelArcRadius);
87
- const labels = [];
88
- items.forEach((d, index) => {
89
- const prevLabel = labels[labels.length - 1];
90
- const text = String(d.data.label || d.data.value);
91
- const shouldUseHtml = dataLabels.html;
92
- const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
93
- const labelWidth = labelSize.maxWidth;
94
- const relatedSegment = data.segments[index];
95
- const getLabelPosition = (angle) => {
96
- let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
67
+ }
68
+ return data;
69
+ };
70
+ const prepareLabels = (prepareLabelsArgs) => {
71
+ const { data, series } = prepareLabelsArgs;
72
+ const { dataLabels } = series[0];
73
+ const labels = [];
74
+ const htmlLabels = [];
75
+ const connectors = [];
76
+ if (!dataLabels.enabled) {
77
+ return { labels, htmlLabels, connectors };
78
+ }
79
+ let line = lineGenerator();
80
+ const curveFactory = getCurveFactory(data);
81
+ if (curveFactory) {
82
+ line = line.curve(curveFactory);
83
+ }
84
+ const { style, connectorPadding, distance } = dataLabels;
85
+ const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
86
+ const connectorStartPointGenerator = arc()
87
+ .innerRadius(data.radius)
88
+ .outerRadius(data.radius);
89
+ const connectorMidPointRadius = data.radius + distance / 2;
90
+ const connectorMidPointGenerator = arc()
91
+ .innerRadius(connectorMidPointRadius)
92
+ .outerRadius(connectorMidPointRadius);
93
+ const connectorArcRadius = data.radius + distance;
94
+ const connectorEndPointGenerator = arc()
95
+ .innerRadius(connectorArcRadius)
96
+ .outerRadius(connectorArcRadius);
97
+ const labelArcRadius = connectorArcRadius + connectorPadding;
98
+ const labelArcGenerator = arc()
99
+ .innerRadius(labelArcRadius)
100
+ .outerRadius(labelArcRadius);
101
+ series.forEach((d, index) => {
102
+ const prevLabel = labels[labels.length - 1];
103
+ const text = String(d.data.label || d.data.value);
104
+ const shouldUseHtml = dataLabels.html;
105
+ const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
106
+ const labelWidth = labelSize.maxWidth;
107
+ const relatedSegment = data.segments[index];
108
+ const getLabelPosition = (angle) => {
109
+ let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
110
+ if (shouldUseHtml) {
111
+ x = x < 0 ? x - labelWidth : x;
112
+ y = y - labelSize.maxHeight;
113
+ }
114
+ else {
97
115
  y = y < 0 ? y - labelHeight : y;
98
- if (shouldUseHtml) {
99
- x = x < 0 ? x - labelWidth : x;
100
- }
101
- x = Math.max(-boundsWidth / 2, x);
102
- return [x, y];
103
- };
104
- const getConnectorPoints = (angle) => {
105
- const connectorStartPoint = connectorStartPointGenerator.centroid(relatedSegment);
106
- const connectorEndPoint = connectorEndPointGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
107
- if (dataLabels.connectorShape === 'straight-line') {
108
- return [connectorStartPoint, connectorEndPoint];
109
- }
110
- const connectorMidPoint = connectorMidPointGenerator.centroid(relatedSegment);
111
- return [connectorStartPoint, connectorMidPoint, connectorEndPoint];
112
- };
113
- const midAngle = Math.max((prevLabel === null || prevLabel === void 0 ? void 0 : prevLabel.angle) || 0, relatedSegment.startAngle +
114
- (relatedSegment.endAngle - relatedSegment.startAngle) / 2);
115
- const [x, y] = getLabelPosition(midAngle);
116
- const label = {
117
- text,
118
- x,
119
- y,
120
- style,
121
- size: { width: labelWidth, height: labelHeight },
122
- maxWidth: labelWidth,
123
- textAnchor: midAngle < Math.PI ? 'start' : 'end',
124
- series: { id: d.id },
125
- active: true,
126
- segment: relatedSegment.data,
127
- angle: midAngle,
128
- };
129
- let overlap = false;
130
- if (prevLabel) {
131
- overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
132
- if (overlap) {
133
- let shouldAdjustAngle = true;
134
- const step = Math.PI / 180;
135
- while (shouldAdjustAngle) {
136
- const newAngle = label.angle + step;
137
- if (newAngle > FULL_CIRCLE &&
138
- newAngle % FULL_CIRCLE > labels[0].angle) {
116
+ }
117
+ x = Math.max(-boundsWidth / 2, x);
118
+ return [x, y];
119
+ };
120
+ const getConnectorPoints = (angle) => {
121
+ const connectorStartPoint = connectorStartPointGenerator.centroid(relatedSegment);
122
+ const connectorEndPoint = connectorEndPointGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
123
+ if (dataLabels.connectorShape === 'straight-line') {
124
+ return [connectorStartPoint, connectorEndPoint];
125
+ }
126
+ const connectorMidPoint = connectorMidPointGenerator.centroid(relatedSegment);
127
+ return [connectorStartPoint, connectorMidPoint, connectorEndPoint];
128
+ };
129
+ const midAngle = Math.max((prevLabel === null || prevLabel === void 0 ? void 0 : prevLabel.angle) || 0, relatedSegment.startAngle +
130
+ (relatedSegment.endAngle - relatedSegment.startAngle) / 2);
131
+ const [x, y] = getLabelPosition(midAngle);
132
+ const label = {
133
+ text,
134
+ x,
135
+ y,
136
+ style,
137
+ size: { width: labelWidth, height: labelHeight },
138
+ maxWidth: labelWidth,
139
+ textAnchor: midAngle < Math.PI ? 'start' : 'end',
140
+ series: { id: d.id },
141
+ active: true,
142
+ segment: relatedSegment.data,
143
+ angle: midAngle,
144
+ };
145
+ let overlap = false;
146
+ if (prevLabel) {
147
+ overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
148
+ if (overlap) {
149
+ let shouldAdjustAngle = true;
150
+ const step = Math.PI / 180;
151
+ while (shouldAdjustAngle) {
152
+ const newAngle = label.angle + step;
153
+ if (newAngle > FULL_CIRCLE && newAngle % FULL_CIRCLE > labels[0].angle) {
154
+ shouldAdjustAngle = false;
155
+ }
156
+ else {
157
+ label.angle = newAngle;
158
+ const [newX, newY] = getLabelPosition(newAngle);
159
+ label.x = newX;
160
+ label.y = newY;
161
+ if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
139
162
  shouldAdjustAngle = false;
140
- }
141
- else {
142
- label.angle = newAngle;
143
- const [newX, newY] = getLabelPosition(newAngle);
144
- label.x = newX;
145
- label.y = newY;
146
- if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
147
- shouldAdjustAngle = false;
148
- overlap = false;
149
- }
163
+ overlap = false;
150
164
  }
151
165
  }
152
166
  }
153
167
  }
154
- if (dataLabels.allowOverlap || !overlap) {
155
- const left = getLeftPosition(label);
156
- if (Math.abs(left) > boundsWidth / 2) {
157
- label.maxWidth = label.size.width - (Math.abs(left) - boundsWidth / 2);
158
- }
159
- else {
160
- const right = left + label.size.width;
161
- if (right > boundsWidth / 2) {
162
- label.maxWidth = label.size.width - (right - boundsWidth / 2);
163
- }
164
- }
165
- if (shouldUseHtml) {
166
- data.htmlElements.push({
167
- x: boundsWidth / 2 + label.x,
168
- y: boundsHeight / 2 + label.y,
169
- content: label.text,
170
- });
171
- }
172
- else {
173
- labels.push(label);
168
+ }
169
+ if (dataLabels.allowOverlap || !overlap) {
170
+ const left = getLeftPosition(label);
171
+ if (Math.abs(left) > boundsWidth / 2) {
172
+ label.maxWidth = label.size.width - (Math.abs(left) - boundsWidth / 2);
173
+ }
174
+ else {
175
+ const right = left + label.size.width;
176
+ if (right > boundsWidth / 2) {
177
+ label.maxWidth = label.size.width - (right - boundsWidth / 2);
174
178
  }
175
- const connector = {
176
- path: line(getConnectorPoints(midAngle)),
177
- color: relatedSegment.data.color,
178
- };
179
- data.connectors.push(connector);
180
179
  }
181
- });
182
- data.labels = labels;
180
+ if (shouldUseHtml) {
181
+ htmlLabels.push({
182
+ x: boundsWidth / 2 + label.x,
183
+ y: boundsHeight / 2 + label.y,
184
+ content: label.text,
185
+ size: label.size,
186
+ });
187
+ }
188
+ else {
189
+ labels.push(label);
190
+ }
191
+ const connector = {
192
+ path: line(getConnectorPoints(midAngle)),
193
+ color: relatedSegment.data.color,
194
+ };
195
+ connectors.push(connector);
196
+ }
197
+ });
198
+ return {
199
+ labels,
200
+ htmlLabels,
201
+ connectors,
202
+ };
203
+ };
204
+ return Array.from(groupedPieSeries).map(([stackId, items]) => {
205
+ const data = prepareItem(stackId, items);
206
+ const preparedLabels = prepareLabels({
207
+ data,
208
+ series: items,
209
+ });
210
+ const allPreparedLabels = [...preparedLabels.labels, ...preparedLabels.htmlLabels];
211
+ const top = Math.min(data.center[1] - data.radius, ...allPreparedLabels.map((l) => l.y + data.center[1]));
212
+ const bottom = Math.max(data.center[1] + data.radius, ...allPreparedLabels.map((l) => data.center[1] + l.y + l.size.height));
213
+ const topAdjustment = Math.floor(top - data.halo.size);
214
+ if (topAdjustment > 0) {
215
+ // should adjust top position and height
216
+ data.radius += topAdjustment / 2;
217
+ data.center[1] -= topAdjustment / 2;
183
218
  }
219
+ const bottomAdjustment = Math.floor(boundsHeight - bottom - data.halo.size);
220
+ if (bottomAdjustment > 0) {
221
+ // should adjust position and radius
222
+ data.radius += bottomAdjustment / 2;
223
+ data.center[1] += bottomAdjustment / 2;
224
+ }
225
+ const { labels, htmlLabels, connectors } = prepareLabels({
226
+ data,
227
+ series: items,
228
+ });
229
+ data.labels = labels;
230
+ data.htmlLabels = htmlLabels;
231
+ data.connectors = connectors;
184
232
  return data;
185
233
  });
186
234
  }
@@ -37,5 +37,5 @@ export type PreparedPieData = {
37
37
  opacity: number;
38
38
  size: number;
39
39
  };
40
- htmlElements: HtmlItem[];
40
+ htmlLabels: HtmlItem[];
41
41
  };
@@ -33,6 +33,7 @@ function getLabels(args) {
33
33
  content: text,
34
34
  x,
35
35
  y,
36
+ size: { width, height: lineHeight },
36
37
  }
37
38
  : {
38
39
  text,
@@ -18,6 +18,10 @@ export type HtmlItem = {
18
18
  x: number;
19
19
  y: number;
20
20
  content: string;
21
+ size: {
22
+ width: number;
23
+ height: number;
24
+ };
21
25
  };
22
26
  export type ShapeDataWithHtmlItems = {
23
27
  htmlElements: HtmlItem[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",