@gravity-ui/charts 1.11.2 → 1.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cjs/components/Axis/AxisY.d.ts +1 -0
  2. package/dist/cjs/components/Axis/AxisY.js +14 -13
  3. package/dist/cjs/components/ChartInner/index.js +3 -3
  4. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +2 -0
  5. package/dist/cjs/components/ChartInner/useChartInnerProps.js +16 -8
  6. package/dist/cjs/components/Legend/index.d.ts +0 -1
  7. package/dist/cjs/components/Legend/index.js +18 -24
  8. package/dist/cjs/hooks/useChartOptions/index.d.ts +6 -2
  9. package/dist/cjs/hooks/useChartOptions/index.js +4 -4
  10. package/dist/cjs/hooks/useSeries/index.d.ts +9 -0
  11. package/dist/cjs/hooks/useSeries/index.js +59 -29
  12. package/dist/cjs/hooks/useSeries/prepare-legend.d.ts +2 -2
  13. package/dist/cjs/hooks/useSeries/prepare-legend.js +9 -7
  14. package/dist/cjs/hooks/useSeries/types.d.ts +2 -0
  15. package/dist/cjs/utils/chart/axis-generators/bottom.js +21 -28
  16. package/dist/esm/components/Axis/AxisY.d.ts +1 -0
  17. package/dist/esm/components/Axis/AxisY.js +14 -13
  18. package/dist/esm/components/ChartInner/index.js +3 -3
  19. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +2 -0
  20. package/dist/esm/components/ChartInner/useChartInnerProps.js +16 -8
  21. package/dist/esm/components/Legend/index.d.ts +0 -1
  22. package/dist/esm/components/Legend/index.js +18 -24
  23. package/dist/esm/hooks/useChartOptions/index.d.ts +6 -2
  24. package/dist/esm/hooks/useChartOptions/index.js +4 -4
  25. package/dist/esm/hooks/useSeries/index.d.ts +9 -0
  26. package/dist/esm/hooks/useSeries/index.js +59 -29
  27. package/dist/esm/hooks/useSeries/prepare-legend.d.ts +2 -2
  28. package/dist/esm/hooks/useSeries/prepare-legend.js +9 -7
  29. package/dist/esm/hooks/useSeries/types.d.ts +2 -0
  30. package/dist/esm/utils/chart/axis-generators/bottom.js +21 -28
  31. package/package.json +1 -1
@@ -10,6 +10,7 @@ type Props = {
10
10
  plotBeforeRef?: React.MutableRefObject<SVGGElement | null>;
11
11
  plotAfterRef?: React.MutableRefObject<SVGGElement | null>;
12
12
  bottomLimit?: number;
13
+ topLimit?: number;
13
14
  };
14
15
  export declare const AxisY: (props: Props) => React.JSX.Element;
15
16
  export {};
@@ -4,11 +4,8 @@ import { block, calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight,
4
4
  import './styles.css';
5
5
  const b = block('axis');
6
6
  function transformLabel(args) {
7
- const { node, axis, isTopOffsetOverload = false } = args;
8
- let topOffset = axis.labels.lineHeight / 2;
9
- if (isTopOffsetOverload) {
10
- topOffset = 0;
11
- }
7
+ const { node, axis, startTopOffset } = args;
8
+ let topOffset = startTopOffset !== null && startTopOffset !== void 0 ? startTopOffset : axis.labels.lineHeight / 2;
12
9
  let leftOffset = axis.labels.margin;
13
10
  if (axis.position === 'left') {
14
11
  leftOffset = leftOffset * -1;
@@ -85,7 +82,7 @@ function getTitlePosition(args) {
85
82
  return { x, y };
86
83
  }
87
84
  export const AxisY = (props) => {
88
- const { axes: allAxes, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, bottomLimit = 0, } = props;
85
+ const { axes: allAxes, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, bottomLimit = 0, topLimit = 0, } = props;
89
86
  const height = getAxisHeight({ split, boundsHeight: totalHeight });
90
87
  const ref = React.useRef(null);
91
88
  const lineGenerator = line();
@@ -140,16 +137,20 @@ export const AxisY = (props) => {
140
137
  .style('transform', function () {
141
138
  return transformLabel({ node: this, axis: d });
142
139
  });
143
- labels.each(function (_d, i) {
144
- if (i === 0) {
145
- const currentElement = this;
146
- const currentElementPosition = currentElement.getBoundingClientRect();
147
- const text = select(currentElement);
148
- if (currentElementPosition.bottom > bottomLimit) {
140
+ labels.each(function (_d, i, nodes) {
141
+ const isFirstNode = i === 0;
142
+ const isLastNode = i === nodes.length - 1;
143
+ if (isFirstNode || isLastNode) {
144
+ const labelNode = this;
145
+ const labelNodeRect = labelNode.getBoundingClientRect();
146
+ const shouldBeTransformed = (isFirstNode && labelNodeRect.bottom > bottomLimit) ||
147
+ (isLastNode && labelNodeRect.top < topLimit);
148
+ if (shouldBeTransformed) {
149
+ const text = select(labelNode);
149
150
  const transform = transformLabel({
150
151
  node: this,
151
152
  axis: d,
152
- isTopOffsetOverload: true,
153
+ startTopOffset: isLastNode ? labelNodeRect.height : 0,
153
154
  });
154
155
  text.style('transform', transform);
155
156
  if (d.labels.rotation) {
@@ -23,7 +23,7 @@ export const ChartInner = (props) => {
23
23
  const plotAfterRef = React.useRef(null);
24
24
  const dispatcher = React.useMemo(() => getDispatcher(), []);
25
25
  const clipPathId = useUniqId();
26
- const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, handleZoomReset, isOutsideBounds, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, svgXPos, svgBottomPos, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher,
26
+ const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, handleZoomReset, isOutsideBounds, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, svgXPos, svgBottomPos, svgTopPos, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher,
27
27
  htmlLayout, svgContainer: svgRef.current, plotNode: plotRef.current, clipPathId }));
28
28
  const { tooltipPinned, togglePinTooltip, unpinTooltip } = useChartInnerState({
29
29
  dispatcher,
@@ -94,13 +94,13 @@ 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, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }),
97
+ React.createElement(AxisY, { bottomLimit: svgBottomPos, topLimit: svgTopPos, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }),
98
98
  xAxis && (React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
99
99
  React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }))))),
100
100
  React.createElement("g", { ref: plotBeforeRef }),
101
101
  shapes,
102
102
  React.createElement("g", { ref: plotAfterRef })),
103
- (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && legendConfig && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayout }))),
103
+ (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && legendConfig && (React.createElement(Legend, { chartSeries: preparedSeries, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayout }))),
104
104
  React.createElement("div", { className: b('html-layer'), ref: setHtmlLayout, style: {
105
105
  '--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
106
106
  } }),
@@ -11,6 +11,7 @@ type Props = ChartInnerProps & {
11
11
  };
12
12
  export declare function useChartInnerProps(props: Props): {
13
13
  svgBottomPos: number | undefined;
14
+ svgTopPos: number | undefined;
14
15
  svgXPos: number | undefined;
15
16
  boundsHeight: number;
16
17
  boundsOffsetLeft: number;
@@ -30,6 +31,7 @@ export declare function useChartInnerProps(props: Props): {
30
31
  end: number;
31
32
  }[];
32
33
  } | undefined;
34
+ maxWidth: number;
33
35
  } | undefined;
34
36
  legendItems: never[] | import("../../hooks").LegendItem[][];
35
37
  preparedLegend: import("../../hooks").PreparedLegend | null;
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapes, useSplit, } from '../../hooks';
2
+ import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapeSeries, useShapes, useSplit, } from '../../hooks';
3
3
  import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
4
  import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
5
5
  import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
6
6
  import { getLegendComponents } from '../../hooks/useSeries/prepare-legend';
7
7
  import { getPreparedOptions } from '../../hooks/useSeries/prepare-options';
8
+ import { getActiveLegendItems } from '../../hooks/useSeries/utils';
8
9
  import { useZoom } from '../../hooks/useZoom';
9
10
  import { getSortedSeriesData, getZoomedSeriesData } from '../../utils';
10
11
  import { hasAtLeastOneSeriesDataPerPlot } from './utils';
@@ -13,8 +14,14 @@ export function useChartInnerProps(props) {
13
14
  const { width, height, data, dispatcher, htmlLayout, svgContainer, plotNode, clipPathId } = props;
14
15
  const prevWidth = usePrevious(width);
15
16
  const prevHeight = usePrevious(height);
17
+ const { chart, title, tooltip, colors } = useChartOptions({
18
+ seriesData: data.series.data,
19
+ chart: data.chart,
20
+ colors: data.colors,
21
+ title: data.title,
22
+ tooltip: data.tooltip,
23
+ });
16
24
  const [zoomState, setZoomState] = React.useState({});
17
- const { chart, title, tooltip, colors } = useChartOptions({ data });
18
25
  const sortedSeriesData = React.useMemo(() => {
19
26
  return getSortedSeriesData(data.series.data);
20
27
  }, [data.series.data]);
@@ -46,12 +53,13 @@ export function useChartInnerProps(props) {
46
53
  seriesData: zoomedSeriesData,
47
54
  seriesOptions: data.series.options,
48
55
  });
49
- const { preparedSeries: preparedShapesSeries } = useSeries({
56
+ const activeLegendItems = React.useMemo(() => getActiveLegendItems(preparedSeries), [preparedSeries]);
57
+ const { preparedSeries: preparedShapesSeries } = useShapeSeries({
50
58
  colors,
51
- legend: data.legend,
52
- originalSeriesData: data.series.data,
53
59
  seriesData: zoomedShapesSeriesData,
54
60
  seriesOptions: data.series.options,
61
+ activeLegendItems,
62
+ preparedLegend,
55
63
  });
56
64
  const { legendConfig, legendItems } = React.useMemo(() => {
57
65
  if (!preparedLegend) {
@@ -63,9 +71,8 @@ export function useChartInnerProps(props) {
63
71
  chartMargin: chart.margin,
64
72
  series: preparedSeries,
65
73
  preparedLegend,
66
- preparedYAxis: yAxis,
67
74
  });
68
- }, [width, height, chart.margin, preparedSeries, preparedLegend, yAxis]);
75
+ }, [width, height, chart.margin, preparedSeries, preparedLegend]);
69
76
  const { boundsWidth, boundsHeight } = useChartDimensions({
70
77
  width,
71
78
  height,
@@ -141,12 +148,13 @@ export function useChartInnerProps(props) {
141
148
  }
142
149
  return acc;
143
150
  }, 0);
144
- const { x, bottom } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
151
+ const { bottom, top, x } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
145
152
  const handleZoomReset = React.useCallback(() => {
146
153
  setZoomState({});
147
154
  }, []);
148
155
  return {
149
156
  svgBottomPos: bottom,
157
+ svgTopPos: top,
150
158
  svgXPos: x,
151
159
  boundsHeight,
152
160
  boundsOffsetLeft,
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import type { LegendConfig, LegendItem, OnLegendItemClick, PreparedLegend, PreparedSeries } from '../../hooks';
3
3
  import './styles.css';
4
4
  type Props = {
5
- boundsWidth: number;
6
5
  chartSeries: PreparedSeries[];
7
6
  legend: PreparedLegend;
8
7
  items: LegendItem[][];
@@ -2,20 +2,20 @@ import React from 'react';
2
2
  import { line as lineGenerator, scaleLinear, select, symbol } from 'd3';
3
3
  import { CONTINUOUS_LEGEND_SIZE } from '../../constants';
4
4
  import { formatNumber } from '../../libs';
5
- import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, handleOverflowingText, } from '../../utils';
5
+ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
7
  import './styles.css';
8
8
  const b = block('legend');
9
9
  const getLegendPosition = (args) => {
10
- const { align, offsetWidth = 0, width, contentWidth } = args;
10
+ const { align, offsetLeft = 0, width, contentWidth } = args;
11
11
  const top = 0;
12
12
  if (align === 'left') {
13
- return { top, left: offsetWidth };
13
+ return { top, left: offsetLeft };
14
14
  }
15
15
  if (align === 'right') {
16
- return { top, left: offsetWidth + width - contentWidth };
16
+ return { top, left: offsetLeft + width - contentWidth };
17
17
  }
18
- return { top, left: offsetWidth + width / 2 - contentWidth / 2 };
18
+ return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
19
19
  };
20
20
  const appendPaginator = (args) => {
21
21
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -135,12 +135,12 @@ function renderLegendSymbol(args) {
135
135
  });
136
136
  }
137
137
  export const Legend = (props) => {
138
- const { boundsWidth, chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
138
+ const { chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
139
139
  const ref = React.useRef(null);
140
140
  const [pageIndex, setPageIndex] = React.useState(0);
141
141
  React.useEffect(() => {
142
142
  setPageIndex(0);
143
- }, [boundsWidth]);
143
+ }, [config.maxWidth]);
144
144
  React.useEffect(() => {
145
145
  async function prepareLegend() {
146
146
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -250,12 +250,12 @@ export const Legend = (props) => {
250
250
  case 'center': {
251
251
  const legendLinePostion = getLegendPosition({
252
252
  align: legend.align,
253
- width: boundsWidth,
254
- offsetWidth: 0,
253
+ width: config.maxWidth,
255
254
  contentWidth,
255
+ offsetLeft: config.offset.left,
256
256
  });
257
257
  left = legendLinePostion.left;
258
- legendWidth = boundsWidth;
258
+ legendWidth = config.maxWidth;
259
259
  break;
260
260
  }
261
261
  case 'start': {
@@ -305,6 +305,7 @@ export const Legend = (props) => {
305
305
  maxTickCount: 4,
306
306
  tickColor: '#fff',
307
307
  labelFormat: (value) => formatNumber(value, { unit: 'auto' }),
308
+ labelsStyle: legend.ticks.style,
308
309
  },
309
310
  domain: {
310
311
  size: legend.width,
@@ -360,9 +361,9 @@ export const Legend = (props) => {
360
361
  }
361
362
  const { left } = getLegendPosition({
362
363
  align: legend.align,
363
- width: boundsWidth,
364
- offsetWidth: config.offset.left,
364
+ width: config.maxWidth,
365
365
  contentWidth: legendWidth,
366
+ offsetLeft: config.offset.left,
366
367
  });
367
368
  svgElement
368
369
  .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
@@ -370,16 +371,9 @@ export const Legend = (props) => {
370
371
  htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
371
372
  }
372
373
  prepareLegend();
373
- }, [
374
- boundsWidth,
375
- chartSeries,
376
- onItemClick,
377
- onUpdate,
378
- legend,
379
- items,
380
- config,
381
- pageIndex,
382
- htmlLayout,
383
- ]);
384
- return React.createElement("g", { className: b(), ref: ref, width: boundsWidth, height: legend.height });
374
+ }, [chartSeries, onItemClick, onUpdate, legend, items, config, pageIndex, htmlLayout]);
375
+ // due to asynchronous processing, we only need to work with the actual element
376
+ // eslint-disable-next-line react-hooks/exhaustive-deps
377
+ const key = React.useMemo(() => getUniqId(), [legend, config]);
378
+ return React.createElement("g", { key: key, className: b(), ref: ref, width: config.maxWidth, height: legend.height });
385
379
  };
@@ -1,7 +1,11 @@
1
- import type { ChartData } from '../../types';
1
+ import type { ChartSeries, ChartTitle, ChartTooltip, ChartOptions as GeneralChartOptions } from '../../types';
2
2
  import type { ChartOptions } from './types';
3
3
  type Args = {
4
- data: ChartData;
4
+ seriesData: ChartSeries[];
5
+ chart?: GeneralChartOptions;
6
+ colors?: string[];
7
+ title?: ChartTitle;
8
+ tooltip?: ChartTooltip;
5
9
  };
6
10
  export declare const useChartOptions: (args: Args) => ChartOptions;
7
11
  export {};
@@ -4,21 +4,21 @@ import { getPreparedChart } from './chart';
4
4
  import { getPreparedTitle } from './title';
5
5
  import { getPreparedTooltip } from './tooltip';
6
6
  export const useChartOptions = (args) => {
7
- const { data: { chart, title, tooltip, colors, series }, } = args;
7
+ const { chart, colors, seriesData, title, tooltip } = args;
8
8
  const options = React.useMemo(() => {
9
9
  const preparedTitle = getPreparedTitle({ title });
10
10
  const preparedTooltip = getPreparedTooltip({ tooltip });
11
11
  const preparedChart = getPreparedChart({
12
12
  chart,
13
13
  preparedTitle,
14
- seriesData: series.data,
14
+ seriesData,
15
15
  });
16
16
  return {
17
+ colors: colors !== null && colors !== void 0 ? colors : DEFAULT_PALETTE,
17
18
  chart: preparedChart,
18
19
  title: preparedTitle,
19
20
  tooltip: preparedTooltip,
20
- colors: colors !== null && colors !== void 0 ? colors : DEFAULT_PALETTE,
21
21
  };
22
- }, [chart, colors, title, tooltip, series.data]);
22
+ }, [chart, colors, seriesData, title, tooltip]);
23
23
  return options;
24
24
  };
@@ -13,4 +13,13 @@ export declare const useSeries: (args: Args) => {
13
13
  preparedSeries: PreparedSeries[];
14
14
  handleLegendItemClick: OnLegendItemClick;
15
15
  };
16
+ export declare const useShapeSeries: ({ seriesData, seriesOptions, colors, preparedLegend, activeLegendItems, }: {
17
+ colors: string[];
18
+ seriesData: ChartData["series"]["data"];
19
+ seriesOptions: ChartData["series"]["options"];
20
+ activeLegendItems: string[];
21
+ preparedLegend?: PreparedLegend | null;
22
+ }) => {
23
+ preparedSeries: PreparedSeries[];
24
+ };
16
25
  export {};
@@ -5,6 +5,38 @@ import { usePrevious } from '../usePrevious';
5
5
  import { getPreparedLegend } from './prepare-legend';
6
6
  import { prepareSeries } from './prepareSeries';
7
7
  import { getActiveLegendItems, getAllLegendItems } from './utils';
8
+ const useVisibleSeries = ({ preparedSeries, activeLegendItems, }) => {
9
+ return React.useMemo(() => {
10
+ return preparedSeries.map((singleSeries) => {
11
+ if (singleSeries.legend.enabled) {
12
+ return Object.assign(Object.assign({}, singleSeries), { visible: activeLegendItems.includes(singleSeries.name) });
13
+ }
14
+ return singleSeries;
15
+ });
16
+ }, [preparedSeries, activeLegendItems]);
17
+ };
18
+ const getPreparedSeries = async ({ seriesData, seriesOptions, colors, preparedLegend, }) => {
19
+ const seriesNames = getSeriesNames(seriesData);
20
+ const colorScale = scaleOrdinal(seriesNames, colors);
21
+ const groupedSeries = group(seriesData, (item) => item.type);
22
+ const acc = [];
23
+ if (!preparedLegend) {
24
+ return acc;
25
+ }
26
+ const list = Array.from(groupedSeries);
27
+ for (let i = 0; i < list.length; i++) {
28
+ const [seriesType, seriesList] = list[i];
29
+ acc.push(...(await prepareSeries({
30
+ type: seriesType,
31
+ series: seriesList,
32
+ seriesOptions,
33
+ legend: preparedLegend,
34
+ colorScale,
35
+ colors,
36
+ })));
37
+ }
38
+ return acc;
39
+ };
8
40
  export const useSeries = (args) => {
9
41
  const { legend, originalSeriesData, seriesData, seriesOptions, colors, preparedLegend: preparedLegendProps = null, } = args;
10
42
  const [preparedLegend, setPreparedLegend] = React.useState(preparedLegendProps);
@@ -17,38 +49,18 @@ export const useSeries = (args) => {
17
49
  const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(preparedSeries));
18
50
  React.useEffect(() => {
19
51
  (async () => {
20
- const seriesNames = getSeriesNames(seriesData);
21
- const colorScale = scaleOrdinal(seriesNames, colors);
22
- const groupedSeries = group(seriesData, (item) => item.type);
23
- const acc = [];
24
- if (!preparedLegend) {
25
- return;
26
- }
27
- const list = Array.from(groupedSeries);
28
- for (let i = 0; i < list.length; i++) {
29
- const [seriesType, seriesList] = list[i];
30
- acc.push(...(await prepareSeries({
31
- type: seriesType,
32
- series: seriesList,
33
- seriesOptions,
34
- legend: preparedLegend,
35
- colorScale,
36
- colors,
37
- })));
38
- }
39
- setPreparedSeries(acc);
40
- setActiveLegendItems(getActiveLegendItems(acc));
52
+ const items = await getPreparedSeries({
53
+ seriesData,
54
+ seriesOptions,
55
+ preparedLegend,
56
+ colors,
57
+ });
58
+ setPreparedSeries(items);
59
+ setActiveLegendItems(getActiveLegendItems(items));
41
60
  })();
42
61
  }, [seriesData, seriesOptions, preparedLegend, colors]);
43
62
  const prevOriginalSeriesData = usePrevious(originalSeriesData);
44
- const chartSeries = React.useMemo(() => {
45
- return preparedSeries.map((singleSeries) => {
46
- if (singleSeries.legend.enabled) {
47
- return Object.assign(Object.assign({}, singleSeries), { visible: activeLegendItems.includes(singleSeries.name) });
48
- }
49
- return singleSeries;
50
- });
51
- }, [preparedSeries, activeLegendItems]);
63
+ const chartSeries = useVisibleSeries({ preparedSeries, activeLegendItems });
52
64
  const handleLegendItemClick = React.useCallback(({ name, metaKey }) => {
53
65
  const allItems = getAllLegendItems(preparedSeries);
54
66
  const onlyItemSelected = activeLegendItems.length === 1 && activeLegendItems.includes(name);
@@ -81,3 +93,21 @@ export const useSeries = (args) => {
81
93
  handleLegendItemClick,
82
94
  };
83
95
  };
96
+ export const useShapeSeries = ({ seriesData, seriesOptions, colors, preparedLegend, activeLegendItems, }) => {
97
+ const [preparedSeries, setPreparedSeries] = React.useState([]);
98
+ React.useEffect(() => {
99
+ (async () => {
100
+ const items = await getPreparedSeries({
101
+ seriesData,
102
+ seriesOptions,
103
+ preparedLegend,
104
+ colors,
105
+ });
106
+ setPreparedSeries(items);
107
+ })();
108
+ }, [seriesData, seriesOptions, preparedLegend, colors]);
109
+ const chartSeries = useVisibleSeries({ preparedSeries, activeLegendItems });
110
+ return {
111
+ preparedSeries: chartSeries,
112
+ };
113
+ };
@@ -1,5 +1,5 @@
1
1
  import type { ChartData } from '../../types';
2
- import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
2
+ import type { PreparedChart } from '../useChartOptions/types';
3
3
  import type { LegendItem, PreparedLegend, PreparedSeries } from './types';
4
4
  export declare function getPreparedLegend(args: {
5
5
  legend: ChartData['legend'];
@@ -11,7 +11,6 @@ export declare function getLegendComponents(args: {
11
11
  chartMargin: PreparedChart['margin'];
12
12
  series: PreparedSeries[];
13
13
  preparedLegend: PreparedLegend;
14
- preparedYAxis: PreparedAxis[];
15
14
  }): {
16
15
  legendConfig: {
17
16
  offset: {
@@ -24,6 +23,7 @@ export declare function getLegendComponents(args: {
24
23
  end: number;
25
24
  }[];
26
25
  } | undefined;
26
+ maxWidth: number;
27
27
  };
28
28
  legendItems: LegendItem[][];
29
29
  };
@@ -4,8 +4,6 @@ import get from 'lodash/get';
4
4
  import merge from 'lodash/merge';
5
5
  import { CONTINUOUS_LEGEND_SIZE, legendDefaults } from '../../constants';
6
6
  import { getDefaultColorStops, getDomainForContinuousColorScale, getLabelsSize } from '../../utils';
7
- import { getBoundsWidth } from '../useChartDimensions';
8
- import { getYAxisWidth } from '../useChartDimensions/utils';
9
7
  export async function getPreparedLegend(args) {
10
8
  var _a, _b, _c, _d, _e, _f, _g;
11
9
  const { legend, series } = args;
@@ -21,9 +19,13 @@ export async function getPreparedLegend(args) {
21
19
  const titleText = isTitleEnabled ? get(legend, 'title.text', '') : '';
22
20
  const titleSize = await getLabelsSize({ labels: [titleText], style: titleStyle });
23
21
  const titleHeight = isTitleEnabled ? titleSize.maxHeight : 0;
22
+ const tickStyle = {
23
+ fontSize: '12px',
24
+ };
24
25
  const ticks = {
25
26
  labelsMargin: 4,
26
- labelsLineHeight: 12,
27
+ labelsLineHeight: (await getLabelsSize({ labels: ['Tmp'], style: tickStyle })).maxHeight,
28
+ style: tickStyle,
27
29
  };
28
30
  const colorScale = {
29
31
  colors: [],
@@ -164,8 +166,8 @@ function getPagination(args) {
164
166
  return { pages };
165
167
  }
166
168
  export function getLegendComponents(args) {
167
- const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
168
- const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
169
+ const { chartWidth, chartHeight, chartMargin, series, preparedLegend } = args;
170
+ const maxLegendWidth = chartWidth - chartMargin.right - chartMargin.left;
169
171
  const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
170
172
  const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
171
173
  const items = getGroupedLegendItems({
@@ -195,8 +197,8 @@ export function getLegendComponents(args) {
195
197
  }
196
198
  const top = chartHeight - chartMargin.bottom - preparedLegend.height;
197
199
  const offset = {
198
- left: chartMargin.left + getYAxisWidth(preparedYAxis[0]),
200
+ left: chartMargin.left,
199
201
  top,
200
202
  };
201
- return { legendConfig: { offset, pagination }, legendItems: items };
203
+ return { legendConfig: { offset, pagination, maxWidth: maxLegendWidth }, legendItems: items };
202
204
  }
@@ -26,6 +26,7 @@ export type PreparedLegend = Required<Omit<ChartLegend, 'title' | 'colorScale'>>
26
26
  ticks: {
27
27
  labelsMargin: number;
28
28
  labelsLineHeight: number;
29
+ style: BaseTextStyle;
29
30
  };
30
31
  colorScale: {
31
32
  colors: string[];
@@ -52,6 +53,7 @@ export type LegendConfig = {
52
53
  left: number;
53
54
  top: number;
54
55
  };
56
+ maxWidth: number;
55
57
  pagination?: {
56
58
  pages: {
57
59
  start: number;
@@ -90,27 +90,15 @@ export async function axisBottom(args) {
90
90
  });
91
91
  }
92
92
  else {
93
- // remove overlapping labels
94
93
  let elementX = 0;
95
- selection
96
- .selectAll('.tick')
97
- .filter(function () {
98
- const node = this;
99
- const r = node.getBoundingClientRect();
100
- if (r.left < elementX) {
101
- return true;
102
- }
103
- elementX = r.right + labelsPaddings;
104
- return false;
105
- })
106
- .remove();
107
94
  // add an ellipsis to the labels that go beyond the boundaries of the chart
95
+ // and remove overlapping labels
108
96
  labels.each(function (_d, i, nodes) {
109
- var _a;
97
+ var _a, _b;
98
+ const currentElement = this;
99
+ const currentElementPosition = currentElement.getBoundingClientRect();
110
100
  if (i === 0) {
111
- const currentElement = this;
112
101
  const text = select(currentElement);
113
- const currentElementPosition = currentElement.getBoundingClientRect();
114
102
  const nextElement = nodes[i + 1];
115
103
  const nextElementPosition = nextElement === null || nextElement === void 0 ? void 0 : nextElement.getBoundingClientRect();
116
104
  if (currentElementPosition.left < leftmostLimit) {
@@ -123,18 +111,23 @@ export async function axisBottom(args) {
123
111
  setEllipsisForOverflowText(text, remainSpace);
124
112
  }
125
113
  }
126
- if (i === nodes.length - 1) {
127
- const currentElement = this;
128
- const prevElement = nodes[i - 1];
129
- const text = select(currentElement);
130
- const currentElementPosition = currentElement.getBoundingClientRect();
131
- const prevElementPosition = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getBoundingClientRect();
132
- const lackingSpace = Math.max(0, currentElementPosition.right - right);
133
- if (lackingSpace) {
134
- const remainSpace = right - ((prevElementPosition === null || prevElementPosition === void 0 ? void 0 : prevElementPosition.right) || 0) - labelsPaddings;
135
- const translateX = -lackingSpace;
136
- text.style('transform', `translate(${translateX}px,${translateY}px)`);
137
- setEllipsisForOverflowText(text, remainSpace);
114
+ else {
115
+ if (currentElementPosition.left < elementX) {
116
+ (_b = currentElement.closest('.tick')) === null || _b === void 0 ? void 0 : _b.remove();
117
+ return;
118
+ }
119
+ elementX = currentElementPosition.right + labelsPaddings;
120
+ if (i === nodes.length - 1) {
121
+ const prevElement = nodes[i - 1];
122
+ const text = select(currentElement);
123
+ const prevElementPosition = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getBoundingClientRect();
124
+ const lackingSpace = Math.max(0, currentElementPosition.right - right);
125
+ if (lackingSpace) {
126
+ const remainSpace = right - ((prevElementPosition === null || prevElementPosition === void 0 ? void 0 : prevElementPosition.right) || 0) - labelsPaddings;
127
+ const translateX = -lackingSpace;
128
+ text.style('transform', `translate(${translateX}px,${translateY}px)`);
129
+ setEllipsisForOverflowText(text, remainSpace);
130
+ }
138
131
  }
139
132
  }
140
133
  });
@@ -10,6 +10,7 @@ type Props = {
10
10
  plotBeforeRef?: React.MutableRefObject<SVGGElement | null>;
11
11
  plotAfterRef?: React.MutableRefObject<SVGGElement | null>;
12
12
  bottomLimit?: number;
13
+ topLimit?: number;
13
14
  };
14
15
  export declare const AxisY: (props: Props) => React.JSX.Element;
15
16
  export {};
@@ -4,11 +4,8 @@ import { block, calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight,
4
4
  import './styles.css';
5
5
  const b = block('axis');
6
6
  function transformLabel(args) {
7
- const { node, axis, isTopOffsetOverload = false } = args;
8
- let topOffset = axis.labels.lineHeight / 2;
9
- if (isTopOffsetOverload) {
10
- topOffset = 0;
11
- }
7
+ const { node, axis, startTopOffset } = args;
8
+ let topOffset = startTopOffset !== null && startTopOffset !== void 0 ? startTopOffset : axis.labels.lineHeight / 2;
12
9
  let leftOffset = axis.labels.margin;
13
10
  if (axis.position === 'left') {
14
11
  leftOffset = leftOffset * -1;
@@ -85,7 +82,7 @@ function getTitlePosition(args) {
85
82
  return { x, y };
86
83
  }
87
84
  export const AxisY = (props) => {
88
- const { axes: allAxes, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, bottomLimit = 0, } = props;
85
+ const { axes: allAxes, width, height: totalHeight, scale, split, plotBeforeRef, plotAfterRef, bottomLimit = 0, topLimit = 0, } = props;
89
86
  const height = getAxisHeight({ split, boundsHeight: totalHeight });
90
87
  const ref = React.useRef(null);
91
88
  const lineGenerator = line();
@@ -140,16 +137,20 @@ export const AxisY = (props) => {
140
137
  .style('transform', function () {
141
138
  return transformLabel({ node: this, axis: d });
142
139
  });
143
- labels.each(function (_d, i) {
144
- if (i === 0) {
145
- const currentElement = this;
146
- const currentElementPosition = currentElement.getBoundingClientRect();
147
- const text = select(currentElement);
148
- if (currentElementPosition.bottom > bottomLimit) {
140
+ labels.each(function (_d, i, nodes) {
141
+ const isFirstNode = i === 0;
142
+ const isLastNode = i === nodes.length - 1;
143
+ if (isFirstNode || isLastNode) {
144
+ const labelNode = this;
145
+ const labelNodeRect = labelNode.getBoundingClientRect();
146
+ const shouldBeTransformed = (isFirstNode && labelNodeRect.bottom > bottomLimit) ||
147
+ (isLastNode && labelNodeRect.top < topLimit);
148
+ if (shouldBeTransformed) {
149
+ const text = select(labelNode);
149
150
  const transform = transformLabel({
150
151
  node: this,
151
152
  axis: d,
152
- isTopOffsetOverload: true,
153
+ startTopOffset: isLastNode ? labelNodeRect.height : 0,
153
154
  });
154
155
  text.style('transform', transform);
155
156
  if (d.labels.rotation) {
@@ -23,7 +23,7 @@ export const ChartInner = (props) => {
23
23
  const plotAfterRef = React.useRef(null);
24
24
  const dispatcher = React.useMemo(() => getDispatcher(), []);
25
25
  const clipPathId = useUniqId();
26
- const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, handleZoomReset, isOutsideBounds, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, svgXPos, svgBottomPos, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher,
26
+ const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, handleZoomReset, isOutsideBounds, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, svgXPos, svgBottomPos, svgTopPos, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher,
27
27
  htmlLayout, svgContainer: svgRef.current, plotNode: plotRef.current, clipPathId }));
28
28
  const { tooltipPinned, togglePinTooltip, unpinTooltip } = useChartInnerState({
29
29
  dispatcher,
@@ -94,13 +94,13 @@ 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, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }),
97
+ React.createElement(AxisY, { bottomLimit: svgBottomPos, topLimit: svgTopPos, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }),
98
98
  xAxis && (React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
99
99
  React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotBeforeRef: plotBeforeRef, plotAfterRef: plotAfterRef }))))),
100
100
  React.createElement("g", { ref: plotBeforeRef }),
101
101
  shapes,
102
102
  React.createElement("g", { ref: plotAfterRef })),
103
- (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && legendConfig && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayout }))),
103
+ (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && legendConfig && (React.createElement(Legend, { chartSeries: preparedSeries, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayout }))),
104
104
  React.createElement("div", { className: b('html-layer'), ref: setHtmlLayout, style: {
105
105
  '--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
106
106
  } }),
@@ -11,6 +11,7 @@ type Props = ChartInnerProps & {
11
11
  };
12
12
  export declare function useChartInnerProps(props: Props): {
13
13
  svgBottomPos: number | undefined;
14
+ svgTopPos: number | undefined;
14
15
  svgXPos: number | undefined;
15
16
  boundsHeight: number;
16
17
  boundsOffsetLeft: number;
@@ -30,6 +31,7 @@ export declare function useChartInnerProps(props: Props): {
30
31
  end: number;
31
32
  }[];
32
33
  } | undefined;
34
+ maxWidth: number;
33
35
  } | undefined;
34
36
  legendItems: never[] | import("../../hooks").LegendItem[][];
35
37
  preparedLegend: import("../../hooks").PreparedLegend | null;
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapes, useSplit, } from '../../hooks';
2
+ import { useAxisScales, useChartDimensions, useChartOptions, usePrevious, useSeries, useShapeSeries, useShapes, useSplit, } from '../../hooks';
3
3
  import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
4
  import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
5
5
  import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
6
6
  import { getLegendComponents } from '../../hooks/useSeries/prepare-legend';
7
7
  import { getPreparedOptions } from '../../hooks/useSeries/prepare-options';
8
+ import { getActiveLegendItems } from '../../hooks/useSeries/utils';
8
9
  import { useZoom } from '../../hooks/useZoom';
9
10
  import { getSortedSeriesData, getZoomedSeriesData } from '../../utils';
10
11
  import { hasAtLeastOneSeriesDataPerPlot } from './utils';
@@ -13,8 +14,14 @@ export function useChartInnerProps(props) {
13
14
  const { width, height, data, dispatcher, htmlLayout, svgContainer, plotNode, clipPathId } = props;
14
15
  const prevWidth = usePrevious(width);
15
16
  const prevHeight = usePrevious(height);
17
+ const { chart, title, tooltip, colors } = useChartOptions({
18
+ seriesData: data.series.data,
19
+ chart: data.chart,
20
+ colors: data.colors,
21
+ title: data.title,
22
+ tooltip: data.tooltip,
23
+ });
16
24
  const [zoomState, setZoomState] = React.useState({});
17
- const { chart, title, tooltip, colors } = useChartOptions({ data });
18
25
  const sortedSeriesData = React.useMemo(() => {
19
26
  return getSortedSeriesData(data.series.data);
20
27
  }, [data.series.data]);
@@ -46,12 +53,13 @@ export function useChartInnerProps(props) {
46
53
  seriesData: zoomedSeriesData,
47
54
  seriesOptions: data.series.options,
48
55
  });
49
- const { preparedSeries: preparedShapesSeries } = useSeries({
56
+ const activeLegendItems = React.useMemo(() => getActiveLegendItems(preparedSeries), [preparedSeries]);
57
+ const { preparedSeries: preparedShapesSeries } = useShapeSeries({
50
58
  colors,
51
- legend: data.legend,
52
- originalSeriesData: data.series.data,
53
59
  seriesData: zoomedShapesSeriesData,
54
60
  seriesOptions: data.series.options,
61
+ activeLegendItems,
62
+ preparedLegend,
55
63
  });
56
64
  const { legendConfig, legendItems } = React.useMemo(() => {
57
65
  if (!preparedLegend) {
@@ -63,9 +71,8 @@ export function useChartInnerProps(props) {
63
71
  chartMargin: chart.margin,
64
72
  series: preparedSeries,
65
73
  preparedLegend,
66
- preparedYAxis: yAxis,
67
74
  });
68
- }, [width, height, chart.margin, preparedSeries, preparedLegend, yAxis]);
75
+ }, [width, height, chart.margin, preparedSeries, preparedLegend]);
69
76
  const { boundsWidth, boundsHeight } = useChartDimensions({
70
77
  width,
71
78
  height,
@@ -141,12 +148,13 @@ export function useChartInnerProps(props) {
141
148
  }
142
149
  return acc;
143
150
  }, 0);
144
- const { x, bottom } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
151
+ const { bottom, top, x } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
145
152
  const handleZoomReset = React.useCallback(() => {
146
153
  setZoomState({});
147
154
  }, []);
148
155
  return {
149
156
  svgBottomPos: bottom,
157
+ svgTopPos: top,
150
158
  svgXPos: x,
151
159
  boundsHeight,
152
160
  boundsOffsetLeft,
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import type { LegendConfig, LegendItem, OnLegendItemClick, PreparedLegend, PreparedSeries } from '../../hooks';
3
3
  import './styles.css';
4
4
  type Props = {
5
- boundsWidth: number;
6
5
  chartSeries: PreparedSeries[];
7
6
  legend: PreparedLegend;
8
7
  items: LegendItem[][];
@@ -2,20 +2,20 @@ import React from 'react';
2
2
  import { line as lineGenerator, scaleLinear, select, symbol } from 'd3';
3
3
  import { CONTINUOUS_LEGEND_SIZE } from '../../constants';
4
4
  import { formatNumber } from '../../libs';
5
- import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, handleOverflowingText, } from '../../utils';
5
+ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
7
  import './styles.css';
8
8
  const b = block('legend');
9
9
  const getLegendPosition = (args) => {
10
- const { align, offsetWidth = 0, width, contentWidth } = args;
10
+ const { align, offsetLeft = 0, width, contentWidth } = args;
11
11
  const top = 0;
12
12
  if (align === 'left') {
13
- return { top, left: offsetWidth };
13
+ return { top, left: offsetLeft };
14
14
  }
15
15
  if (align === 'right') {
16
- return { top, left: offsetWidth + width - contentWidth };
16
+ return { top, left: offsetLeft + width - contentWidth };
17
17
  }
18
- return { top, left: offsetWidth + width / 2 - contentWidth / 2 };
18
+ return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
19
19
  };
20
20
  const appendPaginator = (args) => {
21
21
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -135,12 +135,12 @@ function renderLegendSymbol(args) {
135
135
  });
136
136
  }
137
137
  export const Legend = (props) => {
138
- const { boundsWidth, chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
138
+ const { chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
139
139
  const ref = React.useRef(null);
140
140
  const [pageIndex, setPageIndex] = React.useState(0);
141
141
  React.useEffect(() => {
142
142
  setPageIndex(0);
143
- }, [boundsWidth]);
143
+ }, [config.maxWidth]);
144
144
  React.useEffect(() => {
145
145
  async function prepareLegend() {
146
146
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -250,12 +250,12 @@ export const Legend = (props) => {
250
250
  case 'center': {
251
251
  const legendLinePostion = getLegendPosition({
252
252
  align: legend.align,
253
- width: boundsWidth,
254
- offsetWidth: 0,
253
+ width: config.maxWidth,
255
254
  contentWidth,
255
+ offsetLeft: config.offset.left,
256
256
  });
257
257
  left = legendLinePostion.left;
258
- legendWidth = boundsWidth;
258
+ legendWidth = config.maxWidth;
259
259
  break;
260
260
  }
261
261
  case 'start': {
@@ -305,6 +305,7 @@ export const Legend = (props) => {
305
305
  maxTickCount: 4,
306
306
  tickColor: '#fff',
307
307
  labelFormat: (value) => formatNumber(value, { unit: 'auto' }),
308
+ labelsStyle: legend.ticks.style,
308
309
  },
309
310
  domain: {
310
311
  size: legend.width,
@@ -360,9 +361,9 @@ export const Legend = (props) => {
360
361
  }
361
362
  const { left } = getLegendPosition({
362
363
  align: legend.align,
363
- width: boundsWidth,
364
- offsetWidth: config.offset.left,
364
+ width: config.maxWidth,
365
365
  contentWidth: legendWidth,
366
+ offsetLeft: config.offset.left,
366
367
  });
367
368
  svgElement
368
369
  .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
@@ -370,16 +371,9 @@ export const Legend = (props) => {
370
371
  htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
371
372
  }
372
373
  prepareLegend();
373
- }, [
374
- boundsWidth,
375
- chartSeries,
376
- onItemClick,
377
- onUpdate,
378
- legend,
379
- items,
380
- config,
381
- pageIndex,
382
- htmlLayout,
383
- ]);
384
- return React.createElement("g", { className: b(), ref: ref, width: boundsWidth, height: legend.height });
374
+ }, [chartSeries, onItemClick, onUpdate, legend, items, config, pageIndex, htmlLayout]);
375
+ // due to asynchronous processing, we only need to work with the actual element
376
+ // eslint-disable-next-line react-hooks/exhaustive-deps
377
+ const key = React.useMemo(() => getUniqId(), [legend, config]);
378
+ return React.createElement("g", { key: key, className: b(), ref: ref, width: config.maxWidth, height: legend.height });
385
379
  };
@@ -1,7 +1,11 @@
1
- import type { ChartData } from '../../types';
1
+ import type { ChartSeries, ChartTitle, ChartTooltip, ChartOptions as GeneralChartOptions } from '../../types';
2
2
  import type { ChartOptions } from './types';
3
3
  type Args = {
4
- data: ChartData;
4
+ seriesData: ChartSeries[];
5
+ chart?: GeneralChartOptions;
6
+ colors?: string[];
7
+ title?: ChartTitle;
8
+ tooltip?: ChartTooltip;
5
9
  };
6
10
  export declare const useChartOptions: (args: Args) => ChartOptions;
7
11
  export {};
@@ -4,21 +4,21 @@ import { getPreparedChart } from './chart';
4
4
  import { getPreparedTitle } from './title';
5
5
  import { getPreparedTooltip } from './tooltip';
6
6
  export const useChartOptions = (args) => {
7
- const { data: { chart, title, tooltip, colors, series }, } = args;
7
+ const { chart, colors, seriesData, title, tooltip } = args;
8
8
  const options = React.useMemo(() => {
9
9
  const preparedTitle = getPreparedTitle({ title });
10
10
  const preparedTooltip = getPreparedTooltip({ tooltip });
11
11
  const preparedChart = getPreparedChart({
12
12
  chart,
13
13
  preparedTitle,
14
- seriesData: series.data,
14
+ seriesData,
15
15
  });
16
16
  return {
17
+ colors: colors !== null && colors !== void 0 ? colors : DEFAULT_PALETTE,
17
18
  chart: preparedChart,
18
19
  title: preparedTitle,
19
20
  tooltip: preparedTooltip,
20
- colors: colors !== null && colors !== void 0 ? colors : DEFAULT_PALETTE,
21
21
  };
22
- }, [chart, colors, title, tooltip, series.data]);
22
+ }, [chart, colors, seriesData, title, tooltip]);
23
23
  return options;
24
24
  };
@@ -13,4 +13,13 @@ export declare const useSeries: (args: Args) => {
13
13
  preparedSeries: PreparedSeries[];
14
14
  handleLegendItemClick: OnLegendItemClick;
15
15
  };
16
+ export declare const useShapeSeries: ({ seriesData, seriesOptions, colors, preparedLegend, activeLegendItems, }: {
17
+ colors: string[];
18
+ seriesData: ChartData["series"]["data"];
19
+ seriesOptions: ChartData["series"]["options"];
20
+ activeLegendItems: string[];
21
+ preparedLegend?: PreparedLegend | null;
22
+ }) => {
23
+ preparedSeries: PreparedSeries[];
24
+ };
16
25
  export {};
@@ -5,6 +5,38 @@ import { usePrevious } from '../usePrevious';
5
5
  import { getPreparedLegend } from './prepare-legend';
6
6
  import { prepareSeries } from './prepareSeries';
7
7
  import { getActiveLegendItems, getAllLegendItems } from './utils';
8
+ const useVisibleSeries = ({ preparedSeries, activeLegendItems, }) => {
9
+ return React.useMemo(() => {
10
+ return preparedSeries.map((singleSeries) => {
11
+ if (singleSeries.legend.enabled) {
12
+ return Object.assign(Object.assign({}, singleSeries), { visible: activeLegendItems.includes(singleSeries.name) });
13
+ }
14
+ return singleSeries;
15
+ });
16
+ }, [preparedSeries, activeLegendItems]);
17
+ };
18
+ const getPreparedSeries = async ({ seriesData, seriesOptions, colors, preparedLegend, }) => {
19
+ const seriesNames = getSeriesNames(seriesData);
20
+ const colorScale = scaleOrdinal(seriesNames, colors);
21
+ const groupedSeries = group(seriesData, (item) => item.type);
22
+ const acc = [];
23
+ if (!preparedLegend) {
24
+ return acc;
25
+ }
26
+ const list = Array.from(groupedSeries);
27
+ for (let i = 0; i < list.length; i++) {
28
+ const [seriesType, seriesList] = list[i];
29
+ acc.push(...(await prepareSeries({
30
+ type: seriesType,
31
+ series: seriesList,
32
+ seriesOptions,
33
+ legend: preparedLegend,
34
+ colorScale,
35
+ colors,
36
+ })));
37
+ }
38
+ return acc;
39
+ };
8
40
  export const useSeries = (args) => {
9
41
  const { legend, originalSeriesData, seriesData, seriesOptions, colors, preparedLegend: preparedLegendProps = null, } = args;
10
42
  const [preparedLegend, setPreparedLegend] = React.useState(preparedLegendProps);
@@ -17,38 +49,18 @@ export const useSeries = (args) => {
17
49
  const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(preparedSeries));
18
50
  React.useEffect(() => {
19
51
  (async () => {
20
- const seriesNames = getSeriesNames(seriesData);
21
- const colorScale = scaleOrdinal(seriesNames, colors);
22
- const groupedSeries = group(seriesData, (item) => item.type);
23
- const acc = [];
24
- if (!preparedLegend) {
25
- return;
26
- }
27
- const list = Array.from(groupedSeries);
28
- for (let i = 0; i < list.length; i++) {
29
- const [seriesType, seriesList] = list[i];
30
- acc.push(...(await prepareSeries({
31
- type: seriesType,
32
- series: seriesList,
33
- seriesOptions,
34
- legend: preparedLegend,
35
- colorScale,
36
- colors,
37
- })));
38
- }
39
- setPreparedSeries(acc);
40
- setActiveLegendItems(getActiveLegendItems(acc));
52
+ const items = await getPreparedSeries({
53
+ seriesData,
54
+ seriesOptions,
55
+ preparedLegend,
56
+ colors,
57
+ });
58
+ setPreparedSeries(items);
59
+ setActiveLegendItems(getActiveLegendItems(items));
41
60
  })();
42
61
  }, [seriesData, seriesOptions, preparedLegend, colors]);
43
62
  const prevOriginalSeriesData = usePrevious(originalSeriesData);
44
- const chartSeries = React.useMemo(() => {
45
- return preparedSeries.map((singleSeries) => {
46
- if (singleSeries.legend.enabled) {
47
- return Object.assign(Object.assign({}, singleSeries), { visible: activeLegendItems.includes(singleSeries.name) });
48
- }
49
- return singleSeries;
50
- });
51
- }, [preparedSeries, activeLegendItems]);
63
+ const chartSeries = useVisibleSeries({ preparedSeries, activeLegendItems });
52
64
  const handleLegendItemClick = React.useCallback(({ name, metaKey }) => {
53
65
  const allItems = getAllLegendItems(preparedSeries);
54
66
  const onlyItemSelected = activeLegendItems.length === 1 && activeLegendItems.includes(name);
@@ -81,3 +93,21 @@ export const useSeries = (args) => {
81
93
  handleLegendItemClick,
82
94
  };
83
95
  };
96
+ export const useShapeSeries = ({ seriesData, seriesOptions, colors, preparedLegend, activeLegendItems, }) => {
97
+ const [preparedSeries, setPreparedSeries] = React.useState([]);
98
+ React.useEffect(() => {
99
+ (async () => {
100
+ const items = await getPreparedSeries({
101
+ seriesData,
102
+ seriesOptions,
103
+ preparedLegend,
104
+ colors,
105
+ });
106
+ setPreparedSeries(items);
107
+ })();
108
+ }, [seriesData, seriesOptions, preparedLegend, colors]);
109
+ const chartSeries = useVisibleSeries({ preparedSeries, activeLegendItems });
110
+ return {
111
+ preparedSeries: chartSeries,
112
+ };
113
+ };
@@ -1,5 +1,5 @@
1
1
  import type { ChartData } from '../../types';
2
- import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
2
+ import type { PreparedChart } from '../useChartOptions/types';
3
3
  import type { LegendItem, PreparedLegend, PreparedSeries } from './types';
4
4
  export declare function getPreparedLegend(args: {
5
5
  legend: ChartData['legend'];
@@ -11,7 +11,6 @@ export declare function getLegendComponents(args: {
11
11
  chartMargin: PreparedChart['margin'];
12
12
  series: PreparedSeries[];
13
13
  preparedLegend: PreparedLegend;
14
- preparedYAxis: PreparedAxis[];
15
14
  }): {
16
15
  legendConfig: {
17
16
  offset: {
@@ -24,6 +23,7 @@ export declare function getLegendComponents(args: {
24
23
  end: number;
25
24
  }[];
26
25
  } | undefined;
26
+ maxWidth: number;
27
27
  };
28
28
  legendItems: LegendItem[][];
29
29
  };
@@ -4,8 +4,6 @@ import get from 'lodash/get';
4
4
  import merge from 'lodash/merge';
5
5
  import { CONTINUOUS_LEGEND_SIZE, legendDefaults } from '../../constants';
6
6
  import { getDefaultColorStops, getDomainForContinuousColorScale, getLabelsSize } from '../../utils';
7
- import { getBoundsWidth } from '../useChartDimensions';
8
- import { getYAxisWidth } from '../useChartDimensions/utils';
9
7
  export async function getPreparedLegend(args) {
10
8
  var _a, _b, _c, _d, _e, _f, _g;
11
9
  const { legend, series } = args;
@@ -21,9 +19,13 @@ export async function getPreparedLegend(args) {
21
19
  const titleText = isTitleEnabled ? get(legend, 'title.text', '') : '';
22
20
  const titleSize = await getLabelsSize({ labels: [titleText], style: titleStyle });
23
21
  const titleHeight = isTitleEnabled ? titleSize.maxHeight : 0;
22
+ const tickStyle = {
23
+ fontSize: '12px',
24
+ };
24
25
  const ticks = {
25
26
  labelsMargin: 4,
26
- labelsLineHeight: 12,
27
+ labelsLineHeight: (await getLabelsSize({ labels: ['Tmp'], style: tickStyle })).maxHeight,
28
+ style: tickStyle,
27
29
  };
28
30
  const colorScale = {
29
31
  colors: [],
@@ -164,8 +166,8 @@ function getPagination(args) {
164
166
  return { pages };
165
167
  }
166
168
  export function getLegendComponents(args) {
167
- const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
168
- const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
169
+ const { chartWidth, chartHeight, chartMargin, series, preparedLegend } = args;
170
+ const maxLegendWidth = chartWidth - chartMargin.right - chartMargin.left;
169
171
  const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
170
172
  const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
171
173
  const items = getGroupedLegendItems({
@@ -195,8 +197,8 @@ export function getLegendComponents(args) {
195
197
  }
196
198
  const top = chartHeight - chartMargin.bottom - preparedLegend.height;
197
199
  const offset = {
198
- left: chartMargin.left + getYAxisWidth(preparedYAxis[0]),
200
+ left: chartMargin.left,
199
201
  top,
200
202
  };
201
- return { legendConfig: { offset, pagination }, legendItems: items };
203
+ return { legendConfig: { offset, pagination, maxWidth: maxLegendWidth }, legendItems: items };
202
204
  }
@@ -26,6 +26,7 @@ export type PreparedLegend = Required<Omit<ChartLegend, 'title' | 'colorScale'>>
26
26
  ticks: {
27
27
  labelsMargin: number;
28
28
  labelsLineHeight: number;
29
+ style: BaseTextStyle;
29
30
  };
30
31
  colorScale: {
31
32
  colors: string[];
@@ -52,6 +53,7 @@ export type LegendConfig = {
52
53
  left: number;
53
54
  top: number;
54
55
  };
56
+ maxWidth: number;
55
57
  pagination?: {
56
58
  pages: {
57
59
  start: number;
@@ -90,27 +90,15 @@ export async function axisBottom(args) {
90
90
  });
91
91
  }
92
92
  else {
93
- // remove overlapping labels
94
93
  let elementX = 0;
95
- selection
96
- .selectAll('.tick')
97
- .filter(function () {
98
- const node = this;
99
- const r = node.getBoundingClientRect();
100
- if (r.left < elementX) {
101
- return true;
102
- }
103
- elementX = r.right + labelsPaddings;
104
- return false;
105
- })
106
- .remove();
107
94
  // add an ellipsis to the labels that go beyond the boundaries of the chart
95
+ // and remove overlapping labels
108
96
  labels.each(function (_d, i, nodes) {
109
- var _a;
97
+ var _a, _b;
98
+ const currentElement = this;
99
+ const currentElementPosition = currentElement.getBoundingClientRect();
110
100
  if (i === 0) {
111
- const currentElement = this;
112
101
  const text = select(currentElement);
113
- const currentElementPosition = currentElement.getBoundingClientRect();
114
102
  const nextElement = nodes[i + 1];
115
103
  const nextElementPosition = nextElement === null || nextElement === void 0 ? void 0 : nextElement.getBoundingClientRect();
116
104
  if (currentElementPosition.left < leftmostLimit) {
@@ -123,18 +111,23 @@ export async function axisBottom(args) {
123
111
  setEllipsisForOverflowText(text, remainSpace);
124
112
  }
125
113
  }
126
- if (i === nodes.length - 1) {
127
- const currentElement = this;
128
- const prevElement = nodes[i - 1];
129
- const text = select(currentElement);
130
- const currentElementPosition = currentElement.getBoundingClientRect();
131
- const prevElementPosition = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getBoundingClientRect();
132
- const lackingSpace = Math.max(0, currentElementPosition.right - right);
133
- if (lackingSpace) {
134
- const remainSpace = right - ((prevElementPosition === null || prevElementPosition === void 0 ? void 0 : prevElementPosition.right) || 0) - labelsPaddings;
135
- const translateX = -lackingSpace;
136
- text.style('transform', `translate(${translateX}px,${translateY}px)`);
137
- setEllipsisForOverflowText(text, remainSpace);
114
+ else {
115
+ if (currentElementPosition.left < elementX) {
116
+ (_b = currentElement.closest('.tick')) === null || _b === void 0 ? void 0 : _b.remove();
117
+ return;
118
+ }
119
+ elementX = currentElementPosition.right + labelsPaddings;
120
+ if (i === nodes.length - 1) {
121
+ const prevElement = nodes[i - 1];
122
+ const text = select(currentElement);
123
+ const prevElementPosition = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getBoundingClientRect();
124
+ const lackingSpace = Math.max(0, currentElementPosition.right - right);
125
+ if (lackingSpace) {
126
+ const remainSpace = right - ((prevElementPosition === null || prevElementPosition === void 0 ? void 0 : prevElementPosition.right) || 0) - labelsPaddings;
127
+ const translateX = -lackingSpace;
128
+ text.style('transform', `translate(${translateX}px,${translateY}px)`);
129
+ setEllipsisForOverflowText(text, remainSpace);
130
+ }
138
131
  }
139
132
  }
140
133
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",