@gravity-ui/chartkit 4.4.0 → 4.5.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.
@@ -5,6 +5,7 @@ type Props = {
5
5
  width: number;
6
6
  height: number;
7
7
  scale: ChartScale;
8
+ chartWidth: number;
8
9
  };
9
- export declare const AxisX: ({ axis, width, height, scale }: Props) => React.JSX.Element;
10
+ export declare const AxisX: ({ axis, width, height, scale, chartWidth }: Props) => React.JSX.Element;
10
11
  export {};
@@ -1,29 +1,11 @@
1
1
  import React from 'react';
2
2
  import { axisBottom, select } from 'd3';
3
3
  import { block } from '../../../../utils/cn';
4
- import { formatAxisTickLabel, parseTransformStyle } from '../utils';
4
+ import { formatAxisTickLabel, parseTransformStyle, setEllipsisForOverflowText } from '../utils';
5
5
  const b = block('d3-axis');
6
6
  const EMPTY_SPACE_BETWEEN_LABELS = 10;
7
- // Note: this method do not prepared for rotated labels
8
- const removeOverlappingXTicks = (axis) => {
9
- var _a;
10
- const a = axis.selectAll('g.tick').nodes();
11
- if (a.length <= 1) {
12
- return;
13
- }
14
- for (let i = 0, x = 0; i < a.length; i++) {
15
- const node = a[i];
16
- const r = node.getBoundingClientRect();
17
- if (r.left < x) {
18
- (_a = node === null || node === void 0 ? void 0 : node.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(node);
19
- }
20
- else {
21
- x = r.right + EMPTY_SPACE_BETWEEN_LABELS;
22
- }
23
- }
24
- };
25
7
  // FIXME: add overflow ellipsis for the labels that out of boundaries
26
- export const AxisX = ({ axis, width, height, scale }) => {
8
+ export const AxisX = ({ axis, width, height, scale, chartWidth }) => {
27
9
  const ref = React.useRef(null);
28
10
  React.useEffect(() => {
29
11
  if (!ref.current) {
@@ -64,6 +46,30 @@ export const AxisX = ({ axis, width, height, scale }) => {
64
46
  // Remove tick that has the same x coordinate like domain
65
47
  svgElement.select('.tick').remove();
66
48
  }
49
+ // remove overlapping labels
50
+ let elementX = 0;
51
+ svgElement
52
+ .selectAll('.tick')
53
+ .filter(function () {
54
+ const node = this;
55
+ const r = node.getBoundingClientRect();
56
+ if (r.left < elementX) {
57
+ return true;
58
+ }
59
+ elementX = r.right + EMPTY_SPACE_BETWEEN_LABELS;
60
+ return false;
61
+ })
62
+ .remove();
63
+ // add an ellipsis to the labels on the right that go beyond the boundaries of the chart
64
+ svgElement.selectAll('.tick text').each(function () {
65
+ const node = this;
66
+ const textRect = node.getBoundingClientRect();
67
+ if (textRect.right > chartWidth) {
68
+ const maxWidth = textRect.width - (textRect.right - chartWidth) * 2;
69
+ select(node).call(setEllipsisForOverflowText, maxWidth);
70
+ }
71
+ });
72
+ // add an axis header if necessary
67
73
  if (axis.title.text) {
68
74
  const textY = axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding;
69
75
  svgElement
@@ -73,9 +79,9 @@ export const AxisX = ({ axis, width, height, scale }) => {
73
79
  .attr('x', width / 2)
74
80
  .attr('y', textY)
75
81
  .attr('font-size', axis.title.style.fontSize)
76
- .text(axis.title.text);
82
+ .text(axis.title.text)
83
+ .call(setEllipsisForOverflowText, width);
77
84
  }
78
- removeOverlappingXTicks(svgElement);
79
85
  }, [axis, width, height, scale]);
80
86
  return React.createElement("g", { ref: ref });
81
87
  };
@@ -53,7 +53,7 @@ export const Chart = (props) => {
53
53
  xScale && yScale && (React.createElement(React.Fragment, null,
54
54
  React.createElement(AxisY, { axises: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale }),
55
55
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
56
- React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale })))),
56
+ React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, chartWidth: width })))),
57
57
  shapes),
58
58
  legend.enabled && (React.createElement(Legend, { width: boundsWidth, offsetWidth: chart.margin.left, height: legend.height, legend: legend, offsetHeight: height - legend.height / 2, chartSeries: preparedSeries, onItemClick: handleLegendItemClick }))),
59
59
  React.createElement(Tooltip, { hovered: hovered, pointerPosition: pointerPosition, tooltip: tooltip, xAxis: xAxis, yAxis: yAxis[0] })));
@@ -7,15 +7,15 @@ const isNumericalArrayData = (data) => {
7
7
  };
8
8
  const filterCategoriesByVisibleSeries = (args) => {
9
9
  const { axisDirection, categories, series } = args;
10
- return categories.filter((category) => {
11
- return series.some((s) => {
12
- return (isSeriesWithCategoryValues(s) &&
13
- s.data.some((d) => {
14
- const dataCategory = getDataCategoryValue({ axisDirection, categories, data: d });
15
- return dataCategory === category;
16
- }));
17
- });
10
+ const visibleCategories = new Set();
11
+ series.forEach((s) => {
12
+ if (isSeriesWithCategoryValues(s)) {
13
+ s.data.forEach((d) => {
14
+ visibleCategories.add(getDataCategoryValue({ axisDirection, categories, data: d }));
15
+ });
16
+ }
18
17
  });
18
+ return categories.filter((c) => visibleCategories.has(c));
19
19
  };
20
20
  const createScales = (args) => {
21
21
  const { boundsWidth, boundsHeight, series, xAxis, yAxis } = args;
@@ -33,14 +33,15 @@ const createScales = (args) => {
33
33
  visibleSeries = visibleSeries.length === 0 ? series : visibleSeries;
34
34
  let xScale;
35
35
  let yScale;
36
+ const xAxisMinPadding = boundsWidth * xAxis.maxPadding;
37
+ const xRange = [0, boundsWidth - xAxisMinPadding];
36
38
  switch (xType) {
37
39
  case 'linear': {
38
40
  const domain = getDomainDataXBySeries(visibleSeries);
39
- const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding];
40
41
  if (isNumericalArrayData(domain)) {
41
42
  const [domainXMin, xMax] = extent(domain);
42
43
  const xMinValue = typeof xMin === 'number' ? xMin : domainXMin;
43
- xScale = scaleLinear().domain([xMinValue, xMax]).range(range).nice();
44
+ xScale = scaleLinear().domain([xMinValue, xMax]).range(xRange).nice();
44
45
  }
45
46
  break;
46
47
  }
@@ -52,20 +53,22 @@ const createScales = (args) => {
52
53
  series: visibleSeries,
53
54
  });
54
55
  xScale = scaleBand().domain(filteredCategories).range([0, boundsWidth]);
56
+ if (xScale.step() / 2 < xAxisMinPadding) {
57
+ xScale.range(xRange);
58
+ }
55
59
  }
56
60
  break;
57
61
  }
58
62
  case 'datetime': {
59
- const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding];
60
63
  if (xTimestamps) {
61
64
  const [xMin, xMax] = extent(xTimestamps);
62
- xScale = scaleUtc().domain([xMin, xMax]).range(range).nice();
65
+ xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice();
63
66
  }
64
67
  else {
65
68
  const domain = getDomainDataXBySeries(visibleSeries);
66
69
  if (isNumericalArrayData(domain)) {
67
70
  const [xMin, xMax] = extent(domain);
68
- xScale = scaleUtc().domain([xMin, xMax]).range(range).nice();
71
+ xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice();
69
72
  }
70
73
  }
71
74
  break;
@@ -80,12 +80,8 @@ const getMarginLeft = (args) => {
80
80
  return marginLeft;
81
81
  };
82
82
  const getMarginRight = (args) => {
83
- const { chart, hasAxisRelatedSeries, series, preparedXAxis } = args;
84
- let marginRight = get(chart, 'margin.right', 0);
85
- if (hasAxisRelatedSeries) {
86
- marginRight += getAxisLabelMaxWidth({ axis: preparedXAxis, series: series.data }) / 2;
87
- }
88
- return marginRight;
83
+ const { chart } = args;
84
+ return get(chart, 'margin.right', 0);
89
85
  };
90
86
  export const getPreparedChart = (args) => {
91
87
  const { chart, series, preparedLegend, preparedXAxis, preparedY1Axis, preparedTitle } = args;
@@ -98,7 +94,7 @@ export const getPreparedChart = (args) => {
98
94
  preparedXAxis,
99
95
  });
100
96
  const marginLeft = getMarginLeft({ chart, hasAxisRelatedSeries, series, preparedY1Axis });
101
- const marginRight = getMarginRight({ chart, hasAxisRelatedSeries, series, preparedXAxis });
97
+ const marginRight = getMarginRight({ chart });
102
98
  return {
103
99
  margin: {
104
100
  top: marginTop,
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { group } from 'd3';
3
- import { getRandomCKId } from '../../../../../utils';
4
3
  import { getOnlyVisibleSeries } from '../../utils';
5
4
  import { BarXSeriesShapes } from './bar-x';
6
5
  import { ScatterSeriesShape } from './scatter';
@@ -23,8 +22,7 @@ export const useShapes = (args) => {
23
22
  case 'scatter': {
24
23
  if (xScale && yScale) {
25
24
  const scatterShapes = chartSeries.map((scatterSeries, i) => {
26
- const id = getRandomCKId();
27
- return (React.createElement(ScatterSeriesShape, { key: `${i}-${id}`, top: top, left: left, series: scatterSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
25
+ return (React.createElement(ScatterSeriesShape, { key: i, top: top, left: left, series: scatterSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
28
26
  });
29
27
  acc.push(...scatterShapes);
30
28
  }
@@ -47,16 +47,13 @@ export function ScatterSeriesShape(props) {
47
47
  return;
48
48
  }
49
49
  const svgElement = select(ref.current);
50
- svgElement.selectAll('*').remove();
51
50
  const preparedData = xAxis.type === 'category' || ((_a = yAxis[0]) === null || _a === void 0 ? void 0 : _a.type) === 'category'
52
51
  ? series.data
53
52
  : prepareLinearScatterData(series.data);
54
53
  svgElement
55
- .selectAll('allPoints')
54
+ .selectAll('circle')
56
55
  .data(preparedData)
57
- .enter()
58
- .append('circle')
59
- .attr('class', b('point'))
56
+ .join((enter) => enter.append('circle').attr('class', b('point')), (update) => update, (exit) => exit.remove())
60
57
  .attr('fill', (d) => d.color || series.color || '')
61
58
  .attr('r', (d) => d.radius || DEFAULT_SCATTER_POINT_RADIUS)
62
59
  .attr('cx', (d) => getCxAttr({ point: d, xAxis, xScale }))
@@ -1,6 +1,7 @@
1
1
  import { AxisDomain } from 'd3';
2
2
  import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData, ChartKitWidgetAxisType, ChartKitWidgetAxisLabels } from '../../../../types/widget-data';
3
3
  export * from './math';
4
+ export * from './text';
4
5
  export type AxisDirection = 'x' | 'y';
5
6
  type UnknownSeries = {
6
7
  type: ChartKitWidgetSeries['type'];
@@ -5,6 +5,7 @@ import { dateTime } from '@gravity-ui/date-utils';
5
5
  import { formatNumber } from '../../../shared';
6
6
  import { DEFAULT_AXIS_LABEL_FONT_SIZE } from '../constants';
7
7
  export * from './math';
8
+ export * from './text';
8
9
  const CHARTS_WITHOUT_AXIS = ['pie'];
9
10
  /**
10
11
  * Checks whether the series should be drawn with axes.
@@ -150,8 +151,5 @@ const extractCategoryValue = (args) => {
150
151
  export const getDataCategoryValue = (args) => {
151
152
  const { axisDirection, categories, data } = args;
152
153
  const categoryValue = extractCategoryValue({ axisDirection, categories, data });
153
- if (!categories.includes(categoryValue)) {
154
- throw new Error('It seems you are trying to use category value that is not in categories array');
155
- }
156
154
  return categoryValue;
157
155
  };
@@ -0,0 +1,2 @@
1
+ import { Selection } from 'd3-selection';
2
+ export declare function setEllipsisForOverflowText(selection: Selection<SVGTextElement, any, null, undefined>, maxWidth: number): void;
@@ -0,0 +1,12 @@
1
+ export function setEllipsisForOverflowText(selection, maxWidth) {
2
+ var _a, _b;
3
+ let text = selection.text();
4
+ selection.text(null).attr('text-anchor', 'left').append('title').text(text);
5
+ const tSpan = selection.append('tspan').text(text);
6
+ let textLength = ((_a = tSpan.node()) === null || _a === void 0 ? void 0 : _a.getComputedTextLength()) || 0;
7
+ while (textLength > maxWidth && text.length > 1) {
8
+ text = text.slice(0, -1);
9
+ tSpan.text(text + '…');
10
+ textLength = ((_b = tSpan.node()) === null || _b === void 0 ? void 0 : _b.getComputedTextLength()) || 0;
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "4.4.0",
3
+ "version": "4.5.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",
@@ -48,7 +48,7 @@
48
48
  "dependencies": {
49
49
  "@bem-react/classname": "^1.6.0",
50
50
  "@gravity-ui/date-utils": "^1.4.1",
51
- "@gravity-ui/yagr": "^3.7.13",
51
+ "@gravity-ui/yagr": "^3.8.0",
52
52
  "d3": "^7.8.5",
53
53
  "lodash": "^4.17.21",
54
54
  "react-split-pane": "^0.1.92"