Blog   Home

A Practical Guide to Hacking the Recharts Library

recharts · learning · tutorial · react

photo by William Iven on Unsplash

Recently, I was assigned to a project where we needed to build a graph with some pretty lofty specs. D3 is often considered the best graphing library for frontend developers, but on a cursory glance, the documentation was a bit unclear and seemed denser than I had time to parse, given the scope and timeframe allotted. I shopped around for other graphing libraries and came across Recharts, a React-specific graphing library built on top of D3 that had been used more frequently and had a better rating than Nivo, Victory, or react-vis.

While I know good documentation for D3 exists and there are widespread use cases of it in the wild, Recharts piqued my interest. The website for it was snappy and clean, the documentation seemed pretty robust, there were a good number of customizable props, and it followed the same pattern of importing components and giving them prop values, which would reduce the amount of time I’d have to spend bootstrapping. So Recharts was the winner.

The Project

The client for this project gave us the following specs:

And this is what the graph ended up looking like:

Screenshot of a graph with red negative bars and blue positive bars

The Limitations of Recharts

While using this library to fulfill the requirements, I ran into a few roadblocks:

So How Did I Do It?

Transforming the data

In a perfect world, the data from the backend would come in exactly the correct shape to be easily consumed by Recharts graph, but alas, it did not. The data started like this:

interface ResultShape {
    readonly resultId: string
    readonly userId: string
    readonly score: number
}

const results = [
    {
        resultId: "22f0-23as9-9adf-98sfd",
        userId: "user1",
        score: 1000,
    },
    {
        resultId: "18q0-23as3-9adq-98sq",
        userId: "user1",
        score: -200,
    },
    {
        resultId: "9748-2737-1747-7759",
        userId: "user2",
        score: 1500,
    },
]

And using this function:

interface Coordinate {
    readonly label: string
    readonly value: number
}

export const convertTablesResultsToGraph = (
    results: ReadonlyArray<ResultShape>,
): ReadonlyArray<Coordinate> => {
    // create an ordered sort array of userIds
    const userIds = results.map(r => r.userId)
    const sortOrderOfUserIds = [...new Set([...userIds])]
    // make a basemap of all results by userId
    const basemap = results.reduce((acc, next): BaseMap<
        ReadonlyArray<Coordinate>
    > => {
        const id = next.userId
        const contents = !!acc[id] ? [...acc[id], next] : [next]

        const toReturn = {
            ...acc,
            [id]: contents,
        }

        return toReturn
    }, {})

    // convert this to an array of arrays.
    const basemaps: ReadonlyArray<
        ReadonlyArray<ResultShape>
    > = sortOrderOfUserIds.map(id => basemap[id])
    const coordinates: ReadonlyArray<Coordinate> = basemaps
        .map(rounds => {
            const cc: ReadonlyArray<Coordinate> = rounds.map((item, i) => {
                const { score } = item

                return {
                    label: `${i + 1}`,
                    value: score,
                }
            })
            return cc
        })
        .reduce((acc, next) => acc.concat(next), [])
    return coordinates
}

It became this:

const coordinates = [
    { label: "1", value: 2500 },
    { label: "2", value: 100 },
    { label: "3", value: -1200 },
    { label: "4", value: -4000 },
    { label: "5", value: 1500 },
]

Selecting a bar

Browsing the examples in the Recharts documentation, I came across this jsFiddle, which demonstrated to me that you can use the component’s local state to track the index of the selected bar. It ended up looking like this:

interface ResultGraphState {
    readonly selectedIndex: number | null
}

...
constructor(props: ResultGraphProps) {
    super(props)

    this.state: ResultGraphState = {
        selectedIndex: null
    }
}

...

It also demonstrated a way to make the fill of the bar change color conditionally, using the <Cell /> component. In the example, they use a ternary, but in my case, I needed a function which could output six different colors.

Note: we keep all of our colors in an appColors object as hexcode strings.

export const colorSelector = (
    value: number,
    trueValue: number,
    index: number,
    selectedIndex: number | null,
): string => {
    if (trueValue >= 2000) {
        return appColors.modalPositiveLargeValue
    }
    if (trueValue <= -2000) {
        return appColors.modalNegativeLargeValue
    }
    if (index !== selectedIndex) {
        return value > 0
            ? appColors.modalPositiveGraphValue
            : appColors.modalNegativeGraphValue
    }

    return value > 0
        ? appColors.modalPositiveGraphValueSelected
        : appColors.modalNegativeGraphValueSelected
}

I spun up the click functionality for toggling the selection of the bar in a class method and passed it into the <Bar /> component as shown in the jsFiddle.

Creating and rendering a custom tooltip

The first thing I needed for this part was a custom tooltip, which I built using styled-components and this CSS generator as a resource for giving the tooltip its signature pointy bit.

const CustomTooltip = styled.div`
    display: flex;
    justify-content: center;
    align-items: center;
    ${({ isPositive }: DisplayProps) => !isPositive && "margin-top: 10px;"}

    .tooltip-body {
        width: 100%;
        position: relative;
        background: ${appColors.tableDataOverviewBackground};
        border: 1px solid ${appColors.tableLabelText};
        text-align: center;
        padding: 1px 3px;
        border-radius: 2px;

        &:after {
            content: "";
            position: absolute;
            left: 50%;
            width: 0;
            height: 0;
            border: 10px solid transparent;
            margin-left: -10px;
            ${({ isPositive }: DisplayProps) =>
                isPositive
                    ? `bottom: 0;
            border-top-color: ${appColors.tableLabelText};
            border-bottom: 0;
            margin-bottom: -10px;`
                    : `top: 0;
            border-bottom-color: ${appColors.tableLabelText};
            border-top: 0;
            margin-top: -10px;`}
        }

        p {
            font-size: 0.75em;
            color: ${appColors.tableLabelText};
            margin: 0;
            align-self: center;
        }
    }
`

I probably could have used the blueprintjs library for the tooltip component, but I ended up making my own to ensure that it would be lightweight and not contain any additional animations or properties I didn’t need. The app was already calculating a data transform on a large swath of data, and keeping the graph performant was a key part of the feature.

Originally, I tossed the tooltip inside of the <Bar> component tags and found out the hard way that you can’t just shoehorn a non-Recharts component as a child to a Recharts component, especially given that the whole Recharts graph is actually an SVG. I tried to find a premade tooltip SVG that was free to use, but didn’t see anything that really matched the mockups or seemed easy to implement.

Upon digging through the documentation, I came across the <LabelList />component, which thankfully can take a React element as a content prop, and even came with another handy jsFiddle.

This jsFiddle shows that the function that creates the customized label receives props from the parent <LabelList /> component, which contains the x and y coordinates of where the bar sits relative to the SVG, the width and height of the bar, and the data’s value. From there, I could place the tooltip and feed it the correct data.

The only problem was trying to render the div. It wasn’t showing up, but I knew that something was being rendered based on what was happening in the Chrome dev tools tab. So I started looking into an element called <foreignObject>, which through some magic allows HTML to be nested inside an SVG tag. I added conditional rendering for selected vs. non-selected index with my selectedIndex from state, fed the component, and ended up with this:

gif of the graph being clicked on which prompts a tooltip to appear Rendering the true value of a truncated bar

Initially, I was stumped by this problem, until I remembered that the graph coordinate objects didn’t have to just be an “X” and “Y” value. The coordinate could any shape that I needed it to be, as long as I specified the dataKey prop in the library components.

I ended up updating the coordinate shape from this:

interface Coordinate {
    readonly label: string
    readonly value: number
}

to this:

interface Coordinate {
    readonly label: string
    readonly displayedValue: number
    readonly trueValue: number
}

and updated my data transform utility like so:

export const convertTablesResultsToGraph = (
    results: ReadonlyArray<ResultShape>,
): ReadonlyArray<Coordinate> => {
    ...

    const coordinates: ReadonlyArray<Coordinate> = basemaps
        .map(rounds => {
            const cc: ReadonlyArray<Coordinate> = rounds.map(
                (item, i) => {
                    const { score } = item

                    const isLargeValue = isValueTooLarge(
                        score,
                        2000,
                    )
                    const displayedValue = calculateDisplayValue(
                        score,
                        isLargeValue,
                        2000,
                    )

                    return {
                        label: `${i + 1}`,
                        displayedValue,
                        trueValue: score,
                    }
                },
            )
            return cc
        })
        .reduce((acc, next) => acc.concat(next), [])
    return coordinates
}
...

Using the updated data transformation utility, I was able to write an additional <LabelList> component to track the “true” value of the coordinate while maintaining its height at the very maximum of the graph’s domain, which ended up looking like this:

moving image of the graph scrolling to the right and opening up tooltips

Creating a static Y Axis

I will be the first to admit that this is the hackiest part of the whole process. But it performs well, and it looks pretty good, if I do say so myself.

I opened the modal containing my graph, opened the dev tools, and straight up copy-pasted the whole <g> group of lines that made up the Y-axis, the Y-axis labels, and all of the ticks into a new functional component, which looked something like this (with the bulk of it omitted because visual clutter):

export const CustomYAxis = (): JSX.Element => {
    return (
        <div className="y-axis-wrapper">
            <SVG width="56" height="360" viewBox="5 0 56 420">
                <g className="recharts-layer recharts-cartesian-axis recharts-yAxis yAxis">
                    ...
                </g>
            </SVG>
        </div>
    )
}

The SVG props for width, height, and viewBox were copied from the whole Recharts graph, and then modified to trim as much whitespace as possible. In order to maintain the same sizing and scale, I didn’t delete the original <YAxis> from the graph. I just gave it the property hide={true}.

The whole render method for the graph ended up looking like this:

return (
    <div className="graph-wrapper">
        <CustomYAxis />
        <BarChart {...props}>
            ...
        </BarChart>
    </div>
)

and the CSS (styled from the parent component) looked like this:

> div > .graph-wrapper {
    display: flex;
    flex-direction: row;
    padding-top: 1em;
    max-height: 435px;
    overflow-x: scroll;
    overflow-y: hidden;

    > .y-axis-wrapper {
        background: white;
        position: sticky;
        left: 0;
        z-index: 1003;
    }
}

The result?

Conclusion

In general, I’d say that while Recharts lacked a lot of features I needed to fulfill the specs of my project, it’s still a good entry-level graphing library. The animations are super smooth (though I turned them off to prevent any potential lag), and there are a lot of component props to customize the graph that I’d be interested in exploring in future projects.

The most important, and the most fun part, was getting the chance to flex my problem-solving skills. I enjoyed having the opportunity to come up with ways around the limitations I encountered, and I hope that the documentation of my process helps other people running into problems with Recharts or helps the devs improve the library in the next version.

Anything I missed in my analysis or could’ve done better? Feel free to reach out in the comments!

We are the creators of Shoutbase, a tool to optimize how software developers track and spend their time.

Want to know when we post new articles?