Skip to content

Commit

Permalink
feat(BarChart): support percentage for barSize. Fixes recharts#3640 (r…
Browse files Browse the repository at this point in the history
…echarts#4390)

This adds support for percentage values for `barSize` as a workaround
for recharts#3640. See also my comment at
recharts#3640 (comment)

This is not really the perfect solution, but it allows users to at least
have a good workaround.

If you know how many data points your axis domain expects, you can
hard-code `barSize` to `100%/numberOfDataPoints` (e.g. `'10%'` for 10
data points).

You can also combine this with the automatic calculation, e.g.
`barSize={data.length > 1 ? undefined : '10%'}`.

```
<BarChart
  width={500}
  height={300}
  data={[[4.5, 10]]}
  barSize="30%" /* When there's only one data point on a numerical domain, we cannot automatically calculate the bar size */
  margin={{
    top: 5,
    right: 30,
    left: 20,
    bottom: 5,
  }}
>
  <XAxis dataKey={v => v[0]} type="number" domain={[0, 10]} />
  <YAxis />
  <Tooltip />
  <CartesianGrid strokeDasharray="3 3" />
  <Bar dataKey={v => v[1]} />
</BarChart>
```

![Screenshot 2024-04-04 at 16 31
49](https://github.com/recharts/recharts/assets/898549/d4fcbdad-d6ae-468d-b522-fb008c20a2a4)
# Conflicts:
#	src/chart/generateCategoricalChart.tsx
  • Loading branch information
graup committed Apr 9, 2024
1 parent 981eb8f commit ead05da
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/cartesian/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface BarProps extends InternalBarProps {
xAxisId?: string | number;
yAxisId?: string | number;
stackId?: string | number;
barSize?: number;
barSize?: string | number;
unit?: string | number;
name?: string | number;
dataKey: DataKey<any>;
Expand Down
30 changes: 23 additions & 7 deletions src/chart/generateCategoricalChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ import { getActiveShapeIndexForTooltip, isFunnel, isPie, isScatter } from '../ut
import { Cursor } from '../component/Cursor';
import { ChartLayoutContextProvider } from '../context/chartLayoutContext';
import { AxisMap, CategoricalChartState } from './types';
import { XAxisProps, YAxisProps, ZAxisProps } from '../index';
import { AngleAxisProps, RadiusAxisProps } from '../polar/types';

export interface MousePointer {
pageX: number;
Expand Down Expand Up @@ -831,22 +833,34 @@ export interface CategoricalChartProps {
}

type AxisObj = {
xAxis?: BaseAxisProps;
xAxis?: XAxisProps;
xAxisTicks?: Array<TickItem>;

yAxis?: BaseAxisProps;
yAxis?: YAxisProps;
yAxisTicks?: Array<TickItem>;

zAxis?: BaseAxisProps;
zAxis?: ZAxisProps;
zAxisTicks?: Array<TickItem>;

angleAxis?: BaseAxisProps;
angleAxis?: AngleAxisProps;
angleAxisTicks?: Array<TickItem>;

radiusAxis?: BaseAxisProps;
radiusAxis?: RadiusAxisProps;
radiusAxisTicks?: Array<TickItem>;
};

// Determine the size of the axis, used for calculation of relative bar sizes
const getCartesianAxisSize = (axisObj: AxisObj, axisName: 'xAxis' | 'yAxis' | 'angleAxis' | 'radiusAxis') => {
if (axisName === 'xAxis') {
return axisObj[axisName].width;
}
if (axisName === 'yAxis') {
return axisObj[axisName].height;
}
// This is only supported for Bar charts (i.e. charts with cartesian axes), so we should never get here
return undefined;
};

export const generateCategoricalChart = ({
chartName,
GraphicalChild,
Expand All @@ -862,7 +876,7 @@ export const generateCategoricalChart = ({
const { barSize, layout, barGap, barCategoryGap, maxBarSize: globalMaxBarSize } = props;
const { numericAxisName, cateAxisName } = getAxisNameByLayout(layout);
const hasBar = hasGraphicalBarItem(graphicalItems);
const sizeList = hasBar && getBarSizeList({ barSize, stackGroups });

const formattedItems = [] as any[];

graphicalItems.forEach((item: ReactElement, index: number) => {
Expand Down Expand Up @@ -913,9 +927,11 @@ export const generateCategoricalChart = ({
const itemIsBar = getDisplayName(item.type).indexOf('Bar') >= 0;
const bandSize = getBandSizeOfAxis(cateAxis, cateTicks);
let barPosition: ReadonlyArray<BarPosition> = [];
const sizeList =
hasBar && getBarSizeList({ barSize, stackGroups, totalSize: getCartesianAxisSize(axisObj, cateAxisName) });

if (itemIsBar) {
// 如果是bar,计算bar的位置
// If it is bar, calculate the position of bar
const maxBarSize: number = isNil(childMaxBarSize) ? globalMaxBarSize : childMaxBarSize;
const barBandSize: number = getBandSizeOfAxis(cateAxis, cateTicks, true) ?? maxBarSize ?? 0;
barPosition = getBarPosition({
Expand Down
6 changes: 5 additions & 1 deletion src/util/ChartUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,12 @@ export type BarSetup = {
*/
export const getBarSizeList = ({
barSize: globalSize,
totalSize,
stackGroups = {},
}: {
barSize: number | string;
stackGroups: AxisStackGroups;
totalSize: number;
}): Record<string, ReadonlyArray<BarSetup>> => {
if (!stackGroups) {
return {};
Expand All @@ -250,10 +252,12 @@ export const getBarSizeList = ({
result[cateId] = [];
}

const barSize: string | number | undefined = isNil(selfSize) ? globalSize : selfSize;

result[cateId].push({
item: barItems[0],
stackList: barItems.slice(1),
barSize: isNil(selfSize) ? globalSize : selfSize,
barSize: isNil(barSize) ? undefined : getPercentValue(barSize, totalSize, 0),
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion storybook/stories/API/props/ChartProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ toggling between multiple dataKey.`,
will be calculated by the barCategoryGap, barGap and the quantity of bar groups.`,
table: {
type: {
summary: 'number',
summary: 'number | Percentage',
},
category: 'Bar',
},
Expand Down
27 changes: 27 additions & 0 deletions storybook/stories/Examples/BarChart.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,30 @@ export const WithMinPointSize = {
);
},
};

export const OneDataPointPercentSize = {
render: () => {
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={500}
height={300}
data={[[4.5, 10]]}
barSize="30%" /* When there's only one data point on a numerical domain, we cannot automatically calculate the bar size */
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey={v => v[0]} type="number" domain={[0, 10]} />
<YAxis />
<Tooltip />
<CartesianGrid strokeDasharray="3 3" />
<Bar dataKey={v => v[1]} />
</BarChart>
</ResponsiveContainer>
);
},
};
40 changes: 38 additions & 2 deletions test/cartesian/XAxis.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,54 @@ describe('<XAxis />', () => {
expect(parseInt(bar?.getAttribute('x') as string, 10)).toEqual(66);
});

it('Render Bars with gap for a single data point', () => {
it('Render axis with tick for a single data point', () => {
const { container } = render(
<BarChart width={300} height={300} data={data.slice(0, 1)}>
<Bar dataKey="y" isAnimationActive={false} />
<XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']} padding="gap" />
<XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']} />
<YAxis dataKey="y" />
</BarChart>,
);

const tick = container.querySelector('.xAxis .recharts-cartesian-axis-tick-value');
expect(tick).toBeInTheDocument();
expect(tick.textContent).toEqual('100');
expect(tick?.getAttribute('x')).toEqual('180');

// For a single data point, unless barSize is given, the bar will have no width and thus not be rendered.
// This test merely confirms this known limitation.
const bar = container.querySelector('.recharts-rectangle');
expect(bar).not.toBeInTheDocument();
});

it('Render Bars for a single data point with barSize=50%', () => {
const { container } = render(
<BarChart width={300} height={300} data={data.slice(0, 1)} barSize="50%">
<Bar dataKey="y" isAnimationActive={false} />
<XAxis dataKey="x" type="number" domain={[50, 150]} />
<YAxis dataKey="y" />
</BarChart>,
);

const bar = container.querySelector('.recharts-rectangle');
expect(bar).toBeInTheDocument();
expect(bar?.getAttribute('x')).toEqual('123');
expect(bar?.getAttribute('width')).toEqual('115');
});

it('Render Bars for a single data point with barSize=20% and no-gap', () => {
const { container } = render(
<BarChart width={300} height={300} data={data.slice(0, 1)} barSize="20%">
<Bar dataKey="y" isAnimationActive={false} />
<XAxis dataKey="x" type="number" domain={[100, 150]} padding="no-gap" />
<YAxis dataKey="y" />
</BarChart>,
);

const bar = container.querySelector('.recharts-rectangle');
expect(bar).toBeInTheDocument();
expect(bar?.getAttribute('x')).toEqual('42');
expect(bar?.getAttribute('width')).toEqual('46');
});

test('Render no ticks if type is category and data is empty', () => {
Expand Down

0 comments on commit ead05da

Please sign in to comment.