@gravity-ui/charts 1.13.2 → 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 (67) hide show
  1. package/dist/cjs/components/Axis/AxisX.d.ts +5 -2
  2. package/dist/cjs/components/Axis/AxisX.js +28 -11
  3. package/dist/cjs/components/Axis/AxisY.d.ts +5 -2
  4. package/dist/cjs/components/Axis/AxisY.js +67 -9
  5. package/dist/cjs/components/ChartInner/index.js +2 -2
  6. package/dist/cjs/components/ChartInner/useChartInnerProps.js +7 -2
  7. package/dist/cjs/components/Legend/index.js +5 -4
  8. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +1 -0
  9. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +2 -2
  10. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.d.ts +2 -1
  11. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.js +58 -15
  12. package/dist/cjs/components/Tooltip/index.js +1 -1
  13. package/dist/cjs/hooks/useAxisScales/index.d.ts +1 -0
  14. package/dist/cjs/hooks/useAxisScales/index.js +53 -37
  15. package/dist/cjs/hooks/useChartOptions/title.js +2 -2
  16. package/dist/cjs/hooks/useChartOptions/types.d.ts +1 -1
  17. package/dist/cjs/hooks/useChartOptions/x-axis.d.ts +3 -1
  18. package/dist/cjs/hooks/useChartOptions/x-axis.js +22 -13
  19. package/dist/cjs/hooks/useChartOptions/y-axis.js +17 -6
  20. package/dist/cjs/hooks/useSplit/index.js +2 -2
  21. package/dist/cjs/hooks/utils/bar-x.d.ts +16 -0
  22. package/dist/cjs/hooks/utils/bar-x.js +41 -0
  23. package/dist/cjs/i18n/keysets/en.json +3 -1
  24. package/dist/cjs/i18n/keysets/ru.json +3 -1
  25. package/dist/cjs/types/chart/axis.d.ts +11 -1
  26. package/dist/cjs/types/chart/tooltip.d.ts +14 -0
  27. package/dist/cjs/utils/chart/axis-generators/bottom.d.ts +18 -13
  28. package/dist/cjs/utils/chart/axis-generators/bottom.js +173 -73
  29. package/dist/cjs/utils/chart/index.d.ts +6 -10
  30. package/dist/cjs/utils/chart/index.js +25 -11
  31. package/dist/cjs/validation/index.js +2 -22
  32. package/dist/cjs/validation/validate-axes.d.ts +5 -0
  33. package/dist/cjs/validation/validate-axes.js +46 -0
  34. package/dist/esm/components/Axis/AxisX.d.ts +5 -2
  35. package/dist/esm/components/Axis/AxisX.js +28 -11
  36. package/dist/esm/components/Axis/AxisY.d.ts +5 -2
  37. package/dist/esm/components/Axis/AxisY.js +67 -9
  38. package/dist/esm/components/ChartInner/index.js +2 -2
  39. package/dist/esm/components/ChartInner/useChartInnerProps.js +7 -2
  40. package/dist/esm/components/Legend/index.js +5 -4
  41. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +1 -0
  42. package/dist/esm/components/Tooltip/ChartTooltipContent.js +2 -2
  43. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.d.ts +2 -1
  44. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.js +58 -15
  45. package/dist/esm/components/Tooltip/index.js +1 -1
  46. package/dist/esm/hooks/useAxisScales/index.d.ts +1 -0
  47. package/dist/esm/hooks/useAxisScales/index.js +53 -37
  48. package/dist/esm/hooks/useChartOptions/title.js +2 -2
  49. package/dist/esm/hooks/useChartOptions/types.d.ts +1 -1
  50. package/dist/esm/hooks/useChartOptions/x-axis.d.ts +3 -1
  51. package/dist/esm/hooks/useChartOptions/x-axis.js +22 -13
  52. package/dist/esm/hooks/useChartOptions/y-axis.js +17 -6
  53. package/dist/esm/hooks/useSplit/index.js +2 -2
  54. package/dist/esm/hooks/utils/bar-x.d.ts +16 -0
  55. package/dist/esm/hooks/utils/bar-x.js +41 -0
  56. package/dist/esm/i18n/keysets/en.json +3 -1
  57. package/dist/esm/i18n/keysets/ru.json +3 -1
  58. package/dist/esm/types/chart/axis.d.ts +11 -1
  59. package/dist/esm/types/chart/tooltip.d.ts +14 -0
  60. package/dist/esm/utils/chart/axis-generators/bottom.d.ts +18 -13
  61. package/dist/esm/utils/chart/axis-generators/bottom.js +173 -73
  62. package/dist/esm/utils/chart/index.d.ts +6 -10
  63. package/dist/esm/utils/chart/index.js +25 -11
  64. package/dist/esm/validation/index.js +2 -22
  65. package/dist/esm/validation/validate-axes.d.ts +5 -0
  66. package/dist/esm/validation/validate-axes.js +46 -0
  67. package/package.json +1 -1
@@ -3,13 +3,16 @@ import type { ChartScale, PreparedAxis, PreparedSplit } from '../../hooks';
3
3
  import './styles.css';
4
4
  type Props = {
5
5
  axis: PreparedAxis;
6
- width: number;
6
+ boundsOffsetLeft: number;
7
+ boundsOffsetTop: number;
7
8
  height: number;
9
+ htmlLayout: HTMLElement | null;
8
10
  scale: ChartScale;
9
11
  split: PreparedSplit;
12
+ width: number;
13
+ leftmostLimit?: number;
10
14
  plotBeforeRef?: React.MutableRefObject<SVGGElement | null>;
11
15
  plotAfterRef?: React.MutableRefObject<SVGGElement | null>;
12
- leftmostLimit?: number;
13
16
  };
14
17
  export declare function getTitlePosition(args: {
15
18
  axis: PreparedAxis;
@@ -42,11 +42,11 @@ export function getTitlePosition(args) {
42
42
  return { x, y };
43
43
  }
44
44
  export const AxisX = React.memo(function AxisX(props) {
45
- const { axis, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, leftmostLimit, } = props;
45
+ const { axis, boundsOffsetLeft, boundsOffsetTop, height: totalHeight, htmlLayout, leftmostLimit, plotAfterRef, plotBeforeRef, split, scale, width, } = props;
46
46
  const ref = React.useRef(null);
47
47
  React.useEffect(() => {
48
48
  (async () => {
49
- if (!ref.current) {
49
+ if (!ref.current || !htmlLayout) {
50
50
  return;
51
51
  }
52
52
  const svgElement = select(ref.current);
@@ -65,24 +65,29 @@ export const AxisX = React.memo(function AxisX(props) {
65
65
  }
66
66
  const axisScale = scale;
67
67
  const xAxisGenerator = await axisBottom({
68
+ boundsOffsetLeft,
69
+ boundsOffsetTop,
70
+ domain: {
71
+ size: width,
72
+ color: axis.lineColor,
73
+ },
74
+ htmlLayout,
68
75
  leftmostLimit,
69
76
  scale: axisScale,
70
77
  ticks: {
78
+ count: getTicksCount({ axis, range: width }),
79
+ labelsHtml: axis.labels.html,
71
80
  items: tickItems,
72
81
  labelFormat: getLabelFormatter({ axis, scale }),
73
- labelsPaddings: axis.labels.padding,
82
+ labelsHeight: axis.labels.height,
83
+ labelsLineHeight: axis.labels.lineHeight,
74
84
  labelsMargin: axis.labels.margin,
75
- labelsStyle: axis.labels.style,
76
85
  labelsMaxWidth: axis.labels.maxWidth,
77
- labelsLineHeight: axis.labels.lineHeight,
78
- count: getTicksCount({ axis, range: width }),
86
+ labelsPaddings: axis.labels.padding,
87
+ labelsStyle: axis.labels.style,
79
88
  maxTickCount: getMaxTickCount({ axis, width }),
80
89
  rotation: axis.labels.rotation,
81
90
  },
82
- domain: {
83
- size: width,
84
- color: axis.lineColor,
85
- },
86
91
  });
87
92
  svgElement.call(xAxisGenerator).attr('class', b());
88
93
  // add an axis header if necessary
@@ -219,6 +224,18 @@ export const AxisX = React.memo(function AxisX(props) {
219
224
  setPlotLines(plotAfterContainer, axis.plotLines.filter((d) => d.layerPlacement === 'after'));
220
225
  }
221
226
  })();
222
- }, [axis, width, totalHeight, scale, split, leftmostLimit, plotBeforeRef, plotAfterRef]);
227
+ }, [
228
+ axis,
229
+ boundsOffsetLeft,
230
+ boundsOffsetTop,
231
+ htmlLayout,
232
+ leftmostLimit,
233
+ plotAfterRef,
234
+ plotBeforeRef,
235
+ scale,
236
+ split,
237
+ totalHeight,
238
+ width,
239
+ ]);
223
240
  return React.createElement("g", { ref: ref });
224
241
  });
@@ -1,16 +1,19 @@
1
1
  import React from 'react';
2
2
  import type { ChartScale, PreparedAxis, PreparedSplit } from '../../hooks';
3
3
  import './styles.css';
4
- type Props = {
4
+ interface Props {
5
5
  axes: PreparedAxis[];
6
+ boundsOffsetTop: number;
7
+ boundsOffsetLeft: number;
6
8
  scale: ChartScale[];
7
9
  width: number;
8
10
  height: number;
11
+ htmlLayout: HTMLElement | null;
9
12
  split: PreparedSplit;
10
13
  plotBeforeRef?: React.MutableRefObject<SVGGElement | null>;
11
14
  plotAfterRef?: React.MutableRefObject<SVGGElement | null>;
12
15
  bottomLimit?: number;
13
16
  topLimit?: number;
14
- };
17
+ }
15
18
  export declare const AxisY: (props: Props) => React.JSX.Element;
16
19
  export {};
@@ -3,6 +3,8 @@ import { axisLeft, axisRight, line, select } from 'd3';
3
3
  import { block, calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight, getAxisPlotsPosition, getAxisTitleRows, getBandsPosition, getClosestPointsRange, getLineDashArray, getScaleTicks, getTicksCount, handleOverflowingText, parseTransformStyle, setEllipsisForOverflowTexts, wrapText, } from '../../utils';
4
4
  import './styles.css';
5
5
  const b = block('axis');
6
+ const AXIS_LEFT_HTML_LABELS_DATA_ATTR = 'data-axis-left-html-labels';
7
+ const AXIS_RIGHT_HTML_LABELS_DATA_ATTR = 'data-axis-right-html-labels';
6
8
  function transformLabel(args) {
7
9
  const { node, axis, startTopOffset } = args;
8
10
  let topOffset = startTopOffset !== null && startTopOffset !== void 0 ? startTopOffset : axis.labels.lineHeight / 2;
@@ -36,7 +38,7 @@ function getAxisGenerator(args) {
36
38
  .tickSize(tickSize)
37
39
  .tickPadding(preparedAxis.labels.margin)
38
40
  .tickFormat((value) => {
39
- if (!preparedAxis.labels.enabled) {
41
+ if (!preparedAxis.labels.enabled || preparedAxis.labels.html) {
40
42
  return '';
41
43
  }
42
44
  return formatAxisTickLabel({
@@ -82,17 +84,20 @@ function getTitlePosition(args) {
82
84
  return { x, y };
83
85
  }
84
86
  export const AxisY = (props) => {
85
- const { axes: allAxes, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, bottomLimit = 0, topLimit = 0, } = props;
87
+ const { axes: allAxes, bottomLimit = 0, boundsOffsetLeft, boundsOffsetTop, height: totalHeight, htmlLayout, plotAfterRef, plotBeforeRef, scale, split, topLimit = 0, width, } = props;
86
88
  const height = getAxisHeight({ split, boundsHeight: totalHeight });
87
89
  const ref = React.useRef(null);
88
90
  const lineGenerator = line();
89
91
  React.useEffect(() => {
90
- if (!ref.current) {
92
+ if (!ref.current || !htmlLayout) {
91
93
  return;
92
94
  }
93
95
  const axes = allAxes.filter((a) => a.visible);
94
96
  const svgElement = select(ref.current);
95
97
  svgElement.selectAll('*').remove();
98
+ const htmlSelection = select(htmlLayout);
99
+ htmlSelection.selectAll(`[${AXIS_LEFT_HTML_LABELS_DATA_ATTR}]`).remove();
100
+ htmlSelection.selectAll(`[${AXIS_RIGHT_HTML_LABELS_DATA_ATTR}]`).remove();
96
101
  let plotBeforeContainer = null;
97
102
  let plotAfterContainer = null;
98
103
  const plotDataAttr = 'data-plot-y';
@@ -111,6 +116,7 @@ export const AxisY = (props) => {
111
116
  .attr('class', b())
112
117
  .style('transform', (d) => getAxisPlotsPosition(d, split, width));
113
118
  axisSelection.each((d, index, node) => {
119
+ var _a, _b;
114
120
  const seriesScale = scale[index];
115
121
  const axisItem = select(node[index]);
116
122
  const axisScale = seriesScale;
@@ -125,7 +131,56 @@ export const AxisY = (props) => {
125
131
  // because the standard generator interrupts the desired font
126
132
  // https://github.com/d3/d3-axis/blob/main/src/axis.js#L110
127
133
  axisItem.attr('font-family', null);
128
- if (d.labels.enabled) {
134
+ if (d.labels.enabled && d.labels.html) {
135
+ const offsetTop = ((_a = svgElement.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().top) || 0;
136
+ const offsetLeft = ((_b = svgElement.node()) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect().left) || 0;
137
+ const htmlLabelsData = [];
138
+ axisItem
139
+ .selectAll('.tick')
140
+ .data(axisScale.domain())
141
+ .each(function (tickContent) {
142
+ const rect = this.getBoundingClientRect();
143
+ htmlLabelsData.push({
144
+ content: tickContent,
145
+ right: rect.right,
146
+ top: rect.top,
147
+ });
148
+ });
149
+ const dataAttr = d.position === 'left'
150
+ ? AXIS_LEFT_HTML_LABELS_DATA_ATTR
151
+ : AXIS_RIGHT_HTML_LABELS_DATA_ATTR;
152
+ htmlSelection.append('div').attr(dataAttr, 1).style('position', 'absolute');
153
+ htmlLabelsData.forEach((labelData) => {
154
+ htmlSelection
155
+ .selectAll(`[${dataAttr}]`)
156
+ .data([labelData])
157
+ .append('div')
158
+ .html(function (l) {
159
+ return l.content;
160
+ })
161
+ .style('font-size', d.labels.style.fontSize || '')
162
+ .style('position', 'absolute')
163
+ .style('white-space', 'nowrap')
164
+ .style('color', 'var(--g-color-text-secondary)')
165
+ .style('overflow', 'hidden')
166
+ .style('text-overflow', 'ellipsis')
167
+ .style('height', `${d.labels.height}px`)
168
+ .style('display', 'inline-flex')
169
+ .style('align-items', 'center')
170
+ .style('left', function (l) {
171
+ if (d.position === 'right') {
172
+ return `${l.right - offsetLeft + d.labels.margin + boundsOffsetLeft}px`;
173
+ }
174
+ const rect = this.getBoundingClientRect();
175
+ return `${boundsOffsetLeft - rect.width - d.labels.margin}px`;
176
+ })
177
+ .style('top', function (l) {
178
+ const rect = this.getBoundingClientRect();
179
+ return `${l.top + boundsOffsetTop - offsetTop - rect.height / 2}px`;
180
+ });
181
+ });
182
+ }
183
+ else if (d.labels.enabled) {
129
184
  const labels = axisItem.selectAll('.tick text');
130
185
  const tickTexts = labels
131
186
  // The offset must be applied before the labels are rotated.
@@ -344,15 +399,18 @@ export const AxisY = (props) => {
344
399
  });
345
400
  }, [
346
401
  allAxes,
347
- width,
348
- height,
349
- scale,
350
- split,
351
402
  bottomLimit,
403
+ boundsOffsetLeft,
404
+ boundsOffsetTop,
405
+ height,
406
+ htmlLayout,
352
407
  lineGenerator,
353
- plotBeforeRef,
354
408
  plotAfterRef,
409
+ plotBeforeRef,
410
+ scale,
411
+ split,
355
412
  topLimit,
413
+ width,
356
414
  ]);
357
415
  return React.createElement("g", { ref: ref, className: b('container') });
358
416
  };
@@ -94,9 +94,9 @@ export const ChartInner = (props) => {
94
94
  })),
95
95
  React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[boundsOffsetLeft, boundsOffsetTop].join(',')})`, ref: plotRef },
96
96
  xScale && (yScale === null || yScale === void 0 ? void 0 : yScale.length) && (React.createElement(React.Fragment, null,
97
- React.createElement(AxisY, { bottomLimit: svgBottomPos, topLimit: svgTopPos, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }),
97
+ React.createElement(AxisY, { axes: yAxis, bottomLimit: svgBottomPos, boundsOffsetLeft: boundsOffsetLeft, boundsOffsetTop: boundsOffsetTop, height: boundsHeight, htmlLayout: htmlLayout, plotAfterRef: plotAfterRef, plotBeforeRef: plotBeforeRef, scale: yScale, split: preparedSplit, topLimit: svgTopPos, width: boundsWidth }),
98
98
  xAxis && (React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
99
- React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }))))),
99
+ React.createElement(AxisX, { axis: xAxis, boundsOffsetLeft: boundsOffsetLeft, boundsOffsetTop: boundsOffsetTop, height: boundsHeight, htmlLayout: htmlLayout, leftmostLimit: svgXPos, plotAfterRef: plotAfterRef, plotBeforeRef: plotBeforeRef, scale: xScale, split: preparedSplit, width: boundsWidth }))))),
100
100
  React.createElement("g", { ref: plotBeforeRef }),
101
101
  shapes,
102
102
  React.createElement("g", { ref: plotAfterRef })),
@@ -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([]);
@@ -297,6 +297,11 @@ export const Legend = (props) => {
297
297
  // ticks
298
298
  const scale = scaleLinear(domain, [0, legend.width]);
299
299
  const xAxisGenerator = await axisBottom({
300
+ domain: {
301
+ size: legend.width,
302
+ color: 'transparent',
303
+ },
304
+ htmlLayout,
300
305
  scale,
301
306
  ticks: {
302
307
  items: [[0, -rectHeight]],
@@ -307,10 +312,6 @@ export const Legend = (props) => {
307
312
  labelFormat: (value) => formatNumber(value, { unit: 'auto' }),
308
313
  labelsStyle: legend.ticks.style,
309
314
  },
310
- domain: {
311
- size: legend.width,
312
- color: 'transparent',
313
- },
314
315
  });
315
316
  const tickTop = legend.title.height + legend.title.margin + rectHeight;
316
317
  const legendAxisClassname = b('axis');
@@ -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,11 +7,27 @@ 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
- measureValue && React.createElement("div", { className: b('series-name') }, measureValue),
30
+ measureValue && (React.createElement("div", { className: b('series-name'), dangerouslySetInnerHTML: { __html: measureValue } })),
15
31
  // eslint-disable-next-line complexity
16
32
  hovered.map((seriesItem, i) => {
17
33
  var _a;
@@ -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: 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: 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,5 +1,5 @@
1
1
  import get from 'lodash/get';
2
- import { getHorisontalSvgTextHeight } from '../../utils';
2
+ import { getHorizontalSvgTextHeight } from '../../utils';
3
3
  const DEFAULT_TITLE_FONT_SIZE = '15px';
4
4
  const TITLE_PADDINGS = 8 * 2;
5
5
  export const getPreparedTitle = ({ title, }) => {
@@ -9,7 +9,7 @@ export const getPreparedTitle = ({ title, }) => {
9
9
  fontWeight: get(title, 'style.fontWeight'),
10
10
  };
11
11
  const titleHeight = titleText
12
- ? getHorisontalSvgTextHeight({ text: titleText, style: titleStyle }) + TITLE_PADDINGS
12
+ ? getHorizontalSvgTextHeight({ text: titleText, style: titleStyle }) + TITLE_PADDINGS
13
13
  : 0;
14
14
  const preparedTitle = titleText
15
15
  ? { text: titleText, style: titleStyle, height: titleHeight }
@@ -1,6 +1,6 @@
1
1
  import type { DashStyle } from '../../constants';
2
2
  import type { AxisCrosshair, AxisPlotBand, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisTitleAlignment, ChartAxisType, ChartData, ChartMargin, ChartZoom, DeepRequired, PlotLayerPlacement } from '../../types';
3
- type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation'>> & {
3
+ type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation' | 'html'>> & {
4
4
  style: BaseTextStyle;
5
5
  rotation: number;
6
6
  height: number;