diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Basic-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Basic-1-chromium-linux.png new file mode 100644 index 000000000..ba78d0ba9 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Basic-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-1-chromium-linux.png new file mode 100644 index 000000000..d8cf35a32 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-1-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-2-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-2-chromium-linux.png new file mode 100644 index 000000000..81a6b2c4a Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-2-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-3-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-3-chromium-linux.png new file mode 100644 index 000000000..2768a9511 Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-Paginated-3-chromium-linux.png differ diff --git a/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-With-html-1-chromium-linux.png b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-With-html-1-chromium-linux.png new file mode 100644 index 000000000..c1a3ee8ab Binary files /dev/null and b/src/__snapshots__/legend.visual.test.tsx-snapshots/Legend-Discrete-Position-top-With-html-1-chromium-linux.png differ diff --git a/src/__stories__/Other/LegendPosition.stories.tsx b/src/__stories__/Other/LegendPosition.stories.tsx new file mode 100644 index 000000000..50462e227 --- /dev/null +++ b/src/__stories__/Other/LegendPosition.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import type {Meta} from '@storybook/react-webpack5'; + +import {ChartStory} from '../ChartStory'; +import {lineBasicData} from '../__data__'; + +const meta: Meta = { + title: 'Other', + component: ChartStory, +}; + +export default meta; + +export const LegendPosition = { + name: 'Legend Position', + args: { + position: 'bottom', + }, + argTypes: { + position: { + control: 'inline-radio', + options: ['top', 'bottom'], + }, + }, + render: (args: {position: 'top' | 'bottom'}) => { + const data = { + ...lineBasicData, + legend: { + enabled: true, + position: args.position, + }, + }; + return ; + }, +}; diff --git a/src/__tests__/legend.visual.test.tsx b/src/__tests__/legend.visual.test.tsx index bee8e106a..61ac651a0 100644 --- a/src/__tests__/legend.visual.test.tsx +++ b/src/__tests__/legend.visual.test.tsx @@ -37,28 +37,31 @@ const pieOverflowedLegendItemsData: ChartData = { }, }; +const piePaginatedLegendData: ChartData = { + legend: { + enabled: true, + type: 'discrete', + }, + series: { + data: [ + { + type: 'pie', + dataLabels: {enabled: false}, + data: range(1, 40).map((i) => ({ + name: `Label ${i + 1}`, + value: i, + })), + }, + ], + }, +}; + test.describe('Legend', () => { test.describe('Discrete', () => { test('Pagination svg', async ({mount}) => { - const data: ChartData = { - legend: { - enabled: true, - type: 'discrete', - }, - series: { - data: [ - { - type: 'pie', - dataLabels: {enabled: false}, - data: range(1, 40).map((i) => ({ - name: `Label ${i + 1}`, - value: i, - })), - }, - ], - }, - }; - const component = await mount(); + const component = await mount( + , + ); await expect(component.locator('svg')).toHaveScreenshot(); const arrowNext = component.getByText('▼'); await arrowNext.click(); @@ -123,5 +126,42 @@ test.describe('Legend', () => { await legendItem.click(); 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(); + }); + }); }); }); diff --git a/src/components/ChartInner/useChartInnerProps.ts b/src/components/ChartInner/useChartInnerProps.ts index bc95b6567..c16517fc5 100644 --- a/src/components/ChartInner/useChartInnerProps.ts +++ b/src/components/ChartInner/useChartInnerProps.ts @@ -117,6 +117,7 @@ export function useChartInnerProps(props: Props) { preparedXAxis: xAxis, width, }); + const preparedSplit = useSplit({split: data.split, boundsHeight, chartWidth: width}); const {xScale, yScale} = useAxisScales({ boundsWidth, @@ -184,7 +185,11 @@ export function useChartInnerProps(props: Props) { yScale, }); - const boundsOffsetTop = chart.margin.top; + const boundsOffsetTop = + chart.margin.top + + (preparedLegend?.enabled && preparedLegend.position === 'top' + ? preparedLegend.height + preparedLegend.margin + : 0); // We need to calculate the width of each left axis because the first axis can be hidden const boundsOffsetLeft = chart.margin.left + diff --git a/src/hooks/useChartDimensions/index.ts b/src/hooks/useChartDimensions/index.ts index c5d03dc48..5e58e5c4c 100644 --- a/src/hooks/useChartDimensions/index.ts +++ b/src/hooks/useChartDimensions/index.ts @@ -26,7 +26,7 @@ const getBottomOffset = (args: { const {hasAxisRelatedSeries, preparedLegend, preparedXAxis} = args; let result = 0; - if (preparedLegend?.enabled) { + if (preparedLegend?.enabled && preparedLegend.position === 'bottom') { result += preparedLegend.height + preparedLegend.margin; } @@ -47,6 +47,14 @@ const getBottomOffset = (args: { return result; }; +const getTopOffset = ({preparedLegend}: {preparedLegend: PreparedLegend | null}) => { + if (preparedLegend?.enabled && preparedLegend.position === 'top') { + return preparedLegend.height + preparedLegend.margin; + } + + return 0; +}; + export const useChartDimensions = (args: Args) => { const {margin, width, height, preparedLegend, preparedXAxis, preparedYAxis, preparedSeries} = args; @@ -59,8 +67,9 @@ export const useChartDimensions = (args: Args) => { preparedLegend, preparedXAxis, }); + const topOffset = getTopOffset({preparedLegend}); - const boundsHeight = height - margin.top - margin.bottom - bottomOffset; + const boundsHeight = height - margin.top - margin.bottom - bottomOffset - topOffset; return {boundsWidth, boundsHeight}; }, [margin, width, height, preparedLegend, preparedXAxis, preparedYAxis, preparedSeries]); diff --git a/src/hooks/useSeries/prepare-legend.ts b/src/hooks/useSeries/prepare-legend.ts index 3a00b3010..561d706ba 100644 --- a/src/hooks/useSeries/prepare-legend.ts +++ b/src/hooks/useSeries/prepare-legend.ts @@ -94,6 +94,7 @@ export async function getPreparedLegend(args: { ticks, colorScale, html: get(legend, 'html', false), + position: get(legend, 'position', 'bottom'), }; } @@ -259,7 +260,10 @@ export function getLegendComponents(args: { preparedLegend.height = legendHeight; } - const top = chartHeight - chartMargin.bottom - preparedLegend.height; + const top = + preparedLegend.position === 'top' + ? chartMargin.top + : chartHeight - chartMargin.bottom - preparedLegend.height; const offset: LegendConfig['offset'] = { left: chartMargin.left, top, diff --git a/src/types/chart/legend.ts b/src/types/chart/legend.ts index 5ebabce95..a5790e7b1 100644 --- a/src/types/chart/legend.ts +++ b/src/types/chart/legend.ts @@ -71,6 +71,12 @@ export interface ChartLegend { * @default false * */ html?: boolean; + /** + * The position of the legend box within the chart area. + * + * @default 'bottom' + * */ + position?: 'top' | 'bottom'; } export interface BaseLegendSymbol {