@gravity-ui/chartkit 5.14.0 → 5.15.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.
@@ -22,6 +22,7 @@ export const Chart = (props) => {
22
22
  var _a, _b;
23
23
  const { width, height, data } = props;
24
24
  const svgRef = React.useRef(null);
25
+ const htmlLayerRef = React.useRef(null);
25
26
  const dispatcher = React.useMemo(() => {
26
27
  return getD3Dispatcher();
27
28
  }, []);
@@ -71,6 +72,7 @@ export const Chart = (props) => {
71
72
  yAxis,
72
73
  yScale,
73
74
  split: preparedSplit,
75
+ htmlLayout: htmlLayerRef.current,
74
76
  });
75
77
  const clickHandler = (_b = (_a = data.chart) === null || _a === void 0 ? void 0 : _a.events) === null || _b === void 0 ? void 0 : _b.click;
76
78
  React.useEffect(() => {
@@ -136,5 +138,8 @@ export const Chart = (props) => {
136
138
  React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit })))),
137
139
  shapes),
138
140
  preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick }))),
141
+ React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef, style: {
142
+ transform: `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
143
+ } }),
139
144
  React.createElement(Tooltip, { dispatcher: dispatcher, tooltip: tooltip, svgContainer: svgRef.current, xAxis: xAxis, yAxis: yAxis[0] })));
140
145
  };
@@ -2,6 +2,14 @@
2
2
  position: absolute;
3
3
  }
4
4
 
5
+ .chartkit-d3__html-layer {
6
+ display: contents;
7
+ }
8
+
9
+ .chartkit-d3__html-layer > * {
10
+ transform: inherit;
11
+ }
12
+
5
13
  .chartkit-d3-axis .domain {
6
14
  stroke: var(--g-color-line-generic-active);
7
15
  }
@@ -24,6 +24,7 @@ export function preparePieSeries(args) {
24
24
  connectorShape: get(series, 'dataLabels.connectorShape', 'polyline'),
25
25
  distance: get(series, 'dataLabels.distance', 25),
26
26
  connectorCurve: get(series, 'dataLabels.connectorCurve', 'basic'),
27
+ html: get(series, 'dataLabels.html', false),
27
28
  },
28
29
  label: dataItem.label,
29
30
  value: dataItem.value,
@@ -126,6 +126,7 @@ export type PreparedPieSeries = {
126
126
  connectorShape: ConnectorShape;
127
127
  distance: number;
128
128
  connectorCurve: ConnectorCurve;
129
+ html: boolean;
129
130
  };
130
131
  states: {
131
132
  hover: {
@@ -25,6 +25,7 @@ type Args = {
25
25
  xScale?: ChartScale;
26
26
  yScale?: ChartScale[];
27
27
  split: PreparedSplit;
28
+ htmlLayout: HTMLElement | null;
28
29
  };
29
30
  export declare const useShapes: (args: Args) => {
30
31
  shapes: React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
@@ -15,7 +15,7 @@ import { prepareTreemapData } from './treemap/prepare-data';
15
15
  import { WaterfallSeriesShapes, prepareWaterfallData } from './waterfall';
16
16
  import './styles.css';
17
17
  export const useShapes = (args) => {
18
- const { boundsWidth, boundsHeight, dispatcher, series, seriesOptions, xAxis, xScale, yAxis, yScale, split, } = args;
18
+ const { boundsWidth, boundsHeight, dispatcher, series, seriesOptions, xAxis, xScale, yAxis, yScale, split, htmlLayout, } = args;
19
19
  const shapesComponents = React.useMemo(() => {
20
20
  const visibleSeries = getOnlyVisibleSeries(series);
21
21
  const groupedSeries = group(visibleSeries, (item) => item.type);
@@ -119,7 +119,7 @@ export const useShapes = (args) => {
119
119
  boundsWidth,
120
120
  boundsHeight,
121
121
  });
122
- acc.push(React.createElement(PieSeriesShapes, { key: "pie", dispatcher: dispatcher, preparedData: preparedData, seriesOptions: seriesOptions }));
122
+ acc.push(React.createElement(PieSeriesShapes, { key: "pie", dispatcher: dispatcher, preparedData: preparedData, seriesOptions: seriesOptions, htmlLayout: htmlLayout }));
123
123
  shapesData.push(...preparedData);
124
124
  break;
125
125
  }
@@ -6,6 +6,7 @@ type PreparePieSeriesArgs = {
6
6
  dispatcher: Dispatch<object>;
7
7
  preparedData: PreparedPieData[];
8
8
  seriesOptions: PreparedSeriesOptions;
9
+ htmlLayout: HTMLElement | null;
9
10
  };
10
11
  export declare function getHaloVisibility(d: PieArcDatum<SegmentData>): "" | "hidden";
11
12
  export declare function PieSeriesShapes(args: PreparePieSeriesArgs): React.JSX.Element;
@@ -1,18 +1,24 @@
1
1
  import React from 'react';
2
- import { arc, color, line as lineGenerator, select } from 'd3';
2
+ import { Portal } from '@gravity-ui/uikit';
3
+ import { arc, color, select } from 'd3';
3
4
  import get from 'lodash/get';
4
5
  import { block } from '../../../../../../utils/cn';
5
6
  import { setEllipsisForOverflowTexts } from '../../../utils';
6
7
  import { setActiveState } from '../utils';
7
- import { getCurveFactory } from './utils';
8
8
  const b = block('d3-pie');
9
9
  export function getHaloVisibility(d) {
10
10
  const enabled = d.data.pie.halo.enabled && d.data.hovered;
11
11
  return enabled ? '' : 'hidden';
12
12
  }
13
13
  export function PieSeriesShapes(args) {
14
- const { dispatcher, preparedData, seriesOptions } = args;
14
+ const { dispatcher, preparedData, seriesOptions, htmlLayout } = args;
15
15
  const ref = React.useRef(null);
16
+ const htmlItems = React.useMemo(() => {
17
+ return preparedData.reduce((result, d) => {
18
+ result.push(...d.htmlElements);
19
+ return result;
20
+ }, []);
21
+ }, [preparedData]);
16
22
  React.useEffect(() => {
17
23
  if (!ref.current) {
18
24
  return () => { };
@@ -71,6 +77,7 @@ export function PieSeriesShapes(args) {
71
77
  .attr('class', b('segment'))
72
78
  .attr('fill', (d) => d.data.color)
73
79
  .attr('opacity', (d) => d.data.opacity);
80
+ // render Labels
74
81
  shapesSelection
75
82
  .selectAll('text')
76
83
  .data((pieData) => pieData.labels)
@@ -87,19 +94,12 @@ export function PieSeriesShapes(args) {
87
94
  // Add the polyline between chart and labels
88
95
  shapesSelection
89
96
  .selectAll(connectorSelector)
90
- .data((pieData) => pieData.labels)
97
+ .data((pieData) => pieData.connectors)
91
98
  .enter()
92
99
  .append('path')
93
100
  .attr('class', b('connector'))
94
- .attr('d', (d) => {
95
- let line = lineGenerator();
96
- const curveFactory = getCurveFactory(d.segment.pie);
97
- if (curveFactory) {
98
- line = line.curve(curveFactory);
99
- }
100
- return line(d.connector.points);
101
- })
102
- .attr('stroke', (d) => d.connector.color)
101
+ .attr('d', (d) => d.path)
102
+ .attr('stroke', (d) => d.color)
103
103
  .attr('stroke-width', 1)
104
104
  .attr('stroke-linejoin', 'round')
105
105
  .attr('stroke-linecap', 'round')
@@ -172,5 +172,9 @@ export function PieSeriesShapes(args) {
172
172
  dispatcher.on(eventName, null);
173
173
  };
174
174
  }, [dispatcher, preparedData, seriesOptions]);
175
- return React.createElement("g", { ref: ref, className: b(), style: { zIndex: 9 } });
175
+ return (React.createElement(React.Fragment, null,
176
+ React.createElement("g", { ref: ref, className: b(), style: { zIndex: 9 } }),
177
+ htmlLayout && (React.createElement(Portal, { container: htmlLayout }, htmlItems.map((item, index) => {
178
+ return (React.createElement("div", { key: index, dangerouslySetInnerHTML: { __html: item.content }, style: { position: 'absolute', left: item.x, top: item.y } }));
179
+ })))));
176
180
  }
@@ -1,6 +1,6 @@
1
- import { arc, group } from 'd3';
1
+ import { arc, group, line as lineGenerator } from 'd3';
2
2
  import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
3
- import { pieGenerator } from './utils';
3
+ import { getCurveFactory, pieGenerator } from './utils';
4
4
  const FULL_CIRCLE = Math.PI * 2;
5
5
  const getCenter = (boundsWidth, boundsHeight, center) => {
6
6
  var _a, _b;
@@ -30,6 +30,7 @@ export function preparePieData(args) {
30
30
  radius,
31
31
  segments: [],
32
32
  labels: [],
33
+ connectors: [],
33
34
  borderColor,
34
35
  borderWidth,
35
36
  borderRadius,
@@ -40,6 +41,7 @@ export function preparePieData(args) {
40
41
  opacity: series.states.hover.halo.opacity,
41
42
  size: series.states.hover.halo.size,
42
43
  },
44
+ htmlElements: [],
43
45
  };
44
46
  const segments = items.map((item) => {
45
47
  return {
@@ -53,6 +55,11 @@ export function preparePieData(args) {
53
55
  };
54
56
  });
55
57
  data.segments = pieGenerator(segments);
58
+ let line = lineGenerator();
59
+ const curveFactory = getCurveFactory(data);
60
+ if (curveFactory) {
61
+ line = line.curve(curveFactory);
62
+ }
56
63
  if (dataLabels.enabled) {
57
64
  const { style, connectorPadding, distance } = dataLabels;
58
65
  const { maxHeight: labelHeight } = getLabelsSize({ labels: ['Some Label'], style });
@@ -81,15 +88,17 @@ export function preparePieData(args) {
81
88
  items.forEach((d, index) => {
82
89
  const prevLabel = labels[labels.length - 1];
83
90
  const text = String(d.data.label || d.data.value);
84
- const labelSize = getLabelsSize({ labels: [text], style });
91
+ const shouldUseHtml = dataLabels.html;
92
+ const labelSize = getLabelsSize({ labels: [text], style, html: shouldUseHtml });
85
93
  const labelWidth = labelSize.maxWidth;
86
94
  const relatedSegment = data.segments[index];
87
95
  const getLabelPosition = (angle) => {
88
96
  let [x, y] = labelArcGenerator.centroid(Object.assign(Object.assign({}, relatedSegment), { startAngle: angle, endAngle: angle }));
89
- x = Math.max(-boundsWidth / 2, x);
90
- if (y < 0) {
91
- y -= labelHeight;
97
+ y = y < 0 ? y - labelHeight : y;
98
+ if (shouldUseHtml) {
99
+ x = x < 0 ? x - labelWidth : x;
92
100
  }
101
+ x = Math.max(-boundsWidth / 2, x);
93
102
  return [x, y];
94
103
  };
95
104
  const getConnectorPoints = (angle) => {
@@ -114,12 +123,9 @@ export function preparePieData(args) {
114
123
  textAnchor: midAngle < Math.PI ? 'start' : 'end',
115
124
  series: { id: d.id },
116
125
  active: true,
117
- connector: {
118
- points: getConnectorPoints(midAngle),
119
- color: relatedSegment.data.color,
120
- },
121
126
  segment: relatedSegment.data,
122
127
  angle: midAngle,
128
+ html: shouldUseHtml,
123
129
  };
124
130
  let overlap = false;
125
131
  if (prevLabel) {
@@ -138,7 +144,6 @@ export function preparePieData(args) {
138
144
  const [newX, newY] = getLabelPosition(newAngle);
139
145
  label.x = newX;
140
146
  label.y = newY;
141
- label.connector.points = getConnectorPoints(newAngle);
142
147
  if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
143
148
  shouldAdjustAngle = false;
144
149
  overlap = false;
@@ -158,7 +163,21 @@ export function preparePieData(args) {
158
163
  label.maxWidth = label.size.width - (right - boundsWidth / 2);
159
164
  }
160
165
  }
161
- labels.push(label);
166
+ if (shouldUseHtml) {
167
+ data.htmlElements.push({
168
+ x: boundsWidth / 2 + label.x,
169
+ y: boundsHeight / 2 + label.y,
170
+ content: label.text,
171
+ });
172
+ }
173
+ else {
174
+ labels.push(label);
175
+ }
176
+ const connector = {
177
+ path: line(getConnectorPoints(midAngle)),
178
+ color: relatedSegment.data.color,
179
+ };
180
+ data.connectors.push(connector);
162
181
  }
163
182
  });
164
183
  data.labels = labels;
@@ -1,6 +1,6 @@
1
1
  import type { PieArcDatum } from 'd3';
2
2
  import { ConnectorCurve } from '../../../../../../types';
3
- import { LabelData } from '../../../types';
3
+ import { HtmlItem, LabelData } from '../../../types';
4
4
  import { PreparedPieSeries } from '../../useSeries/types';
5
5
  export type SegmentData = {
6
6
  value: number;
@@ -12,18 +12,19 @@ export type SegmentData = {
12
12
  pie: PreparedPieData;
13
13
  };
14
14
  export type PieLabelData = LabelData & {
15
- connector: {
16
- points: [number, number][];
17
- color: string;
18
- };
19
15
  segment: SegmentData;
20
16
  angle: number;
21
17
  maxWidth: number;
22
18
  };
19
+ export type PieConnectorData = {
20
+ path: string | null;
21
+ color: string;
22
+ };
23
23
  export type PreparedPieData = {
24
24
  id: string;
25
25
  segments: PieArcDatum<SegmentData>[];
26
26
  labels: PieLabelData[];
27
+ connectors: PieConnectorData[];
27
28
  center: [number, number];
28
29
  radius: number;
29
30
  innerRadius: number;
@@ -37,4 +38,5 @@ export type PreparedPieData = {
37
38
  opacity: number;
38
39
  size: number;
39
40
  };
41
+ htmlElements: HtmlItem[];
40
42
  };
@@ -13,4 +13,10 @@ export type LabelData = {
13
13
  id: string;
14
14
  };
15
15
  active?: boolean;
16
+ html?: boolean;
17
+ };
18
+ export type HtmlItem = {
19
+ x: number;
20
+ y: number;
21
+ content: string;
16
22
  };
@@ -9,10 +9,11 @@ export declare function hasOverlappingLabels({ width, labels, padding, style, }:
9
9
  style?: BaseTextStyle;
10
10
  padding?: number;
11
11
  }): boolean;
12
- export declare function getLabelsSize({ labels, style, rotation, }: {
12
+ export declare function getLabelsSize({ labels, style, rotation, html, }: {
13
13
  labels: string[];
14
14
  style?: BaseTextStyle;
15
15
  rotation?: number;
16
+ html?: boolean;
16
17
  }): {
17
18
  maxHeight: number;
18
19
  maxWidth: number;
@@ -63,24 +63,39 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
63
63
  .text((d) => d);
64
64
  return text;
65
65
  }
66
- export function getLabelsSize({ labels, style, rotation, }) {
67
- var _a;
66
+ export function getLabelsSize({ labels, style, rotation, html, }) {
67
+ var _a, _b, _c, _d, _e;
68
68
  if (!labels.filter(Boolean).length) {
69
69
  return { maxHeight: 0, maxWidth: 0 };
70
70
  }
71
71
  const container = select(document.body)
72
72
  .append('div')
73
73
  .attr('class', 'chartkit chartkit-theme_common');
74
- const svg = container.append('svg');
75
- const textSelection = renderLabels(svg, { labels, style });
76
- if (rotation) {
77
- textSelection
78
- .attr('text-anchor', rotation > 0 ? 'start' : 'end')
79
- .style('transform', `rotate(${rotation}deg)`);
74
+ const result = { maxHeight: 0, maxWidth: 0 };
75
+ let labelWrapper;
76
+ if (html) {
77
+ labelWrapper = container.append('div').style('position', 'absolute').node();
78
+ labels.forEach((l) => {
79
+ labelWrapper === null || labelWrapper === void 0 ? void 0 : labelWrapper.insertAdjacentHTML('beforeend', l);
80
+ });
81
+ const rect = labelWrapper === null || labelWrapper === void 0 ? void 0 : labelWrapper.getBoundingClientRect();
82
+ result.maxWidth = (_a = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _a !== void 0 ? _a : 0;
83
+ result.maxHeight = (_b = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _b !== void 0 ? _b : 0;
84
+ }
85
+ else {
86
+ const svg = container.append('svg');
87
+ const textSelection = renderLabels(svg, { labels, style });
88
+ if (rotation) {
89
+ textSelection
90
+ .attr('text-anchor', rotation > 0 ? 'start' : 'end')
91
+ .style('transform', `rotate(${rotation}deg)`);
92
+ }
93
+ const rect = (_c = svg.select('g').node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
94
+ result.maxWidth = (_d = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _d !== void 0 ? _d : 0;
95
+ result.maxHeight = (_e = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _e !== void 0 ? _e : 0;
80
96
  }
81
- const { height = 0, width = 0 } = ((_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) || {};
82
97
  container.remove();
83
- return { maxHeight: height, maxWidth: width };
98
+ return result;
84
99
  }
85
100
  export function wrapText(args) {
86
101
  const { text, style, width } = args;
@@ -21,6 +21,13 @@ export type BaseSeries = {
21
21
  * @default false
22
22
  * */
23
23
  allowOverlap?: boolean;
24
+ /**
25
+ * Allows to use any html-tags to display the content.
26
+ * The element will be displayed outside the box of the SVG element.
27
+ *
28
+ * @default false
29
+ * */
30
+ html?: boolean;
24
31
  };
25
32
  /** You can set the cursor to "pointer" if you have click events attached to the series, to signal to the user that the points and lines can be clicked. */
26
33
  cursor?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "5.14.0",
3
+ "version": "5.15.0",
4
4
  "description": "React component used to render charts based on any sources you need",
5
5
  "license": "MIT",
6
6
  "repository": "git@github.com:gravity-ui/ChartKit.git",
@@ -49,7 +49,7 @@
49
49
  "@bem-react/classname": "^1.6.0",
50
50
  "@gravity-ui/date-utils": "^2.1.0",
51
51
  "@gravity-ui/i18n": "^1.0.0",
52
- "@gravity-ui/yagr": "^4.3.2",
52
+ "@gravity-ui/yagr": "^4.3.4",
53
53
  "afterframe": "^1.0.2",
54
54
  "d3": "^7.8.5",
55
55
  "lodash": "^4.17.21",