Hex Core charts library.
npm i @hex-insights/charts
This component is not a standalone packages, but instead provides useful
functionality for the recharts package.
Because of the way recharts is organized, there is no way to wrap most components.
While this seems like a huge drawback, they do have some reasoning
for this design. Also, the recharts package is still under development, and
is not supported by some huge tech corporation. As a result, if there is some
problem with recharts, instead of opening yet another issue, we should try to
find the problem and open a PR, possibly even try to become a maintainer.
A related issue is that inline maps will often fail to render. Instead,
predefine the mapped values into a variable and then render as a child.
A minimal line chart.
import { LineChart, Line } from "recharts";
import { defaultLineProps } from "@hex-insights/charts";
const data = [
{ value: 0 },
{ value: 69 },
{ value: 109 },
{ value: 138 },
{ value: 160 },
{ value: 179 },
];
function App() {
return (
<LineChart width={400} height={300} data={data}>
<Line {...defaultLineProps} dataKey="value" />
</LineChart>
);
}
Notice that data is sorted according to the order it is given, not based on an x value. So data needs to be sorted ahead of time.
Another example, this time with axes.
const data = [
{
xAxis: 1,
yAxis: 100,
},
{
xAxis: 2,
yAxis: 141,
},
{
xAxis: 3,
yAxis: 173,
},
];
function App() {
return (
<LineChart width={400} height={300} data={data}>
<XAxis dataKey="xValue" />
<YAxis />
<Line {...defaultLineProps} dataKey="yValue" />
</LineChart>
);
}
Finally an example with all the bells and whistles we want in a standard line
chart. Since the underlying components are almost always svg elements, you can
pass standard svg props to most components in recharts. Most commonly these
will be fill, stroke, strokeWidth, and strokeDashArray.
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
import { defaultLineProps } from "./Modules/Props/Line";
const data = [...];
function App() {
return (
<LineChart width={400} height={300} data={data}>
<XAxis dataKey="xValue" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip isAnimationActive={false} />
<Legend />
<Line
{...defaultLineProps}
dataKey="yValue"
stroke="#aec"
strokeWidth={3}
/>
</LineChart>
);
}
As mentioned earlier, when defining multiple child components, special care must
be taken. If the keys are computed or otherwise can't be specified ahead of
time, we need to use a map. This map cannot directly appear as a child of a
chart.
Here we also see an example of using the useColor hook.
import { defaultLineProps, useColors } from "@hex-insights/charts";
const data = [
{ xValue: 1, yValue1: 0, yValue2: 100 },
{ xValue: 2, yValue1: 69, yValue2: 141 },
{ xValue: 3, yValue1: 109, yValue2: 173 },
{ xValue: 4, yValue1: 138, yValue2: 200 },
{ xValue: 5, yValue1: 160, yValue2: 223 },
{ xValue: 6, yValue1: 179, yValue2: 244 },
];
const valueKeys = ["yValue1", "yValue2"];
function App() {
const getColor = useColors("pastel1");
// predefined lines
const lines = React.useMemo(
() =>
valueKeys.map((dataKey, i) => (
<Line
{...defaultLineProps}
key={`${dataKey}-${i}`}
dataKey={dataKey}
stroke={getColor(i)}
strokeWidth={3}
/>
)),
[getColor]
);
return (
<LineChart width={400} height={300} data={data}>
{lines}
</LineChart>
);
}
Adding Labels is very easy in recharts. However, we also want to make sure
labels are visible on dark or bright background colors. For this, we can use the
isHexColorLight helper function from the @hex-insights/core package.
Adding a label is as simple as including the LabelList component.
function App() {
const getColor = useColor("paired");
const bars = React.useMemo(
() =>
["value1", "value2"].map((dataKey, i) => (
<Bar
{...defaultStackedBarProps}
dataKey={dataKey}
fill={getColor(i)}
>
<LabelList
dataKey={dataKey}
fill={isHexColorLight(getColor(i)) ? "black" : "white"}
/>
</Bar>
)),
[getColor]
);
return (
<BarChart width={400} height={300} data={data}>
{bars}
</BarChart>
);
}
There is one Unfortunate missing feature: hiding labels if they are too small. However this could be circumvented by adding a custom label.
Pie charts works slightly differently from any other chart type. Instead of
passing data to the PieChart component and then picking out the key with
the dataKey prop on Pie, both data and dataKey are passed to the Pie
component.
<PieChart width={400} height={300}>
<Pie data={data} dataKey="value1" fill="#f00" label />
</PieChart>
In order to have distinct colors per slice, we need to use the Cell component.
function App() {
const getColor = useColor("tableau10");
const cells = React.useMemo(
() => data.map((_, i) => <Cell fill={getColor(i)} />),
[getColor]
);
return (
<PieChart width={400} height={300}>
<Pie {...defaultPieProps} data={data} dataKey="value">
{cells}
</Pie>
</PieChart>
);
}
We can make "donut" charts by adjusting the innerRadius of a pie chart.
Here is an example of a more stylized "donut" pie chart with some other features as well.
<PieChart width={400} height={300}>
<Pie
{...defaultPieProps}
data={data}
startAngle={90} // start at the top
endAngle={450} // end after going 360 degrees
paddingAngle={2}
cornerRadius={5}
innerRadius="50%"
dataKey="value"
>
{cells}
</Pie>
</PieChart>
One extremely nice feature that "just works" are brushes.
<BarChart data={data}>
<Brush dataKey="xValue" />
{bars}
</BarChart>
recharts handles resizing nicely out of the box. For example, to make a chart
with a fixed aspect ratio but dynamic width, we just need to use the
ResponsiveContainer component.
<ResponsiveContainer width="100%" aspect={16/9}>
<BarChart data={data}>
{bars}
</BarChart>
</ResponsiveContainer>
There are lots of other smaller components which can be seen on the recharts api page. Unfortunately the exact api is slightly out of date, but the components themselves don't seem to be.
You can even mix in svg features to define e.g. gradients.
For example, we can build a simple inline stock chart.
const data = [...Array(50)].map((_, k) => ({
price: Math.floor(100 * Math.sin(0.18 * k) + 25 * Math.sin(123 * k)),
}));
function App() {
const gradientOffset = React.useMemo(() => {
const dataMax = Math.max(...data.map(({ price }) => price));
const dataMin = Math.min(...data.map(({ price }) => price));
if (dataMax <= 0) {
return 0;
}
if (dataMin >= 0) {
return 1;
}
return dataMax / (dataMax - dataMin);
}, []);
return (
<ResponsiveContainer width={100} aspect={2}>
<AreaChart
margin={{
left: 0,
right: 0,
top: 0,
bottom: 0,
}}
data={data}
>
<defs>
<linearGradient id="fillColor" x1="0" y1="0" x2="0" y2="1">
<stop offset={0} stopColor="green" stopOpacity={1} />
<stop
offset={gradientOffset}
stopColor="#fff"
stopOpacity={0}
/>
<stop offset={1} stopColor="red" stopOpacity={1} />
</linearGradient>
<linearGradient
id="strokeColor"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset={gradientOffset}
stopColor="green"
stopOpacity={1}
/>
<stop
offset={gradientOffset}
stopColor="red"
stopOpacity={1}
/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="price"
fill="url(#fillColor)"
stroke="url(#strokeColor)"
strokeWidth={1}
dot={false}
/>
<ReferenceLine
y={0}
stroke={gradientOffset < 0.5 ? "red" : "green"}
strokeDasharray="3 2"
/>
</AreaChart>
</ResponsiveContainer>
);
}