@gravity-ui/charts 1.13.1 → 1.14.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 (53) 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/Legend/index.js +5 -4
  7. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.js +3 -3
  8. package/dist/cjs/hooks/useAxisScales/index.js +13 -27
  9. package/dist/cjs/hooks/useChartOptions/title.js +2 -2
  10. package/dist/cjs/hooks/useChartOptions/types.d.ts +1 -1
  11. package/dist/cjs/hooks/useChartOptions/x-axis.js +18 -10
  12. package/dist/cjs/hooks/useChartOptions/y-axis.js +17 -6
  13. package/dist/cjs/hooks/useShapes/bar-y/prepare-data.js +4 -3
  14. package/dist/cjs/hooks/useSplit/index.js +2 -2
  15. package/dist/cjs/hooks/utils/bar-y.d.ts +0 -1
  16. package/dist/cjs/hooks/utils/bar-y.js +5 -4
  17. package/dist/cjs/i18n/keysets/en.json +3 -1
  18. package/dist/cjs/i18n/keysets/ru.json +3 -1
  19. package/dist/cjs/types/chart/axis.d.ts +11 -1
  20. package/dist/cjs/utils/chart/axis-generators/bottom.d.ts +18 -13
  21. package/dist/cjs/utils/chart/axis-generators/bottom.js +173 -73
  22. package/dist/cjs/utils/chart/index.d.ts +5 -9
  23. package/dist/cjs/utils/chart/index.js +18 -9
  24. package/dist/cjs/validation/index.js +2 -22
  25. package/dist/cjs/validation/validate-axes.d.ts +5 -0
  26. package/dist/cjs/validation/validate-axes.js +46 -0
  27. package/dist/esm/components/Axis/AxisX.d.ts +5 -2
  28. package/dist/esm/components/Axis/AxisX.js +28 -11
  29. package/dist/esm/components/Axis/AxisY.d.ts +5 -2
  30. package/dist/esm/components/Axis/AxisY.js +67 -9
  31. package/dist/esm/components/ChartInner/index.js +2 -2
  32. package/dist/esm/components/Legend/index.js +5 -4
  33. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.js +3 -3
  34. package/dist/esm/hooks/useAxisScales/index.js +13 -27
  35. package/dist/esm/hooks/useChartOptions/title.js +2 -2
  36. package/dist/esm/hooks/useChartOptions/types.d.ts +1 -1
  37. package/dist/esm/hooks/useChartOptions/x-axis.js +18 -10
  38. package/dist/esm/hooks/useChartOptions/y-axis.js +17 -6
  39. package/dist/esm/hooks/useShapes/bar-y/prepare-data.js +4 -3
  40. package/dist/esm/hooks/useSplit/index.js +2 -2
  41. package/dist/esm/hooks/utils/bar-y.d.ts +0 -1
  42. package/dist/esm/hooks/utils/bar-y.js +5 -4
  43. package/dist/esm/i18n/keysets/en.json +3 -1
  44. package/dist/esm/i18n/keysets/ru.json +3 -1
  45. package/dist/esm/types/chart/axis.d.ts +11 -1
  46. package/dist/esm/utils/chart/axis-generators/bottom.d.ts +18 -13
  47. package/dist/esm/utils/chart/axis-generators/bottom.js +173 -73
  48. package/dist/esm/utils/chart/index.d.ts +5 -9
  49. package/dist/esm/utils/chart/index.js +18 -9
  50. package/dist/esm/validation/index.js +2 -22
  51. package/dist/esm/validation/validate-axes.d.ts +5 -0
  52. package/dist/esm/validation/validate-axes.js +46 -0
  53. 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 })),
@@ -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');
@@ -11,7 +11,7 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
11
11
  const measureValue = getMeasureValue({ data: hovered, xAxis, yAxis });
12
12
  const hoveredValues = getHoveredValues({ hovered, xAxis, yAxis });
13
13
  return (React.createElement("div", { className: b('content') },
14
- measureValue && React.createElement("div", { className: b('series-name') }, measureValue),
14
+ measureValue && (React.createElement("div", { className: b('series-name'), dangerouslySetInnerHTML: { __html: measureValue } })),
15
15
  // eslint-disable-next-line complexity
16
16
  hovered.map((seriesItem, i) => {
17
17
  var _a;
@@ -30,7 +30,7 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
30
30
  value: hoveredValues[i],
31
31
  format,
32
32
  });
33
- return (React.createElement(Row, { key: id, active: active, color: color, label: series.name, striped: striped, value: formattedValue }));
33
+ return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
34
34
  }
35
35
  case 'waterfall': {
36
36
  const isTotal = get(data, 'total', false);
@@ -56,7 +56,7 @@ export const DefaultTooltipContent = ({ hovered, xAxis, yAxis, valueFormat, tota
56
56
  value: hoveredValues[i],
57
57
  format,
58
58
  });
59
- return (React.createElement(Row, { key: id, active: active, color: color, label: series.name, striped: striped, value: formattedValue }));
59
+ return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: series.name } }), striped: striped, value: formattedValue }));
60
60
  }
61
61
  case 'pie':
62
62
  case 'treemap': {
@@ -29,39 +29,25 @@ function getYScaleRange(args) {
29
29
  case 'linear':
30
30
  case 'logarithmic': {
31
31
  let range = [boundsHeight, boundsHeight * axis.maxPadding];
32
- switch (axis.order) {
33
- case 'sortDesc':
34
- case 'reverse': {
35
- range.reverse();
36
- }
37
- }
38
32
  const barYSeries = series.filter((s) => s.type === SeriesType.BarY);
39
33
  if (barYSeries.length) {
40
34
  const groupedData = groupBarYDataByYValue(barYSeries, [axis]);
41
- const { barSize, dataLength } = getBarYLayoutForNumericScale({
42
- plotHeight: boundsHeight - boundsHeight * axis.maxPadding,
43
- groupedData,
44
- seriesOptions: seriesOptions,
45
- });
46
- if (dataLength > 1) {
47
- const alreadyCountedStackingIds = new Set();
48
- const offsetMultiplier = barYSeries.reduce((acc, s) => {
49
- let count = 0;
50
- if (s.stackId) {
51
- if (!alreadyCountedStackingIds.has(s.stackId)) {
52
- alreadyCountedStackingIds.add(s.stackId);
53
- count = 1;
54
- }
55
- }
56
- else {
57
- count = 1;
58
- }
59
- return acc + count;
60
- }, 0);
61
- const offset = (barSize * Math.max(offsetMultiplier, 1)) / 2;
35
+ if (Object.keys(groupedData).length > 1) {
36
+ const { bandSize } = getBarYLayoutForNumericScale({
37
+ plotHeight: boundsHeight - boundsHeight * axis.maxPadding,
38
+ groupedData,
39
+ seriesOptions: seriesOptions,
40
+ });
41
+ const offset = bandSize / 2;
62
42
  range = [range[0] - offset, range[1] + offset];
63
43
  }
64
44
  }
45
+ switch (axis.order) {
46
+ case 'sortDesc':
47
+ case 'reverse': {
48
+ range.reverse();
49
+ }
50
+ }
65
51
  return range;
66
52
  }
67
53
  case 'category': {
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, axisCrosshairDefaults, axisLabelsDefaults, xAxisTitleDefaults, } from '../../constants';
3
- import { calculateCos, formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getMaxTickCount, getTicksCount, getXAxisItems, hasOverlappingLabels, wrapText, } from '../../utils';
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
6
  async function getLabelSettings({ axis, seriesData, width, autoRotation = true, }) {
@@ -19,19 +19,22 @@ async function getLabelSettings({ axis, seriesData, width, autoRotation = true,
19
19
  step,
20
20
  });
21
21
  });
22
- const overlapping = hasOverlappingLabels({
23
- width,
24
- labels,
25
- padding: axis.labels.padding,
26
- style: axis.labels.style,
27
- });
22
+ const overlapping = axis.labels.html
23
+ ? false
24
+ : hasOverlappingLabels({
25
+ width,
26
+ labels,
27
+ padding: axis.labels.padding,
28
+ style: axis.labels.style,
29
+ });
28
30
  const defaultRotation = overlapping && autoRotation ? -45 : 0;
29
- const rotation = axis.labels.rotation || defaultRotation;
30
- const labelsHeight = rotation
31
+ const rotation = axis.labels.html ? 0 : axis.labels.rotation || defaultRotation;
32
+ const labelsHeight = rotation || axis.labels.html
31
33
  ? (await getLabelsSize({
32
34
  labels,
33
35
  style: axis.labels.style,
34
36
  rotation,
37
+ html: axis.labels.html,
35
38
  })).maxHeight
36
39
  : axis.labels.lineHeight;
37
40
  const maxHeight = rotation ? calculateCos(rotation) * axis.labels.maxWidth : labelsHeight;
@@ -54,6 +57,10 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
54
57
  const labelsStyle = {
55
58
  fontSize: get(xAxis, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
56
59
  };
60
+ const labelsHtml = get(xAxis, 'labels.html', false);
61
+ const labelsLineHeight = labelsHtml
62
+ ? getHorizontalHtmlTextHeight({ text: 'Tmp', style: labelsStyle })
63
+ : getHorizontalSvgTextHeight({ text: 'Tmp', style: labelsStyle });
57
64
  const preparedXAxis = {
58
65
  type: get(xAxis, 'type', 'linear'),
59
66
  labels: {
@@ -66,8 +73,9 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, }) => {
66
73
  style: labelsStyle,
67
74
  width: 0,
68
75
  height: 0,
69
- lineHeight: getHorisontalSvgTextHeight({ text: 'Tmp', style: labelsStyle }),
76
+ lineHeight: labelsLineHeight,
70
77
  maxWidth: get(xAxis, 'labels.maxWidth', axisLabelsDefaults.maxWidth),
78
+ html: labelsHtml,
71
79
  },
72
80
  lineColor: get(xAxis, 'lineColor'),
73
81
  categories: getAxisCategories(xAxis),
@@ -1,12 +1,12 @@
1
1
  import get from 'lodash/get';
2
2
  import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, axisCrosshairDefaults, axisLabelsDefaults, yAxisTitleDefaults, } from '../../constants';
3
- import { formatAxisTickLabel, getClosestPointsRange, getDefaultMinYAxisValue, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, isAxisRelatedSeries, wrapText, } from '../../utils';
3
+ import { formatAxisTickLabel, getClosestPointsRange, getDefaultMinYAxisValue, getHorizontalHtmlTextHeight, getHorizontalSvgTextHeight, getLabelsSize, getScaleTicks, isAxisRelatedSeries, wrapText, } from '../../utils';
4
4
  import { createYScale } from '../useAxisScales';
5
5
  import { getAxisCategories, prepareAxisPlotLabel } from './utils';
6
6
  const getAxisLabelMaxWidth = async (args) => {
7
7
  const { axis, seriesData, seriesOptions } = args;
8
8
  if (!axis.labels.enabled) {
9
- return 0;
9
+ return { height: 0, width: 0 };
10
10
  }
11
11
  const scale = createYScale({
12
12
  axis,
@@ -26,8 +26,9 @@ const getAxisLabelMaxWidth = async (args) => {
26
26
  labels,
27
27
  style: axis.labels.style,
28
28
  rotation: axis.labels.rotation,
29
+ html: axis.labels.html,
29
30
  });
30
- return size.maxWidth;
31
+ return { height: size.maxHeight, width: size.maxWidth };
31
32
  };
32
33
  export const getPreparedYAxis = ({ height, seriesData, seriesOptions, yAxis, }) => {
33
34
  const axisByPlot = [];
@@ -49,6 +50,10 @@ export const getPreparedYAxis = ({ height, seriesData, seriesOptions, yAxis, })
49
50
  const labelsStyle = {
50
51
  fontSize: get(axisItem, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
51
52
  };
53
+ const labelsHtml = get(axisItem, 'labels.html', false);
54
+ const labelsLineHeight = labelsHtml
55
+ ? getHorizontalHtmlTextHeight({ text: 'Tmp', style: labelsStyle })
56
+ : getHorizontalSvgTextHeight({ text: 'Tmp', style: labelsStyle });
52
57
  const titleText = get(axisItem, 'title.text', '');
53
58
  const titleStyle = Object.assign(Object.assign({}, yAxisTitleDefaults.style), get(axisItem, 'title.style'));
54
59
  const titleMaxRowsCount = get(axisItem, 'title.maxRowCount', yAxisTitleDefaults.maxRowCount);
@@ -72,11 +77,12 @@ export const getPreparedYAxis = ({ height, seriesData, seriesOptions, yAxis, })
72
77
  dateFormat: get(axisItem, 'labels.dateFormat'),
73
78
  numberFormat: get(axisItem, 'labels.numberFormat'),
74
79
  style: labelsStyle,
75
- rotation: get(axisItem, 'labels.rotation', 0),
80
+ rotation: labelsHtml ? 0 : get(axisItem, 'labels.rotation', 0),
76
81
  width: 0,
77
82
  height: 0,
78
- lineHeight: getHorisontalSvgTextHeight({ text: 'TmpLabel', style: labelsStyle }),
83
+ lineHeight: labelsLineHeight,
79
84
  maxWidth: get(axisItem, 'labels.maxWidth', axisLabelsDefaults.maxWidth),
85
+ html: labelsHtml,
80
86
  },
81
87
  lineColor: get(axisItem, 'lineColor'),
82
88
  categories: getAxisCategories(axisItem),
@@ -132,11 +138,16 @@ export const getPreparedYAxis = ({ height, seriesData, seriesOptions, yAxis, })
132
138
  order: axisItem.order,
133
139
  };
134
140
  if (labelsEnabled) {
135
- preparedAxis.labels.width = await getAxisLabelMaxWidth({
141
+ const { height: labelsHeight, width: labelsWidth } = await getAxisLabelMaxWidth({
136
142
  axis: preparedAxis,
137
143
  seriesData,
138
144
  seriesOptions,
139
145
  });
146
+ preparedAxis.labels.height = labelsHeight;
147
+ preparedAxis.labels.width =
148
+ labelsWidth > preparedAxis.labels.maxWidth
149
+ ? preparedAxis.labels.maxWidth
150
+ : labelsWidth;
140
151
  }
141
152
  return preparedAxis;
142
153
  }));
@@ -10,7 +10,8 @@ export const prepareBarYData = async (args) => {
10
10
  const stackGap = seriesOptions['bar-y'].stackGap;
11
11
  const xLinearScale = xScale;
12
12
  const yLinearScale = yScale;
13
- const plotHeight = yLinearScale(yLinearScale.domain()[0]);
13
+ const yScaleRange = yLinearScale.range();
14
+ const plotHeight = Math.abs(yScaleRange[0] - yScaleRange[1]);
14
15
  const sortingOptions = get(seriesOptions, 'bar-y.dataSorting');
15
16
  const comparator = (sortingOptions === null || sortingOptions === void 0 ? void 0 : sortingOptions.direction) === 'desc' ? descending : ascending;
16
17
  const sortKey = (() => {
@@ -40,7 +41,7 @@ export const prepareBarYData = async (args) => {
40
41
  const stacks = Object.values(val);
41
42
  const currentBarHeight = barSize * stacks.length + barGap * (stacks.length - 1);
42
43
  stacks.forEach((measureValues, groupItemIndex) => {
43
- const base = xLinearScale(0 - measureValues[0].series.borderWidth);
44
+ const base = xLinearScale(0) - measureValues[0].series.borderWidth;
44
45
  let stackSum = base;
45
46
  const stackItems = [];
46
47
  const sortedData = sortKey
@@ -85,7 +86,7 @@ export const prepareBarYData = async (args) => {
85
86
  height: barSize,
86
87
  color: data.color || s.color,
87
88
  borderColor: s.borderColor,
88
- borderWidth: s.borderWidth,
89
+ borderWidth: barSize > s.borderWidth * 2 ? s.borderWidth : 0,
89
90
  opacity: get(data, 'opacity', null),
90
91
  data,
91
92
  series: s,
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import get from 'lodash/get';
3
- import { calculateNumericProperty, getHorisontalSvgTextHeight } from '../../utils';
3
+ import { calculateNumericProperty, getHorizontalSvgTextHeight } from '../../utils';
4
4
  const DEFAULT_TITLE_FONT_SIZE = '15px';
5
5
  const TITLE_TOP_BOTTOM_PADDING = 8;
6
6
  function preparePlotTitle(args) {
@@ -11,7 +11,7 @@ function preparePlotTitle(args) {
11
11
  fontWeight: get(title, 'style.fontWeight'),
12
12
  };
13
13
  const titleHeight = titleText
14
- ? getHorisontalSvgTextHeight({ text: titleText, style: titleStyle }) +
14
+ ? getHorizontalSvgTextHeight({ text: titleText, style: titleStyle }) +
15
15
  TITLE_TOP_BOTTOM_PADDING * 2
16
16
  : 0;
17
17
  const top = plotIndex * (plotHeight + gap);
@@ -14,7 +14,6 @@ export declare function getBarYLayoutForNumericScale(args: {
14
14
  bandSize: number;
15
15
  barGap: number;
16
16
  barSize: number;
17
- dataLength: number;
18
17
  };
19
18
  export declare function getBarYLayoutForCategoryScale(args: {
20
19
  groupedData: ReturnType<typeof groupBarYDataByYValue>;
@@ -32,13 +32,14 @@ export function getBarYLayoutForNumericScale(args) {
32
32
  const barMaxWidth = get(seriesOptions, 'bar-y.barMaxWidth');
33
33
  const barPadding = get(seriesOptions, 'bar-y.barPadding');
34
34
  const groupPadding = get(seriesOptions, 'bar-y.groupPadding');
35
- const dataLength = Object.values(groupedData).reduce((sum, items) => sum + Object.keys(items).length, 0);
36
- const bandSize = plotHeight / dataLength;
35
+ const groups = Object.values(groupedData);
36
+ const maxGroupItemCount = groups.reduce((acc, items) => Math.max(acc, Object.keys(items).length), 0);
37
+ const bandSize = plotHeight / groups.length;
37
38
  const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
38
39
  const groupSize = bandSize - groupGap;
39
40
  const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
40
- const barSize = Math.max(MIN_BAR_WIDTH, Math.min(groupSize - barGap, barMaxWidth));
41
- return { bandSize, barGap, barSize, dataLength };
41
+ const barSize = Math.max(MIN_BAR_WIDTH, Math.min((groupSize - barGap) / maxGroupItemCount, barMaxWidth));
42
+ return { bandSize, barGap, barSize };
42
43
  }
43
44
  export function getBarYLayoutForCategoryScale(args) {
44
45
  const { groupedData, seriesOptions, yScale } = args;
@@ -16,7 +16,9 @@
16
16
  "label_axis-plot-band-options-not-equal": "It seems you are trying to use different type for \"{{axis}}\" axis plot band options",
17
17
  "label_invalid-tooltip-totals-aggregation-type": "It seems you are trying to use inappropriate data type for \"tooltip.totals.aggregation\". Available types: string, function.",
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "It seems you are trying to use inappropriate value for built-in \"tooltip.totals.aggregation\". Available values: [{{values}}].",
19
- "label_invalid-axis-type": "It seems you are trying to use inappropriate type for \"{{key}}\" axis. Available types: [{{values}}]."
19
+ "label_invalid-axis-type": "It seems you are trying to use inappropriate type for \"{{key}}\" axis. Available types: [{{values}}].",
20
+ "label_invalid-axis-labels-html-type": "It seems you are trying to use inappropriate type for \"labels.html\" property. Only boolean is allowed.",
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "It seems you are trying to use \"labels.html\" property for an axis with an unsupported type. This property is supported only for \"category\" axis."
20
22
  },
21
23
  "tooltip": {
22
24
  "label_totals_sum": "Sum"
@@ -16,7 +16,9 @@
16
16
  "label_axis-plot-band-options-not-equal": "Похоже, что вы пытаетесь использовать разные типы для для параметра полосы для оси \"{{axis}}\"",
17
17
  "label_invalid-tooltip-totals-aggregation-type": "Похоже, что вы пытаетесь использовать некорректный тип данных для \"tooltip.totals.aggregation\". Доступные типы: string, function.",
18
18
  "label_invalid-tooltip-totals-aggregation-type-str": "Похоже, что вы пытаетесь использовать некорректное значение для встроенной агрегации \"tooltip.totals.aggregation\". Доступные значения: [{{values}}].",
19
- "label_invalid-axis-type": "Похоже, что вы пытаетесь использовать некорректный тип для оси \"{{key}}\". Доступные типы: [{{values}}]."
19
+ "label_invalid-axis-type": "Похоже, что вы пытаетесь использовать некорректный тип для оси \"{{key}}\". Доступные типы: [{{values}}].",
20
+ "label_invalid-axis-labels-html-type": "Похоже, что вы пытаетесь использовать некорректный тип для свойства \"labels.html\". Допускается только использование булевых значений.",
21
+ "label_invalid-axis-labels-html-not-supported-axis-type": "Похоже, что вы пытаетесь использовать свойство \"labels.html\" для оси с неподдерживаемым типом. Это свойство поддерживается только для оси типа \"category\"."
20
22
  },
21
23
  "tooltip": {
22
24
  "label_totals_sum": "Сумма"
@@ -19,12 +19,22 @@ export interface ChartAxisLabels {
19
19
  style?: Partial<BaseTextStyle>;
20
20
  /** For horizontal axes, enable label rotation to prevent overlapping labels.
21
21
  * If there is enough space, labels are not rotated.
22
- * As the chart gets narrower, it will start rotating the labels -45 degrees. */
22
+ * As the chart gets narrower, it will start rotating the labels -45 degrees.
23
+ *
24
+ * Does not apply to html labels.
25
+ */
23
26
  autoRotation?: boolean;
24
27
  /** Rotation of the labels in degrees.
28
+ *
29
+ * Does not apply to html labels.
25
30
  * @default 0
26
31
  */
27
32
  rotation?: number;
33
+ /**
34
+ * Allows to use any html-tags to display labels content. Supports only for axis with type "category".
35
+ * @default false
36
+ * */
37
+ html?: boolean;
28
38
  }
29
39
  export interface ChartAxis {
30
40
  categories?: string[];