@gravity-ui/chartkit 5.8.0 → 5.10.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.
Files changed (27) hide show
  1. package/build/plugins/d3/examples/line/LogarithmicAxis.d.ts +2 -0
  2. package/build/plugins/d3/examples/line/LogarithmicAxis.js +38 -0
  3. package/build/plugins/d3/renderer/components/AxisX.d.ts +8 -0
  4. package/build/plugins/d3/renderer/components/AxisX.js +41 -7
  5. package/build/plugins/d3/renderer/components/AxisY.js +49 -7
  6. package/build/plugins/d3/renderer/components/Chart.js +2 -1
  7. package/build/plugins/d3/renderer/components/styles.css +4 -0
  8. package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +6 -8
  9. package/build/plugins/d3/renderer/constants/defaults/axis.js +7 -1
  10. package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +9 -5
  11. package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +4 -1
  12. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +16 -7
  13. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +2 -1
  14. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +16 -10
  15. package/build/plugins/d3/renderer/utils/axis.d.ts +5 -0
  16. package/build/plugins/d3/renderer/utils/axis.js +21 -0
  17. package/build/plugins/d3/renderer/utils/text.d.ts +10 -0
  18. package/build/plugins/d3/renderer/utils/text.js +61 -8
  19. package/build/plugins/highcharts/renderer/HighchartsWidget.d.ts +2 -0
  20. package/build/plugins/highcharts/renderer/components/HighchartsComponent.js +1 -1
  21. package/build/plugins/highcharts/renderer/components/StyledSplitPane/StyledSplitPane.d.ts +1 -2
  22. package/build/plugins/highcharts/renderer/components/withSplitPane/withSplitPane.d.ts +7 -0
  23. package/build/plugins/highcharts/renderer/components/withSplitPane/withSplitPane.js +8 -6
  24. package/build/plugins/highcharts/renderer/helpers/config/config.js +11 -1
  25. package/build/types/widget-data/axis.d.ts +9 -1
  26. package/build/types/widget.d.ts +3 -0
  27. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const LineWithLogarithmicAxis: () => React.JSX.Element;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { Col, Container, Row, Text } from '@gravity-ui/uikit';
3
+ import { randomNormal } from 'd3';
4
+ import { ChartKit } from '../../../../components/ChartKit';
5
+ import { ExampleWrapper } from '../ExampleWrapper';
6
+ export const LineWithLogarithmicAxis = () => {
7
+ const randomY = randomNormal(0, 100);
8
+ const widgetData = {
9
+ series: {
10
+ data: [
11
+ {
12
+ type: 'line',
13
+ name: 'Line series',
14
+ data: new Array(25).fill(null).map((_, index) => ({
15
+ x: index,
16
+ y: Math.abs(randomY()),
17
+ })),
18
+ },
19
+ ],
20
+ },
21
+ };
22
+ const lineWidgetData = Object.assign(Object.assign({}, widgetData), { title: { text: 'line' } });
23
+ const logarithmicWidgetData = Object.assign(Object.assign({}, widgetData), { title: { text: 'logarithmic' }, yAxis: [
24
+ {
25
+ type: 'logarithmic',
26
+ },
27
+ ] });
28
+ return (React.createElement(Container, { spaceRow: 5 },
29
+ React.createElement(Row, { space: 1 },
30
+ React.createElement(Text, { variant: "header-2" }, "logarithmic VS line")),
31
+ React.createElement(Row, { space: 3 },
32
+ React.createElement(Col, { s: 12, m: 6 },
33
+ React.createElement(ExampleWrapper, null,
34
+ React.createElement(ChartKit, { type: "d3", data: lineWidgetData }))),
35
+ React.createElement(Col, { s: 12, m: 6 },
36
+ React.createElement(ExampleWrapper, null,
37
+ React.createElement(ChartKit, { type: "d3", data: logarithmicWidgetData }))))));
38
+ };
@@ -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,5 +1,5 @@
1
1
  import React from 'react';
2
- import { extent, scaleBand, scaleLinear, scaleUtc } from 'd3';
2
+ import { extent, scaleBand, scaleLinear, scaleLog, scaleUtc } from 'd3';
3
3
  import get from 'lodash/get';
4
4
  import { DEFAULT_AXIS_TYPE } from '../../constants';
5
5
  import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, getAxisHeight, getDataCategoryValue, getDefaultMaxXAxisValue, getDomainDataXBySeries, getDomainDataYBySeries, getOnlyVisibleSeries, isAxisRelatedSeries, isSeriesWithCategoryValues, } from '../../utils';
@@ -24,7 +24,8 @@ export function createYScale(axis, series, boundsHeight) {
24
24
  const yCategories = get(axis, 'categories');
25
25
  const yTimestamps = get(axis, 'timestamps');
26
26
  switch (yType) {
27
- case 'linear': {
27
+ case 'linear':
28
+ case 'logarithmic': {
28
29
  const domain = getDomainDataYBySeries(series);
29
30
  const range = [boundsHeight, boundsHeight * axis.maxPadding];
30
31
  if (isNumericalArrayData(domain)) {
@@ -34,7 +35,8 @@ export function createYScale(axis, series, boundsHeight) {
34
35
  if (series.some((s) => CHART_SERIES_WITH_VOLUME_ON_Y_AXIS.includes(s.type))) {
35
36
  yMaxValue = Math.max(yMaxValue, 0);
36
37
  }
37
- return scaleLinear().domain([yMinValue, yMaxValue]).range(range).nice();
38
+ const scaleFn = yType === 'logarithmic' ? scaleLog : scaleLinear;
39
+ return scaleFn().domain([yMinValue, yMaxValue]).range(range).nice();
38
40
  }
39
41
  break;
40
42
  }
@@ -94,13 +96,15 @@ export function createXScale(axis, series, boundsWidth) {
94
96
  const xAxisMinPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);
95
97
  const xRange = [0, boundsWidth - xAxisMinPadding];
96
98
  switch (xType) {
97
- case 'linear': {
99
+ case 'linear':
100
+ case 'logarithmic': {
98
101
  const domain = getDomainDataXBySeries(series);
99
102
  if (isNumericalArrayData(domain)) {
100
103
  const [domainXMin, domainXMax] = extent(domain);
101
104
  const xMinValue = typeof xMin === 'number' ? xMin : domainXMin;
102
105
  const xMaxValue = typeof xMax === 'number' ? Math.max(xMax, domainXMax) : domainXMax;
103
- return scaleLinear().domain([xMinValue, xMaxValue]).range(xRange).nice();
106
+ const scaleFn = xType === 'logarithmic' ? scaleLog : scaleLinear;
107
+ return scaleFn().domain([xMinValue, xMaxValue]).range(xRange).nice();
104
108
  }
105
109
  break;
106
110
  }
@@ -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
- import { DEFAULT_AXIS_LABEL_FONT_SIZE, axisLabelsDefaults, yAxisTitleDefaults, } from '../../constants';
3
- import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, getWaterfallPointSubtotal, } from '../../utils';
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, 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,10 +57,15 @@ 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
- };
63
- const axisType = get(axisItem, 'type', 'linear');
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 });
68
+ const axisType = get(axisItem, 'type', DEFAULT_AXIS_TYPE);
64
69
  const preparedAxis = {
65
70
  type: axisType,
66
71
  labels: {
@@ -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: {
@@ -124,7 +124,7 @@ export class HighchartsComponent extends React.PureComponent {
124
124
  return null;
125
125
  }
126
126
  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 }));
127
+ 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 }));
128
128
  }
129
129
  getId(refresh = false) {
130
130
  if (refresh) {
@@ -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;
@@ -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);
@@ -379,9 +379,19 @@ function validateCellManipulationConfig(tooltipOptions, property, item) {
379
379
  item[property] = tooltipOptions[property];
380
380
  }
381
381
  }
382
+ function getSeriesTypeFromTooltipContext() {
383
+ var _a, _b;
384
+ if (this.series) {
385
+ return this.series.type;
386
+ }
387
+ if (Array.isArray(this.points)) {
388
+ return (_b = (_a = this.points[0]) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.type;
389
+ }
390
+ return '';
391
+ }
382
392
  function getTooltip(tooltip, options, comments, holidays) {
383
393
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
384
- const serieType = (this.series && this.series.type) || tooltip.chart.options.chart.type;
394
+ const serieType = getSeriesTypeFromTooltipContext.call(this) || tooltip.chart.options.chart.type;
385
395
  const chart = tooltip.chart;
386
396
  const xAxis = chart.xAxis[0];
387
397
  const isDatetimeXAxis = xAxis.options.type === 'datetime';
@@ -1,6 +1,7 @@
1
1
  import type { FormatNumberOptions } from '../../plugins/shared';
2
2
  import type { BaseTextStyle } from './base';
3
- export type ChartKitWidgetAxisType = 'category' | 'datetime' | 'linear';
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.8.0",
3
+ "version": "5.10.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",