Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@korvin89 @kuzmadom
Can you tell me if there are any requirements for centering the legend along the vertical axis?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an additional field such as verticalAlign is suitable for vertical positioning of the legend (similar to horizontal align?: 'left' | 'center' | 'right';)
But this can (should) be done in a separate pr. For the legend to be positioned on the left/right, the current top-only positioning is enough - this is ok in my opinion

Copy link
Contributor Author

@arteria32 arteria32 Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this can (should) be done in a separate pr.

OK, I will try to implement this in the next pr.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does there seem to be enough space from below? I would have expected to see more series in legend to take up all the available space

Copy link
Contributor Author

@arteria32 arteria32 Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected to see more series in legend to take up all the available space

I'll take a look at it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 26 additions & 5 deletions src/__stories__/Other/LegendPosition.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ChartStory> = {
title: 'Other',
Expand All @@ -15,20 +15,41 @@ export default meta;
export const LegendPosition = {
name: 'Legend 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 <ChartStory data={data} />;
Expand Down
1 change: 1 addition & 0 deletions src/__stories__/__data__/other/index.ts
Original file line number Diff line number Diff line change
@@ -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';
40 changes: 40 additions & 0 deletions src/__stories__/__data__/other/legend-position.ts
Original file line number Diff line number Diff line change
@@ -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();
72 changes: 38 additions & 34 deletions src/__tests__/legend.visual.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ChartTestStory data={data} styles={{width: '270px'}} />,
);
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(
<ChartTestStory data={data} styles={{width: '270px'}} />,
);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Paginated', async ({mount}) => {
const data = cloneDeep(piePaginatedLegendData);
set(data, 'legend.position', 'top');

const component = await mount(
<ChartTestStory data={data} styles={{width: '150px'}} />,
);
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(
<ChartTestStory data={data} styles={{width: '270px'}} />,
);
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(
<ChartTestStory data={data} styles={{width: '270px'}} />,
);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Paginated', async ({mount}) => {
const data = cloneDeep(piePaginatedLegendData);
set(data, 'legend.position', position);

const component = await mount(
<ChartTestStory data={data} styles={{width: '150px'}} />,
);
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();
});
});
});
});
Expand Down
77 changes: 59 additions & 18 deletions src/components/ChartInner/useChartInnerProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -195,24 +242,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() ?? {};

Expand Down
Loading
Loading