Search code examples
reactjsrecharts

How to display the last interval as a dotted line in Recharts library with same interpolation as the chart?


Is it possible to display the last interval as a dotted line in the Recharts library?

I'm working with the Recharts library in React and facing a challenge. I have a line chart with multiple intervals. What I've found is that I can display the last interval as a separate Line component. However, it's currently appearing as a straight line. I want this line to continue with the same interpolation as the rest of the chart. How can I accomplish this?

enter image description here

Here's a basic structure for a Recharts component.

<ResponsiveContainer>
   <ComposedChart data={data}>
      <CartesianGrid />
      <XAxis />
      <YAxis />
      <Tooltip />
      <Line type="monotone" dataKey="score" />
      <Line type="monotone" dataKey="median" />
      <Area type="monotone" dataKey="deviation" >
      <Line type="monotone" dataKey="prediction" strokeDasharray="6 4" />
    </ComposedChart>
 </ResponsiveContainer>

And the data example

data = [
    {
        "intervalName": "Interval 1",
        "prediction": null,
        "score": 16991.578,
        "median": 558934.61825,
        "deviation": [
            390456.78457310924,
            727412.4519268909
        ]
    },
    {
        "intervalName": "Interval 2",
        "prediction": null,
        "score": 582619.453,
        "median": 558934.61825,
        "deviation": [
            320355.6152603979,
            797513.6212396022
        ]
    },
    {
        "intervalName": "Interval 3",
        "prediction": null,
        "score": 5539.119,
        "median": 559293.0755,
        "deviation": [
            320626.4142448605,
            797959.7367551395
        ]
    },
    {
        "intervalName": "Interval 4",
        "prediction": null,
        "score": 125833.72200000001,
        "median": 409077.43374999997,
        "deviation": [
            132343.3306994175,
            685811.5368005824
        ]
    },
    {
        "intervalName": "Interval 5",
        "prediction": 76713.714,
        "score": 76713.714,
        "median": 275481.02749999997,
        "deviation": [
            16272.15931816565,
            534689.8956818343
        ]
    },
    {
        "intervalName": "Interval 6",
        "prediction": 368890.673174,
        "score": null,
        "median": 191209.251,
        "deviation": [
            -7836.152843409567,
            390254.65484340955
        ]
    }
];

Solution

  • In order to have the prediction and score fit perfectly in the smooth piecewise interpolating curve, not only do you have to set prediction values equal to score values for all the points (and make the curve for the first n-1 intervals invisible), but you also have to add the predicted point to the score values (and make its curve invisible for the last interval).

    You could only set the values of virtual prediction for only 3-4 points prior to the penultimate point (as the influence of the points before that typically vanes to infinitesimal values), but setting all points seems simpler.

    A possible idea is to create a single curve that will display score for the first n-1 intervals and prediction for the last interval. Changing the color for the last interval can be done using gradients for colors, as described here.

    Since you don't want to change the color, but the dash array, I'd go with creating two identical curves, one for the score, that has the last interval set to fully transparent color and one for the prediction, having the first n-1 interval fully transparent and the last one dashed.

    The markup would look like:

    <div style={styles}>
        <ResponsiveContainer>
            <ComposedChart data={data}>
                <defs>
                    {gradientTwoColors(
                        "hideAllButLastInterval",
                        "rgba(0,0,0,0)",
                        defaultColor,
                        lastIntervalPercent
                    )}
                    {gradientTwoColors(
                        "hideJustLastInterval",
                        defaultColor,
                        "rgba(0,0,0,0)",
                        lastIntervalPercent
                      )}
                </defs>
                <CartesianGrid />
                <XAxis />
                <YAxis />
                <Tooltip formatter={tooltipFormatter1} />
                <Line type="monotone" dataKey="score" strokeDasharray="0 100" />
                <Line type="monotone" dataKey="prediction" strokeDasharray="0 100" />
                <Line type="monotone" dataKey="median" />
                <Area type="monotone" dataKey="deviation" />
                <Line
                        name="line1_noTooltip"
                        type="monotone"
                        stroke="url('#hideJustLastInterval')"
                        dataKey={scoreOrPrediction}
                />
                <Line
                        name="line2_noTooltip"
                        type="monotone"
                        stroke="url('#hideAllButLastInterval')"
                        strokeDasharray="5 5"
                        dataKey={scoreOrPrediction}
                />
            </ComposedChart>
        </ResponsiveContainer>
    </div>
    

    Note that the first two Line components are completely invisible, they are there just to provide the values for the tooltip, while the last two Line components make the two identical curve described above, but don't give any input to the tooltip. The auxiliary functions and variables are

    const scoreOrPrediction = function (data) {
        // provides the data for the two identical curves
        return data.score !== null ? data.score : data.prediction;
    };
    const defaultColor = Line.defaultProps.stroke,
        // great care should be taken when computing lastIntervalPercent
        // the expression below works for data.length - 1 equal intervals
        // but if there are numeric x values in a "linear" axis, the formula
        // should be updated to use those values
        lastIntervalPercent = ((data.length - 2) * 100) / (data.length - 1);
    
    const gradientTwoColors = (id, col1, col2, percentChange) => (
        <linearGradient id={id} x1="0" y1="0" x2="100%" y2="0">
            <stop offset="0%" stopColor={col1} />
            <stop offset={`${percentChange}%`} stopColor={col1} />
            <stop offset={`${percentChange}%`} stopColor={`${col2}`} />
            <stop offset="100%" stopColor={col2} />
        </linearGradient>
    );
    
    const formatIfNumeric = (x) => (typeof x === typeof 1 ? x.toFixed() : x);
    const tooltipFormatter1 = (value, name) => {
        if (name.includes("noTooltip")) {
            return [];
        }
        if (Array.isArray(value)) {
            return value.map(formatIfNumeric).join(" - ");
        }
        return [formatIfNumeric(value), name];
    };
    

    Here's a codesandbox with this solution.