Skip to content

Commit

Permalink
feat(charts): Added Histogram (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
SanjeevLakhwani authored May 27, 2024
2 parents 8a052f6 + b6d2732 commit 41d35fa
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 144 deletions.
137 changes: 137 additions & 0 deletions src/Components/Charts/BaseBarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useCallback } from 'react';
import {
Bar,
BarChart,
BarProps,
CartesianGrid,
Cell,
Label,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import {
TOOL_TIP_STYLE,

Check failure on line 15 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

'"../../constants/chartConstants"' has no exported member named 'TOOL_TIP_STYLE'. Did you mean 'TOOLTIP_STYLE'?

Check failure on line 15 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

'"../../constants/chartConstants"' has no exported member named 'TOOL_TIP_STYLE'. Did you mean 'TOOLTIP_STYLE'?

Check failure on line 15 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

'"../../constants/chartConstants"' has no exported member named 'TOOL_TIP_STYLE'. Did you mean 'TOOLTIP_STYLE'?
COUNT_STYLE,
LABEL_STYLE,
MAX_TICK_LABEL_CHARS,
TITLE_STYLE,
TICKS_SHOW_ALL_LABELS_BELOW,
UNITS_LABEL_OFFSET,
TICK_MARGIN,
COUNT_KEY,
} from '../../constants/chartConstants';

import type { BaseBarChartProps, CategoricalChartDataItem, TooltipPayload } from '../../types/chartTypes';
import { 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) {
return tickLabel;
}
return `${tickLabel.substring(0, MAX_TICK_LABEL_CHARS)}...`;
};

const BAR_CHART_MARGINS = { top: 10, bottom: 100, right: 20 };

const BaseBarChart: React.FC<BaseBarChartProps> = ({
height,
width,
units,
title,
onClick,
chartFill,
otherFill,
...params
}) => {
const t = useChartTranslation();

const fill = (entry: CategoricalChartDataItem, index: number) =>
entry.x === 'missing' ? otherFill : chartFill[index % chartFill.length];

const data = useTransformedChartData(params, true);

const totalCount = data.reduce((sum, e) => sum + e.y, 0);

const onHover: BarProps['onMouseEnter'] = useCallback(
(_data, _index, e) => {
const { target } = e;
if (onClick && target) (target as SVGElement).style.cursor = 'pointer';
},
[onClick]
);

if (data.length === 0) {
return <NoData height={height} />;
}

// Regarding XAxis.ticks below:
// The weird conditional is added from https://github.com/recharts/recharts/issues/2593#issuecomment-1311678397
// Basically, if data is empty, Recharts will default to a domain of [0, "auto"] and our tickFormatter trips up
// on formatting a non-string. This hack manually overrides the ticks for the axis and blanks it out.
// - David L, 2023-01-03
return (
<ChartWrapper>

Check failure on line 78 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

Property 'responsive' is missing in type '{ children: Element[]; }' but required in type 'ChartWrapperProps'.

Check failure on line 78 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

Property 'responsive' is missing in type '{ children: Element[]; }' but required in type 'ChartWrapperProps'.

Check failure on line 78 in src/Components/Charts/BaseBarChart.tsx

View workflow job for this annotation

GitHub Actions / Release

Property 'responsive' is missing in type '{ children: Element[]; }' but required in type 'ChartWrapperProps'.
<div style={TITLE_STYLE}>{title}</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_KEY]} offset={-10} position="left" angle={270} />
</YAxis>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<Tooltip content={<BarTooltip totalCount={totalCount} />} />
<Bar dataKey="y" isAnimationActive={false} onClick={onClick} onMouseEnter={onHover} maxBarSize={70}>
{data.map((entry, index) => (
<Cell key={entry.x} fill={fill(entry, index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</ChartWrapper>
);
};

const BarTooltip = ({
active,
payload,
totalCount,
}: {
active?: boolean;
payload?: TooltipPayload;
totalCount: number;
}) => {
if (!active) {
return null;
}

const name = (payload && payload[0]?.payload?.x) || '';
const value = (payload && payload[0]?.value) || 0;
const percentage = totalCount ? Math.round((value / totalCount) * 100) : 0;

return (
<div style={TOOL_TIP_STYLE}>
<p style={LABEL_STYLE}>{name}</p>
<p style={COUNT_STYLE}>
{value} ({percentage}%)
</p>
</div>
);
};

export default BaseBarChart;
130 changes: 7 additions & 123 deletions src/Components/Charts/BentoBarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,13 @@
import React, { useCallback } from 'react';
import {
Bar,
BarChart,
BarProps,
CartesianGrid,
Cell,
Label,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import type { TooltipProps } from 'recharts';
import React from 'react';
import { BarChartProps } from '../../types/chartTypes';

import {
TOOLTIP_STYLE,
TOOLTIP_OTHER_PROPS,
COUNT_STYLE,
LABEL_STYLE,
MAX_TICK_LABEL_CHARS,
TITLE_STYLE,
TICKS_SHOW_ALL_LABELS_BELOW,
UNITS_LABEL_OFFSET,
TICK_MARGIN,
COUNT_KEY,
} from '../../constants/chartConstants';
import { useChartTheme } from '../../ChartConfigProvider';
import BaseBarChart from './BaseBarChart';

import type { BarChartProps, CategoricalChartDataItem, TooltipPayload } from '../../types/chartTypes';
import { useChartTheme, useChartTranslation } from '../../ChartConfigProvider';
import NoData from '../NoData';
import { useTransformedChartData } from '../../util/chartUtils';
import ChartWrapper from './ChartWrapper';
const BentoBarChart: React.FC<BarChartProps> = ({ colorTheme = 'default', ...params }) => {
const { fill: chartFill, other: otherFill } = useChartTheme().bar[colorTheme];

const tickFormatter = (tickLabel: string) => {
if (tickLabel.length <= MAX_TICK_LABEL_CHARS) {
return tickLabel;
}
return `${tickLabel.substring(0, MAX_TICK_LABEL_CHARS)}...`;
};

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, other } = useChartTheme().bar[colorTheme];

const fill = (entry: CategoricalChartDataItem, index: number) =>
entry.x === 'missing' ? other : chartFill[index % chartFill.length];

const data = useTransformedChartData(params, true);

const totalCount = data.reduce((sum, e) => sum + e.y, 0);

const onHover: BarProps['onMouseEnter'] = useCallback(
(_data, _index, e) => {
const { target } = e;
if (onClick && target) (target as SVGElement).style.cursor = 'pointer';
},
[onClick]
);

if (data.length === 0) {
return <NoData height={height} />;
}

// Regarding XAxis.ticks below:
// The weird conditional is added from https://github.com/recharts/recharts/issues/2593#issuecomment-1311678397
// Basically, if data is empty, Recharts will default to a domain of [0, "auto"] and our tickFormatter trips up
// on formatting a non-string. This hack manually overrides the ticks for the axis and blanks it out.
// - David L, 2023-01-03
return (
<ChartWrapper responsive={typeof width !== 'number'}>
<div style={TITLE_STYLE}>{title}</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_KEY]} offset={-10} position="left" angle={270} />
</YAxis>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<Tooltip {...TOOLTIP_OTHER_PROPS} content={<BarTooltip totalCount={totalCount} />} />
<Bar dataKey="y" isAnimationActive={false} onClick={onClick} onMouseEnter={onHover} maxBarSize={70}>
{data.map((entry, index) => (
<Cell key={entry.x} fill={fill(entry, index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</ChartWrapper>
);
};

interface BarTooltipProps extends TooltipProps<number, string> {
payload?: TooltipPayload;
totalCount: number;
}

const BarTooltip = ({ active, payload, totalCount }: BarTooltipProps) => {
if (!active) {
return null;
}

const name = (payload && payload[0]?.payload?.x) || '';
const value = (payload && payload[0]?.value) || 0;
const percentage = totalCount ? Math.round((value / totalCount) * 100) : 0;

return (
<div style={TOOLTIP_STYLE}>
<p style={LABEL_STYLE}>{name}</p>
<p style={COUNT_STYLE}>
{value} ({percentage}%)
</p>
</div>
);
return <BaseBarChart chartFill={chartFill} otherFill={otherFill} {...params} />;
};

export default BentoBarChart;
13 changes: 13 additions & 0 deletions src/Components/Charts/BentoHistogram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { HistogramProps } from '../../types/chartTypes';

import { useChartTheme } from '../../ChartConfigProvider';
import BaseBarChart from './BaseBarChart';

const BentoHistogram: React.FC<HistogramProps> = ({ colorTheme = 'default', ...params }) => {
const { fill: chartFill, other: otherFill } = useChartTheme().histogram[colorTheme];

return <BaseBarChart chartFill={chartFill} otherFill={otherFill} {...params} />;
};

export default BentoHistogram;
16 changes: 16 additions & 0 deletions src/constants/chartConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const COLORS: HexColor[] = [
'#3B3EAC',
];

export const NEW_CHART_COLORS: HexColor[] = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#2D9CDB'];

export const BAR_CHART_FILL = '#4575b4';
export const CHART_MISSING_FILL = '#bbbbbb';

Expand All @@ -51,12 +53,26 @@ export const DEFAULT_CHART_THEME: ChartTheme = {
fill: COLORS,
other: CHART_MISSING_FILL,
},
new: {
fill: NEW_CHART_COLORS,
other: CHART_MISSING_FILL,
},
},
bar: {
default: {
fill: [BAR_CHART_FILL],
other: CHART_MISSING_FILL,
},
new: {
fill: NEW_CHART_COLORS,
other: CHART_MISSING_FILL,
},
},
histogram: {
default: {
fill: [BAR_CHART_FILL],
other: CHART_MISSING_FILL,
},
},
};

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

// Categorical charts
export { default as BarChart } from './Components/Charts/BentoBarChart';
export { default as Histogram } from './Components/Charts/BentoHistogram';
export { default as PieChart } from './Components/Charts/BentoPie';

// Maps are not included in index.ts - instead, they need to be included from `bento-charts/maps`.
Expand Down
16 changes: 13 additions & 3 deletions src/types/chartTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type ChartTypeContext = {
};
export type ChartTheme = {
pie: ChartTypeContext,
bar: ChartTypeContext
bar: ChartTypeContext,
histogram: ChartTypeContext
};

export type FilterCallback<T> = (value: T, index: number, array: T[]) => boolean;
Expand Down Expand Up @@ -76,9 +77,18 @@ export interface PieChartProps extends BaseCategoricalChartProps {
maxLabelChars?: number;
}

export interface BarChartProps extends BaseCategoricalChartProps {
colorTheme?: keyof ChartTheme['bar'];
export interface BaseBarChartProps extends BaseCategoricalChartProps {
chartFill: HexColor[];
otherFill: HexColor;
title?: string;
units: string;
onClick?: BarProps['onClick'];
}

export interface BarChartProps extends Omit<BaseBarChartProps, 'chartFill' | 'otherFill'> {
colorTheme?: keyof ChartTheme['bar'];
}
export interface HistogramProps extends Omit<BaseBarChartProps, 'chartFill' | 'otherFill'> {
colorTheme?: keyof ChartTheme['bar'];
}

2 changes: 2 additions & 0 deletions test/js/TestBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const TestBarChart: React.FC = () => {
units="management units"
height={sizeStateFixed.height}
width={sizeStateFixed.width}
colorTheme="new"
/>
</ResizableCard>
<ResizableCard title="Responsive Bar Chart" sizeState={sizeStateResponsive} onSizeChange={setSizeStateResponsive}>
Expand All @@ -38,6 +39,7 @@ const TestBarChart: React.FC = () => {
]}
units="management units"
height={sizeStateResponsive.height}
colorTheme="new"
/>
</ResizableCard>
</Space>
Expand Down
Loading

0 comments on commit 41d35fa

Please sign in to comment.