@gravity-ui/charts 1.19.0 → 1.20.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.
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import isEqual from 'lodash/isEqual';
2
3
  import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapeSeries, useShapes, useSplit, } from '../../hooks';
3
4
  import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
5
  import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
@@ -8,7 +9,7 @@ import { getPreparedOptions } from '../../hooks/useSeries/prepare-options';
8
9
  import { getActiveLegendItems } from '../../hooks/useSeries/utils';
9
10
  import { useZoom } from '../../hooks/useZoom';
10
11
  import { getSortedSeriesData, getZoomedSeriesData } from '../../utils';
11
- import { hasAtLeastOneSeriesDataPerPlot, useAsyncState } from './utils';
12
+ import { hasAtLeastOneSeriesDataPerPlot } from './utils';
12
13
  export function useChartInnerProps(props) {
13
14
  var _a;
14
15
  const { width, height, data, dispatcher, htmlLayout, svgContainer, plotNode, clipPathId } = props;
@@ -45,34 +46,51 @@ export function useChartInnerProps(props) {
45
46
  seriesData: zoomedSeriesData,
46
47
  seriesOptions: data.series.options,
47
48
  });
48
- const setAxes = React.useCallback(async () => {
49
- const seriesData = preparedSeries.filter((s) => s.visible);
50
- const xAxis = await getPreparedXAxis({
51
- xAxis: data.xAxis,
52
- width,
53
- seriesData,
54
- seriesOptions: preparedSeriesOptions,
55
- });
56
- let estimatedBoundsHeight = height;
57
- if (xAxis) {
58
- estimatedBoundsHeight =
59
- height -
60
- (xAxis.title.height +
61
- xAxis.title.margin +
62
- xAxis.labels.margin +
63
- xAxis.labels.height +
64
- (preparedLegend ? preparedLegend.height + preparedLegend.margin : 0) +
65
- chart.margin.top +
66
- chart.margin.bottom);
67
- }
68
- const yAxis = await getPreparedYAxis({
69
- height,
70
- boundsHeight: estimatedBoundsHeight,
71
- width,
72
- seriesData,
73
- yAxis: data.yAxis,
74
- });
75
- return { xAxis, yAxis };
49
+ // preparing the X and Y axes
50
+ const [axesState, setValue] = React.useState({ xAxis: null, yAxis: [] });
51
+ const axesStateRunRef = React.useRef(0);
52
+ const prevAxesStateValue = React.useRef(axesState);
53
+ const axesStateReady = React.useRef(false);
54
+ React.useEffect(() => {
55
+ axesStateRunRef.current++;
56
+ axesStateReady.current = false;
57
+ (async function () {
58
+ const currentRun = axesStateRunRef.current;
59
+ const seriesData = preparedSeries.filter((s) => s.visible);
60
+ const xAxis = await getPreparedXAxis({
61
+ xAxis: data.xAxis,
62
+ width,
63
+ seriesData,
64
+ seriesOptions: preparedSeriesOptions,
65
+ });
66
+ let estimatedBoundsHeight = height;
67
+ if (xAxis) {
68
+ estimatedBoundsHeight =
69
+ height -
70
+ (xAxis.title.height +
71
+ xAxis.title.margin +
72
+ xAxis.labels.margin +
73
+ xAxis.labels.height +
74
+ (preparedLegend ? preparedLegend.height + preparedLegend.margin : 0) +
75
+ chart.margin.top +
76
+ chart.margin.bottom);
77
+ }
78
+ const yAxis = await getPreparedYAxis({
79
+ height,
80
+ boundsHeight: estimatedBoundsHeight,
81
+ width,
82
+ seriesData,
83
+ yAxis: data.yAxis,
84
+ });
85
+ const newStateValue = { xAxis, yAxis };
86
+ if (axesStateRunRef.current === currentRun) {
87
+ if (!isEqual(prevAxesStateValue.current, newStateValue)) {
88
+ setValue(newStateValue);
89
+ prevAxesStateValue.current = newStateValue;
90
+ }
91
+ axesStateReady.current = true;
92
+ }
93
+ })();
76
94
  }, [
77
95
  chart.margin,
78
96
  data.xAxis,
@@ -83,7 +101,7 @@ export function useChartInnerProps(props) {
83
101
  preparedSeriesOptions,
84
102
  width,
85
103
  ]);
86
- const { xAxis, yAxis } = useAsyncState({ xAxis: null, yAxis: [] }, setAxes);
104
+ const { xAxis, yAxis } = axesStateReady.current ? axesState : { xAxis: null, yAxis: [] };
87
105
  const activeLegendItems = React.useMemo(() => getActiveLegendItems(preparedSeries), [preparedSeries]);
88
106
  const { preparedSeries: preparedShapesSeries } = useShapeSeries({
89
107
  colors,
@@ -18,7 +18,8 @@
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "It seems you are trying to use inappropriate value for built-in \"tooltip.totals.aggregation\". Available values: [{{values}}].",
19
19
  "label_invalid-axis-type": "It seems you are trying to use inappropriate type for \"{{key}}\" axis. Available types: [{{values}}].",
20
20
  "label_invalid-axis-labels-html-type": "It seems you are trying to use inappropriate type for \"labels.html\" property. Only boolean is allowed.",
21
- "label_invalid-axis-labels-html-not-supported-axis-type": "It seems you are trying to use \"labels.html\" property for an axis with an unsupported type. This property is supported only for \"category\" axis."
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "It seems you are trying to use \"labels.html\" property for an axis with an unsupported type. This property is supported only for \"category\" axis.",
22
+ "label_duplicate-axis-categories": "It seems you have duplicate value \"{{duplicate}}\" found in {{key}}[{{axisIndex}}]."
22
23
  },
23
24
  "tooltip": {
24
25
  "label_totals_sum": "Sum",
@@ -18,7 +18,8 @@
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "Похоже, что вы пытаетесь использовать некорректное значение для встроенной агрегации \"tooltip.totals.aggregation\". Доступные значения: [{{values}}].",
19
19
  "label_invalid-axis-type": "Похоже, что вы пытаетесь использовать некорректный тип для оси \"{{key}}\". Доступные типы: [{{values}}].",
20
20
  "label_invalid-axis-labels-html-type": "Похоже, что вы пытаетесь использовать некорректный тип для свойства \"labels.html\". Допускается только использование булевых значений.",
21
- "label_invalid-axis-labels-html-not-supported-axis-type": "Похоже, что вы пытаетесь использовать свойство \"labels.html\" для оси с неподдерживаемым типом. Это свойство поддерживается только для оси типа \"category\"."
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "Похоже, что вы пытаетесь использовать свойство \"labels.html\" для оси с неподдерживаемым типом. Это свойство поддерживается только для оси типа \"category\".",
22
+ "label_duplicate-axis-categories": "Похоже, что у вас есть дублирующееся значение категории \"{{duplicate}}\" в оси {{key}}[{{axisIndex}}]."
22
23
  },
23
24
  "tooltip": {
24
25
  "label_totals_sum": "Сумма",
@@ -2,6 +2,22 @@ import { AXIS_TYPE } from '../constants';
2
2
  import { i18n } from '../i18n';
3
3
  import { CHART_ERROR_CODE, ChartError } from '../libs';
4
4
  const AVAILABLE_AXIS_TYPES = Object.values(AXIS_TYPE);
5
+ function validateDuplicateCategories({ categories, key, axisIndex, }) {
6
+ const seen = new Set();
7
+ categories.forEach((category) => {
8
+ if (seen.has(category)) {
9
+ throw new ChartError({
10
+ code: CHART_ERROR_CODE.INVALID_DATA,
11
+ message: i18n('error', 'label_duplicate-axis-categories', {
12
+ key,
13
+ axisIndex,
14
+ duplicate: category,
15
+ }),
16
+ });
17
+ }
18
+ seen.add(category);
19
+ });
20
+ }
5
21
  function validateAxisType({ axis, key }) {
6
22
  if (axis.type && !AVAILABLE_AXIS_TYPES.includes(axis.type)) {
7
23
  throw new ChartError({
@@ -38,9 +54,23 @@ export function validateAxes(args) {
38
54
  if (xAxis) {
39
55
  validateAxisType({ axis: xAxis, key: 'x' });
40
56
  validateLabelsHtmlOptions({ axis: xAxis });
57
+ if ((xAxis === null || xAxis === void 0 ? void 0 : xAxis.type) === 'category' && xAxis.categories) {
58
+ validateDuplicateCategories({
59
+ categories: xAxis.categories,
60
+ key: 'x',
61
+ axisIndex: 0,
62
+ });
63
+ }
41
64
  }
42
- yAxis.forEach((axis) => {
65
+ yAxis.forEach((axis, axisIndex) => {
43
66
  validateAxisType({ axis, key: 'y' });
67
+ if (axis.type === 'category' && axis.categories) {
68
+ validateDuplicateCategories({
69
+ categories: axis.categories,
70
+ key: 'y',
71
+ axisIndex,
72
+ });
73
+ }
44
74
  validateLabelsHtmlOptions({ axis });
45
75
  });
46
76
  }
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import isEqual from 'lodash/isEqual';
2
3
  import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapeSeries, useShapes, useSplit, } from '../../hooks';
3
4
  import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
5
  import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
@@ -8,7 +9,7 @@ import { getPreparedOptions } from '../../hooks/useSeries/prepare-options';
8
9
  import { getActiveLegendItems } from '../../hooks/useSeries/utils';
9
10
  import { useZoom } from '../../hooks/useZoom';
10
11
  import { getSortedSeriesData, getZoomedSeriesData } from '../../utils';
11
- import { hasAtLeastOneSeriesDataPerPlot, useAsyncState } from './utils';
12
+ import { hasAtLeastOneSeriesDataPerPlot } from './utils';
12
13
  export function useChartInnerProps(props) {
13
14
  var _a;
14
15
  const { width, height, data, dispatcher, htmlLayout, svgContainer, plotNode, clipPathId } = props;
@@ -45,34 +46,51 @@ export function useChartInnerProps(props) {
45
46
  seriesData: zoomedSeriesData,
46
47
  seriesOptions: data.series.options,
47
48
  });
48
- const setAxes = React.useCallback(async () => {
49
- const seriesData = preparedSeries.filter((s) => s.visible);
50
- const xAxis = await getPreparedXAxis({
51
- xAxis: data.xAxis,
52
- width,
53
- seriesData,
54
- seriesOptions: preparedSeriesOptions,
55
- });
56
- let estimatedBoundsHeight = height;
57
- if (xAxis) {
58
- estimatedBoundsHeight =
59
- height -
60
- (xAxis.title.height +
61
- xAxis.title.margin +
62
- xAxis.labels.margin +
63
- xAxis.labels.height +
64
- (preparedLegend ? preparedLegend.height + preparedLegend.margin : 0) +
65
- chart.margin.top +
66
- chart.margin.bottom);
67
- }
68
- const yAxis = await getPreparedYAxis({
69
- height,
70
- boundsHeight: estimatedBoundsHeight,
71
- width,
72
- seriesData,
73
- yAxis: data.yAxis,
74
- });
75
- return { xAxis, yAxis };
49
+ // preparing the X and Y axes
50
+ const [axesState, setValue] = React.useState({ xAxis: null, yAxis: [] });
51
+ const axesStateRunRef = React.useRef(0);
52
+ const prevAxesStateValue = React.useRef(axesState);
53
+ const axesStateReady = React.useRef(false);
54
+ React.useEffect(() => {
55
+ axesStateRunRef.current++;
56
+ axesStateReady.current = false;
57
+ (async function () {
58
+ const currentRun = axesStateRunRef.current;
59
+ const seriesData = preparedSeries.filter((s) => s.visible);
60
+ const xAxis = await getPreparedXAxis({
61
+ xAxis: data.xAxis,
62
+ width,
63
+ seriesData,
64
+ seriesOptions: preparedSeriesOptions,
65
+ });
66
+ let estimatedBoundsHeight = height;
67
+ if (xAxis) {
68
+ estimatedBoundsHeight =
69
+ height -
70
+ (xAxis.title.height +
71
+ xAxis.title.margin +
72
+ xAxis.labels.margin +
73
+ xAxis.labels.height +
74
+ (preparedLegend ? preparedLegend.height + preparedLegend.margin : 0) +
75
+ chart.margin.top +
76
+ chart.margin.bottom);
77
+ }
78
+ const yAxis = await getPreparedYAxis({
79
+ height,
80
+ boundsHeight: estimatedBoundsHeight,
81
+ width,
82
+ seriesData,
83
+ yAxis: data.yAxis,
84
+ });
85
+ const newStateValue = { xAxis, yAxis };
86
+ if (axesStateRunRef.current === currentRun) {
87
+ if (!isEqual(prevAxesStateValue.current, newStateValue)) {
88
+ setValue(newStateValue);
89
+ prevAxesStateValue.current = newStateValue;
90
+ }
91
+ axesStateReady.current = true;
92
+ }
93
+ })();
76
94
  }, [
77
95
  chart.margin,
78
96
  data.xAxis,
@@ -83,7 +101,7 @@ export function useChartInnerProps(props) {
83
101
  preparedSeriesOptions,
84
102
  width,
85
103
  ]);
86
- const { xAxis, yAxis } = useAsyncState({ xAxis: null, yAxis: [] }, setAxes);
104
+ const { xAxis, yAxis } = axesStateReady.current ? axesState : { xAxis: null, yAxis: [] };
87
105
  const activeLegendItems = React.useMemo(() => getActiveLegendItems(preparedSeries), [preparedSeries]);
88
106
  const { preparedSeries: preparedShapesSeries } = useShapeSeries({
89
107
  colors,
@@ -18,7 +18,8 @@
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "It seems you are trying to use inappropriate value for built-in \"tooltip.totals.aggregation\". Available values: [{{values}}].",
19
19
  "label_invalid-axis-type": "It seems you are trying to use inappropriate type for \"{{key}}\" axis. Available types: [{{values}}].",
20
20
  "label_invalid-axis-labels-html-type": "It seems you are trying to use inappropriate type for \"labels.html\" property. Only boolean is allowed.",
21
- "label_invalid-axis-labels-html-not-supported-axis-type": "It seems you are trying to use \"labels.html\" property for an axis with an unsupported type. This property is supported only for \"category\" axis."
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "It seems you are trying to use \"labels.html\" property for an axis with an unsupported type. This property is supported only for \"category\" axis.",
22
+ "label_duplicate-axis-categories": "It seems you have duplicate value \"{{duplicate}}\" found in {{key}}[{{axisIndex}}]."
22
23
  },
23
24
  "tooltip": {
24
25
  "label_totals_sum": "Sum",
@@ -18,7 +18,8 @@
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "Похоже, что вы пытаетесь использовать некорректное значение для встроенной агрегации \"tooltip.totals.aggregation\". Доступные значения: [{{values}}].",
19
19
  "label_invalid-axis-type": "Похоже, что вы пытаетесь использовать некорректный тип для оси \"{{key}}\". Доступные типы: [{{values}}].",
20
20
  "label_invalid-axis-labels-html-type": "Похоже, что вы пытаетесь использовать некорректный тип для свойства \"labels.html\". Допускается только использование булевых значений.",
21
- "label_invalid-axis-labels-html-not-supported-axis-type": "Похоже, что вы пытаетесь использовать свойство \"labels.html\" для оси с неподдерживаемым типом. Это свойство поддерживается только для оси типа \"category\"."
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "Похоже, что вы пытаетесь использовать свойство \"labels.html\" для оси с неподдерживаемым типом. Это свойство поддерживается только для оси типа \"category\".",
22
+ "label_duplicate-axis-categories": "Похоже, что у вас есть дублирующееся значение категории \"{{duplicate}}\" в оси {{key}}[{{axisIndex}}]."
22
23
  },
23
24
  "tooltip": {
24
25
  "label_totals_sum": "Сумма",
@@ -2,6 +2,22 @@ import { AXIS_TYPE } from '../constants';
2
2
  import { i18n } from '../i18n';
3
3
  import { CHART_ERROR_CODE, ChartError } from '../libs';
4
4
  const AVAILABLE_AXIS_TYPES = Object.values(AXIS_TYPE);
5
+ function validateDuplicateCategories({ categories, key, axisIndex, }) {
6
+ const seen = new Set();
7
+ categories.forEach((category) => {
8
+ if (seen.has(category)) {
9
+ throw new ChartError({
10
+ code: CHART_ERROR_CODE.INVALID_DATA,
11
+ message: i18n('error', 'label_duplicate-axis-categories', {
12
+ key,
13
+ axisIndex,
14
+ duplicate: category,
15
+ }),
16
+ });
17
+ }
18
+ seen.add(category);
19
+ });
20
+ }
5
21
  function validateAxisType({ axis, key }) {
6
22
  if (axis.type && !AVAILABLE_AXIS_TYPES.includes(axis.type)) {
7
23
  throw new ChartError({
@@ -38,9 +54,23 @@ export function validateAxes(args) {
38
54
  if (xAxis) {
39
55
  validateAxisType({ axis: xAxis, key: 'x' });
40
56
  validateLabelsHtmlOptions({ axis: xAxis });
57
+ if ((xAxis === null || xAxis === void 0 ? void 0 : xAxis.type) === 'category' && xAxis.categories) {
58
+ validateDuplicateCategories({
59
+ categories: xAxis.categories,
60
+ key: 'x',
61
+ axisIndex: 0,
62
+ });
63
+ }
41
64
  }
42
- yAxis.forEach((axis) => {
65
+ yAxis.forEach((axis, axisIndex) => {
43
66
  validateAxisType({ axis, key: 'y' });
67
+ if (axis.type === 'category' && axis.categories) {
68
+ validateDuplicateCategories({
69
+ categories: axis.categories,
70
+ key: 'y',
71
+ axisIndex,
72
+ });
73
+ }
44
74
  validateLabelsHtmlOptions({ axis });
45
75
  });
46
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",