@gravity-ui/charts 1.25.0 → 1.26.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.
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { Dispatch } from 'd3';
3
- import type { RangeSliderState, ZoomState } from '../../hooks';
3
+ import type { PreparedLegend, RangeSliderState, ZoomState } from '../../hooks';
4
4
  import type { ChartInnerProps } from './types';
5
5
  type Props = ChartInnerProps & {
6
6
  clipPathId: string;
@@ -35,7 +35,7 @@ export declare function useChartInnerProps(props: Props): {
35
35
  } | undefined;
36
36
  legendItems: never[] | import("../../hooks").LegendItem[][];
37
37
  preparedChart: import("../../hooks").PreparedChart;
38
- preparedLegend: import("../../hooks").PreparedLegend | null;
38
+ preparedLegend: PreparedLegend | null;
39
39
  preparedSeries: import("../../hooks").PreparedSeries[];
40
40
  preparedSeriesOptions: import("../../constants").SeriesOptionsDefaults;
41
41
  preparedSplit: import("../../hooks").PreparedSplit;
@@ -9,6 +9,30 @@ import { hasAtLeastOneSeriesDataPerPlot } from './utils';
9
9
  const CLIP_PATH_BY_SERIES_TYPE = {
10
10
  [SERIES_TYPE.Scatter]: false,
11
11
  };
12
+ function getBoundsOffsetTop(args) {
13
+ const { chartMarginTop, preparedLegend } = args;
14
+ return (chartMarginTop +
15
+ ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'top'
16
+ ? preparedLegend.height + preparedLegend.margin
17
+ : 0));
18
+ }
19
+ function getBoundsOffsetLeft(args) {
20
+ const { chartMarginLeft, preparedLegend, yAxis, getYAxisWidth: getAxisWidth } = args;
21
+ const legendOffset = (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'left'
22
+ ? preparedLegend.width + preparedLegend.margin
23
+ : 0;
24
+ const leftAxisWidth = yAxis.reduce((acc, axis) => {
25
+ if (axis.position !== 'left') {
26
+ return acc;
27
+ }
28
+ const axisWidth = getAxisWidth(axis);
29
+ if (acc < axisWidth) {
30
+ acc = axisWidth;
31
+ }
32
+ return acc;
33
+ }, 0);
34
+ return chartMarginLeft + legendOffset + leftAxisWidth;
35
+ }
12
36
  export function useChartInnerProps(props) {
13
37
  var _a;
14
38
  const { clipPathId, data, dispatcher, height, htmlLayout, plotNode, rangeSliderState, svgContainer, width, updateZoomState, zoomState, } = props;
@@ -129,22 +153,17 @@ export function useChartInnerProps(props) {
129
153
  yAxis,
130
154
  yScale,
131
155
  });
132
- const boundsOffsetTop = chart.margin.top +
133
- ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'top'
134
- ? preparedLegend.height + preparedLegend.margin
135
- : 0);
156
+ const boundsOffsetTop = getBoundsOffsetTop({
157
+ chartMarginTop: chart.margin.top,
158
+ preparedLegend,
159
+ });
136
160
  // We need to calculate the width of each left axis because the first axis can be hidden
137
- const boundsOffsetLeft = chart.margin.left +
138
- yAxis.reduce((acc, axis) => {
139
- if (axis.position !== 'left') {
140
- return acc;
141
- }
142
- const axisWidth = getYAxisWidth(axis);
143
- if (acc < axisWidth) {
144
- acc = axisWidth;
145
- }
146
- return acc;
147
- }, 0);
161
+ const boundsOffsetLeft = getBoundsOffsetLeft({
162
+ chartMarginLeft: chart.margin.left,
163
+ preparedLegend,
164
+ yAxis,
165
+ getYAxisWidth,
166
+ });
148
167
  const { x } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
149
168
  return {
150
169
  allPreparedSeries,
@@ -6,16 +6,21 @@ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineD
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
7
  import './styles.css';
8
8
  const b = block('legend');
9
- const getLegendPosition = (args) => {
10
- const { align, offsetLeft = 0, width, contentWidth } = args;
11
- const top = 0;
12
- if (align === 'left') {
13
- return { top, left: offsetLeft };
14
- }
9
+ const getLegendItemPosition = (args) => {
10
+ const { align, width, contentWidth } = args;
15
11
  if (align === 'right') {
16
- return { top, left: offsetLeft + width - contentWidth };
12
+ return { left: width - contentWidth };
13
+ }
14
+ else if (align === 'left') {
15
+ return { left: 0 };
16
+ }
17
+ else {
18
+ return { left: width / 2 - contentWidth / 2 };
17
19
  }
18
- return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
20
+ };
21
+ const getLegendPosition = (args) => {
22
+ const { offsetLeft, offsetTop, contentWidth, width } = args;
23
+ return { top: offsetTop, left: offsetLeft + width / 2 - contentWidth / 2 };
19
24
  };
20
25
  const appendPaginator = (args) => {
21
26
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -157,6 +162,8 @@ export const Legend = (props) => {
157
162
  ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
158
163
  : null;
159
164
  let legendWidth = 0;
165
+ let legendLeft = 0;
166
+ let legendTop = 0;
160
167
  if (legend.type === 'discrete') {
161
168
  const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
162
169
  const end = (_e = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _e === void 0 ? void 0 : _e.end;
@@ -249,11 +256,10 @@ export const Legend = (props) => {
249
256
  let left = 0;
250
257
  switch (legend.justifyContent) {
251
258
  case 'center': {
252
- const legendLinePostion = getLegendPosition({
259
+ const legendLinePostion = getLegendItemPosition({
253
260
  align: legend.align,
254
261
  width: config.maxWidth,
255
262
  contentWidth,
256
- offsetLeft: config.offset.left,
257
263
  });
258
264
  left = legendLinePostion.left;
259
265
  legendWidth = config.maxWidth;
@@ -280,8 +286,29 @@ export const Legend = (props) => {
280
286
  onArrowClick: setPageIndex,
281
287
  });
282
288
  }
289
+ const { left, top } = getLegendPosition({
290
+ width: config.maxWidth,
291
+ contentWidth: legendWidth,
292
+ offsetLeft: config.offset.left,
293
+ offsetTop: config.offset.top,
294
+ });
295
+ legendLeft = left;
296
+ legendTop = top;
283
297
  }
284
298
  else {
299
+ const { left } = getLegendItemPosition({
300
+ align: legend.align,
301
+ width: config.maxWidth,
302
+ contentWidth: legend.width,
303
+ });
304
+ const { top } = getLegendPosition({
305
+ width: config.maxWidth,
306
+ contentWidth: legendWidth,
307
+ offsetLeft: config.offset.left,
308
+ offsetTop: config.offset.top,
309
+ });
310
+ legendLeft = left;
311
+ legendTop = top;
285
312
  // gradient rect
286
313
  const domain = (_f = legend.colorScale.domain) !== null && _f !== void 0 ? _f : [];
287
314
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
@@ -361,15 +388,10 @@ export const Legend = (props) => {
361
388
  else {
362
389
  svgElement.selectAll(`.${legendTitleClassname}`).remove();
363
390
  }
364
- const { left } = getLegendPosition({
365
- align: legend.align,
366
- width: config.maxWidth,
367
- contentWidth: legendWidth,
368
- });
369
391
  svgElement
370
- .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
392
+ .attr('transform', `translate(${[legendLeft, legendTop].join(',')})`)
371
393
  .style('opacity', 1);
372
- htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
394
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${legendLeft}px, ${legendTop}px)`);
373
395
  }
374
396
  prepareLegend();
375
397
  }, [chartSeries, onItemClick, onUpdate, legend, items, config, pageIndex, htmlLayout]);
@@ -30,6 +30,18 @@ const getTopOffset = ({ preparedLegend }) => {
30
30
  }
31
31
  return 0;
32
32
  };
33
+ const getRightOffset = ({ preparedLegend }) => {
34
+ if ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'right') {
35
+ return preparedLegend.width + preparedLegend.margin;
36
+ }
37
+ return 0;
38
+ };
39
+ const getLeftOffset = ({ preparedLegend }) => {
40
+ if ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'left') {
41
+ return preparedLegend.width + preparedLegend.margin;
42
+ }
43
+ return 0;
44
+ };
33
45
  export const useChartDimensions = (args) => {
34
46
  const { height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width } = args;
35
47
  return React.useMemo(() => {
@@ -41,7 +53,10 @@ export const useChartDimensions = (args) => {
41
53
  preparedXAxis,
42
54
  });
43
55
  const topOffset = getTopOffset({ preparedLegend });
56
+ const rightOffset = getRightOffset({ preparedLegend });
57
+ const leftOffset = getLeftOffset({ preparedLegend });
44
58
  const boundsHeight = height - margin.top - margin.bottom - bottomOffset - topOffset;
45
- return { boundsWidth, boundsHeight };
59
+ const adjustedBoundsWidth = boundsWidth - rightOffset - leftOffset;
60
+ return { boundsWidth: adjustedBoundsWidth, boundsHeight };
46
61
  }, [height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width]);
47
62
  };
@@ -12,7 +12,10 @@ export async function getPreparedLegend(args) {
12
12
  const defaultItemStyle = clone(legendDefaults.itemStyle);
13
13
  const itemStyle = get(legend, 'itemStyle');
14
14
  const computedItemStyle = merge(defaultItemStyle, itemStyle);
15
- const lineHeight = (await getLabelsSize({ labels: ['Tmp'], style: computedItemStyle })).maxHeight;
15
+ const { maxHeight: lineHeight, maxWidth: lineWidth } = await getLabelsSize({
16
+ labels: ['Tmp'],
17
+ style: computedItemStyle,
18
+ });
16
19
  const legendType = get(legend, 'type', 'discrete');
17
20
  const isTitleEnabled = Boolean((_a = legend === null || legend === void 0 ? void 0 : legend.title) === null || _a === void 0 ? void 0 : _a.text);
18
21
  const titleMargin = isTitleEnabled ? get(legend, 'title.margin', 4) : 0;
@@ -34,9 +37,11 @@ export async function getPreparedLegend(args) {
34
37
  stops: [],
35
38
  };
36
39
  let height = 0;
40
+ let legendWidth = 0;
37
41
  if (enabled) {
38
42
  height += titleHeight + titleMargin;
39
43
  if (legendType === 'continuous') {
44
+ legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width);
40
45
  height += CONTINUOUS_LEGEND_SIZE.height;
41
46
  height += ticks.labelsLineHeight + ticks.labelsMargin;
42
47
  colorScale.colors = (_c = (_b = legend === null || legend === void 0 ? void 0 : legend.colorScale) === null || _b === void 0 ? void 0 : _b.colors) !== null && _c !== void 0 ? _c : [];
@@ -47,9 +52,9 @@ export async function getPreparedLegend(args) {
47
52
  }
48
53
  else {
49
54
  height += lineHeight;
55
+ legendWidth = get(legend, 'width', lineWidth);
50
56
  }
51
57
  }
52
- const legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width);
53
58
  return {
54
59
  align: get(legend, 'align', legendDefaults.align),
55
60
  justifyContent: get(legend, 'justifyContent', legendDefaults.justifyContent),
@@ -168,10 +173,61 @@ function getPagination(args) {
168
173
  });
169
174
  return { pages };
170
175
  }
176
+ function getLegendOffset(args) {
177
+ const { position, chartWidth, chartHeight, chartMargin, legendWidth, legendHeight } = args;
178
+ switch (position) {
179
+ case 'top':
180
+ return {
181
+ top: chartMargin.top,
182
+ left: chartMargin.left,
183
+ };
184
+ case 'right':
185
+ return {
186
+ top: chartMargin.top,
187
+ left: chartWidth - chartMargin.right - legendWidth,
188
+ };
189
+ case 'left':
190
+ return {
191
+ top: chartMargin.top,
192
+ left: chartMargin.left,
193
+ };
194
+ case 'bottom':
195
+ default:
196
+ return {
197
+ top: chartHeight - chartMargin.bottom - legendHeight,
198
+ left: chartMargin.left,
199
+ };
200
+ }
201
+ }
202
+ function getMaxLegendWidth(args) {
203
+ const { chartWidth, chartMargin, preparedLegend, isVerticalPosition } = args;
204
+ if (isVerticalPosition) {
205
+ return (chartWidth - chartMargin.right - chartMargin.left - preparedLegend.margin) / 2;
206
+ }
207
+ return chartWidth - chartMargin.right - chartMargin.left;
208
+ }
209
+ function getMaxLegendHeight(args) {
210
+ const { chartHeight, chartMargin, preparedLegend, isVerticalPosition } = args;
211
+ if (isVerticalPosition) {
212
+ return chartHeight - chartMargin.top - chartMargin.bottom;
213
+ }
214
+ return (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
215
+ }
171
216
  export function getLegendComponents(args) {
172
217
  const { chartWidth, chartHeight, chartMargin, series, preparedLegend } = args;
173
- const maxLegendWidth = chartWidth - chartMargin.right - chartMargin.left;
174
- const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
218
+ const isVerticalPosition = preparedLegend.position === 'right' || preparedLegend.position === 'left';
219
+ const maxLegendWidth = getMaxLegendWidth({
220
+ chartWidth,
221
+ chartMargin,
222
+ preparedLegend,
223
+ isVerticalPosition,
224
+ });
225
+ const maxLegendHeight = getMaxLegendHeight({
226
+ chartHeight,
227
+ chartMargin,
228
+ preparedLegend,
229
+ isVerticalPosition,
230
+ });
175
231
  const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
176
232
  const items = getGroupedLegendItems({
177
233
  maxLegendWidth,
@@ -197,13 +253,15 @@ export function getLegendComponents(args) {
197
253
  });
198
254
  }
199
255
  preparedLegend.height = legendHeight;
256
+ preparedLegend.width = Math.max(maxLegendWidth, preparedLegend.width);
200
257
  }
201
- const top = preparedLegend.position === 'top'
202
- ? chartMargin.top
203
- : chartHeight - chartMargin.bottom - preparedLegend.height;
204
- const offset = {
205
- left: chartMargin.left,
206
- top,
207
- };
258
+ const offset = getLegendOffset({
259
+ position: preparedLegend.position,
260
+ chartWidth,
261
+ chartHeight,
262
+ chartMargin,
263
+ legendWidth: preparedLegend.width,
264
+ legendHeight: preparedLegend.height,
265
+ });
208
266
  return { legendConfig: { offset, pagination, maxWidth: maxLegendWidth }, legendItems: items };
209
267
  }
@@ -66,7 +66,7 @@ export interface ChartLegend extends ChartLegendItem {
66
66
  *
67
67
  * @default 'bottom'
68
68
  * */
69
- position?: 'top' | 'bottom';
69
+ position?: 'top' | 'bottom' | 'left' | 'right';
70
70
  }
71
71
  export interface BaseLegendSymbol {
72
72
  /**
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { Dispatch } from 'd3';
3
- import type { RangeSliderState, ZoomState } from '../../hooks';
3
+ import type { PreparedLegend, RangeSliderState, ZoomState } from '../../hooks';
4
4
  import type { ChartInnerProps } from './types';
5
5
  type Props = ChartInnerProps & {
6
6
  clipPathId: string;
@@ -35,7 +35,7 @@ export declare function useChartInnerProps(props: Props): {
35
35
  } | undefined;
36
36
  legendItems: never[] | import("../../hooks").LegendItem[][];
37
37
  preparedChart: import("../../hooks").PreparedChart;
38
- preparedLegend: import("../../hooks").PreparedLegend | null;
38
+ preparedLegend: PreparedLegend | null;
39
39
  preparedSeries: import("../../hooks").PreparedSeries[];
40
40
  preparedSeriesOptions: import("../../constants").SeriesOptionsDefaults;
41
41
  preparedSplit: import("../../hooks").PreparedSplit;
@@ -9,6 +9,30 @@ import { hasAtLeastOneSeriesDataPerPlot } from './utils';
9
9
  const CLIP_PATH_BY_SERIES_TYPE = {
10
10
  [SERIES_TYPE.Scatter]: false,
11
11
  };
12
+ function getBoundsOffsetTop(args) {
13
+ const { chartMarginTop, preparedLegend } = args;
14
+ return (chartMarginTop +
15
+ ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'top'
16
+ ? preparedLegend.height + preparedLegend.margin
17
+ : 0));
18
+ }
19
+ function getBoundsOffsetLeft(args) {
20
+ const { chartMarginLeft, preparedLegend, yAxis, getYAxisWidth: getAxisWidth } = args;
21
+ const legendOffset = (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'left'
22
+ ? preparedLegend.width + preparedLegend.margin
23
+ : 0;
24
+ const leftAxisWidth = yAxis.reduce((acc, axis) => {
25
+ if (axis.position !== 'left') {
26
+ return acc;
27
+ }
28
+ const axisWidth = getAxisWidth(axis);
29
+ if (acc < axisWidth) {
30
+ acc = axisWidth;
31
+ }
32
+ return acc;
33
+ }, 0);
34
+ return chartMarginLeft + legendOffset + leftAxisWidth;
35
+ }
12
36
  export function useChartInnerProps(props) {
13
37
  var _a;
14
38
  const { clipPathId, data, dispatcher, height, htmlLayout, plotNode, rangeSliderState, svgContainer, width, updateZoomState, zoomState, } = props;
@@ -129,22 +153,17 @@ export function useChartInnerProps(props) {
129
153
  yAxis,
130
154
  yScale,
131
155
  });
132
- const boundsOffsetTop = chart.margin.top +
133
- ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'top'
134
- ? preparedLegend.height + preparedLegend.margin
135
- : 0);
156
+ const boundsOffsetTop = getBoundsOffsetTop({
157
+ chartMarginTop: chart.margin.top,
158
+ preparedLegend,
159
+ });
136
160
  // We need to calculate the width of each left axis because the first axis can be hidden
137
- const boundsOffsetLeft = chart.margin.left +
138
- yAxis.reduce((acc, axis) => {
139
- if (axis.position !== 'left') {
140
- return acc;
141
- }
142
- const axisWidth = getYAxisWidth(axis);
143
- if (acc < axisWidth) {
144
- acc = axisWidth;
145
- }
146
- return acc;
147
- }, 0);
161
+ const boundsOffsetLeft = getBoundsOffsetLeft({
162
+ chartMarginLeft: chart.margin.left,
163
+ preparedLegend,
164
+ yAxis,
165
+ getYAxisWidth,
166
+ });
148
167
  const { x } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
149
168
  return {
150
169
  allPreparedSeries,
@@ -6,16 +6,21 @@ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineD
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
7
  import './styles.css';
8
8
  const b = block('legend');
9
- const getLegendPosition = (args) => {
10
- const { align, offsetLeft = 0, width, contentWidth } = args;
11
- const top = 0;
12
- if (align === 'left') {
13
- return { top, left: offsetLeft };
14
- }
9
+ const getLegendItemPosition = (args) => {
10
+ const { align, width, contentWidth } = args;
15
11
  if (align === 'right') {
16
- return { top, left: offsetLeft + width - contentWidth };
12
+ return { left: width - contentWidth };
13
+ }
14
+ else if (align === 'left') {
15
+ return { left: 0 };
16
+ }
17
+ else {
18
+ return { left: width / 2 - contentWidth / 2 };
17
19
  }
18
- return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
20
+ };
21
+ const getLegendPosition = (args) => {
22
+ const { offsetLeft, offsetTop, contentWidth, width } = args;
23
+ return { top: offsetTop, left: offsetLeft + width / 2 - contentWidth / 2 };
19
24
  };
20
25
  const appendPaginator = (args) => {
21
26
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -157,6 +162,8 @@ export const Legend = (props) => {
157
162
  ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
158
163
  : null;
159
164
  let legendWidth = 0;
165
+ let legendLeft = 0;
166
+ let legendTop = 0;
160
167
  if (legend.type === 'discrete') {
161
168
  const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
162
169
  const end = (_e = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _e === void 0 ? void 0 : _e.end;
@@ -249,11 +256,10 @@ export const Legend = (props) => {
249
256
  let left = 0;
250
257
  switch (legend.justifyContent) {
251
258
  case 'center': {
252
- const legendLinePostion = getLegendPosition({
259
+ const legendLinePostion = getLegendItemPosition({
253
260
  align: legend.align,
254
261
  width: config.maxWidth,
255
262
  contentWidth,
256
- offsetLeft: config.offset.left,
257
263
  });
258
264
  left = legendLinePostion.left;
259
265
  legendWidth = config.maxWidth;
@@ -280,8 +286,29 @@ export const Legend = (props) => {
280
286
  onArrowClick: setPageIndex,
281
287
  });
282
288
  }
289
+ const { left, top } = getLegendPosition({
290
+ width: config.maxWidth,
291
+ contentWidth: legendWidth,
292
+ offsetLeft: config.offset.left,
293
+ offsetTop: config.offset.top,
294
+ });
295
+ legendLeft = left;
296
+ legendTop = top;
283
297
  }
284
298
  else {
299
+ const { left } = getLegendItemPosition({
300
+ align: legend.align,
301
+ width: config.maxWidth,
302
+ contentWidth: legend.width,
303
+ });
304
+ const { top } = getLegendPosition({
305
+ width: config.maxWidth,
306
+ contentWidth: legendWidth,
307
+ offsetLeft: config.offset.left,
308
+ offsetTop: config.offset.top,
309
+ });
310
+ legendLeft = left;
311
+ legendTop = top;
285
312
  // gradient rect
286
313
  const domain = (_f = legend.colorScale.domain) !== null && _f !== void 0 ? _f : [];
287
314
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
@@ -361,15 +388,10 @@ export const Legend = (props) => {
361
388
  else {
362
389
  svgElement.selectAll(`.${legendTitleClassname}`).remove();
363
390
  }
364
- const { left } = getLegendPosition({
365
- align: legend.align,
366
- width: config.maxWidth,
367
- contentWidth: legendWidth,
368
- });
369
391
  svgElement
370
- .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
392
+ .attr('transform', `translate(${[legendLeft, legendTop].join(',')})`)
371
393
  .style('opacity', 1);
372
- htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
394
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${legendLeft}px, ${legendTop}px)`);
373
395
  }
374
396
  prepareLegend();
375
397
  }, [chartSeries, onItemClick, onUpdate, legend, items, config, pageIndex, htmlLayout]);
@@ -30,6 +30,18 @@ const getTopOffset = ({ preparedLegend }) => {
30
30
  }
31
31
  return 0;
32
32
  };
33
+ const getRightOffset = ({ preparedLegend }) => {
34
+ if ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'right') {
35
+ return preparedLegend.width + preparedLegend.margin;
36
+ }
37
+ return 0;
38
+ };
39
+ const getLeftOffset = ({ preparedLegend }) => {
40
+ if ((preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && preparedLegend.position === 'left') {
41
+ return preparedLegend.width + preparedLegend.margin;
42
+ }
43
+ return 0;
44
+ };
33
45
  export const useChartDimensions = (args) => {
34
46
  const { height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width } = args;
35
47
  return React.useMemo(() => {
@@ -41,7 +53,10 @@ export const useChartDimensions = (args) => {
41
53
  preparedXAxis,
42
54
  });
43
55
  const topOffset = getTopOffset({ preparedLegend });
56
+ const rightOffset = getRightOffset({ preparedLegend });
57
+ const leftOffset = getLeftOffset({ preparedLegend });
44
58
  const boundsHeight = height - margin.top - margin.bottom - bottomOffset - topOffset;
45
- return { boundsWidth, boundsHeight };
59
+ const adjustedBoundsWidth = boundsWidth - rightOffset - leftOffset;
60
+ return { boundsWidth: adjustedBoundsWidth, boundsHeight };
46
61
  }, [height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width]);
47
62
  };
@@ -12,7 +12,10 @@ export async function getPreparedLegend(args) {
12
12
  const defaultItemStyle = clone(legendDefaults.itemStyle);
13
13
  const itemStyle = get(legend, 'itemStyle');
14
14
  const computedItemStyle = merge(defaultItemStyle, itemStyle);
15
- const lineHeight = (await getLabelsSize({ labels: ['Tmp'], style: computedItemStyle })).maxHeight;
15
+ const { maxHeight: lineHeight, maxWidth: lineWidth } = await getLabelsSize({
16
+ labels: ['Tmp'],
17
+ style: computedItemStyle,
18
+ });
16
19
  const legendType = get(legend, 'type', 'discrete');
17
20
  const isTitleEnabled = Boolean((_a = legend === null || legend === void 0 ? void 0 : legend.title) === null || _a === void 0 ? void 0 : _a.text);
18
21
  const titleMargin = isTitleEnabled ? get(legend, 'title.margin', 4) : 0;
@@ -34,9 +37,11 @@ export async function getPreparedLegend(args) {
34
37
  stops: [],
35
38
  };
36
39
  let height = 0;
40
+ let legendWidth = 0;
37
41
  if (enabled) {
38
42
  height += titleHeight + titleMargin;
39
43
  if (legendType === 'continuous') {
44
+ legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width);
40
45
  height += CONTINUOUS_LEGEND_SIZE.height;
41
46
  height += ticks.labelsLineHeight + ticks.labelsMargin;
42
47
  colorScale.colors = (_c = (_b = legend === null || legend === void 0 ? void 0 : legend.colorScale) === null || _b === void 0 ? void 0 : _b.colors) !== null && _c !== void 0 ? _c : [];
@@ -47,9 +52,9 @@ export async function getPreparedLegend(args) {
47
52
  }
48
53
  else {
49
54
  height += lineHeight;
55
+ legendWidth = get(legend, 'width', lineWidth);
50
56
  }
51
57
  }
52
- const legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width);
53
58
  return {
54
59
  align: get(legend, 'align', legendDefaults.align),
55
60
  justifyContent: get(legend, 'justifyContent', legendDefaults.justifyContent),
@@ -168,10 +173,61 @@ function getPagination(args) {
168
173
  });
169
174
  return { pages };
170
175
  }
176
+ function getLegendOffset(args) {
177
+ const { position, chartWidth, chartHeight, chartMargin, legendWidth, legendHeight } = args;
178
+ switch (position) {
179
+ case 'top':
180
+ return {
181
+ top: chartMargin.top,
182
+ left: chartMargin.left,
183
+ };
184
+ case 'right':
185
+ return {
186
+ top: chartMargin.top,
187
+ left: chartWidth - chartMargin.right - legendWidth,
188
+ };
189
+ case 'left':
190
+ return {
191
+ top: chartMargin.top,
192
+ left: chartMargin.left,
193
+ };
194
+ case 'bottom':
195
+ default:
196
+ return {
197
+ top: chartHeight - chartMargin.bottom - legendHeight,
198
+ left: chartMargin.left,
199
+ };
200
+ }
201
+ }
202
+ function getMaxLegendWidth(args) {
203
+ const { chartWidth, chartMargin, preparedLegend, isVerticalPosition } = args;
204
+ if (isVerticalPosition) {
205
+ return (chartWidth - chartMargin.right - chartMargin.left - preparedLegend.margin) / 2;
206
+ }
207
+ return chartWidth - chartMargin.right - chartMargin.left;
208
+ }
209
+ function getMaxLegendHeight(args) {
210
+ const { chartHeight, chartMargin, preparedLegend, isVerticalPosition } = args;
211
+ if (isVerticalPosition) {
212
+ return chartHeight - chartMargin.top - chartMargin.bottom;
213
+ }
214
+ return (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
215
+ }
171
216
  export function getLegendComponents(args) {
172
217
  const { chartWidth, chartHeight, chartMargin, series, preparedLegend } = args;
173
- const maxLegendWidth = chartWidth - chartMargin.right - chartMargin.left;
174
- const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
218
+ const isVerticalPosition = preparedLegend.position === 'right' || preparedLegend.position === 'left';
219
+ const maxLegendWidth = getMaxLegendWidth({
220
+ chartWidth,
221
+ chartMargin,
222
+ preparedLegend,
223
+ isVerticalPosition,
224
+ });
225
+ const maxLegendHeight = getMaxLegendHeight({
226
+ chartHeight,
227
+ chartMargin,
228
+ preparedLegend,
229
+ isVerticalPosition,
230
+ });
175
231
  const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
176
232
  const items = getGroupedLegendItems({
177
233
  maxLegendWidth,
@@ -197,13 +253,15 @@ export function getLegendComponents(args) {
197
253
  });
198
254
  }
199
255
  preparedLegend.height = legendHeight;
256
+ preparedLegend.width = Math.max(maxLegendWidth, preparedLegend.width);
200
257
  }
201
- const top = preparedLegend.position === 'top'
202
- ? chartMargin.top
203
- : chartHeight - chartMargin.bottom - preparedLegend.height;
204
- const offset = {
205
- left: chartMargin.left,
206
- top,
207
- };
258
+ const offset = getLegendOffset({
259
+ position: preparedLegend.position,
260
+ chartWidth,
261
+ chartHeight,
262
+ chartMargin,
263
+ legendWidth: preparedLegend.width,
264
+ legendHeight: preparedLegend.height,
265
+ });
208
266
  return { legendConfig: { offset, pagination, maxWidth: maxLegendWidth }, legendItems: items };
209
267
  }
@@ -66,7 +66,7 @@ export interface ChartLegend extends ChartLegendItem {
66
66
  *
67
67
  * @default 'bottom'
68
68
  * */
69
- position?: 'top' | 'bottom';
69
+ position?: 'top' | 'bottom' | 'left' | 'right';
70
70
  }
71
71
  export interface BaseLegendSymbol {
72
72
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",