@gravity-ui/charts 1.25.0 → 1.26.1

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,19 @@ 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 getLegendItemLeftPosition = (args) => {
10
+ const { align, width, contentWidth } = args;
15
11
  if (align === 'right') {
16
- return { top, left: offsetLeft + width - contentWidth };
12
+ return width - contentWidth;
13
+ }
14
+ if (align === 'left') {
15
+ return 0;
17
16
  }
18
- return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
17
+ return width / 2 - contentWidth / 2;
18
+ };
19
+ const getLegendPosition = (args) => {
20
+ const { offsetLeft, offsetTop, contentWidth, width } = args;
21
+ return { top: offsetTop, left: offsetLeft + width / 2 - contentWidth / 2 };
19
22
  };
20
23
  const appendPaginator = (args) => {
21
24
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -157,6 +160,8 @@ export const Legend = (props) => {
157
160
  ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
158
161
  : null;
159
162
  let legendWidth = 0;
163
+ let legendLeft = 0;
164
+ let legendTop = 0;
160
165
  if (legend.type === 'discrete') {
161
166
  const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
162
167
  const end = (_e = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _e === void 0 ? void 0 : _e.end;
@@ -249,13 +254,11 @@ export const Legend = (props) => {
249
254
  let left = 0;
250
255
  switch (legend.justifyContent) {
251
256
  case 'center': {
252
- const legendLinePostion = getLegendPosition({
257
+ left = getLegendItemLeftPosition({
253
258
  align: legend.align,
254
259
  width: config.maxWidth,
255
260
  contentWidth,
256
- offsetLeft: config.offset.left,
257
261
  });
258
- left = legendLinePostion.left;
259
262
  legendWidth = config.maxWidth;
260
263
  break;
261
264
  }
@@ -280,8 +283,39 @@ export const Legend = (props) => {
280
283
  onArrowClick: setPageIndex,
281
284
  });
282
285
  }
286
+ const { left, top } = getLegendPosition({
287
+ width: config.maxWidth,
288
+ contentWidth: legendWidth,
289
+ offsetLeft: config.offset.left,
290
+ offsetTop: config.offset.top,
291
+ });
292
+ legendLeft = left;
293
+ legendTop = top;
283
294
  }
284
295
  else {
296
+ let left = 0;
297
+ switch (legend.align) {
298
+ case 'right': {
299
+ left = config.offset.left + config.maxWidth - legend.width;
300
+ break;
301
+ }
302
+ case 'left': {
303
+ left = config.offset.left;
304
+ break;
305
+ }
306
+ case 'center': {
307
+ left = config.offset.left + config.maxWidth / 2 - legend.width / 2;
308
+ break;
309
+ }
310
+ }
311
+ const { top } = getLegendPosition({
312
+ width: config.maxWidth,
313
+ contentWidth: legendWidth,
314
+ offsetLeft: config.offset.left,
315
+ offsetTop: config.offset.top,
316
+ });
317
+ legendLeft = left;
318
+ legendTop = top;
285
319
  // gradient rect
286
320
  const domain = (_f = legend.colorScale.domain) !== null && _f !== void 0 ? _f : [];
287
321
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
@@ -361,15 +395,10 @@ export const Legend = (props) => {
361
395
  else {
362
396
  svgElement.selectAll(`.${legendTitleClassname}`).remove();
363
397
  }
364
- const { left } = getLegendPosition({
365
- align: legend.align,
366
- width: config.maxWidth,
367
- contentWidth: legendWidth,
368
- });
369
398
  svgElement
370
- .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
399
+ .attr('transform', `translate(${[legendLeft, legendTop].join(',')})`)
371
400
  .style('opacity', 1);
372
- htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
401
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${legendLeft}px, ${legendTop}px)`);
373
402
  }
374
403
  prepareLegend();
375
404
  }, [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,19 @@ 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 getLegendItemLeftPosition = (args) => {
10
+ const { align, width, contentWidth } = args;
15
11
  if (align === 'right') {
16
- return { top, left: offsetLeft + width - contentWidth };
12
+ return width - contentWidth;
13
+ }
14
+ if (align === 'left') {
15
+ return 0;
17
16
  }
18
- return { top, left: offsetLeft + width / 2 - contentWidth / 2 };
17
+ return width / 2 - contentWidth / 2;
18
+ };
19
+ const getLegendPosition = (args) => {
20
+ const { offsetLeft, offsetTop, contentWidth, width } = args;
21
+ return { top: offsetTop, left: offsetLeft + width / 2 - contentWidth / 2 };
19
22
  };
20
23
  const appendPaginator = (args) => {
21
24
  const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
@@ -157,6 +160,8 @@ export const Legend = (props) => {
157
160
  ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
158
161
  : null;
159
162
  let legendWidth = 0;
163
+ let legendLeft = 0;
164
+ let legendTop = 0;
160
165
  if (legend.type === 'discrete') {
161
166
  const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
162
167
  const end = (_e = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _e === void 0 ? void 0 : _e.end;
@@ -249,13 +254,11 @@ export const Legend = (props) => {
249
254
  let left = 0;
250
255
  switch (legend.justifyContent) {
251
256
  case 'center': {
252
- const legendLinePostion = getLegendPosition({
257
+ left = getLegendItemLeftPosition({
253
258
  align: legend.align,
254
259
  width: config.maxWidth,
255
260
  contentWidth,
256
- offsetLeft: config.offset.left,
257
261
  });
258
- left = legendLinePostion.left;
259
262
  legendWidth = config.maxWidth;
260
263
  break;
261
264
  }
@@ -280,8 +283,39 @@ export const Legend = (props) => {
280
283
  onArrowClick: setPageIndex,
281
284
  });
282
285
  }
286
+ const { left, top } = getLegendPosition({
287
+ width: config.maxWidth,
288
+ contentWidth: legendWidth,
289
+ offsetLeft: config.offset.left,
290
+ offsetTop: config.offset.top,
291
+ });
292
+ legendLeft = left;
293
+ legendTop = top;
283
294
  }
284
295
  else {
296
+ let left = 0;
297
+ switch (legend.align) {
298
+ case 'right': {
299
+ left = config.offset.left + config.maxWidth - legend.width;
300
+ break;
301
+ }
302
+ case 'left': {
303
+ left = config.offset.left;
304
+ break;
305
+ }
306
+ case 'center': {
307
+ left = config.offset.left + config.maxWidth / 2 - legend.width / 2;
308
+ break;
309
+ }
310
+ }
311
+ const { top } = getLegendPosition({
312
+ width: config.maxWidth,
313
+ contentWidth: legendWidth,
314
+ offsetLeft: config.offset.left,
315
+ offsetTop: config.offset.top,
316
+ });
317
+ legendLeft = left;
318
+ legendTop = top;
285
319
  // gradient rect
286
320
  const domain = (_f = legend.colorScale.domain) !== null && _f !== void 0 ? _f : [];
287
321
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
@@ -361,15 +395,10 @@ export const Legend = (props) => {
361
395
  else {
362
396
  svgElement.selectAll(`.${legendTitleClassname}`).remove();
363
397
  }
364
- const { left } = getLegendPosition({
365
- align: legend.align,
366
- width: config.maxWidth,
367
- contentWidth: legendWidth,
368
- });
369
398
  svgElement
370
- .attr('transform', `translate(${[left, config.offset.top].join(',')})`)
399
+ .attr('transform', `translate(${[legendLeft, legendTop].join(',')})`)
371
400
  .style('opacity', 1);
372
- htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
401
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${legendLeft}px, ${legendTop}px)`);
373
402
  }
374
403
  prepareLegend();
375
404
  }, [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.1",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",