I using chartjs and react-chartjs-2 line chart for positive and negative value, for positive value I want the filled area is green, and negative area is red.
https://codesandbox.io/p/devbox/recursing-drake-v2rsj7
import React from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
} from "chart.js";
import { Line } from "react-chartjs-2";
import faker from "faker";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
);
export const options = {
responsive: true,
plugins: {
legend: {
display: false,
position: "top" as const,
},
title: {
display: false,
text: "Chart.js Line Chart",
},
},
scales: {
x: {
grid: {
display: false, // Hides X-axis grid lines
},
ticks: {
display: false, // Hides X-axis labels
},
border: {
display: false, // Hides X-axis border line
},
},
y: {
grid: {
display: false, // Hides Y-axis grid lines
},
ticks: {
display: false, // Hides Y-axis labels
},
border: {
display: false, // Hides X-axis border line
},
},
},
};
const generateChartData = () => {
const values = [3000, 4000, -1000, -2000, 2000, 3000, 5000];
const backgroundColors = values.map((val) =>
val >= 0 ? "rgba(50, 183, 73, 0.2)" : "rgba(212, 68, 90, 0.2)",
);
return {
labels: [
"Page A",
"Page B",
"Page C",
"Page D",
"Page E",
"Page F",
"Page G",
],
datasets: [
{
label: "Dataset 1",
data: values,
fill: true,
backgroundColor: backgroundColors,
borderColor: "rgb(75, 192, 192)",
tension: 0.4,
},
],
};
};
export function App() {
const data = generateChartData();
return <Line options={options} data={data} />;
}
This is expected result
Update2
I've just realized that there is a much simpler and more effective solution for the line color, and that is to set a vertical gradient for the whole height of plot, with a change of color at the y=0 height.
Unlike the solution in the previous update, it doesn't need a supplemental update
and works OK even during animation. That means that it also works OK with chart data updates. Here it is:
const values = [3000, 2000, -1000, -6000, 1000, 3000, 5000];
const lineColorPlus = "rgba(50, 183, 73, 0.75)",
lineColorMinus = "rgba(212, 68, 90, 0.75)";
const data = {
labels: [
"Page A",
"Page B",
"Page C",
"Page D",
"Page E",
"Page F",
"Page G",
],
datasets: [
{
label: 'Dataset 1',
data: values,
tension: 0.4,
segment: {
borderWidth: 4,
borderColor: function({p0DataIndex, p1DataIndex, datasetIndex, chart}){
const y0 = data.datasets[datasetIndex].data[p0DataIndex],
y1 = data.datasets[datasetIndex].data[p1DataIndex];
if(y0 >= 0 && y1 >= 0){
return lineColorPlus;
}
if(y0 <= 0 && y1 <= 0){
return lineColorMinus;
}
const yScale = chart.getDatasetMeta(datasetIndex).yScale,
ygZero = yScale.getPixelForValue(0),
ygMax = yScale.getPixelForValue(yScale.max),
ygMin = yScale.getPixelForValue(yScale.min);
const fracGradient = (ygZero - ygMin)/(ygMax - ygMin);
const ctx = chart.ctx;
const gradient = ctx.createLinearGradient(0, ygMin, 0, ygMax);
gradient.addColorStop(0, lineColorMinus);
gradient.addColorStop(fracGradient, lineColorMinus);
gradient.addColorStop(fracGradient, lineColorPlus);
gradient.addColorStop(1, lineColorPlus);
return gradient;
},
},
pointRadius: 5,
fill: {
target: 'origin',
above: "rgba(50, 183, 73, 0.5)",
below: "rgba(212, 68, 90, 0.5)"
}
}
]
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
},
};
const chart = new Chart(document.querySelector('#chart1'), config);
const interval = setInterval(
()=> {
values.forEach(
(value, i) => {values[i] = value + Math.round(Math.random() * 4 - 2) * 1000}
);
chart.update();
},
5000
);
const buttonStop = document.querySelector('#stop');
buttonStop.onclick = () => {clearInterval(interval); buttonStop.disabled = true;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div style="min-height: 80vh">
<canvas id="chart1">
</canvas>
</div>
<button id="stop">Stop</button>
Original post
To set the fill according to color, one may use the fill
property
with target
, above
and below
, as per the documentation.
Setting the colors for the line is more complicated, for as far as I know,
there is no option to do that depending on the value. So one has to
compute the colors for each segment, which involves some interpolation
for those segments that intersect y
axis.
Even so, this is linear interpolation, and if high values are used for tension
and the points are sparse (large intervals), the difference between this and
the actual cubic curve might become visible at the intersections.
Here's a possible implementation:, based on your example:
const values = [3000, 4000, -1000, -2000, 2000, 3000, 5000];
const lineColorPlus = "rgba(50, 183, 73, 0.75)",
lineColorMinus = "rgba(212, 68, 90, 0.75)";
const data = {
labels: [
"Page A",
"Page B",
"Page C",
"Page D",
"Page E",
"Page F",
"Page G",
],
datasets: [
{
label: 'Dataset 1',
data: values,
tension: 0.4,
segment: {
borderWidth: 4,
borderColor: function(dataCtx){
const {p0, p1, p0DataIndex, p1DataIndex, datasetIndex, chart} = dataCtx;
const y0 = data.datasets[datasetIndex].data[p0DataIndex],
y1 = data.datasets[datasetIndex].data[p1DataIndex];
if(y0 >= 0 && y1 >= 0){
return lineColorPlus;
}
if(y0 <= 0 && y1 <= 0){
return lineColorMinus;
}
const frac = Math.abs(y0 / (y0 - y1));
const xg0 = p0.$animations?.x?._to || p0.x,
yg0 = p0.$animations?.y?._to || p0.y,
xg1 = p1.$animations?.x?._to || p1.x,
yg1 = p1.$animations?.y?._to || p1.y;
const ctx = chart.ctx;
const gradient = ctx.createLinearGradient(xg0, yg0, xg1, yg1);
const [col1, col2] = (y0 <= 0) ? [lineColorMinus, lineColorPlus] : [lineColorPlus, lineColorMinus] ;
gradient.addColorStop(0, col1);
gradient.addColorStop(frac, col1);
gradient.addColorStop(frac, col2);
gradient.addColorStop(1, col2);
return gradient;
},
},
pointRadius: 0,
fill: {
target: 'origin',
above: "rgba(50, 183, 73, 0.5)",
below: "rgba(212, 68, 90, 0.5)"
}
}
]
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
},
};
new Chart(document.querySelector('#chart1'), config);
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div style="min-height: 60vh">
<canvas id="chart1">
</canvas>
</div>
Update
I made a more complicated variant that takes into account the bezier curve used with tension
to optimize the computation of the
gradient cutoff. One has to take into account the fact that the final Bezier coefficients are only computed after the initial animation has completed, so one has to either disable animation or to add an chart.update('none')
in the animation.onComplete
handler, as it is shown in the example below.
const _bezierInterpolation = Chart.helpers._bezierInterpolation; // note: different access for modules
const values = [3000, 2000, -1000, -6000, 1000, 3000, 5000];
const NMaxOptimization = 20, // break optimization loop after NMaxOptimization
zeroFraction = 1e-4; // break optimization loop if the bezier absolute value < zeroFraction * yAxisRange
const lineColorPlus = "rgba(50, 183, 73, 0.75)",
lineColorMinus = "rgba(212, 68, 90, 0.75)";
const data = {
labels: [
"Page A",
"Page B",
"Page C",
"Page D",
"Page E",
"Page F",
"Page G",
],
datasets: [
{
label: 'Dataset 1',
data: values,
tension: 0.8,
segment: {
borderWidth: 4,
borderColor: function({p0, p1, p0DataIndex, p1DataIndex, datasetIndex, chart}){
const y0 = data.datasets[datasetIndex].data[p0DataIndex],
y1 = data.datasets[datasetIndex].data[p1DataIndex];
if(y0 >= 0 && y1 >= 0){
return lineColorPlus;
}
if(y0 <= 0 && y1 <= 0){
return lineColorMinus;
}
const xg0 = p0.$animations?.x?._to || p0.x,
yg0 = p0.$animations?.y?._to || p0.y,
xg1 = p1.$animations?.x?._to || p1.x,
yg1 = p1.$animations?.y?._to || p1.y;
const optimize = !(p0.$animations?.x?._active || p0.$animations?.y?._active);
// don't optimize on active animation, bezier coeffs are not final, nor useful now
let frac = Math.abs(y0 / (y0 - y1));
let xgZero = xg0 + (xg1 - xg0) * frac,
ygZero = yg0 + (yg1 - yg0) * frac;
if(optimize && (chart.options.elements.line.tension || data.datasets[datasetIndex].tension)){
const p0Final = {...p0, x: xg0, y: yg0},
p1Final = {...p1, x: xg1, y: yg1};
const yScale = chart.getDatasetMeta(datasetIndex).yScale;
let {y: ygFrac} = _bezierInterpolation(p0Final, p1Final, frac);
let fracLeft = 0;
let fracRight = 1;
let ygLeft = yg0;
let ygRight = yg1;
let yForLeft = yScale.getValueForPixel(ygLeft);
let yForFrac = yScale.getValueForPixel(ygFrac);
let yForRight = yScale.getValueForPixel(ygRight);
const dy = yScale.max - yScale.min;
for(let kOpt = 0; kOpt < NMaxOptimization; kOpt++){
if(yForFrac * yForLeft <= 0){
fracRight = frac;
ygRight = ygFrac;
yForRight = yForFrac;
}
else{
fracLeft = frac;
ygLeft = ygFrac;
yForLeft = yForFrac;
}
frac = (fracRight + fracLeft) / 2;
({x: xgFrac, y: ygFrac} = _bezierInterpolation(p0Final, p1Final, frac));
yForFrac = yScale.getValueForPixel(ygFrac);
if(Math.abs(yForFrac) < zeroFraction * dy){
break;
}
}
// intersection
xgZero = xgFrac;
ygZero = ygFrac;
}
const ctx = chart.ctx;
let fracGradient = (xgZero - xg0)/(xg1 - xg0);
const gradient = ctx.createLinearGradient(xg0, ygZero, xg1, ygZero);
const [colLeft, colRight] = (y0 <= 0) ? [lineColorMinus, lineColorPlus] : [lineColorPlus, lineColorMinus] ;
gradient.addColorStop(0, colLeft);
gradient.addColorStop(fracGradient, colLeft);
gradient.addColorStop(fracGradient, colRight);
gradient.addColorStop(1, colRight);
return gradient;
},
},
pointRadius: 5,
fill: {
target: 'origin',
above: "rgba(50, 183, 73, 0.5)",
below: "rgba(212, 68, 90, 0.5)"
}
}
]
};
const config = {
type: 'line',
data: data,
options: {
animation:{
onComplete({chart, initial}){
if(initial){
chart.update('none');
}
}
},
responsive: true,
},
};
new Chart(document.querySelector('#chart1'), config);
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div style="min-height: 60vh">
<canvas id="chart1">
</canvas>
</div>