@gravity-ui/charts 1.18.2 → 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.
Files changed (108) hide show
  1. package/dist/cjs/components/AxisY/AxisY.js +7 -5
  2. package/dist/cjs/components/AxisY/prepare-axis-data.js +8 -5
  3. package/dist/cjs/components/AxisY/types.d.ts +1 -1
  4. package/dist/cjs/components/AxisY/utils.js +1 -1
  5. package/dist/cjs/components/ChartInner/index.js +20 -26
  6. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +2 -2
  7. package/dist/cjs/components/ChartInner/useChartInnerProps.js +57 -31
  8. package/dist/cjs/components/ChartInner/utils.d.ts +1 -0
  9. package/dist/cjs/components/ChartInner/utils.js +21 -0
  10. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +1 -1
  11. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +3 -2
  12. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.js +1 -0
  13. package/dist/cjs/components/Tooltip/DefaultTooltipContent/utils.js +2 -1
  14. package/dist/cjs/constants/chart-types.d.ts +1 -0
  15. package/dist/cjs/constants/chart-types.js +1 -0
  16. package/dist/cjs/constants/defaults/series-options.js +8 -0
  17. package/dist/cjs/hooks/useAxisScales/index.js +47 -8
  18. package/dist/cjs/hooks/useChartOptions/tooltip.js +1 -1
  19. package/dist/cjs/hooks/useChartOptions/x-axis.d.ts +1 -1
  20. package/dist/cjs/hooks/useChartOptions/x-axis.js +15 -4
  21. package/dist/cjs/hooks/useChartOptions/y-axis.js +15 -7
  22. package/dist/cjs/hooks/useSeries/prepare-heatmap.d.ts +11 -0
  23. package/dist/cjs/hooks/useSeries/prepare-heatmap.js +37 -0
  24. package/dist/cjs/hooks/useSeries/prepareSeries.js +9 -0
  25. package/dist/cjs/hooks/useSeries/types.d.ts +14 -2
  26. package/dist/cjs/hooks/useShapes/heatmap/index.d.ts +13 -0
  27. package/dist/cjs/hooks/useShapes/heatmap/index.js +74 -0
  28. package/dist/cjs/hooks/useShapes/heatmap/prepare-data.d.ts +13 -0
  29. package/dist/cjs/hooks/useShapes/heatmap/prepare-data.js +97 -0
  30. package/dist/cjs/hooks/useShapes/heatmap/types.d.ts +24 -0
  31. package/dist/cjs/hooks/useShapes/heatmap/types.js +1 -0
  32. package/dist/cjs/hooks/useShapes/index.d.ts +2 -1
  33. package/dist/cjs/hooks/useShapes/index.js +15 -0
  34. package/dist/cjs/hooks/useShapes/styles.css +4 -0
  35. package/dist/cjs/hooks/useTooltip/index.js +11 -1
  36. package/dist/cjs/hooks/utils/bar-y.d.ts +0 -5
  37. package/dist/cjs/hooks/utils/bar-y.js +2 -29
  38. package/dist/cjs/hooks/utils/get-band-size.d.ts +5 -0
  39. package/dist/cjs/hooks/utils/get-band-size.js +29 -0
  40. package/dist/cjs/i18n/keysets/en.json +2 -1
  41. package/dist/cjs/i18n/keysets/ru.json +2 -1
  42. package/dist/cjs/types/chart/axis.d.ts +3 -1
  43. package/dist/cjs/types/chart/heatmap.d.ts +47 -0
  44. package/dist/cjs/types/chart/heatmap.js +1 -0
  45. package/dist/cjs/types/chart/series.d.ts +19 -2
  46. package/dist/cjs/types/chart/tooltip.d.ts +7 -1
  47. package/dist/cjs/types/index.d.ts +1 -0
  48. package/dist/cjs/types/index.js +1 -0
  49. package/dist/cjs/utils/chart/color.js +3 -2
  50. package/dist/cjs/utils/chart/get-closest-data.js +18 -1
  51. package/dist/cjs/utils/chart/index.js +10 -13
  52. package/dist/cjs/utils/chart/series/waterfall.d.ts +1 -1
  53. package/dist/cjs/utils/chart/series/waterfall.js +3 -3
  54. package/dist/cjs/validation/validate-axes.js +31 -1
  55. package/dist/esm/components/AxisY/AxisY.js +7 -5
  56. package/dist/esm/components/AxisY/prepare-axis-data.js +8 -5
  57. package/dist/esm/components/AxisY/types.d.ts +1 -1
  58. package/dist/esm/components/AxisY/utils.js +1 -1
  59. package/dist/esm/components/ChartInner/index.js +20 -26
  60. package/dist/esm/components/ChartInner/useChartInnerProps.js +57 -31
  61. package/dist/esm/components/ChartInner/utils.d.ts +1 -0
  62. package/dist/esm/components/ChartInner/utils.js +21 -0
  63. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +1 -1
  64. package/dist/esm/components/Tooltip/ChartTooltipContent.js +3 -2
  65. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.js +1 -0
  66. package/dist/esm/components/Tooltip/DefaultTooltipContent/utils.js +2 -1
  67. package/dist/esm/constants/chart-types.d.ts +1 -0
  68. package/dist/esm/constants/chart-types.js +1 -0
  69. package/dist/esm/constants/defaults/series-options.js +8 -0
  70. package/dist/esm/hooks/useAxisScales/index.js +47 -8
  71. package/dist/esm/hooks/useChartOptions/tooltip.js +1 -1
  72. package/dist/esm/hooks/useChartOptions/x-axis.d.ts +1 -1
  73. package/dist/esm/hooks/useChartOptions/x-axis.js +15 -4
  74. package/dist/esm/hooks/useChartOptions/y-axis.js +15 -7
  75. package/dist/esm/hooks/useSeries/prepare-heatmap.d.ts +11 -0
  76. package/dist/esm/hooks/useSeries/prepare-heatmap.js +37 -0
  77. package/dist/esm/hooks/useSeries/prepareSeries.js +9 -0
  78. package/dist/esm/hooks/useSeries/types.d.ts +14 -2
  79. package/dist/esm/hooks/useShapes/heatmap/index.d.ts +13 -0
  80. package/dist/esm/hooks/useShapes/heatmap/index.js +74 -0
  81. package/dist/esm/hooks/useShapes/heatmap/prepare-data.d.ts +13 -0
  82. package/dist/esm/hooks/useShapes/heatmap/prepare-data.js +97 -0
  83. package/dist/esm/hooks/useShapes/heatmap/types.d.ts +24 -0
  84. package/dist/esm/hooks/useShapes/heatmap/types.js +1 -0
  85. package/dist/esm/hooks/useShapes/index.d.ts +2 -1
  86. package/dist/esm/hooks/useShapes/index.js +15 -0
  87. package/dist/esm/hooks/useShapes/styles.css +4 -0
  88. package/dist/esm/hooks/useTooltip/index.js +11 -1
  89. package/dist/esm/hooks/utils/bar-y.d.ts +0 -5
  90. package/dist/esm/hooks/utils/bar-y.js +2 -29
  91. package/dist/esm/hooks/utils/get-band-size.d.ts +5 -0
  92. package/dist/esm/hooks/utils/get-band-size.js +29 -0
  93. package/dist/esm/i18n/keysets/en.json +2 -1
  94. package/dist/esm/i18n/keysets/ru.json +2 -1
  95. package/dist/esm/types/chart/axis.d.ts +3 -1
  96. package/dist/esm/types/chart/heatmap.d.ts +47 -0
  97. package/dist/esm/types/chart/heatmap.js +1 -0
  98. package/dist/esm/types/chart/series.d.ts +19 -2
  99. package/dist/esm/types/chart/tooltip.d.ts +7 -1
  100. package/dist/esm/types/index.d.ts +1 -0
  101. package/dist/esm/types/index.js +1 -0
  102. package/dist/esm/utils/chart/color.js +3 -2
  103. package/dist/esm/utils/chart/get-closest-data.js +18 -1
  104. package/dist/esm/utils/chart/index.js +10 -13
  105. package/dist/esm/utils/chart/series/waterfall.d.ts +1 -1
  106. package/dist/esm/utils/chart/series/waterfall.js +3 -3
  107. package/dist/esm/validation/validate-axes.js +31 -1
  108. package/package.json +1 -1
@@ -49,7 +49,7 @@ export function getClosestPoints(args) {
49
49
  const groups = groupBy(shapesData, getSeriesType);
50
50
  // eslint-disable-next-line complexity
51
51
  Object.entries(groups).forEach(([seriesType, list]) => {
52
- var _a, _b, _c;
52
+ var _a, _b, _c, _d;
53
53
  switch (seriesType) {
54
54
  case 'bar-x': {
55
55
  const points = list.map((d) => ({
@@ -177,6 +177,23 @@ export function getClosestPoints(args) {
177
177
  }
178
178
  break;
179
179
  }
180
+ case 'heatmap': {
181
+ const data = list;
182
+ const closestPoint = (_d = data[0]) === null || _d === void 0 ? void 0 : _d.items.find((cell) => {
183
+ return (pointerX >= cell.x &&
184
+ pointerX <= cell.x + cell.width &&
185
+ pointerY >= cell.y &&
186
+ pointerY <= cell.y + cell.height);
187
+ });
188
+ if (closestPoint) {
189
+ result.push({
190
+ data: closestPoint.data,
191
+ series: data[0].series,
192
+ closest: true,
193
+ });
194
+ }
195
+ break;
196
+ }
180
197
  case 'sankey': {
181
198
  const [data] = list;
182
199
  const closestLink = data.links.find((d) => {
@@ -2,7 +2,7 @@ import { group, select } from 'd3';
2
2
  import get from 'lodash/get';
3
3
  import isNil from 'lodash/isNil';
4
4
  import sortBy from 'lodash/sortBy';
5
- import { DEFAULT_AXIS_LABEL_FONT_SIZE } from '../../constants';
5
+ import { DEFAULT_AXIS_LABEL_FONT_SIZE, SeriesType } from '../../constants';
6
6
  import { getSeriesStackId } from '../../hooks/useSeries/utils';
7
7
  import { getWaterfallPointSubtotal } from './series/waterfall';
8
8
  export * from './axis';
@@ -109,19 +109,16 @@ export function getDefaultMinXAxisValue(series) {
109
109
  }
110
110
  export function getDefaultMinYAxisValue(series) {
111
111
  if (series === null || series === void 0 ? void 0 : series.some((s) => CHART_SERIES_WITH_VOLUME_ON_Y_AXIS.includes(s.type))) {
112
+ if (series.some((s) => s.type === SeriesType.Waterfall)) {
113
+ const seriesData = series.map((s) => s.data).flat();
114
+ const minSubTotal = seriesData.reduce((res, d) => Math.min(res, getWaterfallPointSubtotal(d, seriesData) || 0), 0);
115
+ return Math.min(0, minSubTotal);
116
+ }
112
117
  return series.reduce((minValue, s) => {
113
- switch (s.type) {
114
- case 'waterfall': {
115
- const minSubTotal = s.data.reduce((res, d) => Math.min(res, getWaterfallPointSubtotal(d, s) || 0), 0);
116
- return Math.min(minValue, minSubTotal);
117
- }
118
- default: {
119
- // https://github.com/gravity-ui/charts/issues/160
120
- // @ts-expect-error
121
- const minYValue = s.data.reduce((res, d) => Math.min(res, get(d, 'y', 0)), 0);
122
- return Math.min(minValue, minYValue);
123
- }
124
- }
118
+ // https://github.com/gravity-ui/charts/issues/160
119
+ // @ts-expect-error
120
+ const minYValue = s.data.reduce((res, d) => Math.min(res, get(d, 'y', 0)), 0);
121
+ return Math.min(minValue, minYValue);
125
122
  }, 0);
126
123
  }
127
124
  return undefined;
@@ -1,4 +1,4 @@
1
1
  import type { PreparedWaterfallSeries, PreparedWaterfallSeriesData } from '../../../hooks';
2
2
  import type { WaterfallSeriesData } from '../../../types';
3
3
  export declare function getWaterfallPointColor(point: WaterfallSeriesData, series: PreparedWaterfallSeries): string;
4
- export declare function getWaterfallPointSubtotal(point: PreparedWaterfallSeriesData, series: PreparedWaterfallSeries): number | null;
4
+ export declare function getWaterfallPointSubtotal(point: PreparedWaterfallSeriesData, data: PreparedWaterfallSeriesData[]): number | null;
@@ -4,12 +4,12 @@ export function getWaterfallPointColor(point, series) {
4
4
  }
5
5
  return series.color;
6
6
  }
7
- export function getWaterfallPointSubtotal(point, series) {
8
- const pointIndex = series.data.indexOf(point);
7
+ export function getWaterfallPointSubtotal(point, data) {
8
+ const pointIndex = data.indexOf(point);
9
9
  if (pointIndex === -1) {
10
10
  return null;
11
11
  }
12
- return series.data.reduce((sum, d, index) => {
12
+ return data.reduce((sum, d, index) => {
13
13
  if (index <= pointIndex) {
14
14
  const value = d.total ? 0 : Number(d.y);
15
15
  return sum + value;
@@ -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
  }
@@ -43,11 +43,13 @@ export const AxisY = (props) => {
43
43
  .attr('y', (d) => d.y)
44
44
  .attr('text-anchor', 'start');
45
45
  }
46
- svgElement
47
- .append('path')
48
- .attr('class', b('domain'))
49
- .attr('d', lineGenerator([preparedAxisData.domain.start, preparedAxisData.domain.end]))
50
- .style('stroke', preparedAxisData.domain.lineColor);
46
+ if (preparedAxisData.domain) {
47
+ svgElement
48
+ .append('path')
49
+ .attr('class', b('domain'))
50
+ .attr('d', lineGenerator([preparedAxisData.domain.start, preparedAxisData.domain.end]))
51
+ .style('stroke', preparedAxisData.domain.lineColor);
52
+ }
51
53
  const tickClassName = b('tick');
52
54
  const ticks = svgElement
53
55
  .selectAll(`.${tickClassName}`)
@@ -86,11 +86,14 @@ export async function prepareAxisData({ axis, split, scale, top: topOffset, widt
86
86
  const axisPlotTopPosition = ((_a = split.plots[axis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
87
87
  const axisHeight = ((_b = split.plots[axis.plotIndex]) === null || _b === void 0 ? void 0 : _b.height) || height;
88
88
  const domainX = axis.position === 'left' ? 0 : width;
89
- const domain = {
90
- start: [domainX, axisPlotTopPosition],
91
- end: [domainX, axisPlotTopPosition + axisHeight],
92
- lineColor: (_c = axis.lineColor) !== null && _c !== void 0 ? _c : '',
93
- };
89
+ let domain = null;
90
+ if (axis.visible) {
91
+ domain = {
92
+ start: [domainX, axisPlotTopPosition],
93
+ end: [domainX, axisPlotTopPosition + axisHeight],
94
+ lineColor: (_c = axis.lineColor) !== null && _c !== void 0 ? _c : '',
95
+ };
96
+ }
94
97
  const ticks = [];
95
98
  const getTextSize = getTextSizeFn({ style: axis.labels.style });
96
99
  const labelLineHeight = (await getTextSize('Tmp')).height;
@@ -74,7 +74,7 @@ export type AxisDomainData = {
74
74
  export type AxisYData = {
75
75
  id: string;
76
76
  title: AxisTitleData | null;
77
- domain: AxisDomainData;
77
+ domain: AxisDomainData | null;
78
78
  ticks: AxisTickData[];
79
79
  plotLines: AxisPlotLineData[];
80
80
  plotBands: AxisPlotBandData[];
@@ -15,8 +15,8 @@ export function getTickValues({ scale, axis, labelLineHeight, series, }) {
15
15
  }
16
16
  const getScaleTicks = () => {
17
17
  var _a;
18
+ const domainData = getDomainDataYBySeries(series);
18
19
  if (series.some((s) => s.type === 'bar-y')) {
19
- const domainData = getDomainDataYBySeries(series);
20
20
  if (domainData.length < 3) {
21
21
  return domainData;
22
22
  }
@@ -13,6 +13,7 @@ import { Tooltip } from '../Tooltip';
13
13
  import { useChartInnerHandlers } from './useChartInnerHandlers';
14
14
  import { useChartInnerProps } from './useChartInnerProps';
15
15
  import { useChartInnerState } from './useChartInnerState';
16
+ import { useAsyncState } from './utils';
16
17
  import './styles.css';
17
18
  const b = block('chart');
18
19
  export const ChartInner = (props) => {
@@ -82,34 +83,27 @@ export const ChartInner = (props) => {
82
83
  unpinTooltip === null || unpinTooltip === void 0 ? void 0 : unpinTooltip();
83
84
  }
84
85
  }, [prevWidth, width, prevHeight, height, tooltipPinned, unpinTooltip]);
85
- const [yAxisDataItems, setYAxisDataItems] = React.useState([]);
86
- const countedRef = React.useRef(0);
87
- React.useEffect(() => {
88
- countedRef.current++;
89
- (async function () {
90
- const currentRun = countedRef.current;
91
- const items = [];
92
- for (let i = 0; i < yAxis.length; i++) {
93
- const axis = yAxis[i];
94
- const scale = yScale === null || yScale === void 0 ? void 0 : yScale[i];
95
- if (scale) {
96
- const axisData = await prepareAxisData({
97
- axis,
98
- scale,
99
- top: boundsOffsetTop,
100
- width: boundsWidth,
101
- height: boundsHeight,
102
- split: preparedSplit,
103
- series: preparedSeries,
104
- });
105
- items.push(axisData);
106
- }
107
- }
108
- if (countedRef.current === currentRun) {
109
- setYAxisDataItems(items);
86
+ const setYAxisDataItems = React.useCallback(async () => {
87
+ const items = [];
88
+ for (let i = 0; i < yAxis.length; i++) {
89
+ const axis = yAxis[i];
90
+ const scale = yScale === null || yScale === void 0 ? void 0 : yScale[i];
91
+ if (scale) {
92
+ const axisData = await prepareAxisData({
93
+ axis,
94
+ scale,
95
+ top: boundsOffsetTop,
96
+ width: boundsWidth,
97
+ height: boundsHeight,
98
+ split: preparedSplit,
99
+ series: preparedSeries.filter((s) => s.visible),
100
+ });
101
+ items.push(axisData);
110
102
  }
111
- })();
103
+ }
104
+ return items;
112
105
  }, [boundsHeight, boundsOffsetTop, boundsWidth, preparedSeries, preparedSplit, yAxis, yScale]);
106
+ const yAxisDataItems = useAsyncState([], setYAxisDataItems);
113
107
  return (React.createElement("div", { className: b() },
114
108
  React.createElement("svg", { ref: svgRef, width: width, height: height,
115
109
  // We use onPointerMove here because onMouseMove works incorrectly when the zoom setting is enabled:
@@ -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';
@@ -38,37 +39,6 @@ export function useChartInnerProps(props) {
38
39
  zoomState,
39
40
  });
40
41
  }, [data.xAxis, data.yAxis, sortedSeriesData, zoomState]);
41
- const [xAxis, setXAxis] = React.useState(null);
42
- React.useEffect(() => {
43
- setXAxis(null);
44
- getPreparedXAxis({
45
- xAxis: data.xAxis,
46
- width,
47
- seriesData: zoomedSeriesData,
48
- seriesOptions: preparedSeriesOptions,
49
- }).then((val) => setXAxis(val));
50
- }, [data.xAxis, preparedSeriesOptions, width, zoomedSeriesData]);
51
- const estimatedBoundsHeight = React.useMemo(() => {
52
- if (xAxis) {
53
- return (height -
54
- xAxis.title.height +
55
- xAxis.title.margin +
56
- xAxis.labels.margin +
57
- parseInt(xAxis.labels.style.fontSize, 10));
58
- }
59
- return 0;
60
- }, [height, xAxis]);
61
- const [yAxis, setYAxis] = React.useState([]);
62
- React.useEffect(() => {
63
- setYAxis([]);
64
- getPreparedYAxis({
65
- height,
66
- boundsHeight: estimatedBoundsHeight,
67
- width,
68
- seriesData: zoomedSeriesData,
69
- yAxis: data.yAxis,
70
- }).then((val) => setYAxis(val));
71
- }, [data.yAxis, estimatedBoundsHeight, height, width, zoomedSeriesData]);
72
42
  const { preparedSeries, preparedLegend, handleLegendItemClick } = useSeries({
73
43
  colors,
74
44
  legend: data.legend,
@@ -76,6 +46,62 @@ export function useChartInnerProps(props) {
76
46
  seriesData: zoomedSeriesData,
77
47
  seriesOptions: data.series.options,
78
48
  });
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
+ })();
94
+ }, [
95
+ chart.margin,
96
+ data.xAxis,
97
+ data.yAxis,
98
+ height,
99
+ preparedLegend,
100
+ preparedSeries,
101
+ preparedSeriesOptions,
102
+ width,
103
+ ]);
104
+ const { xAxis, yAxis } = axesStateReady.current ? axesState : { xAxis: null, yAxis: [] };
79
105
  const activeLegendItems = React.useMemo(() => getActiveLegendItems(preparedSeries), [preparedSeries]);
80
106
  const { preparedSeries: preparedShapesSeries } = useShapeSeries({
81
107
  colors,
@@ -1,3 +1,4 @@
1
1
  import type { PreparedAxis } from '../../hooks/useChartOptions/types';
2
2
  import type { ChartSeries } from '../../types';
3
3
  export declare function hasAtLeastOneSeriesDataPerPlot(seriesData: ChartSeries[], yAxes?: PreparedAxis[]): boolean;
4
+ export declare function useAsyncState<T>(value: T, setState: () => Promise<T>): T;
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+ import isEqual from 'lodash/isEqual';
1
3
  export function hasAtLeastOneSeriesDataPerPlot(seriesData, yAxes = []) {
2
4
  const hasDataMap = new Map();
3
5
  yAxes.forEach((yAxis) => {
@@ -26,3 +28,22 @@ export function hasAtLeastOneSeriesDataPerPlot(seriesData, yAxes = []) {
26
28
  });
27
29
  return [...hasDataMap.values()].every((hasData) => hasData);
28
30
  }
31
+ export function useAsyncState(value, setState) {
32
+ const [stateValue, setValue] = React.useState(value);
33
+ const countedRef = React.useRef(0);
34
+ const prevValue = React.useRef(value);
35
+ const ready = React.useRef(false);
36
+ React.useEffect(() => {
37
+ countedRef.current++;
38
+ (async function () {
39
+ const currentRun = countedRef.current;
40
+ const newValue = await setState();
41
+ ready.current = true;
42
+ if (countedRef.current === currentRun && !isEqual(prevValue.current, newValue)) {
43
+ setValue(newValue);
44
+ prevValue.current = newValue;
45
+ }
46
+ })();
47
+ }, [setState]);
48
+ return stateValue;
49
+ }
@@ -12,4 +12,4 @@ export interface ChartTooltipContentProps {
12
12
  yAxis?: ChartYAxis;
13
13
  qa?: string;
14
14
  }
15
- export declare const ChartTooltipContent: (props: ChartTooltipContentProps) => React.JSX.Element | null;
15
+ export declare const ChartTooltipContent: React.MemoExoticComponent<(props: ChartTooltipContentProps) => React.JSX.Element | null>;
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
2
  import isNil from 'lodash/isNil';
3
3
  import { DefaultTooltipContent } from './DefaultTooltipContent';
4
- export const ChartTooltipContent = (props) => {
4
+ export const ChartTooltipContent = React.memo((props) => {
5
5
  const { hovered, xAxis, yAxis, renderer, rowRenderer, valueFormat, headerFormat, totals, pinned, qa, } = props;
6
6
  if (!hovered) {
7
7
  return null;
8
8
  }
9
9
  const customTooltip = renderer === null || renderer === void 0 ? void 0 : renderer({ hovered, xAxis, yAxis });
10
10
  return isNil(customTooltip) ? (React.createElement(DefaultTooltipContent, { hovered: hovered, pinned: pinned, rowRenderer: rowRenderer, totals: totals, valueFormat: valueFormat, headerFormat: headerFormat, xAxis: xAxis, yAxis: yAxis, qa: qa })) : (customTooltip);
11
- };
11
+ });
12
+ ChartTooltipContent.displayName = 'ChartTooltipContent';
@@ -150,6 +150,7 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
150
150
  });
151
151
  }
152
152
  case 'pie':
153
+ case 'heatmap':
153
154
  case 'treemap': {
154
155
  const seriesData = data;
155
156
  const formattedValue = getFormattedValue({
@@ -39,7 +39,7 @@ export function getDefaultValueFormat({ axis, closestPointsRange, }) {
39
39
  }
40
40
  export const getMeasureValue = ({ data, xAxis, yAxis, headerFormat, }) => {
41
41
  var _a, _b, _c, _d, _e, _f;
42
- if (data.every((item) => ['pie', 'treemap', 'waterfall', 'sankey'].includes(item.series.type))) {
42
+ if (data.every((item) => ['pie', 'treemap', 'waterfall', 'sankey', 'heatmap'].includes(item.series.type))) {
43
43
  return null;
44
44
  }
45
45
  if (data.some((item) => item.series.type === 'radar')) {
@@ -78,6 +78,7 @@ export function getHoveredValues(args) {
78
78
  }
79
79
  case 'pie':
80
80
  case 'radar':
81
+ case 'heatmap':
81
82
  case 'treemap': {
82
83
  const seriesData = data;
83
84
  return seriesData.value;
@@ -9,4 +9,5 @@ export declare const SeriesType: {
9
9
  readonly Waterfall: "waterfall";
10
10
  readonly Sankey: "sankey";
11
11
  readonly Radar: "radar";
12
+ readonly Heatmap: "heatmap";
12
13
  };
@@ -9,4 +9,5 @@ export const SeriesType = {
9
9
  Waterfall: 'waterfall',
10
10
  Sankey: 'sankey',
11
11
  Radar: 'radar',
12
+ Heatmap: 'heatmap',
12
13
  };
@@ -117,4 +117,12 @@ export const seriesOptionsDefaults = {
117
117
  },
118
118
  },
119
119
  },
120
+ heatmap: {
121
+ states: {
122
+ hover: {
123
+ enabled: true,
124
+ brightness: 0.3,
125
+ },
126
+ },
127
+ },
120
128
  };
@@ -3,8 +3,8 @@ import { extent, scaleBand, scaleLinear, scaleLog, scaleUtc } from 'd3';
3
3
  import get from 'lodash/get';
4
4
  import { DEFAULT_AXIS_TYPE, SeriesType } from '../../constants';
5
5
  import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, getAxisHeight, getDataCategoryValue, getDefaultMaxXAxisValue, getDefaultMinXAxisValue, getDomainDataXBySeries, getDomainDataYBySeries, getOnlyVisibleSeries, isAxisRelatedSeries, isSeriesWithCategoryValues, } from '../../utils';
6
- import { getBandSize } from '../utils';
7
6
  import { getBarXLayoutForNumericScale, groupBarXDataByXValue } from '../utils/bar-x';
7
+ import { getBandSize } from '../utils/get-band-size';
8
8
  const X_AXIS_ZOOM_PADDING = 0.02;
9
9
  function validateArrayData(data) {
10
10
  let hasNumberAndNullValues;
@@ -59,6 +59,10 @@ function getYScaleRange(args) {
59
59
  }
60
60
  }
61
61
  }
62
+ function isSeriesWithYAxisOffset(series) {
63
+ const types = [SeriesType.BarY, SeriesType.Heatmap];
64
+ return series.some((s) => types.includes(s.type));
65
+ }
62
66
  // eslint-disable-next-line complexity
63
67
  export function createYScale(args) {
64
68
  const { axis, boundsHeight, series } = args;
@@ -89,9 +93,9 @@ export function createYScale(args) {
89
93
  const scaleFn = axis.type === 'logarithmic' ? scaleLog : scaleLinear;
90
94
  const scale = scaleFn().domain([yMin, yMax]).range(range);
91
95
  let offsetMin = 0;
92
- let offsetMax = boundsHeight * axis.maxPadding;
93
- const barYSeries = series.filter((s) => s.type === SeriesType.BarY);
94
- if (barYSeries.length) {
96
+ // We should ignore padding if we are drawing only one point on the plot.
97
+ let offsetMax = yMin === yMax ? 0 : boundsHeight * axis.maxPadding;
98
+ if (isSeriesWithYAxisOffset(series)) {
95
99
  if (domain.length > 1) {
96
100
  const bandWidth = getBandSize({
97
101
  scale: scale,
@@ -138,8 +142,7 @@ export function createYScale(args) {
138
142
  const scale = scaleUtc().domain([yMin, yMax]).range(range);
139
143
  let offsetMin = 0;
140
144
  let offsetMax = boundsHeight * axis.maxPadding;
141
- const barYSeries = series.filter((s) => s.type === SeriesType.BarY);
142
- if (barYSeries.length) {
145
+ if (isSeriesWithYAxisOffset(series)) {
143
146
  if (Object.keys(domain).length > 1) {
144
147
  const bandWidth = getBandSize({
145
148
  scale: scale,
@@ -175,6 +178,10 @@ function calculateXAxisPadding(series) {
175
178
  });
176
179
  return result;
177
180
  }
181
+ function isSeriesWithXAxisOffset(series) {
182
+ const types = [SeriesType.Heatmap];
183
+ return series.some((s) => types.includes(s.type));
184
+ }
178
185
  function getXScaleRange({ boundsWidth, series, seriesOptions, hasZoomX, axis, maxPadding, }) {
179
186
  const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
180
187
  const xRange = [0, boundsWidth - maxPadding];
@@ -252,7 +259,23 @@ export function createXScale(args) {
252
259
  }
253
260
  const scaleFn = xType === 'logarithmic' ? scaleLog : scaleLinear;
254
261
  const scale = scaleFn().domain([xMin, xMax]).range(range);
255
- if (!hasZoomX) {
262
+ let offsetMin = 0;
263
+ let offsetMax = 0;
264
+ const hasOffset = isSeriesWithXAxisOffset(series);
265
+ if (hasOffset) {
266
+ if (domainData.length > 1) {
267
+ const bandWidth = getBandSize({
268
+ scale: scale,
269
+ domain: domainData,
270
+ });
271
+ offsetMin += bandWidth / 2;
272
+ offsetMax += bandWidth / 2;
273
+ }
274
+ }
275
+ const domainOffsetMin = Math.abs(scale.invert(offsetMin) - scale.invert(0));
276
+ const domainOffsetMax = Math.abs(scale.invert(offsetMax) - scale.invert(0));
277
+ scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
278
+ if (!hasZoomX && !hasOffset) {
256
279
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
257
280
  scale.nice(Math.max(10, domainData.length));
258
281
  }
@@ -288,7 +311,23 @@ export function createXScale(args) {
288
311
  const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
289
312
  domain = [xMin, xMax];
290
313
  const scale = scaleUtc().domain(domain).range(range);
291
- if (!hasZoomX) {
314
+ let offsetMin = 0;
315
+ let offsetMax = 0;
316
+ const hasOffset = isSeriesWithXAxisOffset(series);
317
+ if (hasOffset) {
318
+ if (domainData.length > 1) {
319
+ const bandWidth = getBandSize({
320
+ scale: scale,
321
+ domain: domainData,
322
+ });
323
+ offsetMin += bandWidth / 2;
324
+ offsetMax += bandWidth / 2;
325
+ }
326
+ }
327
+ const domainOffsetMin = Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
328
+ const domainOffsetMax = Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
329
+ scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
330
+ if (!hasZoomX && !hasOffset) {
292
331
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
293
332
  scale.nice(Math.max(10, domainData.length));
294
333
  }
@@ -2,7 +2,7 @@ import get from 'lodash/get';
2
2
  import { getDefaultValueFormat } from '../../components/Tooltip/DefaultTooltipContent/utils';
3
3
  import { getDomainDataXBySeries, getDomainDataYBySeries, getMinSpaceBetween } from '../../utils';
4
4
  function getDefaultHeaderFormat({ seriesData, yAxes, xAxis, }) {
5
- if (seriesData.every((item) => ['pie', 'treemap', 'waterfall', 'sankey', 'radar'].includes(item.type))) {
5
+ if (seriesData.every((item) => ['pie', 'treemap', 'waterfall', 'sankey', 'radar', 'heatmap'].includes(item.type))) {
6
6
  return undefined;
7
7
  }
8
8
  if (seriesData.some((item) => item.type === 'bar-y')) {
@@ -6,4 +6,4 @@ export declare const getPreparedXAxis: ({ xAxis, seriesData, seriesOptions, widt
6
6
  seriesData: ChartSeries[];
7
7
  seriesOptions: PreparedSeriesOptions;
8
8
  width: number;
9
- }) => Promise<PreparedAxis>;
9
+ }) => Promise<PreparedAxis | null>;