@gravity-ui/chartkit 5.9.0 → 5.10.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 (28) hide show
  1. package/build/plugins/d3/renderer/components/AxisX.d.ts +8 -0
  2. package/build/plugins/d3/renderer/components/AxisX.js +41 -7
  3. package/build/plugins/d3/renderer/components/AxisY.js +49 -7
  4. package/build/plugins/d3/renderer/components/Chart.js +2 -1
  5. package/build/plugins/d3/renderer/components/styles.css +4 -0
  6. package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +6 -8
  7. package/build/plugins/d3/renderer/constants/defaults/axis.js +7 -1
  8. package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +4 -1
  9. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +16 -7
  10. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +2 -1
  11. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +14 -8
  12. package/build/plugins/d3/renderer/utils/axis.d.ts +5 -0
  13. package/build/plugins/d3/renderer/utils/axis.js +21 -0
  14. package/build/plugins/d3/renderer/utils/text.d.ts +10 -0
  15. package/build/plugins/d3/renderer/utils/text.js +61 -8
  16. package/build/plugins/highcharts/renderer/HighchartsWidget.d.ts +2 -0
  17. package/build/plugins/highcharts/renderer/components/HighchartsComponent.d.ts +1 -0
  18. package/build/plugins/highcharts/renderer/components/HighchartsComponent.js +7 -7
  19. package/build/plugins/highcharts/renderer/components/HighchartsReact.d.ts +2 -0
  20. package/build/plugins/highcharts/renderer/components/HighchartsReact.js +21 -0
  21. package/build/plugins/highcharts/renderer/components/StyledSplitPane/StyledSplitPane.d.ts +1 -2
  22. package/build/plugins/highcharts/renderer/components/useElementSize.d.ts +6 -0
  23. package/build/plugins/highcharts/renderer/components/useElementSize.js +50 -0
  24. package/build/plugins/highcharts/renderer/components/withSplitPane/withSplitPane.d.ts +7 -0
  25. package/build/plugins/highcharts/renderer/components/withSplitPane/withSplitPane.js +8 -6
  26. package/build/types/widget-data/axis.d.ts +8 -0
  27. package/build/types/widget.d.ts +3 -0
  28. package/package.json +1 -1
@@ -7,5 +7,13 @@ type Props = {
7
7
  scale: ChartScale;
8
8
  split: PreparedSplit;
9
9
  };
10
+ export declare function getTitlePosition(args: {
11
+ axis: PreparedAxis;
12
+ width: number;
13
+ rowCount: number;
14
+ }): {
15
+ x: number;
16
+ y: number;
17
+ };
10
18
  export declare const AxisX: React.NamedExoticComponent<Props>;
11
19
  export {};
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { select } from 'd3';
3
3
  import { block } from '../../../../utils/cn';
4
- import { formatAxisTickLabel, getClosestPointsRange, getMaxTickCount, getScaleTicks, getTicksCount, setEllipsisForOverflowText, } from '../utils';
4
+ import { formatAxisTickLabel, getAxisTitleRows, getClosestPointsRange, getMaxTickCount, getScaleTicks, getTicksCount, handleOverflowingText, } from '../utils';
5
5
  import { axisBottom } from '../utils/axis-generators';
6
6
  const b = block('d3-axis');
7
7
  function getLabelFormatter({ axis, scale }) {
@@ -18,6 +18,29 @@ function getLabelFormatter({ axis, scale }) {
18
18
  });
19
19
  };
20
20
  }
21
+ export function getTitlePosition(args) {
22
+ const { axis, width, rowCount } = args;
23
+ if (rowCount < 1) {
24
+ return { x: 0, y: 0 };
25
+ }
26
+ let x;
27
+ const y = axis.title.height / rowCount + axis.title.margin + axis.labels.height + axis.labels.margin;
28
+ switch (axis.title.align) {
29
+ case 'left': {
30
+ x = axis.title.width / 2;
31
+ break;
32
+ }
33
+ case 'right': {
34
+ x = width - axis.title.width / 2;
35
+ break;
36
+ }
37
+ case 'center': {
38
+ x = width / 2;
39
+ break;
40
+ }
41
+ }
42
+ return { x, y };
43
+ }
21
44
  export const AxisX = React.memo(function AxisX(props) {
22
45
  const { axis, width, height: totalHeight, scale, split } = props;
23
46
  const ref = React.useRef(null);
@@ -58,16 +81,27 @@ export const AxisX = React.memo(function AxisX(props) {
58
81
  svgElement.call(xAxisGenerator).attr('class', b());
59
82
  // add an axis header if necessary
60
83
  if (axis.title.text) {
61
- const y = axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin;
84
+ const titleRows = getAxisTitleRows({ axis, textMaxWidth: width });
62
85
  svgElement
63
86
  .append('text')
64
87
  .attr('class', b('title'))
65
- .attr('text-anchor', 'middle')
66
- .attr('x', width / 2)
67
- .attr('y', y)
88
+ .attr('transform', () => {
89
+ const { x, y } = getTitlePosition({ axis, width, rowCount: titleRows.length });
90
+ return `translate(${x}, ${y})`;
91
+ })
68
92
  .attr('font-size', axis.title.style.fontSize)
69
- .text(axis.title.text)
70
- .call(setEllipsisForOverflowText, width);
93
+ .attr('text-anchor', 'middle')
94
+ .selectAll('tspan')
95
+ .data(titleRows)
96
+ .join('tspan')
97
+ .attr('x', 0)
98
+ .attr('y', (d) => d.y)
99
+ .text((d) => d.text)
100
+ .each((_d, index, nodes) => {
101
+ if (index === axis.title.maxRowCount - 1) {
102
+ handleOverflowingText(nodes[index], width);
103
+ }
104
+ });
71
105
  }
72
106
  }, [axis, width, totalHeight, scale, split]);
73
107
  return React.createElement("g", { ref: ref });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { axisLeft, axisRight, line, select } from 'd3';
3
3
  import { block } from '../../../../utils/cn';
4
- import { calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight, getClosestPointsRange, getScaleTicks, getTicksCount, parseTransformStyle, setEllipsisForOverflowText, setEllipsisForOverflowTexts, } from '../utils';
4
+ import { calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight, getAxisTitleRows, getClosestPointsRange, getScaleTicks, getTicksCount, handleOverflowingText, parseTransformStyle, setEllipsisForOverflowTexts, wrapText, } from '../utils';
5
5
  const b = block('d3-axis');
6
6
  function transformLabel(args) {
7
7
  const { node, axis } = args;
@@ -51,6 +51,33 @@ function getAxisGenerator(args) {
51
51
  }
52
52
  return axisGenerator;
53
53
  }
54
+ function getTitlePosition(args) {
55
+ const { axis, axisHeight, rowCount } = args;
56
+ if (rowCount < 1) {
57
+ return { x: 0, y: 0 };
58
+ }
59
+ const x = -(axis.title.height -
60
+ axis.title.height / rowCount +
61
+ axis.title.margin +
62
+ axis.labels.margin +
63
+ axis.labels.width);
64
+ let y;
65
+ switch (axis.title.align) {
66
+ case 'left': {
67
+ y = axisHeight - axis.title.width / 2;
68
+ break;
69
+ }
70
+ case 'right': {
71
+ y = axis.title.width / 2;
72
+ break;
73
+ }
74
+ case 'center': {
75
+ y = axisHeight / 2;
76
+ break;
77
+ }
78
+ }
79
+ return { x, y };
80
+ }
54
81
  export const AxisY = (props) => {
55
82
  const { axes, width, height: totalHeight, scale, split } = props;
56
83
  const height = getAxisHeight({ split, boundsHeight: totalHeight });
@@ -144,13 +171,28 @@ export const AxisY = (props) => {
144
171
  .append('text')
145
172
  .attr('class', b('title'))
146
173
  .attr('text-anchor', 'middle')
147
- .attr('dy', (d) => -(d.title.margin + d.labels.margin + d.labels.width))
148
- .attr('dx', (d) => (d.position === 'left' ? -height / 2 : height / 2))
149
174
  .attr('font-size', (d) => d.title.style.fontSize)
150
- .attr('transform', (d) => (d.position === 'left' ? 'rotate(-90)' : 'rotate(90)'))
151
- .text((d) => d.title.text)
152
- .each((_d, index, node) => {
153
- return setEllipsisForOverflowText(select(node[index]), height);
175
+ .attr('transform', (d) => {
176
+ const titleRows = wrapText({
177
+ text: d.title.text,
178
+ style: d.title.style,
179
+ width: height,
180
+ });
181
+ const rowCount = Math.min(titleRows.length, d.title.maxRowCount);
182
+ const { x, y } = getTitlePosition({ axis: d, axisHeight: height, rowCount });
183
+ const angle = d.position === 'left' ? -90 : 90;
184
+ return `translate(${x}, ${y}) rotate(${angle})`;
185
+ })
186
+ .selectAll('tspan')
187
+ .data((d) => getAxisTitleRows({ axis: d, textMaxWidth: height }))
188
+ .join('tspan')
189
+ .attr('x', 0)
190
+ .attr('y', (d) => d.y)
191
+ .text((d) => d.text)
192
+ .each((_d, index, nodes) => {
193
+ if (index === nodes.length - 1) {
194
+ handleOverflowingText(nodes[index], height);
195
+ }
154
196
  });
155
197
  }, [axes, width, height, scale, split]);
156
198
  return React.createElement("g", { ref: ref, className: b('container') });
@@ -32,7 +32,8 @@ export const Chart = (props) => {
32
32
  const yAxis = React.useMemo(() => getPreparedYAxis({
33
33
  series: data.series.data,
34
34
  yAxis: data.yAxis,
35
- }), [data]);
35
+ height,
36
+ }), [data, height]);
36
37
  const { legendItems, legendConfig, preparedSeries, preparedSeriesOptions, preparedLegend, handleLegendItemClick, } = useSeries({
37
38
  chartWidth: width,
38
39
  chartHeight: height,
@@ -20,6 +20,10 @@
20
20
  fill: var(--g-color-text-secondary);
21
21
  }
22
22
 
23
+ .chartkit-d3-axis__title tspan {
24
+ alignment-baseline: after-edge;
25
+ }
26
+
23
27
  .chartkit-d3-legend__item {
24
28
  cursor: pointer;
25
29
  user-select: none;
@@ -1,16 +1,14 @@
1
- import { ChartKitWidgetAxisType } from '../../../../../types';
1
+ import type { BaseTextStyle, ChartKitWidgetAxis, ChartKitWidgetAxisType } from '../../../../../types';
2
2
  export declare const axisLabelsDefaults: {
3
3
  margin: number;
4
4
  padding: number;
5
5
  fontSize: number;
6
6
  maxWidth: number;
7
7
  };
8
- export declare const xAxisTitleDefaults: {
9
- margin: number;
10
- fontSize: string;
11
- };
12
- export declare const yAxisTitleDefaults: {
13
- margin: number;
14
- fontSize: string;
8
+ type AxisTitleDefaults = Required<ChartKitWidgetAxis['title']> & {
9
+ style: BaseTextStyle;
15
10
  };
11
+ export declare const xAxisTitleDefaults: AxisTitleDefaults;
12
+ export declare const yAxisTitleDefaults: AxisTitleDefaults;
16
13
  export declare const DEFAULT_AXIS_TYPE: ChartKitWidgetAxisType;
14
+ export {};
@@ -5,7 +5,13 @@ export const axisLabelsDefaults = {
5
5
  maxWidth: 80,
6
6
  };
7
7
  const axisTitleDefaults = {
8
- fontSize: '14px',
8
+ text: '',
9
+ margin: 0,
10
+ style: {
11
+ fontSize: '14px',
12
+ },
13
+ align: 'center',
14
+ maxRowCount: 1,
9
15
  };
10
16
  export const xAxisTitleDefaults = Object.assign(Object.assign({}, axisTitleDefaults), { margin: 4 });
11
17
  export const yAxisTitleDefaults = Object.assign(Object.assign({}, axisTitleDefaults), { margin: 8 });
@@ -1,4 +1,4 @@
1
- import type { BaseTextStyle, ChartKitWidgetAxis, ChartKitWidgetAxisLabels, ChartKitWidgetAxisType, ChartKitWidgetData, ChartMargin } from '../../../../../types';
1
+ import type { BaseTextStyle, ChartKitWidgetAxis, ChartKitWidgetAxisLabels, ChartKitWidgetAxisTitleAlignment, ChartKitWidgetAxisType, ChartKitWidgetData, ChartMargin } from '../../../../../types';
2
2
  type PreparedAxisLabels = Omit<ChartKitWidgetAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartKitWidgetAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation'>> & {
3
3
  style: BaseTextStyle;
4
4
  rotation: number;
@@ -15,9 +15,12 @@ export type PreparedAxis = Omit<ChartKitWidgetAxis, 'type' | 'labels'> & {
15
15
  labels: PreparedAxisLabels;
16
16
  title: {
17
17
  height: number;
18
+ width: number;
18
19
  text: string;
19
20
  margin: number;
20
21
  style: BaseTextStyle;
22
+ align: ChartKitWidgetAxisTitleAlignment;
23
+ maxRowCount: number;
21
24
  };
22
25
  min?: number;
23
26
  grid: {
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { DEFAULT_AXIS_LABEL_FONT_SIZE, axisLabelsDefaults, xAxisTitleDefaults, } from '../../constants';
3
- import { CHART_SERIES_WITH_VOLUME_ON_X_AXIS, calculateCos, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getMaxTickCount, getTicksCount, getXAxisItems, hasOverlappingLabels, } from '../../utils';
3
+ import { CHART_SERIES_WITH_VOLUME_ON_X_AXIS, calculateCos, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getMaxTickCount, getTicksCount, getXAxisItems, hasOverlappingLabels, wrapText, } from '../../utils';
4
4
  import { createXScale } from '../useAxisScales';
5
5
  function getLabelSettings({ axis, series, width, autoRotation = true, }) {
6
6
  const scale = createXScale(axis, series, width);
@@ -50,9 +50,17 @@ function getAxisMin(axis, series) {
50
50
  export const getPreparedXAxis = ({ xAxis, series, width, }) => {
51
51
  var _a;
52
52
  const titleText = get(xAxis, 'title.text', '');
53
- const titleStyle = {
54
- fontSize: get(xAxis, 'title.style.fontSize', xAxisTitleDefaults.fontSize),
55
- };
53
+ const titleStyle = Object.assign(Object.assign({}, xAxisTitleDefaults.style), get(xAxis, 'title.style'));
54
+ const titleMaxRowsCount = get(xAxis, 'title.maxRowCount', xAxisTitleDefaults.maxRowCount);
55
+ const estimatedTitleRows = wrapText({
56
+ text: titleText,
57
+ style: titleStyle,
58
+ width,
59
+ }).slice(0, titleMaxRowsCount);
60
+ const titleSize = getLabelsSize({
61
+ labels: [titleText],
62
+ style: titleStyle,
63
+ });
56
64
  const labelsStyle = {
57
65
  fontSize: get(xAxis, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
58
66
  };
@@ -78,9 +86,10 @@ export const getPreparedXAxis = ({ xAxis, series, width, }) => {
78
86
  text: titleText,
79
87
  style: titleStyle,
80
88
  margin: get(xAxis, 'title.margin', xAxisTitleDefaults.margin),
81
- height: titleText
82
- ? getHorisontalSvgTextHeight({ text: titleText, style: titleStyle })
83
- : 0,
89
+ height: titleSize.maxHeight * estimatedTitleRows.length,
90
+ width: titleSize.maxWidth,
91
+ align: get(xAxis, 'title.align', xAxisTitleDefaults.align),
92
+ maxRowCount: get(xAxis, 'title.maxRowCount', xAxisTitleDefaults.maxRowCount),
84
93
  },
85
94
  min: getAxisMin(xAxis, series),
86
95
  maxPadding: get(xAxis, 'maxPadding', 0.01),
@@ -1,6 +1,7 @@
1
1
  import type { ChartKitWidgetSeries, ChartKitWidgetYAxis } from '../../../../../types';
2
2
  import type { PreparedAxis } from './types';
3
- export declare const getPreparedYAxis: ({ series, yAxis, }: {
3
+ export declare const getPreparedYAxis: ({ series, yAxis, height, }: {
4
4
  series: ChartKitWidgetSeries[];
5
5
  yAxis: ChartKitWidgetYAxis[] | undefined;
6
+ height: number;
6
7
  }) => PreparedAxis[];
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, axisLabelsDefaults, yAxisTitleDefaults, } from '../../constants';
3
- import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, getWaterfallPointSubtotal, } from '../../utils';
3
+ import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, getWaterfallPointSubtotal, wrapText, } from '../../utils';
4
4
  import { createYScale } from '../useAxisScales';
5
5
  const getAxisLabelMaxWidth = (args) => {
6
6
  const { axis, series } = args;
@@ -41,7 +41,7 @@ function getAxisMin(axis, series) {
41
41
  }
42
42
  return min;
43
43
  }
44
- export const getPreparedYAxis = ({ series, yAxis, }) => {
44
+ export const getPreparedYAxis = ({ series, yAxis, height, }) => {
45
45
  const axisByPlot = [];
46
46
  const axisItems = yAxis || [{}];
47
47
  return axisItems.map((axisItem) => {
@@ -57,9 +57,14 @@ export const getPreparedYAxis = ({ series, yAxis, }) => {
57
57
  fontSize: get(axisItem, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
58
58
  };
59
59
  const titleText = get(axisItem, 'title.text', '');
60
- const titleStyle = {
61
- fontSize: get(axisItem, 'title.style.fontSize', yAxisTitleDefaults.fontSize),
62
- };
60
+ const titleStyle = Object.assign(Object.assign({}, yAxisTitleDefaults.style), get(axisItem, 'title.style'));
61
+ const titleMaxRowsCount = get(axisItem, 'title.maxRowCount', yAxisTitleDefaults.maxRowCount);
62
+ const estimatedTitleRows = wrapText({
63
+ text: titleText,
64
+ style: titleStyle,
65
+ width: height,
66
+ }).slice(0, titleMaxRowsCount);
67
+ const titleSize = getLabelsSize({ labels: [titleText], style: titleStyle });
63
68
  const axisType = get(axisItem, 'type', DEFAULT_AXIS_TYPE);
64
69
  const preparedAxis = {
65
70
  type: axisType,
@@ -87,9 +92,10 @@ export const getPreparedYAxis = ({ series, yAxis, }) => {
87
92
  text: titleText,
88
93
  margin: get(axisItem, 'title.margin', yAxisTitleDefaults.margin),
89
94
  style: titleStyle,
90
- height: titleText
91
- ? getHorisontalSvgTextHeight({ text: titleText, style: titleStyle })
92
- : 0,
95
+ width: titleSize.maxWidth,
96
+ height: titleSize.maxHeight * estimatedTitleRows.length,
97
+ align: get(axisItem, 'title.align', yAxisTitleDefaults.align),
98
+ maxRowCount: titleMaxRowsCount,
93
99
  },
94
100
  min: getAxisMin(axisItem, series),
95
101
  maxPadding: get(axisItem, 'maxPadding', 0.05),
@@ -1,5 +1,6 @@
1
1
  import type { AxisDomain, AxisScale, ScaleBand } from 'd3';
2
2
  import type { PreparedAxis, PreparedSplit } from '../hooks';
3
+ import type { TextRow } from './text';
3
4
  export declare function getTicksCount({ axis, range }: {
4
5
  axis: PreparedAxis;
5
6
  range: number;
@@ -24,3 +25,7 @@ export declare function getAxisHeight(args: {
24
25
  split: PreparedSplit;
25
26
  boundsHeight: number;
26
27
  }): number;
28
+ export declare function getAxisTitleRows(args: {
29
+ axis: PreparedAxis;
30
+ textMaxWidth: number;
31
+ }): TextRow[];
@@ -1,3 +1,4 @@
1
+ import { wrapText } from './text';
1
2
  export function getTicksCount({ axis, range }) {
2
3
  let ticksCount;
3
4
  if (axis.ticks.pixelInterval) {
@@ -48,3 +49,23 @@ export function getAxisHeight(args) {
48
49
  }
49
50
  return boundsHeight;
50
51
  }
52
+ export function getAxisTitleRows(args) {
53
+ const { axis, textMaxWidth } = args;
54
+ if (axis.title.maxRowCount < 1) {
55
+ return [];
56
+ }
57
+ const textRows = wrapText({
58
+ text: axis.title.text,
59
+ style: axis.title.style,
60
+ width: textMaxWidth,
61
+ });
62
+ return textRows.reduce((acc, row, index) => {
63
+ if (index < axis.title.maxRowCount) {
64
+ acc.push(row);
65
+ }
66
+ else {
67
+ acc[axis.title.maxRowCount - 1].text += row.text;
68
+ }
69
+ return acc;
70
+ }, []);
71
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Selection } from 'd3';
2
2
  import { BaseTextStyle } from '../../../../types';
3
+ export declare function handleOverflowingText(tSpan: SVGTSpanElement | null, maxWidth: number): void;
3
4
  export declare function setEllipsisForOverflowText<T>(selection: Selection<SVGTextElement, T, null, unknown>, maxWidth: number): void;
4
5
  export declare function setEllipsisForOverflowTexts<T>(selection: Selection<SVGTextElement, T, any, unknown>, maxWidth: ((datum: T) => number) | number): void;
5
6
  export declare function hasOverlappingLabels({ width, labels, padding, style, }: {
@@ -16,3 +17,12 @@ export declare function getLabelsSize({ labels, style, rotation, }: {
16
17
  maxHeight: number;
17
18
  maxWidth: number;
18
19
  };
20
+ export type TextRow = {
21
+ text: string;
22
+ y: number;
23
+ };
24
+ export declare function wrapText(args: {
25
+ text: string;
26
+ style?: BaseTextStyle;
27
+ width: number;
28
+ }): TextRow[];
@@ -1,15 +1,32 @@
1
1
  import { select } from 'd3-selection';
2
- export function setEllipsisForOverflowText(selection, maxWidth) {
3
- var _a, _b, _c, _d;
4
- let text = selection.text();
5
- selection.text(null).append('title').text(text);
6
- const tSpan = selection.append('tspan').text(text).style('alignment-baseline', 'inherit');
7
- let textLength = ((_b = (_a = tSpan.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
2
+ export function handleOverflowingText(tSpan, maxWidth) {
3
+ var _a, _b, _c;
4
+ if (!tSpan) {
5
+ return;
6
+ }
7
+ const svg = tSpan.closest('svg');
8
+ if (!svg) {
9
+ return;
10
+ }
11
+ const textNode = tSpan.closest('text');
12
+ const angle = ((_a = Array.from((textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal) || []).find((item) => item.angle)) === null || _a === void 0 ? void 0 : _a.angle) || 0;
13
+ const revertRotation = svg.createSVGTransform();
14
+ revertRotation.setRotate(-angle, 0, 0);
15
+ textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.appendItem(revertRotation);
16
+ let text = tSpan.textContent || '';
17
+ let textLength = ((_b = tSpan.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
8
18
  while (textLength > maxWidth && text.length > 1) {
9
19
  text = text.slice(0, -1);
10
- tSpan.text(text + '…');
11
- textLength = ((_d = (_c = tSpan.node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect()) === null || _d === void 0 ? void 0 : _d.width) || 0;
20
+ tSpan.textContent = text + '…';
21
+ textLength = ((_c = tSpan.getBoundingClientRect()) === null || _c === void 0 ? void 0 : _c.width) || 0;
12
22
  }
23
+ textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.removeItem((textNode === null || textNode === void 0 ? void 0 : textNode.transform.baseVal.length) - 1);
24
+ }
25
+ export function setEllipsisForOverflowText(selection, maxWidth) {
26
+ const text = selection.text();
27
+ selection.text(null).append('title').text(text);
28
+ const tSpan = selection.append('tspan').text(text).style('alignment-baseline', 'inherit');
29
+ handleOverflowingText(tSpan.node(), maxWidth);
13
30
  }
14
31
  export function setEllipsisForOverflowTexts(selection, maxWidth) {
15
32
  selection.each(function (datum) {
@@ -48,6 +65,9 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
48
65
  }
49
66
  export function getLabelsSize({ labels, style, rotation, }) {
50
67
  var _a;
68
+ if (!labels.filter(Boolean).length) {
69
+ return { maxHeight: 0, maxWidth: 0 };
70
+ }
51
71
  const container = select(document.body)
52
72
  .append('div')
53
73
  .attr('class', 'chartkit chartkit-theme_common');
@@ -62,3 +82,36 @@ export function getLabelsSize({ labels, style, rotation, }) {
62
82
  container.remove();
63
83
  return { maxHeight: height, maxWidth: width };
64
84
  }
85
+ export function wrapText(args) {
86
+ const { text, style, width } = args;
87
+ const height = getLabelsSize({
88
+ labels: [text],
89
+ style: style,
90
+ }).maxHeight;
91
+ // @ts-ignore
92
+ const segmenter = new Intl.Segmenter([], { granularity: 'word' });
93
+ const segments = Array.from(segmenter.segment(text));
94
+ return segments.reduce((acc, s) => {
95
+ const item = s;
96
+ if (!acc.length) {
97
+ acc.push({
98
+ text: '',
99
+ y: acc.length * height,
100
+ });
101
+ }
102
+ let lastRow = acc[acc.length - 1];
103
+ if (item.isWordLike &&
104
+ getLabelsSize({
105
+ labels: [lastRow.text + item.segment],
106
+ style,
107
+ }).maxWidth > width) {
108
+ lastRow = {
109
+ text: '',
110
+ y: acc.length * height,
111
+ };
112
+ acc.push(lastRow);
113
+ }
114
+ lastRow.text += item.segment;
115
+ return acc;
116
+ }, []);
117
+ }
@@ -15,6 +15,8 @@ declare const HighchartsWidget: React.ForwardRefExoticComponent<{
15
15
  hoistConfigError?: boolean | undefined;
16
16
  nonBodyScroll?: boolean | undefined;
17
17
  splitTooltip?: boolean | undefined;
18
+ paneSplitOrientation?: import("react-split-pane").Split | undefined;
19
+ onSplitPaneOrientationChange?: ((orientation: import("react-split-pane").Split) => void) | undefined;
18
20
  onChange?: ((data: {
19
21
  type: "PARAMS_CHANGED";
20
22
  data: {
@@ -574,5 +574,6 @@ export declare class HighchartsComponent extends React.PureComponent<Props, Stat
574
574
  updateParams: (params: StringParams) => void;
575
575
  private getId;
576
576
  private onLoad;
577
+ private needRenderCallback;
577
578
  }
578
579
  export {};
@@ -103,12 +103,7 @@ export class HighchartsComponent extends React.PureComponent {
103
103
  });
104
104
  }
105
105
  componentDidUpdate() {
106
- var _a, _b;
107
- const needRenderCallback = this.props.onRender && !this.state.isError && !this.props.splitTooltip;
108
- if (needRenderCallback) {
109
- (_b = (_a = this.props).onRender) === null || _b === void 0 ? void 0 : _b.call(_a, {
110
- renderTime: getChartPerformanceDuration(this.getId()),
111
- });
106
+ if (this.needRenderCallback()) {
112
107
  const widget = this.chartComponent.current ? this.chartComponent.current.chart : null;
113
108
  if (this.state.callback && widget) {
114
109
  this.state.callback(widget);
@@ -124,7 +119,7 @@ export class HighchartsComponent extends React.PureComponent {
124
119
  return null;
125
120
  }
126
121
  markChartPerformance(this.getId(true));
127
- return (React.createElement(Component, { key: Math.random(), options: options, highcharts: Highcharts, onSplitPaneMountCallback: this.state.callback || undefined, callback: this.extendChartInstance, constructorType: (options === null || options === void 0 ? void 0 : options.useHighStock) ? 'stockChart' : 'chart', containerProps: { className: 'chartkit-graph' }, ref: this.chartComponent }));
122
+ return (React.createElement(Component, { key: Math.random(), options: options, highcharts: Highcharts, onSplitPaneMountCallback: this.state.callback || undefined, onSplitPaneOrientationChange: this.props.onSplitPaneOrientationChange, paneSplitOrientation: this.props.paneSplitOrientation, callback: this.extendChartInstance, constructorType: (options === null || options === void 0 ? void 0 : options.useHighStock) ? 'stockChart' : 'chart', containerProps: { className: 'chartkit-graph' }, ref: this.chartComponent, onRender: this.needRenderCallback() && this.props.onRender }));
128
123
  }
129
124
  getId(refresh = false) {
130
125
  if (refresh) {
@@ -149,6 +144,11 @@ export class HighchartsComponent extends React.PureComponent {
149
144
  window.requestAnimationFrame(this.reflow);
150
145
  }
151
146
  }
147
+ needRenderCallback() {
148
+ const { splitTooltip, onRender } = this.props;
149
+ const { isError } = this.state;
150
+ return !splitTooltip && onRender && !isError;
151
+ }
152
152
  }
153
153
  HighchartsComponent.defaultProps = {
154
154
  hoistConfigError: true,
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import Highcharts from 'highcharts';
3
+ import type { ChartKitProps } from '../../../../types';
3
4
  interface HighchartsReactRefObject {
4
5
  chart: Highcharts.Chart | null | undefined;
5
6
  container: React.RefObject<HTMLDivElement | undefined>;
@@ -13,6 +14,7 @@ interface HighchartsReactProps {
13
14
  highcharts?: typeof Highcharts;
14
15
  options: Highcharts.Options;
15
16
  callback?: Highcharts.ChartCallbackFunction;
17
+ onRender?: ChartKitProps<any>['onRender'];
16
18
  }
17
19
  export declare const HighchartsReact: React.ForwardRefExoticComponent<React.PropsWithoutRef<HighchartsReactProps> & React.RefAttributes<HighchartsReactRefObject>>;
18
20
  export default HighchartsReact;
@@ -1,9 +1,15 @@
1
1
  /* eslint no-console: ["error", { allow: ["warn", "error"]}] */
2
2
  import React from 'react';
3
+ import afterFrame from 'afterframe';
4
+ import { measurePerformance } from '../../../../utils';
5
+ import { useElementSize } from './useElementSize';
3
6
  const useIsomorphicLayoutEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect;
4
7
  export const HighchartsReact = React.memo(React.forwardRef(function HighchartsReact(props, ref) {
8
+ const { onRender } = props;
5
9
  const containerRef = React.useRef(null);
6
10
  const chartRef = React.useRef();
11
+ const { width, height } = useElementSize(containerRef);
12
+ const performanceMeasure = React.useRef(measurePerformance());
7
13
  useIsomorphicLayoutEffect(() => {
8
14
  function createChart() {
9
15
  const { highcharts: HighchartsComponent } = props;
@@ -47,6 +53,21 @@ export const HighchartsReact = React.memo(React.forwardRef(function HighchartsRe
47
53
  },
48
54
  container: containerRef,
49
55
  }), []);
56
+ React.useLayoutEffect(() => {
57
+ if (width && height) {
58
+ if (!performanceMeasure.current) {
59
+ performanceMeasure.current = measurePerformance();
60
+ }
61
+ afterFrame(() => {
62
+ var _a;
63
+ const renderTime = (_a = performanceMeasure.current) === null || _a === void 0 ? void 0 : _a.end();
64
+ if (typeof renderTime === 'number') {
65
+ onRender === null || onRender === void 0 ? void 0 : onRender({ renderTime });
66
+ }
67
+ performanceMeasure.current = null;
68
+ });
69
+ }
70
+ }, [width, height, onRender]);
50
71
  return React.createElement("div", Object.assign({}, props.containerProps, { ref: containerRef }));
51
72
  }));
52
73
  HighchartsReact.displayName = 'HighchartsReact';
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
- import { Split, SplitPaneProps } from 'react-split-pane';
2
+ import { SplitPaneProps } from 'react-split-pane';
3
3
  import './StyledSplitPane.css';
4
- export type PaneSplit = Split;
5
4
  type Props = SplitPaneProps & {
6
5
  paneOneRender: () => React.ReactNode;
7
6
  paneTwoRender: () => React.ReactNode;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export interface UseElementSizeResult {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ export declare function useElementSize<T extends HTMLElement = HTMLDivElement>(ref: React.MutableRefObject<T | null> | null, key?: string): UseElementSizeResult;
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import debounce from 'lodash/debounce';
3
+ import round from 'lodash/round';
4
+ const RESIZE_DEBOUNCE = 200;
5
+ const ROUND_PRESICION = 2;
6
+ export function useElementSize(ref,
7
+ // can be used, when it is needed to force reassign observer to element
8
+ // in order to get correct measures. might be related to below
9
+ // https://github.com/WICG/resize-observer/issues/65
10
+ key) {
11
+ const [size, setSize] = React.useState({
12
+ width: 0,
13
+ height: 0,
14
+ });
15
+ React.useLayoutEffect(() => {
16
+ if (!(ref === null || ref === void 0 ? void 0 : ref.current)) {
17
+ return undefined;
18
+ }
19
+ const handleResize = (entries) => {
20
+ if (!Array.isArray(entries)) {
21
+ return;
22
+ }
23
+ const entry = entries[0];
24
+ if (entry && entry.borderBoxSize) {
25
+ const borderBoxSize = entry.borderBoxSize[0]
26
+ ? entry.borderBoxSize[0]
27
+ : entry.borderBoxSize;
28
+ // ...but old versions of Firefox treat it as a single item
29
+ // https://github.com/mdn/dom-examples/blob/main/resize-observer/resize-observer-text.html#L88
30
+ setSize({
31
+ width: round(borderBoxSize.inlineSize, ROUND_PRESICION),
32
+ height: round(borderBoxSize.blockSize, ROUND_PRESICION),
33
+ });
34
+ }
35
+ else if (entry) {
36
+ const target = entry.target;
37
+ setSize({
38
+ width: round(target.offsetWidth, ROUND_PRESICION),
39
+ height: round(target.offsetHeight, ROUND_PRESICION),
40
+ });
41
+ }
42
+ };
43
+ const observer = new ResizeObserver(debounce(handleResize, RESIZE_DEBOUNCE));
44
+ observer.observe(ref.current);
45
+ return () => {
46
+ observer.disconnect();
47
+ };
48
+ }, [ref, key]);
49
+ return size;
50
+ }
@@ -1,11 +1,18 @@
1
1
  import React from 'react';
2
2
  import type { Highcharts } from '../../../types';
3
3
  import './WithSplitPane.css';
4
+ declare enum PaneSplits {
5
+ VERTICAL = "vertical",
6
+ HORIZONTAL = "horizontal"
7
+ }
4
8
  export declare const withSplitPane: <ComposedComponentProps extends {}>(ComposedComponent: React.ComponentType<ComposedComponentProps>) => React.ForwardRefExoticComponent<React.PropsWithoutRef<ComposedComponentProps & {
5
9
  onPaneChange?: (() => void) | undefined;
6
10
  onSplitPaneMountCallback?: ((chart: Highcharts.Chart) => void) | undefined;
11
+ paneSplitOrientation?: PaneSplits | undefined;
12
+ onSplitPaneOrientationChange?: ((orientation?: PaneSplits) => void) | undefined;
7
13
  } & {
8
14
  current: any;
9
15
  forwardedRef: React.Ref<ComposedComponentProps>;
10
16
  callback?: Highcharts.ChartCallbackFunction | undefined;
11
17
  }> & React.RefAttributes<ComposedComponentProps>>;
18
+ export {};
@@ -79,9 +79,10 @@ export const withSplitPane = (ComposedComponent) => {
79
79
  this.state = {
80
80
  paneSize: undefined,
81
81
  maxPaneSize: undefined,
82
- paneSplit: window.innerWidth > window.innerHeight
83
- ? PaneSplits.VERTICAL
84
- : PaneSplits.HORIZONTAL,
82
+ paneSplit: this.props.paneSplitOrientation ||
83
+ (window.innerWidth > window.innerHeight
84
+ ? PaneSplits.VERTICAL
85
+ : PaneSplits.HORIZONTAL),
85
86
  componentKey: getRandomCKId(),
86
87
  };
87
88
  this.tooltipContainerRef = React.createRef();
@@ -136,15 +137,16 @@ export const withSplitPane = (ComposedComponent) => {
136
137
  };
137
138
  this.handleOrientationChange = () => {
138
139
  const handleResizeAfterOrientationChange = () => {
140
+ var _a, _b;
139
141
  const deviceWidth = window.innerWidth;
140
142
  const deviceHeight = window.innerHeight;
143
+ const aspectRatioOrientation = deviceWidth > deviceHeight ? PaneSplits.VERTICAL : PaneSplits.HORIZONTAL;
141
144
  this.setState({
142
- paneSplit: deviceWidth > deviceHeight
143
- ? PaneSplits.VERTICAL
144
- : PaneSplits.HORIZONTAL,
145
+ paneSplit: this.props.paneSplitOrientation || aspectRatioOrientation,
145
146
  }, () => {
146
147
  this.setInitialState(true);
147
148
  });
149
+ (_b = (_a = this.props).onSplitPaneOrientationChange) === null || _b === void 0 ? void 0 : _b.call(_a, aspectRatioOrientation);
148
150
  window.removeEventListener('resize', handleResizeAfterOrientationChange);
149
151
  };
150
152
  window.addEventListener('resize', handleResizeAfterOrientationChange);
@@ -1,6 +1,7 @@
1
1
  import type { FormatNumberOptions } from '../../plugins/shared';
2
2
  import type { BaseTextStyle } from './base';
3
3
  export type ChartKitWidgetAxisType = 'category' | 'datetime' | 'linear' | 'logarithmic';
4
+ export type ChartKitWidgetAxisTitleAlignment = 'left' | 'center' | 'right';
4
5
  export type ChartKitWidgetAxisLabels = {
5
6
  /** Enable or disable the axis labels. */
6
7
  enabled?: boolean;
@@ -37,11 +38,18 @@ export type ChartKitWidgetAxis = {
37
38
  lineColor?: string;
38
39
  title?: {
39
40
  text?: string;
41
+ /** CSS styles for the title */
42
+ style?: Partial<BaseTextStyle>;
40
43
  /** The pixel distance between the axis labels or line and the title.
41
44
  *
42
45
  * Defaults to 4 for horizontal axes, 8 for vertical.
43
46
  * */
44
47
  margin?: number;
48
+ /** Alignment of the title. */
49
+ align?: ChartKitWidgetAxisTitleAlignment;
50
+ /** Allows limiting of the contents of a title block to the specified number of lines.
51
+ * Defaults to 1. */
52
+ maxRowCount?: number;
45
53
  };
46
54
  /** The minimum value of the axis. If undefined the min value is automatically calculate. */
47
55
  min?: number;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="react" />
2
+ import type { Split } from 'react-split-pane';
2
3
  import type { Highcharts, HighchartsWidgetData, StringParams } from '../plugins/highcharts/types';
3
4
  import type { IndicatorWidgetData } from '../plugins/indicator/types';
4
5
  import type { CustomTooltipProps, Yagr, YagrWidgetData } from '../plugins/yagr/types';
@@ -20,6 +21,8 @@ export interface ChartKitWidget {
20
21
  hoistConfigError?: boolean;
21
22
  nonBodyScroll?: boolean;
22
23
  splitTooltip?: boolean;
24
+ paneSplitOrientation?: Split;
25
+ onSplitPaneOrientationChange?: (orientation: Split) => void;
23
26
  onChange?: (data: {
24
27
  type: 'PARAMS_CHANGED';
25
28
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "5.9.0",
3
+ "version": "5.10.1",
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",