@gravity-ui/chartkit 4.4.1 → 4.6.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 (61) hide show
  1. package/build/plugins/d3/renderer/components/AxisX.d.ts +2 -1
  2. package/build/plugins/d3/renderer/components/AxisX.js +51 -49
  3. package/build/plugins/d3/renderer/components/AxisY.js +28 -31
  4. package/build/plugins/d3/renderer/components/Chart.js +19 -7
  5. package/build/plugins/d3/renderer/components/Legend.d.ts +5 -6
  6. package/build/plugins/d3/renderer/components/Legend.js +139 -84
  7. package/build/plugins/d3/renderer/components/styles.css +27 -0
  8. package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +5 -0
  9. package/build/plugins/d3/renderer/constants/defaults/axis.js +5 -0
  10. package/build/plugins/d3/renderer/constants/defaults/index.d.ts +2 -0
  11. package/build/plugins/d3/renderer/constants/defaults/index.js +2 -0
  12. package/build/plugins/d3/renderer/constants/defaults/legend.d.ts +4 -0
  13. package/build/plugins/d3/renderer/constants/defaults/legend.js +8 -0
  14. package/build/plugins/d3/renderer/{constants.d.ts → constants/index.d.ts} +1 -1
  15. package/build/plugins/d3/renderer/{constants.js → constants/index.js} +1 -1
  16. package/build/plugins/d3/renderer/hooks/useAxisScales/index.d.ts +3 -1
  17. package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +64 -59
  18. package/build/plugins/d3/renderer/hooks/useChartDimensions/index.d.ts +7 -4
  19. package/build/plugins/d3/renderer/hooks/useChartDimensions/index.js +65 -7
  20. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.d.ts +6 -0
  21. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.js +7 -0
  22. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.d.ts +1 -3
  23. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +12 -75
  24. package/build/plugins/d3/renderer/hooks/useChartOptions/index.d.ts +3 -1
  25. package/build/plugins/d3/renderer/hooks/useChartOptions/index.js +3 -8
  26. package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +3 -6
  27. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +4 -2
  28. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +3 -2
  29. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +31 -4
  30. package/build/plugins/d3/renderer/hooks/useSeries/constants.d.ts +1 -1
  31. package/build/plugins/d3/renderer/hooks/useSeries/constants.js +1 -1
  32. package/build/plugins/d3/renderer/hooks/useSeries/index.d.ts +19 -7
  33. package/build/plugins/d3/renderer/hooks/useSeries/index.js +26 -8
  34. package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.d.ts +27 -0
  35. package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.js +92 -0
  36. package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.d.ts +1 -2
  37. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +26 -1
  38. package/build/plugins/d3/renderer/utils/axis-generators/bottom.d.ts +21 -0
  39. package/build/plugins/d3/renderer/utils/axis-generators/bottom.js +75 -0
  40. package/build/plugins/d3/renderer/utils/axis-generators/index.d.ts +1 -0
  41. package/build/plugins/d3/renderer/utils/axis-generators/index.js +1 -0
  42. package/build/plugins/d3/renderer/utils/axis.d.ts +22 -0
  43. package/build/plugins/d3/renderer/utils/axis.js +49 -0
  44. package/build/plugins/d3/renderer/utils/index.d.ts +8 -4
  45. package/build/plugins/d3/renderer/utils/index.js +17 -6
  46. package/build/plugins/d3/renderer/utils/text.d.ts +20 -0
  47. package/build/plugins/d3/renderer/utils/text.js +62 -0
  48. package/build/plugins/d3/renderer/utils/time.d.ts +3 -0
  49. package/build/plugins/d3/renderer/utils/time.js +34 -0
  50. package/build/plugins/highcharts/renderer/components/HighchartsComponent.js +3 -3
  51. package/build/plugins/shared/format-number/format-number.d.ts +1 -0
  52. package/build/plugins/shared/format-number/format-number.js +19 -20
  53. package/build/types/widget-data/axis.d.ts +13 -1
  54. package/build/types/widget-data/legend.d.ts +24 -7
  55. package/build/utils/common.d.ts +1 -0
  56. package/build/utils/common.js +1 -1
  57. package/build/utils/index.d.ts +1 -1
  58. package/build/utils/index.js +1 -1
  59. package/package.json +2 -2
  60. package/build/plugins/d3/renderer/hooks/useChartOptions/legend.d.ts +0 -6
  61. package/build/plugins/d3/renderer/hooks/useChartOptions/legend.js +0 -12
@@ -3,9 +3,11 @@ import { group, scaleOrdinal } from 'd3';
3
3
  import { DEFAULT_PALETTE } from '../../constants';
4
4
  import { getSeriesNames } from '../../utils';
5
5
  import { getActiveLegendItems, getAllLegendItems } from './utils';
6
+ import { getPreparedLegend, getLegendComponents } from './prepare-legend';
6
7
  import { prepareSeries } from './prepareSeries';
7
8
  export const useSeries = (args) => {
8
- const { series: { data: series }, legend, } = args;
9
+ const { chartWidth, chartHeight, chartMargin, legend, preparedYAxis, series: { data: series }, } = args;
10
+ const preparedLegend = React.useMemo(() => getPreparedLegend({ legend, series }), [legend, series]);
9
11
  const preparedSeries = React.useMemo(() => {
10
12
  const seriesNames = getSeriesNames(series);
11
13
  const colorScale = scaleOrdinal(seriesNames, DEFAULT_PALETTE);
@@ -14,12 +16,12 @@ export const useSeries = (args) => {
14
16
  acc.push(...prepareSeries({
15
17
  type: seriesType,
16
18
  series: seriesList,
17
- legend,
19
+ legend: preparedLegend,
18
20
  colorScale,
19
21
  }));
20
22
  return acc;
21
23
  }, []);
22
- }, [series, legend]);
24
+ }, [series, preparedLegend]);
23
25
  const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(preparedSeries));
24
26
  const chartSeries = React.useMemo(() => {
25
27
  return preparedSeries.map((singleSeries) => {
@@ -29,10 +31,16 @@ export const useSeries = (args) => {
29
31
  return singleSeries;
30
32
  });
31
33
  }, [preparedSeries, activeLegendItems]);
32
- // FIXME: remove effect. It initiates extra rerender
33
- React.useEffect(() => {
34
- setActiveLegendItems(getActiveLegendItems(preparedSeries));
35
- }, [preparedSeries]);
34
+ const { legendConfig, legendItems } = React.useMemo(() => {
35
+ return getLegendComponents({
36
+ chartHeight,
37
+ chartMargin,
38
+ chartWidth,
39
+ series: chartSeries,
40
+ preparedLegend,
41
+ preparedYAxis,
42
+ });
43
+ }, [chartWidth, chartHeight, chartMargin, chartSeries, preparedLegend, preparedYAxis]);
36
44
  const handleLegendItemClick = React.useCallback(({ name, metaKey }) => {
37
45
  const onlyItemSelected = activeLegendItems.length === 1 && activeLegendItems.includes(name);
38
46
  let nextActiveLegendItems;
@@ -50,5 +58,15 @@ export const useSeries = (args) => {
50
58
  }
51
59
  setActiveLegendItems(nextActiveLegendItems);
52
60
  }, [preparedSeries, activeLegendItems]);
53
- return { preparedSeries: chartSeries, handleLegendItemClick };
61
+ // FIXME: remove effect. It initiates extra rerender
62
+ React.useEffect(() => {
63
+ setActiveLegendItems(getActiveLegendItems(preparedSeries));
64
+ }, [preparedSeries]);
65
+ return {
66
+ legendItems,
67
+ legendConfig,
68
+ preparedLegend,
69
+ preparedSeries: chartSeries,
70
+ handleLegendItemClick,
71
+ };
54
72
  };
@@ -0,0 +1,27 @@
1
+ import type { ChartKitWidgetData } from '../../../../../types/widget-data';
2
+ import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
3
+ import type { PreparedLegend, PreparedSeries, LegendItem } from './types';
4
+ export declare const getPreparedLegend: (args: {
5
+ legend: ChartKitWidgetData['legend'];
6
+ series: ChartKitWidgetData['series']['data'];
7
+ }) => PreparedLegend;
8
+ export declare const getLegendComponents: (args: {
9
+ chartWidth: number;
10
+ chartHeight: number;
11
+ chartMargin: PreparedChart['margin'];
12
+ series: PreparedSeries[];
13
+ preparedLegend: PreparedLegend;
14
+ preparedYAxis: PreparedAxis[];
15
+ }) => {
16
+ legendConfig: {
17
+ offset: {
18
+ left: number;
19
+ top: number;
20
+ };
21
+ pagination: {
22
+ limit: number;
23
+ maxPage: number;
24
+ } | undefined;
25
+ };
26
+ legendItems: LegendItem[][];
27
+ };
@@ -0,0 +1,92 @@
1
+ import clone from 'lodash/clone';
2
+ import get from 'lodash/get';
3
+ import merge from 'lodash/merge';
4
+ import { select } from 'd3';
5
+ import { legendDefaults } from '../../constants';
6
+ import { getHorisontalSvgTextHeight } from '../../utils';
7
+ import { getBoundsWidth } from '../useChartDimensions';
8
+ export const getPreparedLegend = (args) => {
9
+ const { legend, series } = args;
10
+ const enabled = typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : series.length > 1;
11
+ const defaultItemStyle = clone(legendDefaults.itemStyle);
12
+ const itemStyle = get(legend, 'itemStyle');
13
+ const computedItemStyle = merge(defaultItemStyle, itemStyle);
14
+ const lineHeight = getHorisontalSvgTextHeight({ text: 'Tmp', style: computedItemStyle });
15
+ const height = enabled ? lineHeight : 0;
16
+ return {
17
+ align: get(legend, 'align', legendDefaults.align),
18
+ enabled,
19
+ height,
20
+ itemDistance: get(legend, 'itemDistance', legendDefaults.itemDistance),
21
+ itemStyle: computedItemStyle,
22
+ lineHeight,
23
+ margin: get(legend, 'margin', legendDefaults.margin),
24
+ };
25
+ };
26
+ const getFlattenLegendItems = (series) => {
27
+ return series.reduce((acc, s) => {
28
+ const legendEnabled = get(s, 'legend.enabled', true);
29
+ if (legendEnabled) {
30
+ acc.push(Object.assign(Object.assign({}, s), { symbol: s.legend.symbol }));
31
+ }
32
+ return acc;
33
+ }, []);
34
+ };
35
+ const getGroupedLegendItems = (args) => {
36
+ const { maxLegendWidth, items, preparedLegend } = args;
37
+ const result = [[]];
38
+ let textWidthsInLine = [0];
39
+ let lineIndex = 0;
40
+ items.forEach((item) => {
41
+ select(document.body)
42
+ .append('text')
43
+ .text(item.name)
44
+ .style('font-size', preparedLegend.itemStyle.fontSize)
45
+ .each(function () {
46
+ const resultItem = clone(item);
47
+ const textWidth = this.getBoundingClientRect().width;
48
+ resultItem.textWidth = textWidth;
49
+ textWidthsInLine.push(textWidth);
50
+ const textsWidth = textWidthsInLine.reduce((acc, width) => acc + width, 0);
51
+ result[lineIndex].push(resultItem);
52
+ const symbolsWidth = result[lineIndex].reduce((acc, { symbol }) => {
53
+ return acc + symbol.width + symbol.padding;
54
+ }, 0);
55
+ const distancesWidth = (result[lineIndex].length - 1) * preparedLegend.itemDistance;
56
+ const isOverfilled = maxLegendWidth < textsWidth + symbolsWidth + distancesWidth;
57
+ if (isOverfilled) {
58
+ result[lineIndex].pop();
59
+ lineIndex += 1;
60
+ textWidthsInLine = [textWidth];
61
+ const nextLineIndex = lineIndex;
62
+ result[nextLineIndex] = [];
63
+ result[nextLineIndex].push(resultItem);
64
+ }
65
+ })
66
+ .remove();
67
+ });
68
+ return result;
69
+ };
70
+ export const getLegendComponents = (args) => {
71
+ const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
72
+ const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
73
+ const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
74
+ const flattenLegendItems = getFlattenLegendItems(series);
75
+ const items = getGroupedLegendItems({
76
+ maxLegendWidth,
77
+ items: flattenLegendItems,
78
+ preparedLegend,
79
+ });
80
+ let legendHeight = preparedLegend.lineHeight * items.length;
81
+ let pagination;
82
+ if (maxLegendHeight < legendHeight) {
83
+ const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight);
84
+ const maxPage = Math.ceil(items.length / limit);
85
+ pagination = { limit, maxPage };
86
+ legendHeight = maxLegendHeight;
87
+ }
88
+ preparedLegend.height = legendHeight;
89
+ const top = chartHeight - chartMargin.bottom - preparedLegend.height + preparedLegend.lineHeight / 2;
90
+ const offset = { left: chartMargin.left, top };
91
+ return { legendConfig: { offset, pagination }, legendItems: items };
92
+ };
@@ -1,7 +1,6 @@
1
1
  import type { ScaleOrdinal } from 'd3';
2
2
  import type { ChartKitWidgetSeries } from '../../../../../types/widget-data';
3
- import type { PreparedLegend } from '../useChartOptions/types';
4
- import type { PreparedSeries } from './types';
3
+ import type { PreparedLegend, PreparedSeries } from './types';
5
4
  export declare function prepareSeries(args: {
6
5
  type: ChartKitWidgetSeries['type'];
7
6
  series: ChartKitWidgetSeries[];
@@ -1,8 +1,33 @@
1
- import { BarXSeries, BarXSeriesData, BaseTextStyle, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
1
+ import { BarXSeries, BarXSeriesData, BaseTextStyle, ChartKitWidgetLegend, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
2
2
  export type RectLegendSymbol = {
3
3
  shape: 'rect';
4
4
  } & Required<RectLegendSymbolOptions>;
5
5
  export type PreparedLegendSymbol = RectLegendSymbol;
6
+ export type PreparedLegend = Required<ChartKitWidgetLegend> & {
7
+ height: number;
8
+ lineHeight: number;
9
+ };
10
+ export type OnLegendItemClick = (data: {
11
+ name: string;
12
+ metaKey: boolean;
13
+ }) => void;
14
+ export type LegendItem = {
15
+ color: string;
16
+ name: string;
17
+ symbol: PreparedLegendSymbol;
18
+ textWidth: number;
19
+ visible?: boolean;
20
+ };
21
+ export type LegendConfig = {
22
+ offset: {
23
+ left: number;
24
+ top: number;
25
+ };
26
+ pagination?: {
27
+ limit: number;
28
+ maxPage: number;
29
+ };
30
+ };
6
31
  type BasePreparedSeries = {
7
32
  color: string;
8
33
  name: string;
@@ -0,0 +1,21 @@
1
+ import type { AxisDomain, AxisScale, Selection } from 'd3';
2
+ import { BaseTextStyle } from '../../../../../types';
3
+ type AxisBottomArgs = {
4
+ scale: AxisScale<AxisDomain>;
5
+ ticks: {
6
+ count?: number;
7
+ maxTickCount: number;
8
+ labelFormat: (value: any) => string;
9
+ labelsPaddings?: number;
10
+ labelsMargin?: number;
11
+ labelsStyle?: BaseTextStyle;
12
+ size: number;
13
+ autoRotation?: boolean;
14
+ };
15
+ domain: {
16
+ size: number;
17
+ color?: string;
18
+ };
19
+ };
20
+ export declare function axisBottom(args: AxisBottomArgs): (selection: Selection<SVGGElement, unknown, null, undefined>) => void;
21
+ export {};
@@ -0,0 +1,75 @@
1
+ import { getXAxisItems, getXAxisOffset, getXTickPosition } from '../axis';
2
+ import { hasOverlappingLabels } from '../text';
3
+ function addDomain(selection, options) {
4
+ const { size, color } = options;
5
+ selection
6
+ .selectAll('.domain')
7
+ .data([null])
8
+ .enter()
9
+ .insert('path', '.tick')
10
+ .attr('class', 'domain')
11
+ .attr('stroke', color || 'currentColor')
12
+ .attr('d', `M0,0V0H${size}`);
13
+ }
14
+ export function axisBottom(args) {
15
+ const { scale, ticks: { labelFormat, labelsPaddings = 0, labelsMargin = 0, labelsStyle, size: tickSize, count: ticksCount, maxTickCount, autoRotation = true, }, domain: { size: domainSize, color: domainColor }, } = args;
16
+ const offset = getXAxisOffset();
17
+ const spacing = Math.max(tickSize, 0) + labelsMargin;
18
+ const position = getXTickPosition({ scale, offset });
19
+ const values = getXAxisItems({ scale, count: ticksCount, maxCount: maxTickCount });
20
+ return function (selection) {
21
+ var _a, _b;
22
+ selection
23
+ .selectAll('.tick')
24
+ .data(values)
25
+ .order()
26
+ .join((el) => {
27
+ const tick = el.append('g').attr('class', 'tick');
28
+ tick.append('line').attr('stroke', 'currentColor').attr('y2', tickSize);
29
+ tick.append('text')
30
+ .attr('fill', 'currentColor')
31
+ .attr('y', spacing)
32
+ .attr('dy', '0.71em')
33
+ .text(labelFormat);
34
+ return tick;
35
+ })
36
+ .attr('transform', function (d) {
37
+ return `translate(${position(d) + offset},0)`;
38
+ });
39
+ const labels = selection.selectAll('.tick text');
40
+ const labelNodes = labels.nodes();
41
+ const overlapping = hasOverlappingLabels({
42
+ width: domainSize,
43
+ labels: values.map(labelFormat),
44
+ padding: labelsPaddings,
45
+ style: labelsStyle,
46
+ });
47
+ if (overlapping) {
48
+ if (autoRotation) {
49
+ const labelHeight = (_b = (_a = labelNodes[0]) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.height;
50
+ const labelOffset = (labelHeight / 2 + labelsMargin) / 2;
51
+ labels
52
+ .attr('text-anchor', 'end')
53
+ .attr('transform', `rotate(-45) translate(-${labelOffset}, -${labelOffset})`);
54
+ }
55
+ else {
56
+ // remove overlapping labels
57
+ let elementX = 0;
58
+ selection
59
+ .selectAll('.tick')
60
+ .filter(function () {
61
+ const node = this;
62
+ const r = node.getBoundingClientRect();
63
+ if (r.left < elementX) {
64
+ return true;
65
+ }
66
+ elementX = r.right + labelsPaddings;
67
+ return false;
68
+ })
69
+ .remove();
70
+ }
71
+ }
72
+ selection.call(addDomain, { size: domainSize, color: domainColor });
73
+ selection.attr('text-anchor', 'middle').style('font-size', (labelsStyle === null || labelsStyle === void 0 ? void 0 : labelsStyle.fontSize) || '');
74
+ };
75
+ }
@@ -0,0 +1 @@
1
+ export * from './bottom';
@@ -0,0 +1 @@
1
+ export * from './bottom';
@@ -0,0 +1,22 @@
1
+ import { PreparedAxis } from '../hooks';
2
+ import { AxisDomain, AxisScale, ScaleBand } from 'd3';
3
+ export declare function getTicksCount({ axis, range }: {
4
+ axis: PreparedAxis;
5
+ range: number;
6
+ }): number | undefined;
7
+ export declare function isBandScale(scale: AxisScale<AxisDomain>): scale is ScaleBand<AxisDomain>;
8
+ export declare function getScaleTicks(scale: AxisScale<AxisDomain>, ticksCount?: number): any;
9
+ export declare function getXAxisOffset(): 0 | 0.5;
10
+ export declare function getXTickPosition({ scale, offset }: {
11
+ scale: AxisScale<AxisDomain>;
12
+ offset: number;
13
+ }): (d: AxisDomain) => number;
14
+ export declare function getXAxisItems({ scale, count, maxCount, }: {
15
+ scale: AxisScale<AxisDomain>;
16
+ count?: number;
17
+ maxCount: number;
18
+ }): any;
19
+ export declare function getMaxTickCount({ axis, width }: {
20
+ axis: PreparedAxis;
21
+ width: number;
22
+ }): number;
@@ -0,0 +1,49 @@
1
+ export function getTicksCount({ axis, range }) {
2
+ let ticksCount;
3
+ if (axis.ticks.pixelInterval) {
4
+ ticksCount = Math.ceil(range / axis.ticks.pixelInterval);
5
+ }
6
+ return ticksCount;
7
+ }
8
+ export function isBandScale(scale) {
9
+ return 'bandwidth' in scale && typeof scale.bandwidth === 'function';
10
+ }
11
+ export function getScaleTicks(scale, ticksCount) {
12
+ return 'ticks' in scale && typeof scale.ticks === 'function'
13
+ ? scale.ticks(ticksCount)
14
+ : scale.domain();
15
+ }
16
+ export function getXAxisOffset() {
17
+ return typeof window !== 'undefined' && window.devicePixelRatio > 1 ? 0 : 0.5;
18
+ }
19
+ function number(scale) {
20
+ return (d) => Number(scale(d));
21
+ }
22
+ function center(scale, offset) {
23
+ offset = Math.max(0, scale.bandwidth() - offset * 2) / 2;
24
+ if (scale.round()) {
25
+ offset = Math.round(offset);
26
+ }
27
+ return (d) => Number(scale(d)) + offset;
28
+ }
29
+ export function getXTickPosition({ scale, offset }) {
30
+ return isBandScale(scale) ? center(scale.copy(), offset) : number(scale.copy());
31
+ }
32
+ export function getXAxisItems({ scale, count, maxCount, }) {
33
+ const offset = getXAxisOffset();
34
+ let values = getScaleTicks(scale, count);
35
+ const position = getXTickPosition({ scale, offset });
36
+ if (values.length > maxCount) {
37
+ const step = Math.ceil(values.length / maxCount);
38
+ values = values.filter((_, i) => i % step === 0);
39
+ }
40
+ // Remove tick that has the same x coordinate like domain
41
+ if (values.length && position(values[0]) === 0) {
42
+ values = values.slice(1);
43
+ }
44
+ return values;
45
+ }
46
+ export function getMaxTickCount({ axis, width }) {
47
+ const minTickWidth = parseInt(axis.labels.style.fontSize) + axis.labels.padding;
48
+ return Math.floor(width / minTickWidth);
49
+ }
@@ -1,6 +1,10 @@
1
1
  import { AxisDomain } from 'd3';
2
- import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData, ChartKitWidgetAxisType, ChartKitWidgetAxisLabels } from '../../../../types/widget-data';
2
+ import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData } from '../../../../types/widget-data';
3
+ import { PreparedAxis } from '../hooks';
3
4
  export * from './math';
5
+ export * from './text';
6
+ export * from './time';
7
+ export * from './axis';
4
8
  export type AxisDirection = 'x' | 'y';
5
9
  type UnknownSeries = {
6
10
  type: ChartKitWidgetSeries['type'];
@@ -42,10 +46,9 @@ export declare const parseTransformStyle: (style: string | null) => {
42
46
  y?: number | undefined;
43
47
  };
44
48
  export declare const formatAxisTickLabel: (args: {
45
- axisType: ChartKitWidgetAxisType;
49
+ axis: PreparedAxis;
46
50
  value: AxisDomain;
47
- dateFormat?: ChartKitWidgetAxisLabels['dateFormat'];
48
- numberFormat?: ChartKitWidgetAxisLabels['numberFormat'];
51
+ step?: number;
49
52
  }) => string;
50
53
  /**
51
54
  * Calculates the height of a text element in a horizontal SVG layout.
@@ -64,3 +67,4 @@ export declare const getDataCategoryValue: (args: {
64
67
  categories: string[];
65
68
  data: ChartKitWidgetSeriesData;
66
69
  }) => string;
70
+ export declare function getClosestPointsRange(axis: PreparedAxis, points: AxisDomain[]): number | undefined;
@@ -4,7 +4,12 @@ import isNil from 'lodash/isNil';
4
4
  import { dateTime } from '@gravity-ui/date-utils';
5
5
  import { formatNumber } from '../../../shared';
6
6
  import { DEFAULT_AXIS_LABEL_FONT_SIZE } from '../constants';
7
+ import { getNumberUnitRate } from '../../../shared/format-number/format-number';
8
+ import { getDefaultDateFormat } from './time';
7
9
  export * from './math';
10
+ export * from './text';
11
+ export * from './time';
12
+ export * from './axis';
8
13
  const CHARTS_WITHOUT_AXIS = ['pie'];
9
14
  /**
10
15
  * Checks whether the series should be drawn with axes.
@@ -88,20 +93,20 @@ export const parseTransformStyle = (style) => {
88
93
  const y = Number.isNaN(Number(yString)) ? undefined : Number(yString);
89
94
  return { x, y };
90
95
  };
91
- const defaultFormatNumberOptions = {
92
- precision: 0,
93
- };
94
96
  export const formatAxisTickLabel = (args) => {
95
- const { axisType, value, dateFormat = 'DD.MM.YY', numberFormat = defaultFormatNumberOptions, } = args;
96
- switch (axisType) {
97
+ const { axis, value, step } = args;
98
+ switch (axis.type) {
97
99
  case 'category': {
98
100
  return value;
99
101
  }
100
102
  case 'datetime': {
101
- return dateTime({ input: value }).format(dateFormat);
103
+ const date = value;
104
+ const format = axis.labels.dateFormat || getDefaultDateFormat(step);
105
+ return dateTime({ input: date }).format(format);
102
106
  }
103
107
  case 'linear':
104
108
  default: {
109
+ const numberFormat = Object.assign({ unitRate: value && step ? getNumberUnitRate(step) : undefined }, axis.labels.numberFormat);
105
110
  return formatNumber(value, numberFormat);
106
111
  }
107
112
  }
@@ -152,3 +157,9 @@ export const getDataCategoryValue = (args) => {
152
157
  const categoryValue = extractCategoryValue({ axisDirection, categories, data });
153
158
  return categoryValue;
154
159
  };
160
+ export function getClosestPointsRange(axis, points) {
161
+ if (axis.type === 'category') {
162
+ return undefined;
163
+ }
164
+ return points[1] - points[0];
165
+ }
@@ -0,0 +1,20 @@
1
+ import type { Selection } from 'd3';
2
+ import { BaseTextStyle } from '../../../../types';
3
+ export declare function setEllipsisForOverflowText(selection: Selection<SVGTextElement, unknown, null, unknown>, maxWidth: number): void;
4
+ export declare function setEllipsisForOverflowTexts(selection: Selection<SVGTextElement, string, any, unknown>, maxWidth: number): void;
5
+ export declare function hasOverlappingLabels({ width, labels, padding, style, }: {
6
+ width: number;
7
+ labels: string[];
8
+ style?: BaseTextStyle;
9
+ padding?: number;
10
+ }): boolean;
11
+ export declare function getLabelsMaxWidth({ labels, style, transform, }: {
12
+ labels: string[];
13
+ style?: BaseTextStyle;
14
+ transform?: string;
15
+ }): number;
16
+ export declare function getLabelsMaxHeight({ labels, style, transform, }: {
17
+ labels: string[];
18
+ style?: BaseTextStyle;
19
+ transform?: string;
20
+ }): number;
@@ -0,0 +1,62 @@
1
+ import { select } from 'd3';
2
+ export function setEllipsisForOverflowText(selection, maxWidth) {
3
+ var _a, _b;
4
+ let text = selection.text();
5
+ selection.text(null).attr('text-anchor', 'left').append('title').text(text);
6
+ const tSpan = selection.append('tspan').text(text);
7
+ let textLength = ((_a = tSpan.node()) === null || _a === void 0 ? void 0 : _a.getComputedTextLength()) || 0;
8
+ while (textLength > maxWidth && text.length > 1) {
9
+ text = text.slice(0, -1);
10
+ tSpan.text(text + '…');
11
+ textLength = ((_b = tSpan.node()) === null || _b === void 0 ? void 0 : _b.getComputedTextLength()) || 0;
12
+ }
13
+ }
14
+ export function setEllipsisForOverflowTexts(selection, maxWidth) {
15
+ selection.each(function () {
16
+ setEllipsisForOverflowText(select(this), maxWidth);
17
+ });
18
+ }
19
+ export function hasOverlappingLabels({ width, labels, padding = 0, style, }) {
20
+ const maxWidth = (width - padding * (labels.length - 1)) / labels.length;
21
+ const textElement = select(document.body)
22
+ .append('text')
23
+ .style('font-size', (style === null || style === void 0 ? void 0 : style.fontSize) || '');
24
+ const result = labels.some((label) => {
25
+ var _a, _b;
26
+ const textWidth = ((_b = (_a = textElement.text(label).node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
27
+ return textWidth > maxWidth;
28
+ });
29
+ textElement.remove();
30
+ return result;
31
+ }
32
+ function renderLabels(selection, { labels, style, transform, }) {
33
+ const text = selection
34
+ .append('g')
35
+ .append('text')
36
+ .attr('transform', transform || '')
37
+ .style('font-size', (style === null || style === void 0 ? void 0 : style.fontSize) || '');
38
+ text.selectAll('tspan')
39
+ .data(labels)
40
+ .enter()
41
+ .append('tspan')
42
+ .attr('x', 0)
43
+ .attr('dy', 0)
44
+ .text((d) => d);
45
+ return text;
46
+ }
47
+ export function getLabelsMaxWidth({ labels, style, transform, }) {
48
+ var _a, _b;
49
+ const svg = select(document.body).append('svg');
50
+ svg.call(renderLabels, { labels, style, transform });
51
+ const maxWidth = ((_b = (_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
52
+ svg.remove();
53
+ return maxWidth;
54
+ }
55
+ export function getLabelsMaxHeight({ labels, style, transform, }) {
56
+ var _a, _b;
57
+ const svg = select(document.body).append('svg');
58
+ svg.call(renderLabels, { labels, style, transform });
59
+ const maxHeight = ((_b = (_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.height) || 0;
60
+ svg.remove();
61
+ return maxHeight;
62
+ }
@@ -0,0 +1,3 @@
1
+ export declare const TIME_UNITS: Record<string, number>;
2
+ export declare const DATETIME_LABEL_FORMATS: Record<keyof typeof TIME_UNITS, string>;
3
+ export declare function getDefaultDateFormat(range?: number): string;
@@ -0,0 +1,34 @@
1
+ export const TIME_UNITS = {
2
+ millisecond: 1,
3
+ second: 1000,
4
+ minute: 60000,
5
+ hour: 3600000,
6
+ day: 24 * 3600000,
7
+ week: 7 * 24 * 3600000,
8
+ month: 28 * 24 * 3600000,
9
+ year: 364 * 24 * 3600000,
10
+ };
11
+ export const DATETIME_LABEL_FORMATS = {
12
+ millisecond: 'DD.MM.YY HH:mm:ss.SSS',
13
+ second: 'DD.MM.YY HH:mm:ss',
14
+ minute: 'DD.MM.YY HH:mm',
15
+ hour: 'DD.MM.YY HH:mm',
16
+ day: 'DD.MM.YY',
17
+ week: 'DD.MM.YY',
18
+ month: "MMM 'YY",
19
+ year: 'YYYY',
20
+ };
21
+ function getTimeUnit(range) {
22
+ const units = Object.keys(TIME_UNITS);
23
+ const index = units.findIndex((unit) => range < TIME_UNITS[unit]);
24
+ return index === -1 ? 'year' : units[index - 1];
25
+ }
26
+ export function getDefaultDateFormat(range) {
27
+ if (range) {
28
+ const unit = getTimeUnit(range);
29
+ if (unit in DATETIME_LABEL_FORMATS) {
30
+ return DATETIME_LABEL_FORMATS[unit];
31
+ }
32
+ }
33
+ return DATETIME_LABEL_FORMATS.day;
34
+ }
@@ -43,7 +43,7 @@ export class HighchartsComponent extends React.PureComponent {
43
43
  };
44
44
  }
45
45
  static getDerivedStateFromProps(nextProps, prevState) {
46
- var _a;
46
+ var _a, _b;
47
47
  const isCurrentTooltipSplitted = get(prevState, 'options.tooltip.splitTooltip');
48
48
  const tooltipTypeWasChanged = isCurrentTooltipSplitted !== nextProps.splitTooltip;
49
49
  if (nextProps.data === prevState.prevData && !tooltipTypeWasChanged) {
@@ -54,14 +54,14 @@ export class HighchartsComponent extends React.PureComponent {
54
54
  const entryId = get(nextProps, 'data.entryId');
55
55
  const configOptions = Object.assign({
56
56
  nonBodyScroll,
57
- drillDownData: nextProps.data.config.drillDown,
57
+ drillDownData: (_a = nextProps.data.config) === null || _a === void 0 ? void 0 : _a.drillDown,
58
58
  splitTooltip: nextProps.splitTooltip,
59
59
  highcharts: libraryConfig,
60
60
  entryId,
61
61
  }, config);
62
62
  const { config: options, callback } = getGraph({
63
63
  options: configOptions,
64
- holidays: (_a = settings.get('extra')) === null || _a === void 0 ? void 0 : _a.holidays,
64
+ holidays: (_b = settings.get('extra')) === null || _b === void 0 ? void 0 : _b.holidays,
65
65
  data,
66
66
  comments,
67
67
  isMobile,
@@ -1,4 +1,5 @@
1
1
  import type { FormatOptions, FormatNumberOptions } from './types';
2
2
  export declare const formatBytes: (value: number, options?: FormatOptions) => string;
3
3
  export declare const formatDuration: (value: number, options?: FormatOptions) => string;
4
+ export declare const getNumberUnitRate: (value: number) => number;
4
5
  export declare const formatNumber: (value: number | string, options?: FormatNumberOptions) => string;