diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Basic-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Basic-1-chromium-linux.png new file mode 100644 index 00000000..700aafbe Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Basic-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-1-chromium-linux.png new file mode 100644 index 00000000..9f6dd1e6 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-2-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-2-chromium-linux.png new file mode 100644 index 00000000..ac13b050 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-2-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-3-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-3-chromium-linux.png new file mode 100644 index 00000000..90994aa0 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-Paginated-3-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-With-html-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-With-html-1-chromium-linux.png new file mode 100644 index 00000000..c4923a58 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-bottom-With-html-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Basic-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Basic-1-chromium-linux.png new file mode 100644 index 00000000..594717ea Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Basic-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-1-chromium-linux.png new file mode 100644 index 00000000..6d0d8dc6 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-2-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-2-chromium-linux.png new file mode 100644 index 00000000..f0987496 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-2-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-3-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-3-chromium-linux.png new file mode 100644 index 00000000..8c1cbb2f Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-Paginated-3-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-With-html-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-With-html-1-chromium-linux.png new file mode 100644 index 00000000..5e93d7b0 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-left-With-html-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Basic-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Basic-1-chromium-linux.png new file mode 100644 index 00000000..ffde8b26 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Basic-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-1-chromium-linux.png new file mode 100644 index 00000000..f846705d Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-2-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-2-chromium-linux.png new file mode 100644 index 00000000..94ffd2eb Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-2-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-3-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-3-chromium-linux.png new file mode 100644 index 00000000..655a3127 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-Paginated-3-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-With-html-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-With-html-1-chromium-linux.png new file mode 100644 index 00000000..cf55fc78 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-right-With-html-1-chromium-linux.png differ diff --git a/src/__snapshots__/pie-series.visual.test.tsx-snapshots/Pie-series-Donut-with-small-center-text-1-chromium-linux.png b/src/__snapshots__/pie-series.visual.test.tsx-snapshots/Pie-series-Donut-with-small-center-text-1-chromium-linux.png index e30142f9..f03602b8 100644 Binary files a/src/__snapshots__/pie-series.visual.test.tsx-snapshots/Pie-series-Donut-with-small-center-text-1-chromium-linux.png and b/src/__snapshots__/pie-series.visual.test.tsx-snapshots/Pie-series-Donut-with-small-center-text-1-chromium-linux.png differ diff --git a/src/__stories__/Other/Legend/LegendPosition.stories.tsx b/src/__stories__/Other/Legend/LegendPosition.stories.tsx index 5b5d6364..e276cde9 100644 --- a/src/__stories__/Other/Legend/LegendPosition.stories.tsx +++ b/src/__stories__/Other/Legend/LegendPosition.stories.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type {Meta} from '@storybook/react-webpack5'; import {ChartStory} from '../../ChartStory'; -import {lineBasicData} from '../../__data__'; +import {legendPositionData} from '../../__data__'; const meta: Meta = { title: 'Other/Legend', @@ -15,20 +15,41 @@ export default meta; export const LegendPosition = { name: 'Position', args: { + enabled: true, position: 'bottom', + align: 'center', + justifyContent: 'center', }, argTypes: { + enabled: { + control: 'boolean', + }, position: { control: 'inline-radio', - options: ['top', 'bottom'], + options: ['top', 'bottom', 'left', 'right'], + }, + align: { + control: 'inline-radio', + options: ['left', 'center', 'right'], + }, + justifyContent: { + control: 'inline-radio', + options: ['start', 'center'], }, }, - render: (args: {position: 'top' | 'bottom'}) => { + render: (args: { + enabled: boolean; + position: 'top' | 'bottom' | 'left' | 'right'; + align: 'left' | 'center' | 'right'; + justifyContent: 'start' | 'center'; + }) => { const data = { - ...lineBasicData, + ...legendPositionData, legend: { - enabled: true, + enabled: args.enabled, position: args.position, + align: args.align, + justifyContent: args.justifyContent, }, }; return ; diff --git a/src/__stories__/__data__/other/index.ts b/src/__stories__/__data__/other/index.ts index 0fd95195..c7982f85 100644 --- a/src/__stories__/__data__/other/index.ts +++ b/src/__stories__/__data__/other/index.ts @@ -1,5 +1,6 @@ export * from './bands'; export * from './crosshair'; +export * from './legend-position'; export * from './line-and-bar'; export * from './lines'; export * from './tooltip'; diff --git a/src/__stories__/__data__/other/legend-position.ts b/src/__stories__/__data__/other/legend-position.ts new file mode 100644 index 00000000..5031d083 --- /dev/null +++ b/src/__stories__/__data__/other/legend-position.ts @@ -0,0 +1,40 @@ +import type {ChartData, LineSeries} from '../../../types'; +import {lineBasicData} from '../line/basic'; + +function prepareData(): ChartData { + const baseSeries = lineBasicData.series.data[0] as LineSeries; + const seriesNames = [ + 'Series 1', + 'Very looooooooooooooooooooooooooooooooooooooooong series name', + ]; + + const series: LineSeries[] = Array.from({length: 20}, (_, i) => ({ + ...baseSeries, + name: seriesNames[i % seriesNames.length], + data: baseSeries.data.slice(0, 20).map((point) => ({ + ...point, + y: typeof point.y === 'number' ? point.y + Math.random() * 10 - 5 : point.y, + })), + })); + + return { + series: { + data: series, + }, + yAxis: [ + { + title: { + text: 'User score', + }, + }, + ], + xAxis: { + type: 'datetime', + title: { + text: 'Release dates', + }, + }, + }; +} + +export const legendPositionData = prepareData(); diff --git a/src/__tests__/legend.visual.test.tsx b/src/__tests__/legend.visual.test.tsx index cf521962..83895015 100644 --- a/src/__tests__/legend.visual.test.tsx +++ b/src/__tests__/legend.visual.test.tsx @@ -127,40 +127,44 @@ test.describe('Legend', () => { await expect(component.locator('svg')).toHaveScreenshot(); }); - test.describe('Position top', () => { - test('Basic', async ({mount}) => { - const data = cloneDeep(pieOverflowedLegendItemsData); - set(data, 'legend.position', 'top'); - const component = await mount( - , - ); - await expect(component.locator('svg')).toHaveScreenshot(); - }); - - test('With html', async ({mount}) => { - const data = cloneDeep(pieOverflowedLegendItemsData); - set(data, 'legend.position', 'top'); - set(data, 'legend.html', true); - const component = await mount( - , - ); - await expect(component.locator('svg')).toHaveScreenshot(); - }); - - test('Paginated', async ({mount}) => { - const data = cloneDeep(piePaginatedLegendData); - set(data, 'legend.position', 'top'); - - const component = await mount( - , - ); - await expect(component.locator('svg')).toHaveScreenshot(); - - const arrowNext = component.getByText('▼'); - await arrowNext.click(); - await expect(component.locator('svg')).toHaveScreenshot(); - await arrowNext.click(); - await expect(component.locator('svg')).toHaveScreenshot(); + const positions = ['top', 'bottom', 'left', 'right'] as const; + + positions.forEach((position) => { + test.describe(`Position ${position}`, () => { + test('Basic', async ({mount}) => { + const data = cloneDeep(pieOverflowedLegendItemsData); + set(data, 'legend.position', position); + const component = await mount( + , + ); + await expect(component.locator('svg')).toHaveScreenshot(); + }); + + test('With html', async ({mount}) => { + const data = cloneDeep(pieOverflowedLegendItemsData); + set(data, 'legend.position', position); + set(data, 'legend.html', true); + const component = await mount( + , + ); + await expect(component.locator('svg')).toHaveScreenshot(); + }); + + test('Paginated', async ({mount}) => { + const data = cloneDeep(piePaginatedLegendData); + set(data, 'legend.position', position); + + const component = await mount( + , + ); + await expect(component.locator('svg')).toHaveScreenshot(); + + const arrowNext = component.getByText('▼'); + await arrowNext.click(); + await expect(component.locator('svg')).toHaveScreenshot(); + await arrowNext.click(); + await expect(component.locator('svg')).toHaveScreenshot(); + }); }); }); diff --git a/src/components/ChartInner/useChartInnerProps.ts b/src/components/ChartInner/useChartInnerProps.ts index 3e4ab645..82430e04 100644 --- a/src/components/ChartInner/useChartInnerProps.ts +++ b/src/components/ChartInner/useChartInnerProps.ts @@ -15,7 +15,13 @@ import { useSplit, useZoom, } from '../../hooks'; -import type {ClipPathBySeriesType, RangeSliderState, ZoomState} from '../../hooks'; +import type { + ClipPathBySeriesType, + PreparedAxis, + PreparedLegend, + RangeSliderState, + ZoomState, +} from '../../hooks'; import {getYAxisWidth} from '../../hooks/useChartDimensions/utils'; import {getLegendComponents} from '../../hooks/useSeries/prepare-legend'; import {getPreparedOptions} from '../../hooks/useSeries/prepare-options'; @@ -39,6 +45,47 @@ const CLIP_PATH_BY_SERIES_TYPE: ClipPathBySeriesType = { [SERIES_TYPE.Scatter]: false, }; +function getBoundsOffsetTop(args: { + chartMarginTop: number; + preparedLegend: PreparedLegend | null; +}): number { + const {chartMarginTop, preparedLegend} = args; + + return ( + chartMarginTop + + (preparedLegend?.enabled && preparedLegend.position === 'top' + ? preparedLegend.height + preparedLegend.margin + : 0) + ); +} + +function getBoundsOffsetLeft(args: { + chartMarginLeft: number; + preparedLegend: PreparedLegend | null; + yAxis: PreparedAxis[]; + getYAxisWidth: (axis: PreparedAxis) => number; +}): number { + const {chartMarginLeft, preparedLegend, yAxis, getYAxisWidth: getAxisWidth} = args; + + const legendOffset = + preparedLegend?.enabled && preparedLegend.position === 'left' + ? preparedLegend.width + preparedLegend.margin + : 0; + + const leftAxisWidth = yAxis.reduce((acc, axis) => { + if (axis.position !== 'left') { + return acc; + } + const axisWidth = getAxisWidth(axis); + if (acc < axisWidth) { + acc = axisWidth; + } + return acc; + }, 0); + + return chartMarginLeft + legendOffset + leftAxisWidth; +} + export function useChartInnerProps(props: Props) { const { clipPathId, @@ -193,24 +240,18 @@ export function useChartInnerProps(props: Props) { yScale, }); - const boundsOffsetTop = - chart.margin.top + - (preparedLegend?.enabled && preparedLegend.position === 'top' - ? preparedLegend.height + preparedLegend.margin - : 0); + const boundsOffsetTop = getBoundsOffsetTop({ + chartMarginTop: chart.margin.top, + preparedLegend, + }); + // We need to calculate the width of each left axis because the first axis can be hidden - const boundsOffsetLeft = - chart.margin.left + - yAxis.reduce((acc, axis) => { - if (axis.position !== 'left') { - return acc; - } - const axisWidth = getYAxisWidth(axis); - if (acc < axisWidth) { - acc = axisWidth; - } - return acc; - }, 0); + const boundsOffsetLeft = getBoundsOffsetLeft({ + chartMarginLeft: chart.margin.left, + preparedLegend, + yAxis, + getYAxisWidth, + }); const {x} = svgContainer?.getBoundingClientRect() ?? {}; diff --git a/src/components/Legend/index.tsx b/src/components/Legend/index.tsx index bdd7fab5..dbd28f20 100644 --- a/src/components/Legend/index.tsx +++ b/src/components/Legend/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {line as lineGenerator, scaleLinear, select, symbol} from 'd3'; import type {AxisDomain, AxisScale, BaseType, Selection} from 'd3'; +import {line as lineGenerator, scaleLinear, select, symbol} from 'd3'; import {CONTINUOUS_LEGEND_SIZE} from '../../constants'; import type { @@ -39,24 +39,32 @@ type Props = { onUpdate?: () => void; }; -const getLegendPosition = (args: { +const getLegendItemPosition = (args: { align: PreparedLegend['align']; contentWidth: number; width: number; offsetLeft?: number; }) => { - const {align, offsetLeft = 0, width, contentWidth} = args; - const top = 0; - - if (align === 'left') { - return {top, left: offsetLeft}; - } + const {align, width, contentWidth} = args; if (align === 'right') { - return {top, left: offsetLeft + width - contentWidth}; + return {left: width - contentWidth}; + } else if (align === 'left') { + return {left: 0}; + } else { + return {left: width / 2 - contentWidth / 2}; } +}; + +const getLegendPosition = (args: { + contentWidth: number; + width: number; + offsetLeft: number; + offsetTop: number; +}) => { + const {offsetLeft, offsetTop, contentWidth, width} = args; - return {top, left: offsetLeft + width / 2 - contentWidth / 2}; + return {top: offsetTop, left: offsetLeft + width / 2 - contentWidth / 2}; }; const appendPaginator = (args: { @@ -235,6 +243,8 @@ export const Legend = (props: Props) => { : null; let legendWidth = 0; + let legendLeft = 0; + let legendTop = 0; if (legend.type === 'discrete') { const start = config.pagination?.pages[pageIndex]?.start; const end = config.pagination?.pages[pageIndex]?.end; @@ -339,11 +349,10 @@ export const Legend = (props: Props) => { let left = 0; switch (legend.justifyContent) { case 'center': { - const legendLinePostion = getLegendPosition({ + const legendLinePostion = getLegendItemPosition({ align: legend.align, width: config.maxWidth, contentWidth, - offsetLeft: config.offset.left, }); left = legendLinePostion.left; legendWidth = config.maxWidth; @@ -374,7 +383,29 @@ export const Legend = (props: Props) => { onArrowClick: setPageIndex, }); } + const {left, top} = getLegendPosition({ + width: config.maxWidth, + contentWidth: legendWidth, + offsetLeft: config.offset.left, + offsetTop: config.offset.top, + }); + + legendLeft = left; + legendTop = top; } else { + const {left} = getLegendItemPosition({ + align: legend.align, + width: config.maxWidth, + contentWidth: legend.width, + }); + const {top} = getLegendPosition({ + width: config.maxWidth, + contentWidth: legendWidth, + offsetLeft: config.offset.left, + offsetTop: config.offset.top, + }); + legendLeft = left; + legendTop = top; // gradient rect const domain = legend.colorScale.domain ?? []; const rectHeight = CONTINUOUS_LEGEND_SIZE.height; @@ -459,16 +490,10 @@ export const Legend = (props: Props) => { svgElement.selectAll(`.${legendTitleClassname}`).remove(); } - const {left} = getLegendPosition({ - align: legend.align, - width: config.maxWidth, - contentWidth: legendWidth, - }); - svgElement - .attr('transform', `translate(${[left, config.offset.top].join(',')})`) + .attr('transform', `translate(${[legendLeft, legendTop].join(',')})`) .style('opacity', 1); - htmlContainer?.style('transform', `translate(${left}px, ${config.offset.top}px)`); + htmlContainer?.style('transform', `translate(${legendLeft}px, ${legendTop}px)`); } prepareLegend(); diff --git a/src/hooks/useChartDimensions/index.ts b/src/hooks/useChartDimensions/index.ts index 0f3e37f0..88243669 100644 --- a/src/hooks/useChartDimensions/index.ts +++ b/src/hooks/useChartDimensions/index.ts @@ -59,6 +59,22 @@ const getTopOffset = ({preparedLegend}: {preparedLegend: PreparedLegend | null}) return 0; }; +const getRightOffset = ({preparedLegend}: {preparedLegend: PreparedLegend | null}) => { + if (preparedLegend?.enabled && preparedLegend.position === 'right') { + return preparedLegend.width + preparedLegend.margin; + } + + return 0; +}; + +const getLeftOffset = ({preparedLegend}: {preparedLegend: PreparedLegend | null}) => { + if (preparedLegend?.enabled && preparedLegend.position === 'left') { + return preparedLegend.width + preparedLegend.margin; + } + + return 0; +}; + export const useChartDimensions = (args: Args) => { const {height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width} = args; @@ -72,9 +88,12 @@ export const useChartDimensions = (args: Args) => { preparedXAxis, }); const topOffset = getTopOffset({preparedLegend}); + const rightOffset = getRightOffset({preparedLegend}); + const leftOffset = getLeftOffset({preparedLegend}); const boundsHeight = height - margin.top - margin.bottom - bottomOffset - topOffset; + const adjustedBoundsWidth = boundsWidth - rightOffset - leftOffset; - return {boundsWidth, boundsHeight}; + return {boundsWidth: adjustedBoundsWidth, boundsHeight}; }, [height, margin, preparedLegend, preparedSeries, preparedXAxis, preparedYAxis, width]); }; diff --git a/src/hooks/useSeries/prepare-legend.ts b/src/hooks/useSeries/prepare-legend.ts index 5694f186..bd3c4108 100644 --- a/src/hooks/useSeries/prepare-legend.ts +++ b/src/hooks/useSeries/prepare-legend.ts @@ -24,8 +24,10 @@ export async function getPreparedLegend(args: { const defaultItemStyle = clone(legendDefaults.itemStyle); const itemStyle = get(legend, 'itemStyle'); const computedItemStyle = merge(defaultItemStyle, itemStyle); - const lineHeight = (await getLabelsSize({labels: ['Tmp'], style: computedItemStyle})).maxHeight; - + const {maxHeight: lineHeight, maxWidth: lineWidth} = await getLabelsSize({ + labels: ['Tmp'], + style: computedItemStyle, + }); const legendType = get(legend, 'type', 'discrete'); const isTitleEnabled = Boolean(legend?.title?.text); const titleMargin = isTitleEnabled ? get(legend, 'title.margin', 4) : 0; @@ -37,7 +39,6 @@ export async function getPreparedLegend(args: { const titleText = isTitleEnabled ? get(legend, 'title.text', '') : ''; const titleSize = await getLabelsSize({labels: [titleText], style: titleStyle}); const titleHeight = isTitleEnabled ? titleSize.maxHeight : 0; - const tickStyle: BaseTextStyle = { fontSize: '12px', }; @@ -55,9 +56,11 @@ export async function getPreparedLegend(args: { }; let height = 0; + let legendWidth = 0; if (enabled) { height += titleHeight + titleMargin; if (legendType === 'continuous') { + legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width); height += CONTINUOUS_LEGEND_SIZE.height; height += ticks.labelsLineHeight + ticks.labelsMargin; @@ -68,11 +71,9 @@ export async function getPreparedLegend(args: { legend?.colorScale?.domain ?? getDomainForContinuousColorScale({series}); } else { height += lineHeight; + legendWidth = get(legend, 'width', lineWidth); } } - - const legendWidth = get(legend, 'width', CONTINUOUS_LEGEND_SIZE.width); - return { align: get(legend, 'align', legendDefaults.align), justifyContent: get(legend, 'justifyContent', legendDefaults.justifyContent), @@ -221,6 +222,71 @@ function getPagination(args: { return {pages}; } +function getLegendOffset(args: { + position: PreparedLegend['position']; + chartWidth: number; + chartHeight: number; + chartMargin: PreparedChart['margin']; + legendWidth: number; + legendHeight: number; +}): LegendConfig['offset'] { + const {position, chartWidth, chartHeight, chartMargin, legendWidth, legendHeight} = args; + + switch (position) { + case 'top': + return { + top: chartMargin.top, + left: chartMargin.left, + }; + case 'right': + return { + top: chartMargin.top, + left: chartWidth - chartMargin.right - legendWidth, + }; + case 'left': + return { + top: chartMargin.top, + left: chartMargin.left, + }; + case 'bottom': + default: + return { + top: chartHeight - chartMargin.bottom - legendHeight, + left: chartMargin.left, + }; + } +} + +function getMaxLegendWidth(args: { + chartWidth: number; + chartMargin: PreparedChart['margin']; + preparedLegend: PreparedLegend; + isVerticalPosition: boolean; +}): number { + const {chartWidth, chartMargin, preparedLegend, isVerticalPosition} = args; + + if (isVerticalPosition) { + return (chartWidth - chartMargin.right - chartMargin.left - preparedLegend.margin) / 2; + } + + return chartWidth - chartMargin.right - chartMargin.left; +} + +function getMaxLegendHeight(args: { + chartHeight: number; + chartMargin: PreparedChart['margin']; + preparedLegend: PreparedLegend; + isVerticalPosition: boolean; +}): number { + const {chartHeight, chartMargin, preparedLegend, isVerticalPosition} = args; + + if (isVerticalPosition) { + return chartHeight - chartMargin.top - chartMargin.bottom; + } + + return (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2; +} + export function getLegendComponents(args: { chartWidth: number; chartHeight: number; @@ -229,9 +295,21 @@ export function getLegendComponents(args: { preparedLegend: PreparedLegend; }) { const {chartWidth, chartHeight, chartMargin, series, preparedLegend} = args; - const maxLegendWidth = chartWidth - chartMargin.right - chartMargin.left; - const maxLegendHeight = - (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2; + + const isVerticalPosition = + preparedLegend.position === 'right' || preparedLegend.position === 'left'; + const maxLegendWidth = getMaxLegendWidth({ + chartWidth, + chartMargin, + preparedLegend, + isVerticalPosition, + }); + const maxLegendHeight = getMaxLegendHeight({ + chartHeight, + chartMargin, + preparedLegend, + isVerticalPosition, + }); const flattenLegendItems = getFlattenLegendItems(series, preparedLegend); const items = getGroupedLegendItems({ maxLegendWidth, @@ -262,16 +340,17 @@ export function getLegendComponents(args: { } preparedLegend.height = legendHeight; + preparedLegend.width = Math.max(maxLegendWidth, preparedLegend.width); } - const top = - preparedLegend.position === 'top' - ? chartMargin.top - : chartHeight - chartMargin.bottom - preparedLegend.height; - const offset: LegendConfig['offset'] = { - left: chartMargin.left, - top, - }; + const offset = getLegendOffset({ + position: preparedLegend.position, + chartWidth, + chartHeight, + chartMargin, + legendWidth: preparedLegend.width, + legendHeight: preparedLegend.height, + }); return {legendConfig: {offset, pagination, maxWidth: maxLegendWidth}, legendItems: items}; } diff --git a/src/types/chart/legend.ts b/src/types/chart/legend.ts index be95d289..8ac4484f 100644 --- a/src/types/chart/legend.ts +++ b/src/types/chart/legend.ts @@ -79,7 +79,7 @@ export interface ChartLegend extends ChartLegendItem { * * @default 'bottom' * */ - position?: 'top' | 'bottom'; + position?: 'top' | 'bottom' | 'left' | 'right'; } export interface BaseLegendSymbol {