@gravity-ui/charts 1.14.0 → 1.15.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 (31) hide show
  1. package/dist/cjs/components/ChartInner/useChartInnerProps.js +7 -2
  2. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +1 -0
  3. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +2 -2
  4. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.d.ts +2 -1
  5. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.js +57 -14
  6. package/dist/cjs/components/Tooltip/index.js +1 -1
  7. package/dist/cjs/hooks/useAxisScales/index.d.ts +1 -0
  8. package/dist/cjs/hooks/useAxisScales/index.js +53 -37
  9. package/dist/cjs/hooks/useChartOptions/x-axis.d.ts +3 -1
  10. package/dist/cjs/hooks/useChartOptions/x-axis.js +4 -3
  11. package/dist/cjs/hooks/utils/bar-x.d.ts +16 -0
  12. package/dist/cjs/hooks/utils/bar-x.js +41 -0
  13. package/dist/cjs/types/chart/tooltip.d.ts +14 -0
  14. package/dist/cjs/utils/chart/index.d.ts +1 -1
  15. package/dist/cjs/utils/chart/index.js +7 -2
  16. package/dist/esm/components/ChartInner/useChartInnerProps.js +7 -2
  17. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +1 -0
  18. package/dist/esm/components/Tooltip/ChartTooltipContent.js +2 -2
  19. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.d.ts +2 -1
  20. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.js +57 -14
  21. package/dist/esm/components/Tooltip/index.js +1 -1
  22. package/dist/esm/hooks/useAxisScales/index.d.ts +1 -0
  23. package/dist/esm/hooks/useAxisScales/index.js +53 -37
  24. package/dist/esm/hooks/useChartOptions/x-axis.d.ts +3 -1
  25. package/dist/esm/hooks/useChartOptions/x-axis.js +4 -3
  26. package/dist/esm/hooks/utils/bar-x.d.ts +16 -0
  27. package/dist/esm/hooks/utils/bar-x.js +41 -0
  28. package/dist/esm/types/chart/tooltip.d.ts +14 -0
  29. package/dist/esm/utils/chart/index.d.ts +1 -1
  30. package/dist/esm/utils/chart/index.js +7 -2
  31. package/package.json +1 -1
@@ -39,8 +39,13 @@ export function useChartInnerProps(props) {
39
39
  const [xAxis, setXAxis] = React.useState(null);
40
40
  React.useEffect(() => {
41
41
  setXAxis(null);
42
- getPreparedXAxis({ xAxis: data.xAxis, width, seriesData: zoomedSeriesData }).then((val) => setXAxis(val));
43
- }, [data.xAxis, width, zoomedSeriesData]);
42
+ getPreparedXAxis({
43
+ xAxis: data.xAxis,
44
+ width,
45
+ seriesData: zoomedSeriesData,
46
+ seriesOptions: preparedSeriesOptions,
47
+ }).then((val) => setXAxis(val));
48
+ }, [data.xAxis, preparedSeriesOptions, width, zoomedSeriesData]);
44
49
  const [yAxis, setYAxis] = React.useState([]);
45
50
  React.useEffect(() => {
46
51
  setYAxis([]);
@@ -5,6 +5,7 @@ export interface ChartTooltipContentProps {
5
5
  xAxis?: ChartXAxis | null;
6
6
  yAxis?: ChartYAxis;
7
7
  renderer?: ChartTooltip['renderer'];
8
+ rowRenderer?: ChartTooltip['rowRenderer'];
8
9
  valueFormat?: ChartTooltip['valueFormat'];
9
10
  totals?: ChartTooltip['totals'];
10
11
  }
@@ -2,10 +2,10 @@ import React from 'react';
2
2
  import isNil from 'lodash/isNil';
3
3
  import { DefaultTooltipContent } from './DefaultTooltipContent';
4
4
  export const ChartTooltipContent = (props) => {
5
- const { hovered, xAxis, yAxis, renderer, valueFormat, totals } = props;
5
+ const { hovered, xAxis, yAxis, renderer, rowRenderer, valueFormat, totals } = 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
- return isNil(customTooltip) ? (React.createElement(DefaultTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, valueFormat: valueFormat, totals: totals })) : (customTooltip);
10
+ return isNil(customTooltip) ? (React.createElement(DefaultTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, valueFormat: valueFormat, totals: totals, rowRenderer: rowRenderer })) : (customTooltip);
11
11
  };
@@ -6,6 +6,7 @@ type Props = {
6
6
  valueFormat?: ValueFormat;
7
7
  xAxis?: ChartXAxis | null;
8
8
  yAxis?: ChartYAxis;
9
+ rowRenderer?: ChartTooltip['rowRenderer'];
9
10
  };
10
- export declare const DefaultTooltipContent: ({ hovered, xAxis, yAxis, valueFormat, totals }: Props) => React.JSX.Element;
11
+ export declare const DefaultTooltipContent: ({ hovered, xAxis, yAxis, valueFormat, totals, rowRenderer, }: Props) => React.JSX.Element;
11
12
  export {};
@@ -7,9 +7,25 @@ import { Row } from './Row';
7
7
  import { RowTotals } from './RowTotals';
8
8
  import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getXRowData, } from './utils';
9
9
  const b = block('tooltip');
10
- export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, totals }) => {
10
+ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, totals, rowRenderer, }) => {
11
11
  const measureValue = getMeasureValue({ data: hovered, xAxis, yAxis });
12
12
  const hoveredValues = getHoveredValues({ hovered, xAxis, yAxis });
13
+ const renderRow = ({ id, name, color, active, striped, value, formattedValue, }) => {
14
+ if (typeof rowRenderer === 'function') {
15
+ return rowRenderer({
16
+ id,
17
+ name,
18
+ color,
19
+ value,
20
+ formattedValue,
21
+ striped,
22
+ active,
23
+ className: b('content-row', { active, striped }),
24
+ hovered,
25
+ });
26
+ }
27
+ return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
28
+ };
13
29
  return (React.createElement("div", { className: b('content') },
14
30
  measureValue && (React.createElement("div", { className: b('series-name'), dangerouslySetInnerHTML: { __html: measureValue } })),
15
31
  // eslint-disable-next-line complexity
@@ -30,7 +46,15 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
30
46
  value: hoveredValues[i],
31
47
  format,
32
48
  });
33
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
49
+ return renderRow({
50
+ id,
51
+ active,
52
+ color,
53
+ name: series.name,
54
+ striped,
55
+ value: hoveredValues[i],
56
+ formattedValue,
57
+ });
34
58
  }
35
59
  case 'waterfall': {
36
60
  const isTotal = get(data, 'total', false);
@@ -56,7 +80,15 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
56
80
  value: hoveredValues[i],
57
81
  format,
58
82
  });
59
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
83
+ return renderRow({
84
+ id,
85
+ active,
86
+ color,
87
+ name: series.name,
88
+ striped,
89
+ value: hoveredValues[i],
90
+ formattedValue,
91
+ });
60
92
  }
61
93
  case 'pie':
62
94
  case 'treemap': {
@@ -65,11 +97,13 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
65
97
  value: hoveredValues[i],
66
98
  format: valueFormat || { type: 'number' },
67
99
  });
68
- return (React.createElement(Row, { key: id, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: {
69
- __html: [seriesData.name || seriesData.id]
70
- .flat()
71
- .join('\n'),
72
- } }), value: formattedValue }));
100
+ return renderRow({
101
+ id,
102
+ color,
103
+ name: [seriesData.name || seriesData.id].flat().join('\n'),
104
+ value: hoveredValues[i],
105
+ formattedValue,
106
+ });
73
107
  }
74
108
  case 'sankey': {
75
109
  const { target, data: source } = seriesItem;
@@ -77,11 +111,13 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
77
111
  value: hoveredValues[i],
78
112
  format: valueFormat || { type: 'number' },
79
113
  });
80
- return (React.createElement(Row, { key: id, color: source.color, label: React.createElement("span", null,
81
- source.name,
82
- " \u2192 ", target === null || target === void 0 ? void 0 :
83
- target.name,
84
- ":"), value: formattedValue }));
114
+ return renderRow({
115
+ id,
116
+ color,
117
+ name: `${source.name} → ${target === null || target === void 0 ? void 0 : target.name}`,
118
+ value: hoveredValues[i],
119
+ formattedValue,
120
+ });
85
121
  }
86
122
  case 'radar': {
87
123
  const radarSeries = series;
@@ -89,7 +125,14 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
89
125
  value: hoveredValues[i],
90
126
  format: valueFormat || { type: 'number' },
91
127
  });
92
- return (React.createElement(Row, { key: id, active: active, color: color, label: radarSeries.name || radarSeries.id, value: formattedValue }));
128
+ return renderRow({
129
+ id,
130
+ color,
131
+ active,
132
+ name: radarSeries.name || radarSeries.id,
133
+ value: hoveredValues[i],
134
+ formattedValue,
135
+ });
93
136
  }
94
137
  default: {
95
138
  return null;
@@ -23,5 +23,5 @@ export const Tooltip = (props) => {
23
23
  }, [left, top]);
24
24
  return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { anchorElement: anchor, className: b({ pinned: tooltipPinned }), disableTransition: true, floatingStyles: tooltipPinned ? undefined : { pointerEvents: 'none' }, offset: { mainAxis: 20 }, onOpenChange: tooltipPinned ? handleOnOpenChange : undefined, open: true, placement: ['right', 'left', 'top', 'bottom'] },
25
25
  React.createElement("div", { className: b('popup-content') },
26
- React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer, valueFormat: tooltip.valueFormat, totals: tooltip.totals })))) : null;
26
+ React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer, rowRenderer: tooltip.rowRenderer, valueFormat: tooltip.valueFormat, totals: tooltip.totals })))) : null;
27
27
  };
@@ -29,6 +29,7 @@ export declare function createXScale(args: {
29
29
  axis: PreparedAxis | ChartAxis;
30
30
  boundsWidth: number;
31
31
  series: (PreparedSeries | ChartSeries)[];
32
+ seriesOptions: PreparedSeriesOptions;
32
33
  hasZoomX?: boolean;
33
34
  }): ScaleBand<string> | ScaleLinear<number, number, never> | ScaleTime<number, number, never>;
34
35
  /**
@@ -4,6 +4,7 @@ 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
6
  import { getBarYLayoutForNumericScale, groupBarYDataByYValue } from '../utils';
7
+ import { getBarXLayoutForNumericScale, groupBarXDataByXValue } from '../utils/bar-x';
7
8
  const X_AXIS_ZOOM_PADDING = 0.02;
8
9
  function isNumericalArrayData(data) {
9
10
  return data.every((d) => typeof d === 'number' || d === null);
@@ -130,32 +131,55 @@ function calculateXAxisPadding(series) {
130
131
  });
131
132
  return result;
132
133
  }
134
+ function getXScaleRange({ boundsWidth, series, seriesOptions, hasZoomX, axis, maxPadding, }) {
135
+ const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
136
+ const xRange = [0, boundsWidth - maxPadding];
137
+ const xRangeZoom = [0 + xAxisZoomPadding, boundsWidth - xAxisZoomPadding];
138
+ const range = hasZoomX ? xRangeZoom : xRange;
139
+ const barXSeries = series.filter((s) => s.type === SeriesType.BarX);
140
+ if (barXSeries.length) {
141
+ const groupedData = groupBarXDataByXValue(barXSeries, axis);
142
+ if (Object.keys(groupedData).length > 1) {
143
+ const { bandSize } = getBarXLayoutForNumericScale({
144
+ plotWidth: boundsWidth - maxPadding,
145
+ groupedData,
146
+ seriesOptions,
147
+ });
148
+ const offset = bandSize / 2;
149
+ return [range[0] + offset, range[1] - offset];
150
+ }
151
+ }
152
+ return range;
153
+ }
133
154
  // eslint-disable-next-line complexity
134
155
  export function createXScale(args) {
135
- const { axis, boundsWidth, series, hasZoomX } = args;
156
+ const { axis, boundsWidth, series, seriesOptions, hasZoomX } = args;
136
157
  const xMinProps = get(axis, 'min');
137
158
  const xMaxProps = get(axis, 'max');
138
159
  const xType = get(axis, 'type', DEFAULT_AXIS_TYPE);
139
160
  const xCategories = get(axis, 'categories');
140
- const xTimestamps = get(axis, 'timestamps');
141
161
  const maxPadding = get(axis, 'maxPadding', 0);
142
162
  const xAxisMaxPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);
143
- const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
144
- const xRange = [0, boundsWidth - xAxisMaxPadding];
145
- const xRangeZoom = [0 + xAxisZoomPadding, boundsWidth - xAxisZoomPadding];
163
+ const range = getXScaleRange({
164
+ boundsWidth,
165
+ series,
166
+ seriesOptions,
167
+ hasZoomX,
168
+ axis,
169
+ maxPadding: xAxisMaxPadding,
170
+ });
146
171
  switch (axis.order) {
147
172
  case 'sortDesc':
148
173
  case 'reverse': {
149
- xRange.reverse();
150
- xRangeZoom.reverse();
174
+ range.reverse();
151
175
  }
152
176
  }
153
177
  switch (xType) {
154
178
  case 'linear':
155
179
  case 'logarithmic': {
156
- const domain = getDomainDataXBySeries(series);
157
- if (isNumericalArrayData(domain)) {
158
- const [xMinDomain, xMaxDomain] = extent(domain);
180
+ const domainData = getDomainDataXBySeries(series);
181
+ if (isNumericalArrayData(domainData)) {
182
+ const [xMinDomain, xMaxDomain] = extent(domainData);
159
183
  let xMin;
160
184
  let xMax;
161
185
  if (typeof xMinProps === 'number') {
@@ -176,11 +200,10 @@ export function createXScale(args) {
176
200
  : xMaxDomain;
177
201
  }
178
202
  const scaleFn = xType === 'logarithmic' ? scaleLog : scaleLinear;
179
- const scale = scaleFn()
180
- .domain([xMin, xMax])
181
- .range(hasZoomX ? xRangeZoom : xRange);
203
+ const scale = scaleFn().domain([xMin, xMax]).range(range);
182
204
  if (!hasZoomX) {
183
- scale.nice();
205
+ // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
206
+ scale.nice(Math.max(10, domainData.length));
184
207
  }
185
208
  return scale;
186
209
  }
@@ -195,40 +218,27 @@ export function createXScale(args) {
195
218
  });
196
219
  const xScale = scaleBand().domain(filteredCategories).range([0, boundsWidth]);
197
220
  if (xScale.step() / 2 < xAxisMaxPadding) {
198
- xScale.range(xRange);
221
+ xScale.range(range);
199
222
  }
200
223
  return xScale;
201
224
  }
202
225
  break;
203
226
  }
204
227
  case 'datetime': {
205
- if (xTimestamps) {
206
- const [xMinTimestamp, xMaxTimestamp] = extent(xTimestamps);
228
+ let domain = null;
229
+ const domainData = get(axis, 'timestamps') || getDomainDataXBySeries(series);
230
+ if (isNumericalArrayData(domainData)) {
231
+ const [xMinTimestamp, xMaxTimestamp] = extent(domainData);
207
232
  const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
208
233
  const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
209
- const scale = scaleUtc()
210
- .domain([xMin, xMax])
211
- .range(hasZoomX ? xRangeZoom : xRange);
234
+ domain = [xMin, xMax];
235
+ const scale = scaleUtc().domain(domain).range(range);
212
236
  if (!hasZoomX) {
213
- scale.nice();
237
+ // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
238
+ scale.nice(Math.max(10, domainData.length));
214
239
  }
215
240
  return scale;
216
241
  }
217
- else {
218
- const domain = getDomainDataXBySeries(series);
219
- if (isNumericalArrayData(domain)) {
220
- const [xMinTimestamp, xMaxTimestamp] = extent(domain);
221
- const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
222
- const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
223
- const scale = scaleUtc()
224
- .domain([xMin, xMax])
225
- .range(hasZoomX ? xRangeZoom : xRange);
226
- if (!hasZoomX) {
227
- scale.nice();
228
- }
229
- return scale;
230
- }
231
- }
232
242
  break;
233
243
  }
234
244
  }
@@ -242,7 +252,13 @@ const createScales = (args) => {
242
252
  visibleSeries = visibleSeries.length === 0 ? series : visibleSeries;
243
253
  return {
244
254
  xScale: xAxis
245
- ? createXScale({ axis: xAxis, boundsWidth, series: visibleSeries, hasZoomX })
255
+ ? createXScale({
256
+ axis: xAxis,
257
+ boundsWidth,
258
+ series: visibleSeries,
259
+ seriesOptions,
260
+ hasZoomX,
261
+ })
246
262
  : undefined,
247
263
  yScale: yAxis.map((axis, index) => {
248
264
  const axisSeries = series.filter((s) => {
@@ -1,7 +1,9 @@
1
1
  import type { ChartSeries, ChartXAxis } from '../../types';
2
+ import type { PreparedSeriesOptions } from '../useSeries/types';
2
3
  import type { PreparedAxis } from './types';
3
- export declare const getPreparedXAxis: ({ xAxis, seriesData, width, }: {
4
+ export declare const getPreparedXAxis: ({ xAxis, seriesData, seriesOptions, width, }: {
4
5
  xAxis?: ChartXAxis;
5
6
  seriesData: ChartSeries[];
7
+ seriesOptions: PreparedSeriesOptions;
6
8
  width: number;
7
9
  }) => Promise<PreparedAxis>;
@@ -3,8 +3,8 @@ import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, axisCrosshairDefaults, axisLa
3
3
  import { calculateCos, formatAxisTickLabel, getClosestPointsRange, getHorizontalHtmlTextHeight, getHorizontalSvgTextHeight, getLabelsSize, getMaxTickCount, getTicksCount, getXAxisItems, hasOverlappingLabels, wrapText, } from '../../utils';
4
4
  import { createXScale } from '../useAxisScales';
5
5
  import { getAxisCategories, prepareAxisPlotLabel } from './utils';
6
- async function getLabelSettings({ axis, seriesData, width, autoRotation = true, }) {
7
- const scale = createXScale({ axis, series: seriesData, boundsWidth: width });
6
+ async function getLabelSettings({ axis, seriesData, seriesOptions, width, autoRotation = true, }) {
7
+ const scale = createXScale({ axis, series: seriesData, seriesOptions, boundsWidth: width });
8
8
  const tickCount = getTicksCount({ axis, range: width });
9
9
  const ticks = getXAxisItems({
10
10
  scale: scale,
@@ -40,7 +40,7 @@ async function getLabelSettings({ axis, seriesData, width, autoRotation = true,
40
40
  const maxHeight = rotation ? calculateCos(rotation) * axis.labels.maxWidth : labelsHeight;
41
41
  return { height: Math.min(maxHeight, labelsHeight), rotation };
42
42
  }
43
- export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
43
+ export const getPreparedXAxis = async ({ xAxis, seriesData, seriesOptions, width, }) => {
44
44
  var _a;
45
45
  const titleText = get(xAxis, 'title.text', '');
46
46
  const titleStyle = Object.assign(Object.assign({}, xAxisTitleDefaults.style), get(xAxis, 'title.style'));
@@ -132,6 +132,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
132
132
  const { height, rotation } = await getLabelSettings({
133
133
  axis: preparedXAxis,
134
134
  seriesData,
135
+ seriesOptions,
135
136
  width,
136
137
  autoRotation: (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.labels) === null || _a === void 0 ? void 0 : _a.autoRotation,
137
138
  });
@@ -0,0 +1,16 @@
1
+ import type { BarXSeries, BarXSeriesData } from '../../types';
2
+ import type { PreparedAxis } from '../useChartOptions/types';
3
+ import type { PreparedBarXSeries, PreparedSeriesOptions } from '../useSeries/types';
4
+ export declare function groupBarXDataByXValue<T extends BarXSeries | PreparedBarXSeries>(series: T[], xAxis: PreparedAxis): Record<string | number, Record<string, {
5
+ data: BarXSeriesData;
6
+ series: T;
7
+ }[]>>;
8
+ export declare function getBarXLayoutForNumericScale(args: {
9
+ plotWidth: number;
10
+ seriesOptions: PreparedSeriesOptions;
11
+ groupedData: ReturnType<typeof groupBarXDataByXValue>;
12
+ }): {
13
+ bandSize: number;
14
+ barGap: number;
15
+ barSize: number;
16
+ };
@@ -0,0 +1,41 @@
1
+ import get from 'lodash/get';
2
+ import { getDataCategoryValue } from '../../utils';
3
+ import { MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH } from '../constants';
4
+ import { getSeriesStackId } from '../useSeries/utils';
5
+ export function groupBarXDataByXValue(series, xAxis) {
6
+ const data = {};
7
+ series.forEach((s) => {
8
+ s.data.forEach((d) => {
9
+ var _a;
10
+ const categories = (_a = xAxis.categories) !== null && _a !== void 0 ? _a : [];
11
+ const key = xAxis.type === 'category'
12
+ ? getDataCategoryValue({ axisDirection: 'x', categories, data: d })
13
+ : d.x;
14
+ if (key) {
15
+ if (!data[key]) {
16
+ data[key] = {};
17
+ }
18
+ const stackId = getSeriesStackId(s);
19
+ if (!data[key][stackId]) {
20
+ data[key][stackId] = [];
21
+ }
22
+ data[key][stackId].push({ data: d, series: s });
23
+ }
24
+ });
25
+ });
26
+ return data;
27
+ }
28
+ export function getBarXLayoutForNumericScale(args) {
29
+ const { plotWidth, groupedData, seriesOptions } = args;
30
+ const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth');
31
+ const barPadding = get(seriesOptions, 'bar-x.barPadding');
32
+ const groupPadding = get(seriesOptions, 'bar-x.groupPadding');
33
+ const groups = Object.values(groupedData);
34
+ const maxGroupItemCount = groups.reduce((acc, items) => Math.max(acc, Object.keys(items).length), 0);
35
+ const bandSize = plotWidth / groups.length;
36
+ const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
37
+ const groupSize = bandSize - groupGap;
38
+ const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
39
+ const barSize = Math.max(MIN_BAR_WIDTH, Math.min((groupSize - barGap) / maxGroupItemCount, barMaxWidth));
40
+ return { bandSize, barGap, barSize };
41
+ }
@@ -84,10 +84,24 @@ export interface ChartTooltipTotalsAggregationArgs<T = MeaningfulAny> extends Ch
84
84
  }
85
85
  export type ChartTooltipTotalsBuiltInAggregation = (typeof TOOLTIP_TOTALS_BUILT_IN_AGGREGATION)[keyof typeof TOOLTIP_TOTALS_BUILT_IN_AGGREGATION];
86
86
  export type ChartTooltipTotalsAggregationValue = number | string | undefined;
87
+ export type ChartTooltipRowRendererArgs = {
88
+ id: string;
89
+ name: string;
90
+ active?: boolean;
91
+ color?: string;
92
+ striped?: boolean;
93
+ value: string | number | null | undefined;
94
+ formattedValue?: string;
95
+ hovered?: TooltipDataChunk<unknown>[];
96
+ className?: string;
97
+ };
87
98
  export interface ChartTooltip<T = MeaningfulAny> {
88
99
  enabled?: boolean;
89
100
  /** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
90
101
  renderer?: (args: ChartTooltipRendererArgs<T>) => React.ReactElement | null;
102
+ /** Defines the way a single data/series is displayed (corresponding to a separate selected point/ruler/shape on the chart).
103
+ * It is useful in cases where you need to display additional information, but keep the general format of the tooltip. */
104
+ rowRenderer?: (args: ChartTooltipRowRendererArgs) => React.ReactElement | null;
91
105
  pin?: {
92
106
  enabled?: boolean;
93
107
  modifierKey?: 'altKey' | 'metaKey';
@@ -43,7 +43,7 @@ export declare function isSeriesWithCategoryValues(series: UnknownSeries): serie
43
43
  category: string;
44
44
  }[];
45
45
  };
46
- export declare const getDomainDataXBySeries: (series: UnknownSeries[]) => unknown[];
46
+ export declare const getDomainDataXBySeries: (series: UnknownSeries[]) => ({} | undefined)[];
47
47
  export declare function getDefaultMaxXAxisValue(series: UnknownSeries[]): 0 | undefined;
48
48
  export declare function getDefaultMinXAxisValue(series: UnknownSeries[]): number | undefined;
49
49
  export declare function getDefaultMinYAxisValue(series?: UnknownSeries[]): number | undefined;
@@ -51,7 +51,11 @@ function getDomainDataForStackedSeries(seriesList, keyAttr = 'x', valueAttr = 'y
51
51
  seriesStack.forEach((singleSeries) => {
52
52
  const data = new Map();
53
53
  singleSeries.data.forEach((point) => {
54
- const key = String(point[keyAttr]);
54
+ const keyValue = point[keyAttr];
55
+ if (keyValue === null) {
56
+ return;
57
+ }
58
+ const key = String(keyValue);
55
59
  let value = 0;
56
60
  if (valueAttr in point && typeof point[valueAttr] === 'number') {
57
61
  value = point[valueAttr];
@@ -71,7 +75,7 @@ function getDomainDataForStackedSeries(seriesList, keyAttr = 'x', valueAttr = 'y
71
75
  }
72
76
  export const getDomainDataXBySeries = (series) => {
73
77
  const groupedSeries = group(series, (item) => item.type);
74
- return Array.from(groupedSeries).reduce((acc, [type, seriesList]) => {
78
+ const values = Array.from(groupedSeries).reduce((acc, [type, seriesList]) => {
75
79
  switch (type) {
76
80
  case 'bar-y': {
77
81
  acc.push(...getDomainDataForStackedSeries(seriesList, 'y', 'x'));
@@ -85,6 +89,7 @@ export const getDomainDataXBySeries = (series) => {
85
89
  }
86
90
  return acc;
87
91
  }, []);
92
+ return Array.from(new Set(values.filter((v) => v !== null)));
88
93
  };
89
94
  export function getDefaultMaxXAxisValue(series) {
90
95
  if (series.some((s) => s.type === 'bar-y')) {
@@ -39,8 +39,13 @@ export function useChartInnerProps(props) {
39
39
  const [xAxis, setXAxis] = React.useState(null);
40
40
  React.useEffect(() => {
41
41
  setXAxis(null);
42
- getPreparedXAxis({ xAxis: data.xAxis, width, seriesData: zoomedSeriesData }).then((val) => setXAxis(val));
43
- }, [data.xAxis, width, zoomedSeriesData]);
42
+ getPreparedXAxis({
43
+ xAxis: data.xAxis,
44
+ width,
45
+ seriesData: zoomedSeriesData,
46
+ seriesOptions: preparedSeriesOptions,
47
+ }).then((val) => setXAxis(val));
48
+ }, [data.xAxis, preparedSeriesOptions, width, zoomedSeriesData]);
44
49
  const [yAxis, setYAxis] = React.useState([]);
45
50
  React.useEffect(() => {
46
51
  setYAxis([]);
@@ -5,6 +5,7 @@ export interface ChartTooltipContentProps {
5
5
  xAxis?: ChartXAxis | null;
6
6
  yAxis?: ChartYAxis;
7
7
  renderer?: ChartTooltip['renderer'];
8
+ rowRenderer?: ChartTooltip['rowRenderer'];
8
9
  valueFormat?: ChartTooltip['valueFormat'];
9
10
  totals?: ChartTooltip['totals'];
10
11
  }
@@ -2,10 +2,10 @@ import React from 'react';
2
2
  import isNil from 'lodash/isNil';
3
3
  import { DefaultTooltipContent } from './DefaultTooltipContent';
4
4
  export const ChartTooltipContent = (props) => {
5
- const { hovered, xAxis, yAxis, renderer, valueFormat, totals } = props;
5
+ const { hovered, xAxis, yAxis, renderer, rowRenderer, valueFormat, totals } = 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
- return isNil(customTooltip) ? (React.createElement(DefaultTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, valueFormat: valueFormat, totals: totals })) : (customTooltip);
10
+ return isNil(customTooltip) ? (React.createElement(DefaultTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, valueFormat: valueFormat, totals: totals, rowRenderer: rowRenderer })) : (customTooltip);
11
11
  };
@@ -6,6 +6,7 @@ type Props = {
6
6
  valueFormat?: ValueFormat;
7
7
  xAxis?: ChartXAxis | null;
8
8
  yAxis?: ChartYAxis;
9
+ rowRenderer?: ChartTooltip['rowRenderer'];
9
10
  };
10
- export declare const DefaultTooltipContent: ({ hovered, xAxis, yAxis, valueFormat, totals }: Props) => React.JSX.Element;
11
+ export declare const DefaultTooltipContent: ({ hovered, xAxis, yAxis, valueFormat, totals, rowRenderer, }: Props) => React.JSX.Element;
11
12
  export {};
@@ -7,9 +7,25 @@ import { Row } from './Row';
7
7
  import { RowTotals } from './RowTotals';
8
8
  import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getXRowData, } from './utils';
9
9
  const b = block('tooltip');
10
- export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, totals }) => {
10
+ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, totals, rowRenderer, }) => {
11
11
  const measureValue = getMeasureValue({ data: hovered, xAxis, yAxis });
12
12
  const hoveredValues = getHoveredValues({ hovered, xAxis, yAxis });
13
+ const renderRow = ({ id, name, color, active, striped, value, formattedValue, }) => {
14
+ if (typeof rowRenderer === 'function') {
15
+ return rowRenderer({
16
+ id,
17
+ name,
18
+ color,
19
+ value,
20
+ formattedValue,
21
+ striped,
22
+ active,
23
+ className: b('content-row', { active, striped }),
24
+ hovered,
25
+ });
26
+ }
27
+ return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
28
+ };
13
29
  return (React.createElement("div", { className: b('content') },
14
30
  measureValue && (React.createElement("div", { className: b('series-name'), dangerouslySetInnerHTML: { __html: measureValue } })),
15
31
  // eslint-disable-next-line complexity
@@ -30,7 +46,15 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
30
46
  value: hoveredValues[i],
31
47
  format,
32
48
  });
33
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
49
+ return renderRow({
50
+ id,
51
+ active,
52
+ color,
53
+ name: series.name,
54
+ striped,
55
+ value: hoveredValues[i],
56
+ formattedValue,
57
+ });
34
58
  }
35
59
  case 'waterfall': {
36
60
  const isTotal = get(data, 'total', false);
@@ -56,7 +80,15 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
56
80
  value: hoveredValues[i],
57
81
  format,
58
82
  });
59
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
83
+ return renderRow({
84
+ id,
85
+ active,
86
+ color,
87
+ name: series.name,
88
+ striped,
89
+ value: hoveredValues[i],
90
+ formattedValue,
91
+ });
60
92
  }
61
93
  case 'pie':
62
94
  case 'treemap': {
@@ -65,11 +97,13 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
65
97
  value: hoveredValues[i],
66
98
  format: valueFormat || { type: 'number' },
67
99
  });
68
- return (React.createElement(Row, { key: id, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: {
69
- __html: [seriesData.name || seriesData.id]
70
- .flat()
71
- .join('\n'),
72
- } }), value: formattedValue }));
100
+ return renderRow({
101
+ id,
102
+ color,
103
+ name: [seriesData.name || seriesData.id].flat().join('\n'),
104
+ value: hoveredValues[i],
105
+ formattedValue,
106
+ });
73
107
  }
74
108
  case 'sankey': {
75
109
  const { target, data: source } = seriesItem;
@@ -77,11 +111,13 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
77
111
  value: hoveredValues[i],
78
112
  format: valueFormat || { type: 'number' },
79
113
  });
80
- return (React.createElement(Row, { key: id, color: source.color, label: React.createElement("span", null,
81
- source.name,
82
- " \u2192 ", target === null || target === void 0 ? void 0 :
83
- target.name,
84
- ":"), value: formattedValue }));
114
+ return renderRow({
115
+ id,
116
+ color,
117
+ name: `${source.name} → ${target === null || target === void 0 ? void 0 : target.name}`,
118
+ value: hoveredValues[i],
119
+ formattedValue,
120
+ });
85
121
  }
86
122
  case 'radar': {
87
123
  const radarSeries = series;
@@ -89,7 +125,14 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
89
125
  value: hoveredValues[i],
90
126
  format: valueFormat || { type: 'number' },
91
127
  });
92
- return (React.createElement(Row, { key: id, active: active, color: color, label: radarSeries.name || radarSeries.id, value: formattedValue }));
128
+ return renderRow({
129
+ id,
130
+ color,
131
+ active,
132
+ name: radarSeries.name || radarSeries.id,
133
+ value: hoveredValues[i],
134
+ formattedValue,
135
+ });
93
136
  }
94
137
  default: {
95
138
  return null;
@@ -23,5 +23,5 @@ export const Tooltip = (props) => {
23
23
  }, [left, top]);
24
24
  return (hovered === null || hovered === void 0 ? void 0 : hovered.length) ? (React.createElement(Popup, { anchorElement: anchor, className: b({ pinned: tooltipPinned }), disableTransition: true, floatingStyles: tooltipPinned ? undefined : { pointerEvents: 'none' }, offset: { mainAxis: 20 }, onOpenChange: tooltipPinned ? handleOnOpenChange : undefined, open: true, placement: ['right', 'left', 'top', 'bottom'] },
25
25
  React.createElement("div", { className: b('popup-content') },
26
- React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer, valueFormat: tooltip.valueFormat, totals: tooltip.totals })))) : null;
26
+ React.createElement(ChartTooltipContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis, renderer: tooltip.renderer, rowRenderer: tooltip.rowRenderer, valueFormat: tooltip.valueFormat, totals: tooltip.totals })))) : null;
27
27
  };
@@ -29,6 +29,7 @@ export declare function createXScale(args: {
29
29
  axis: PreparedAxis | ChartAxis;
30
30
  boundsWidth: number;
31
31
  series: (PreparedSeries | ChartSeries)[];
32
+ seriesOptions: PreparedSeriesOptions;
32
33
  hasZoomX?: boolean;
33
34
  }): ScaleBand<string> | ScaleLinear<number, number, never> | ScaleTime<number, number, never>;
34
35
  /**
@@ -4,6 +4,7 @@ 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
6
  import { getBarYLayoutForNumericScale, groupBarYDataByYValue } from '../utils';
7
+ import { getBarXLayoutForNumericScale, groupBarXDataByXValue } from '../utils/bar-x';
7
8
  const X_AXIS_ZOOM_PADDING = 0.02;
8
9
  function isNumericalArrayData(data) {
9
10
  return data.every((d) => typeof d === 'number' || d === null);
@@ -130,32 +131,55 @@ function calculateXAxisPadding(series) {
130
131
  });
131
132
  return result;
132
133
  }
134
+ function getXScaleRange({ boundsWidth, series, seriesOptions, hasZoomX, axis, maxPadding, }) {
135
+ const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
136
+ const xRange = [0, boundsWidth - maxPadding];
137
+ const xRangeZoom = [0 + xAxisZoomPadding, boundsWidth - xAxisZoomPadding];
138
+ const range = hasZoomX ? xRangeZoom : xRange;
139
+ const barXSeries = series.filter((s) => s.type === SeriesType.BarX);
140
+ if (barXSeries.length) {
141
+ const groupedData = groupBarXDataByXValue(barXSeries, axis);
142
+ if (Object.keys(groupedData).length > 1) {
143
+ const { bandSize } = getBarXLayoutForNumericScale({
144
+ plotWidth: boundsWidth - maxPadding,
145
+ groupedData,
146
+ seriesOptions,
147
+ });
148
+ const offset = bandSize / 2;
149
+ return [range[0] + offset, range[1] - offset];
150
+ }
151
+ }
152
+ return range;
153
+ }
133
154
  // eslint-disable-next-line complexity
134
155
  export function createXScale(args) {
135
- const { axis, boundsWidth, series, hasZoomX } = args;
156
+ const { axis, boundsWidth, series, seriesOptions, hasZoomX } = args;
136
157
  const xMinProps = get(axis, 'min');
137
158
  const xMaxProps = get(axis, 'max');
138
159
  const xType = get(axis, 'type', DEFAULT_AXIS_TYPE);
139
160
  const xCategories = get(axis, 'categories');
140
- const xTimestamps = get(axis, 'timestamps');
141
161
  const maxPadding = get(axis, 'maxPadding', 0);
142
162
  const xAxisMaxPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);
143
- const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
144
- const xRange = [0, boundsWidth - xAxisMaxPadding];
145
- const xRangeZoom = [0 + xAxisZoomPadding, boundsWidth - xAxisZoomPadding];
163
+ const range = getXScaleRange({
164
+ boundsWidth,
165
+ series,
166
+ seriesOptions,
167
+ hasZoomX,
168
+ axis,
169
+ maxPadding: xAxisMaxPadding,
170
+ });
146
171
  switch (axis.order) {
147
172
  case 'sortDesc':
148
173
  case 'reverse': {
149
- xRange.reverse();
150
- xRangeZoom.reverse();
174
+ range.reverse();
151
175
  }
152
176
  }
153
177
  switch (xType) {
154
178
  case 'linear':
155
179
  case 'logarithmic': {
156
- const domain = getDomainDataXBySeries(series);
157
- if (isNumericalArrayData(domain)) {
158
- const [xMinDomain, xMaxDomain] = extent(domain);
180
+ const domainData = getDomainDataXBySeries(series);
181
+ if (isNumericalArrayData(domainData)) {
182
+ const [xMinDomain, xMaxDomain] = extent(domainData);
159
183
  let xMin;
160
184
  let xMax;
161
185
  if (typeof xMinProps === 'number') {
@@ -176,11 +200,10 @@ export function createXScale(args) {
176
200
  : xMaxDomain;
177
201
  }
178
202
  const scaleFn = xType === 'logarithmic' ? scaleLog : scaleLinear;
179
- const scale = scaleFn()
180
- .domain([xMin, xMax])
181
- .range(hasZoomX ? xRangeZoom : xRange);
203
+ const scale = scaleFn().domain([xMin, xMax]).range(range);
182
204
  if (!hasZoomX) {
183
- scale.nice();
205
+ // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
206
+ scale.nice(Math.max(10, domainData.length));
184
207
  }
185
208
  return scale;
186
209
  }
@@ -195,40 +218,27 @@ export function createXScale(args) {
195
218
  });
196
219
  const xScale = scaleBand().domain(filteredCategories).range([0, boundsWidth]);
197
220
  if (xScale.step() / 2 < xAxisMaxPadding) {
198
- xScale.range(xRange);
221
+ xScale.range(range);
199
222
  }
200
223
  return xScale;
201
224
  }
202
225
  break;
203
226
  }
204
227
  case 'datetime': {
205
- if (xTimestamps) {
206
- const [xMinTimestamp, xMaxTimestamp] = extent(xTimestamps);
228
+ let domain = null;
229
+ const domainData = get(axis, 'timestamps') || getDomainDataXBySeries(series);
230
+ if (isNumericalArrayData(domainData)) {
231
+ const [xMinTimestamp, xMaxTimestamp] = extent(domainData);
207
232
  const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
208
233
  const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
209
- const scale = scaleUtc()
210
- .domain([xMin, xMax])
211
- .range(hasZoomX ? xRangeZoom : xRange);
234
+ domain = [xMin, xMax];
235
+ const scale = scaleUtc().domain(domain).range(range);
212
236
  if (!hasZoomX) {
213
- scale.nice();
237
+ // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
238
+ scale.nice(Math.max(10, domainData.length));
214
239
  }
215
240
  return scale;
216
241
  }
217
- else {
218
- const domain = getDomainDataXBySeries(series);
219
- if (isNumericalArrayData(domain)) {
220
- const [xMinTimestamp, xMaxTimestamp] = extent(domain);
221
- const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
222
- const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
223
- const scale = scaleUtc()
224
- .domain([xMin, xMax])
225
- .range(hasZoomX ? xRangeZoom : xRange);
226
- if (!hasZoomX) {
227
- scale.nice();
228
- }
229
- return scale;
230
- }
231
- }
232
242
  break;
233
243
  }
234
244
  }
@@ -242,7 +252,13 @@ const createScales = (args) => {
242
252
  visibleSeries = visibleSeries.length === 0 ? series : visibleSeries;
243
253
  return {
244
254
  xScale: xAxis
245
- ? createXScale({ axis: xAxis, boundsWidth, series: visibleSeries, hasZoomX })
255
+ ? createXScale({
256
+ axis: xAxis,
257
+ boundsWidth,
258
+ series: visibleSeries,
259
+ seriesOptions,
260
+ hasZoomX,
261
+ })
246
262
  : undefined,
247
263
  yScale: yAxis.map((axis, index) => {
248
264
  const axisSeries = series.filter((s) => {
@@ -1,7 +1,9 @@
1
1
  import type { ChartSeries, ChartXAxis } from '../../types';
2
+ import type { PreparedSeriesOptions } from '../useSeries/types';
2
3
  import type { PreparedAxis } from './types';
3
- export declare const getPreparedXAxis: ({ xAxis, seriesData, width, }: {
4
+ export declare const getPreparedXAxis: ({ xAxis, seriesData, seriesOptions, width, }: {
4
5
  xAxis?: ChartXAxis;
5
6
  seriesData: ChartSeries[];
7
+ seriesOptions: PreparedSeriesOptions;
6
8
  width: number;
7
9
  }) => Promise<PreparedAxis>;
@@ -3,8 +3,8 @@ import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, axisCrosshairDefaults, axisLa
3
3
  import { calculateCos, formatAxisTickLabel, getClosestPointsRange, getHorizontalHtmlTextHeight, getHorizontalSvgTextHeight, getLabelsSize, getMaxTickCount, getTicksCount, getXAxisItems, hasOverlappingLabels, wrapText, } from '../../utils';
4
4
  import { createXScale } from '../useAxisScales';
5
5
  import { getAxisCategories, prepareAxisPlotLabel } from './utils';
6
- async function getLabelSettings({ axis, seriesData, width, autoRotation = true, }) {
7
- const scale = createXScale({ axis, series: seriesData, boundsWidth: width });
6
+ async function getLabelSettings({ axis, seriesData, seriesOptions, width, autoRotation = true, }) {
7
+ const scale = createXScale({ axis, series: seriesData, seriesOptions, boundsWidth: width });
8
8
  const tickCount = getTicksCount({ axis, range: width });
9
9
  const ticks = getXAxisItems({
10
10
  scale: scale,
@@ -40,7 +40,7 @@ async function getLabelSettings({ axis, seriesData, width, autoRotation = true,
40
40
  const maxHeight = rotation ? calculateCos(rotation) * axis.labels.maxWidth : labelsHeight;
41
41
  return { height: Math.min(maxHeight, labelsHeight), rotation };
42
42
  }
43
- export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
43
+ export const getPreparedXAxis = async ({ xAxis, seriesData, seriesOptions, width, }) => {
44
44
  var _a;
45
45
  const titleText = get(xAxis, 'title.text', '');
46
46
  const titleStyle = Object.assign(Object.assign({}, xAxisTitleDefaults.style), get(xAxis, 'title.style'));
@@ -132,6 +132,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
132
132
  const { height, rotation } = await getLabelSettings({
133
133
  axis: preparedXAxis,
134
134
  seriesData,
135
+ seriesOptions,
135
136
  width,
136
137
  autoRotation: (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.labels) === null || _a === void 0 ? void 0 : _a.autoRotation,
137
138
  });
@@ -0,0 +1,16 @@
1
+ import type { BarXSeries, BarXSeriesData } from '../../types';
2
+ import type { PreparedAxis } from '../useChartOptions/types';
3
+ import type { PreparedBarXSeries, PreparedSeriesOptions } from '../useSeries/types';
4
+ export declare function groupBarXDataByXValue<T extends BarXSeries | PreparedBarXSeries>(series: T[], xAxis: PreparedAxis): Record<string | number, Record<string, {
5
+ data: BarXSeriesData;
6
+ series: T;
7
+ }[]>>;
8
+ export declare function getBarXLayoutForNumericScale(args: {
9
+ plotWidth: number;
10
+ seriesOptions: PreparedSeriesOptions;
11
+ groupedData: ReturnType<typeof groupBarXDataByXValue>;
12
+ }): {
13
+ bandSize: number;
14
+ barGap: number;
15
+ barSize: number;
16
+ };
@@ -0,0 +1,41 @@
1
+ import get from 'lodash/get';
2
+ import { getDataCategoryValue } from '../../utils';
3
+ import { MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH } from '../constants';
4
+ import { getSeriesStackId } from '../useSeries/utils';
5
+ export function groupBarXDataByXValue(series, xAxis) {
6
+ const data = {};
7
+ series.forEach((s) => {
8
+ s.data.forEach((d) => {
9
+ var _a;
10
+ const categories = (_a = xAxis.categories) !== null && _a !== void 0 ? _a : [];
11
+ const key = xAxis.type === 'category'
12
+ ? getDataCategoryValue({ axisDirection: 'x', categories, data: d })
13
+ : d.x;
14
+ if (key) {
15
+ if (!data[key]) {
16
+ data[key] = {};
17
+ }
18
+ const stackId = getSeriesStackId(s);
19
+ if (!data[key][stackId]) {
20
+ data[key][stackId] = [];
21
+ }
22
+ data[key][stackId].push({ data: d, series: s });
23
+ }
24
+ });
25
+ });
26
+ return data;
27
+ }
28
+ export function getBarXLayoutForNumericScale(args) {
29
+ const { plotWidth, groupedData, seriesOptions } = args;
30
+ const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth');
31
+ const barPadding = get(seriesOptions, 'bar-x.barPadding');
32
+ const groupPadding = get(seriesOptions, 'bar-x.groupPadding');
33
+ const groups = Object.values(groupedData);
34
+ const maxGroupItemCount = groups.reduce((acc, items) => Math.max(acc, Object.keys(items).length), 0);
35
+ const bandSize = plotWidth / groups.length;
36
+ const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
37
+ const groupSize = bandSize - groupGap;
38
+ const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
39
+ const barSize = Math.max(MIN_BAR_WIDTH, Math.min((groupSize - barGap) / maxGroupItemCount, barMaxWidth));
40
+ return { bandSize, barGap, barSize };
41
+ }
@@ -84,10 +84,24 @@ export interface ChartTooltipTotalsAggregationArgs<T = MeaningfulAny> extends Ch
84
84
  }
85
85
  export type ChartTooltipTotalsBuiltInAggregation = (typeof TOOLTIP_TOTALS_BUILT_IN_AGGREGATION)[keyof typeof TOOLTIP_TOTALS_BUILT_IN_AGGREGATION];
86
86
  export type ChartTooltipTotalsAggregationValue = number | string | undefined;
87
+ export type ChartTooltipRowRendererArgs = {
88
+ id: string;
89
+ name: string;
90
+ active?: boolean;
91
+ color?: string;
92
+ striped?: boolean;
93
+ value: string | number | null | undefined;
94
+ formattedValue?: string;
95
+ hovered?: TooltipDataChunk<unknown>[];
96
+ className?: string;
97
+ };
87
98
  export interface ChartTooltip<T = MeaningfulAny> {
88
99
  enabled?: boolean;
89
100
  /** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
90
101
  renderer?: (args: ChartTooltipRendererArgs<T>) => React.ReactElement | null;
102
+ /** Defines the way a single data/series is displayed (corresponding to a separate selected point/ruler/shape on the chart).
103
+ * It is useful in cases where you need to display additional information, but keep the general format of the tooltip. */
104
+ rowRenderer?: (args: ChartTooltipRowRendererArgs) => React.ReactElement | null;
91
105
  pin?: {
92
106
  enabled?: boolean;
93
107
  modifierKey?: 'altKey' | 'metaKey';
@@ -43,7 +43,7 @@ export declare function isSeriesWithCategoryValues(series: UnknownSeries): serie
43
43
  category: string;
44
44
  }[];
45
45
  };
46
- export declare const getDomainDataXBySeries: (series: UnknownSeries[]) => unknown[];
46
+ export declare const getDomainDataXBySeries: (series: UnknownSeries[]) => ({} | undefined)[];
47
47
  export declare function getDefaultMaxXAxisValue(series: UnknownSeries[]): 0 | undefined;
48
48
  export declare function getDefaultMinXAxisValue(series: UnknownSeries[]): number | undefined;
49
49
  export declare function getDefaultMinYAxisValue(series?: UnknownSeries[]): number | undefined;
@@ -51,7 +51,11 @@ function getDomainDataForStackedSeries(seriesList, keyAttr = 'x', valueAttr = 'y
51
51
  seriesStack.forEach((singleSeries) => {
52
52
  const data = new Map();
53
53
  singleSeries.data.forEach((point) => {
54
- const key = String(point[keyAttr]);
54
+ const keyValue = point[keyAttr];
55
+ if (keyValue === null) {
56
+ return;
57
+ }
58
+ const key = String(keyValue);
55
59
  let value = 0;
56
60
  if (valueAttr in point && typeof point[valueAttr] === 'number') {
57
61
  value = point[valueAttr];
@@ -71,7 +75,7 @@ function getDomainDataForStackedSeries(seriesList, keyAttr = 'x', valueAttr = 'y
71
75
  }
72
76
  export const getDomainDataXBySeries = (series) => {
73
77
  const groupedSeries = group(series, (item) => item.type);
74
- return Array.from(groupedSeries).reduce((acc, [type, seriesList]) => {
78
+ const values = Array.from(groupedSeries).reduce((acc, [type, seriesList]) => {
75
79
  switch (type) {
76
80
  case 'bar-y': {
77
81
  acc.push(...getDomainDataForStackedSeries(seriesList, 'y', 'x'));
@@ -85,6 +89,7 @@ export const getDomainDataXBySeries = (series) => {
85
89
  }
86
90
  return acc;
87
91
  }, []);
92
+ return Array.from(new Set(values.filter((v) => v !== null)));
88
93
  };
89
94
  export function getDefaultMaxXAxisValue(series) {
90
95
  if (series.some((s) => s.type === 'bar-y')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",