Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom width / fully-responsive charts #10

Merged
merged 9 commits into from
Nov 27, 2023
61 changes: 32 additions & 29 deletions src/Components/Charts/BentoBarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { useCallback } from 'react';
import { BarChart, Bar, Cell, XAxis, YAxis, Tooltip, Label, BarProps } from 'recharts';
import { BarChart, Bar, Cell, XAxis, YAxis, Tooltip, Label, BarProps, ResponsiveContainer } from 'recharts';
import {
TOOL_TIP_STYLE,
COUNT_STYLE,
LABEL_STYLE,
CHART_WRAPPER_STYLE,
MAX_TICK_LABEL_CHARS,
TITLE_STYLE,
ASPECT_RATIO,
TICKS_SHOW_ALL_LABELS_BELOW,
UNITS_LABEL_OFFSET,
TICK_MARGIN,
Expand All @@ -17,6 +15,7 @@ import type { BarChartProps, CategoricalChartDataItem, TooltipPayload } from '..
import { useChartTheme, useChartTranslation } from '../../ChartConfigProvider';
import NoData from '../NoData';
import { useTransformedChartData } from '../../util/chartUtils';
import ChartWrapper from './ChartWrapper';

const tickFormatter = (tickLabel: string) => {
if (tickLabel.length <= MAX_TICK_LABEL_CHARS) {
Expand All @@ -25,7 +24,9 @@ const tickFormatter = (tickLabel: string) => {
return `${tickLabel.substring(0, MAX_TICK_LABEL_CHARS)}...`;
};

const BentoBarChart = ({ height, units, title, onClick, colorTheme = 'default', ...params }: BarChartProps) => {
const BAR_CHART_MARGINS = { top: 10, bottom: 100, right: 20 };

const BentoBarChart = ({ height, width, units, title, onClick, colorTheme = 'default', ...params }: BarChartProps) => {
const t = useChartTranslation();
const { fill: chartFill, missing } = useChartTheme().bar[colorTheme];

Expand Down Expand Up @@ -53,32 +54,34 @@ const BentoBarChart = ({ height, units, title, onClick, colorTheme = 'default',
// on formatting a non-string. This hack manually overrides the ticks for the axis and blanks it out.
// - David L, 2023-01-03
return (
<div style={CHART_WRAPPER_STYLE}>
<ChartWrapper>
<div style={TITLE_STYLE}>{title}</div>
<BarChart width={height * ASPECT_RATIO} height={height} data={data} margin={{ top: 10, bottom: 100, right: 20 }}>
<XAxis
dataKey="x"
height={20}
angle={-45}
ticks={data.length ? undefined : ['']}
tickFormatter={tickFormatter}
tickMargin={TICK_MARGIN}
textAnchor="end"
interval={data.length < TICKS_SHOW_ALL_LABELS_BELOW ? 0 : 'preserveStartEnd'}
>
<Label value={units} offset={UNITS_LABEL_OFFSET} position="insideBottom" />
</XAxis>
<YAxis>
<Label value={t['Count']} offset={-10} position="left" angle={270} />
</YAxis>
<Tooltip content={<BarTooltip totalCount={totalCount} />} />
<Bar dataKey="y" isAnimationActive={false} onClick={onClick} onMouseEnter={onHover}>
{data.map((entry) => (
<Cell key={entry.x} fill={fill(entry)} />
))}
</Bar>
</BarChart>
</div>
<ResponsiveContainer width={width ?? "100%"} height={height}>
<BarChart data={data} margin={BAR_CHART_MARGINS}>
<XAxis
dataKey="x"
height={20}
angle={-45}
ticks={data.length ? undefined : ['']}
tickFormatter={tickFormatter}
tickMargin={TICK_MARGIN}
textAnchor="end"
interval={data.length < TICKS_SHOW_ALL_LABELS_BELOW ? 0 : 'preserveStartEnd'}
>
<Label value={units} offset={UNITS_LABEL_OFFSET} position="insideBottom" />
</XAxis>
<YAxis>
<Label value={t['Count']} offset={-10} position="left" angle={270} />
</YAxis>
<Tooltip content={<BarTooltip totalCount={totalCount} />} />
<Bar dataKey="y" isAnimationActive={false} onClick={onClick} onMouseEnter={onHover}>
{data.map((entry) => (
<Cell key={entry.x} fill={fill(entry)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</ChartWrapper>
);
};

Expand Down
83 changes: 46 additions & 37 deletions src/Components/Charts/BentoPie.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React, { useCallback, useMemo, useState } from 'react';
import { PieChart, Pie, Cell, Curve, Tooltip, Sector, PieProps, PieLabelRenderProps } from 'recharts';
import {
PieChart,
Pie,
Cell,
Curve,
Tooltip,
Sector,
PieProps,
PieLabelRenderProps,
ResponsiveContainer,
} from 'recharts';
import type CSS from 'csstype';

import {
TOOL_TIP_STYLE,
LABEL_STYLE,
COUNT_STYLE,
CHART_MISSING_FILL,
CHART_WRAPPER_STYLE,
RADIAN,
CHART_ASPECT_RATIO,
LABEL_THRESHOLD,
COUNT_TEXT_STYLE,
TEXT_STYLE,
Expand All @@ -23,6 +31,7 @@ import {
} from '../../ChartConfigProvider';
import { polarToCartesian, useTransformedChartData } from '../../util/chartUtils';
import NoData from '../NoData';
import ChartWrapper from './ChartWrapper';

const labelShortName = (name: string, maxChars: number) => {
if (name.length <= maxChars) {
Expand All @@ -32,11 +41,9 @@ const labelShortName = (name: string, maxChars: number) => {
return `${name.substring(0, maxChars - 3)}\u2026`;
};

const OUTER_RADIUS_REDUCTION_FACTOR = 3.75; // originally from 300 / 80
const INNER_RADIUS_REDUCTION_FACTOR = 8.5; // roughly originally from 300 / 35

const BentoPie = ({
height,
width,
onClick,
sort = true,
colorTheme = 'default',
Expand Down Expand Up @@ -97,37 +104,39 @@ const BentoPie = ({
}, []);

return (
<div style={CHART_WRAPPER_STYLE}>
<PieChart height={height} width={height * CHART_ASPECT_RATIO}>
<Pie
data={data}
dataKey="value"
cx="50%"
cy="50%"
innerRadius={height / INNER_RADIUS_REDUCTION_FACTOR}
outerRadius={height / OUTER_RADIUS_REDUCTION_FACTOR}
label={renderLabel(maxLabelChars)}
labelLine={false}
isAnimationActive={false}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
onMouseOver={onHover}
activeIndex={activeIndex}
activeShape={RenderActiveLabel}
onClick={onClick}
>
{data.map((entry, index) => {
const fill = entry.name.toLowerCase() === 'missing' ? CHART_MISSING_FILL : theme[index % theme.length];
return <Cell key={index} fill={fill} />;
})}
</Pie>
<Tooltip
content={<CustomTooltip totalCount={sum} />}
isAnimationActive={false}
allowEscapeViewBox={{ x: true, y: true }}
/>
</PieChart>
</div>
<ChartWrapper>
<ResponsiveContainer width={width ?? "100%"} height={height}>
<PieChart>
<Pie
data={data}
dataKey="value"
cx="50%"
cy="50%"
innerRadius="25%"
outerRadius="55%"
label={renderLabel(maxLabelChars)}
labelLine={false}
isAnimationActive={false}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
onMouseOver={onHover}
activeIndex={activeIndex}
activeShape={RenderActiveLabel}
onClick={onClick}
>
{data.map((entry, index) => {
const fill = entry.name.toLowerCase() === 'missing' ? CHART_MISSING_FILL : theme[index % theme.length];
return <Cell key={index} fill={fill} />;
})}
</Pie>
<Tooltip
content={<CustomTooltip totalCount={sum} />}
isAnimationActive={false}
allowEscapeViewBox={{ x: true, y: true }}
/>
</PieChart>
</ResponsiveContainer>
</ChartWrapper>
);
};

Expand Down
15 changes: 15 additions & 0 deletions src/Components/Charts/ChartWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ForwardedRef, forwardRef, ReactNode } from 'react';
import { CHART_WRAPPER_STYLE } from '../../constants/chartConstants';

interface ChartWrapperProps {
children: ReactNode;
}

const ChartWrapper = forwardRef(({children}: ChartWrapperProps, ref: ForwardedRef<HTMLDivElement>) => (
<div style={CHART_WRAPPER_STYLE} ref={ref}>
{children}
</div>
));
ChartWrapper.displayName = "ChartWrapper";

export default ChartWrapper;
4 changes: 2 additions & 2 deletions src/constants/chartConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export const CHART_WRAPPER_STYLE: CSS.Properties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflowX: 'auto',
overflowY: 'hidden',
};

// bar chart
Expand All @@ -104,14 +106,12 @@ export const COUNT_TEXT_STYLE: CSS.Properties = {

// ################### CHART CONSTANTS ###################
// bar chart
export const ASPECT_RATIO = 1.2;
export const MAX_TICK_LABEL_CHARS = 15;
export const UNITS_LABEL_OFFSET = -75;
export const TICKS_SHOW_ALL_LABELS_BELOW = 11; // Below this # of X-axis ticks, force-show all labels
export const TICK_MARGIN = 5; // vertical spacing between tick line and tick label

// pie chart
export const CHART_ASPECT_RATIO = 1.4;
export const LABEL_THRESHOLD = 0.05;

// ################### UTIL CONSTANTS ###################
Expand Down
3 changes: 3 additions & 0 deletions src/types/chartTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface CategoricalChartDataWithTransforms {
// ################### COMPONENT PROPS #####################
export interface BaseChartComponentProps {
height: number;
// Width is useful to have, to force re-render / force a specific width, but it is optional.
// Otherwise, it will be set to 100%.
width?: number | string;
}

export interface BaseCategoricalChartProps extends BaseChartComponentProps, CategoricalChartDataWithTransforms {}
Expand Down
29 changes: 20 additions & 9 deletions test/js/TestBarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import React from 'react';
import { Typography } from 'antd';
import { BarChart } from '../../src/index';

const TestBarChart = () => (
<BarChart
data={[{x: "AB", y: 50}, {x: "NB", y: 75}, {x: "SB", y: 60}]}
units="management units"
onClick={(f) => {
console.log(f);
alert(JSON.stringify(f, null, 2));
}}
height={600}
/>
<>
<Typography.Title level={2}>Responsive bar chart:</Typography.Title>
<BarChart
data={[{x: "AB", y: 50}, {x: "NB", y: 75}, {x: "SB", y: 60}, {x: "AU", y: 30}, {x: "XA", y: 80}]}
units="management units"
height={600}
/>
<Typography.Title level={2}>Fixed-width bar chart:</Typography.Title>
<BarChart
data={[{x: "AB", y: 50}, {x: "NB", y: 75}, {x: "SB", y: 60}]}
units="management units"
onClick={(f) => {
console.log(f);
alert(JSON.stringify(f, null, 2));
}}
height={600}
width={960}
/>
</>
);

export default TestBarChart;