@gravity-ui/charts 1.5.0 → 1.5.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.
@@ -1,7 +1,7 @@
1
1
  import { arc, group, line as lineGenerator } from 'd3';
2
2
  import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
3
3
  import { getFormattedValue } from '../../../utils/chart/format';
4
- import { getCurveFactory, pieGenerator } from './utils';
4
+ import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
5
5
  const FULL_CIRCLE = Math.PI * 2;
6
6
  const getCenter = (boundsWidth, boundsHeight, center) => {
7
7
  var _a, _b;
@@ -103,6 +103,7 @@ export function preparePieData(args) {
103
103
  const labelArcGenerator = arc()
104
104
  .innerRadius((d) => d.data.radius + distance + connectorPadding)
105
105
  .outerRadius((d) => d.data.radius + distance + connectorPadding);
106
+ let shouldStopLabelPlacement = false;
106
107
  series.forEach((d, index) => {
107
108
  const prevLabel = labels[labels.length - 1];
108
109
  const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
@@ -158,8 +159,13 @@ export function preparePieData(args) {
158
159
  let overlap = false;
159
160
  if (prevLabel) {
160
161
  overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
162
+ const startAngle = relatedSegment.startAngle +
163
+ (relatedSegment.endAngle - relatedSegment.startAngle) / 2;
161
164
  if (overlap) {
162
- let shouldAdjustAngle = true;
165
+ let shouldAdjustAngle = !shouldStopLabelPlacement;
166
+ const connectorPoints = getConnectorPoints(startAngle);
167
+ const pointA = connectorPoints[0];
168
+ const pointB = connectorPoints[connectorPoints.length - 1];
163
169
  const step = Math.PI / 180;
164
170
  while (shouldAdjustAngle) {
165
171
  const newAngle = label.angle + step;
@@ -171,6 +177,11 @@ export function preparePieData(args) {
171
177
  const [newX, newY] = getLabelPosition(newAngle);
172
178
  label.x = newX;
173
179
  label.y = newY;
180
+ const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
181
+ if (inscribedAngle > 90) {
182
+ shouldAdjustAngle = false;
183
+ shouldStopLabelPlacement = true;
184
+ }
174
185
  if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
175
186
  shouldAdjustAngle = false;
176
187
  overlap = false;
@@ -180,7 +191,7 @@ export function preparePieData(args) {
180
191
  }
181
192
  }
182
193
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
183
- if (!isLabelOverlapped && label.maxWidth > 0) {
194
+ if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
184
195
  if (shouldUseHtml) {
185
196
  htmlLabels.push({
186
197
  x: data.center[0] + label.x,
@@ -194,7 +205,7 @@ export function preparePieData(args) {
194
205
  labels.push(label);
195
206
  }
196
207
  const connector = {
197
- path: line(getConnectorPoints(midAngle)),
208
+ path: line(getConnectorPoints(label.angle)),
198
209
  color: relatedSegment.data.color,
199
210
  };
200
211
  connectors.push(connector);
@@ -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;
@@ -13,3 +13,21 @@ export function getCurveFactory(data) {
13
13
  }
14
14
  return undefined;
15
15
  }
16
+ /**
17
+ * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
18
+ *
19
+ * The order of B and C does not affect the result.
20
+ *
21
+ * @see: https://en.wikipedia.org/wiki/Inscribed_angle
22
+ * @returns The angle in degrees, in the range [0, 180].
23
+ */
24
+ export function getInscribedAngle(a, b, c) {
25
+ const ux = b[0] - a[0];
26
+ const uy = b[1] - a[1];
27
+ const vx = c[0] - a[0];
28
+ const vy = c[1] - a[1];
29
+ const dot = ux * vx + uy * vy;
30
+ const cross = ux * vy - uy * vx;
31
+ const radians = Math.atan2(Math.abs(cross), dot);
32
+ return (radians * 180) / Math.PI;
33
+ }
@@ -4,13 +4,13 @@ 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
10
  texts.forEach((text, index) => {
11
11
  var _a;
12
12
  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 : {};
13
+ const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
14
14
  const left = d.x0 + padding;
15
15
  const right = d.x1 - padding;
16
16
  const spaceWidth = Math.max(0, right - left);
@@ -35,6 +35,10 @@ function getLabels(args) {
35
35
  break;
36
36
  }
37
37
  }
38
+ const bottom = y + lineHeight;
39
+ if (bottom > d.y1) {
40
+ return;
41
+ }
38
42
  const item = html
39
43
  ? {
40
44
  content: label,
@@ -120,7 +124,7 @@ export function prepareTreemapData(args) {
120
124
  const { html, style: dataLabelsStyle } = series.dataLabels;
121
125
  const labels = getLabels({ data: leaves, options: series.dataLabels });
122
126
  if (html) {
123
- const htmlItems = labels.map((l) => (Object.assign({ style: dataLabelsStyle }, l)));
127
+ 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
128
  htmlElements.push(...htmlItems);
125
129
  }
126
130
  else {
@@ -64,7 +64,7 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
64
64
  return text;
65
65
  }
66
66
  export function getLabelsSize({ labels, style, rotation, html, }) {
67
- var _a, _b, _c;
67
+ var _a, _b, _c, _d, _e;
68
68
  if (!labels.filter(Boolean).length) {
69
69
  return { maxHeight: 0, maxWidth: 0 };
70
70
  }
@@ -74,7 +74,12 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
74
74
  const result = { maxHeight: 0, maxWidth: 0 };
75
75
  let labelWrapper;
76
76
  if (html) {
77
- labelWrapper = container.append('div').style('position', 'absolute').node();
77
+ labelWrapper = container
78
+ .append('div')
79
+ .style('position', 'absolute')
80
+ .style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
81
+ .style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
82
+ .node();
78
83
  const { height, width } = labels.reduce((acc, l) => {
79
84
  var _a, _b;
80
85
  if (labelWrapper) {
@@ -97,9 +102,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
97
102
  .attr('text-anchor', rotation > 0 ? 'start' : 'end')
98
103
  .style('transform', `rotate(${rotation}deg)`);
99
104
  }
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;
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;
103
108
  }
104
109
  container.remove();
105
110
  return result;
@@ -1,7 +1,7 @@
1
1
  import { arc, group, line as lineGenerator } from 'd3';
2
2
  import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
3
3
  import { getFormattedValue } from '../../../utils/chart/format';
4
- import { getCurveFactory, pieGenerator } from './utils';
4
+ import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
5
5
  const FULL_CIRCLE = Math.PI * 2;
6
6
  const getCenter = (boundsWidth, boundsHeight, center) => {
7
7
  var _a, _b;
@@ -103,6 +103,7 @@ export function preparePieData(args) {
103
103
  const labelArcGenerator = arc()
104
104
  .innerRadius((d) => d.data.radius + distance + connectorPadding)
105
105
  .outerRadius((d) => d.data.radius + distance + connectorPadding);
106
+ let shouldStopLabelPlacement = false;
106
107
  series.forEach((d, index) => {
107
108
  const prevLabel = labels[labels.length - 1];
108
109
  const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
@@ -158,8 +159,13 @@ export function preparePieData(args) {
158
159
  let overlap = false;
159
160
  if (prevLabel) {
160
161
  overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
162
+ const startAngle = relatedSegment.startAngle +
163
+ (relatedSegment.endAngle - relatedSegment.startAngle) / 2;
161
164
  if (overlap) {
162
- let shouldAdjustAngle = true;
165
+ let shouldAdjustAngle = !shouldStopLabelPlacement;
166
+ const connectorPoints = getConnectorPoints(startAngle);
167
+ const pointA = connectorPoints[0];
168
+ const pointB = connectorPoints[connectorPoints.length - 1];
163
169
  const step = Math.PI / 180;
164
170
  while (shouldAdjustAngle) {
165
171
  const newAngle = label.angle + step;
@@ -171,6 +177,11 @@ export function preparePieData(args) {
171
177
  const [newX, newY] = getLabelPosition(newAngle);
172
178
  label.x = newX;
173
179
  label.y = newY;
180
+ const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
181
+ if (inscribedAngle > 90) {
182
+ shouldAdjustAngle = false;
183
+ shouldStopLabelPlacement = true;
184
+ }
174
185
  if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
175
186
  shouldAdjustAngle = false;
176
187
  overlap = false;
@@ -180,7 +191,7 @@ export function preparePieData(args) {
180
191
  }
181
192
  }
182
193
  const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
183
- if (!isLabelOverlapped && label.maxWidth > 0) {
194
+ if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
184
195
  if (shouldUseHtml) {
185
196
  htmlLabels.push({
186
197
  x: data.center[0] + label.x,
@@ -194,7 +205,7 @@ export function preparePieData(args) {
194
205
  labels.push(label);
195
206
  }
196
207
  const connector = {
197
- path: line(getConnectorPoints(midAngle)),
208
+ path: line(getConnectorPoints(label.angle)),
198
209
  color: relatedSegment.data.color,
199
210
  };
200
211
  connectors.push(connector);
@@ -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;
@@ -13,3 +13,21 @@ export function getCurveFactory(data) {
13
13
  }
14
14
  return undefined;
15
15
  }
16
+ /**
17
+ * Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
18
+ *
19
+ * The order of B and C does not affect the result.
20
+ *
21
+ * @see: https://en.wikipedia.org/wiki/Inscribed_angle
22
+ * @returns The angle in degrees, in the range [0, 180].
23
+ */
24
+ export function getInscribedAngle(a, b, c) {
25
+ const ux = b[0] - a[0];
26
+ const uy = b[1] - a[1];
27
+ const vx = c[0] - a[0];
28
+ const vy = c[1] - a[1];
29
+ const dot = ux * vx + uy * vy;
30
+ const cross = ux * vy - uy * vx;
31
+ const radians = Math.atan2(Math.abs(cross), dot);
32
+ return (radians * 180) / Math.PI;
33
+ }
@@ -4,13 +4,13 @@ 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
10
  texts.forEach((text, index) => {
11
11
  var _a;
12
12
  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 : {};
13
+ const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
14
14
  const left = d.x0 + padding;
15
15
  const right = d.x1 - padding;
16
16
  const spaceWidth = Math.max(0, right - left);
@@ -35,6 +35,10 @@ function getLabels(args) {
35
35
  break;
36
36
  }
37
37
  }
38
+ const bottom = y + lineHeight;
39
+ if (bottom > d.y1) {
40
+ return;
41
+ }
38
42
  const item = html
39
43
  ? {
40
44
  content: label,
@@ -120,7 +124,7 @@ export function prepareTreemapData(args) {
120
124
  const { html, style: dataLabelsStyle } = series.dataLabels;
121
125
  const labels = getLabels({ data: leaves, options: series.dataLabels });
122
126
  if (html) {
123
- const htmlItems = labels.map((l) => (Object.assign({ style: dataLabelsStyle }, l)));
127
+ 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
128
  htmlElements.push(...htmlItems);
125
129
  }
126
130
  else {
@@ -64,7 +64,7 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
64
64
  return text;
65
65
  }
66
66
  export function getLabelsSize({ labels, style, rotation, html, }) {
67
- var _a, _b, _c;
67
+ var _a, _b, _c, _d, _e;
68
68
  if (!labels.filter(Boolean).length) {
69
69
  return { maxHeight: 0, maxWidth: 0 };
70
70
  }
@@ -74,7 +74,12 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
74
74
  const result = { maxHeight: 0, maxWidth: 0 };
75
75
  let labelWrapper;
76
76
  if (html) {
77
- labelWrapper = container.append('div').style('position', 'absolute').node();
77
+ labelWrapper = container
78
+ .append('div')
79
+ .style('position', 'absolute')
80
+ .style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
81
+ .style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
82
+ .node();
78
83
  const { height, width } = labels.reduce((acc, l) => {
79
84
  var _a, _b;
80
85
  if (labelWrapper) {
@@ -97,9 +102,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
97
102
  .attr('text-anchor', rotation > 0 ? 'start' : 'end')
98
103
  .style('transform', `rotate(${rotation}deg)`);
99
104
  }
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;
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;
103
108
  }
104
109
  container.remove();
105
110
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",