How to use segment property to color line / border color based on value in chart js?

Viewed 699

I am trying to create a chart with line/border color with different color based on value, like this image, so color change above and below a certain point.

enter image description here

I have already went through the segment sample on chartjs docs (https://www.chartjs.org/docs/latest/samples/line/segments.html), but this is using skip and points, whereas I am trying to do this with value, which I can't figure out.

Here is my code, it's in ejs file (only diff from js is variable passed in <%- %> over ${}) :

<canvas id="LineChart" height="100"></canvas>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.min.js"
    integrity="sha512-asxKqQghC1oBShyhiBwA+YgotaSYKxGP1rcSYTDrB0U6DxwlJjU59B67U8+5/++uFjcuVM8Hh5cokLjZlhm3Vg=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>


<script>
var ctx=document.getElementById( 'LineChart' ).getContext( "2d" );

var myChart=new Chart( ctx, {
                type: 'line',
                data: {
                    labels: <%- JSON.stringify( labels )%>,
                    datasets: [ {
                        label: ['a','b','c','d'],
                        data: ['1','2','3','4'],
                        backgroundColor:'rgba(54, 162, 235, 0.5)',
                        borderColor: rgb( 255, 255, 255 ), 
                        segment: {
                           borderColor: function black() {
                               if ( <%=maxValue%> >99) {
                                     return 'black';
                                 }
                             if ( <%=minValue%> >99) {
                                 return 'red';
                             }
                        }
                    },
}],
}
});
</script>

Here is the related sample code on chartjs website, (for quick reference) :

const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;

const down = (ctx, value) => ctx.p0.parsed.y > ctx.p1.parsed.y ? value : undefined;

borderColor: ctx => skipped(ctx, 'rgb(0,0,0,0.2)') || down(ctx, 'rgb(192,75,75)')

I will be really grateful for any help! Thanks a lot.

Update: I was able to figure this out mostly (I can make a linear gradient with color stops on certain value), but the problem I am facing is in selecting x/y-axis to fetch pixel for value, as I can't select chart properly, I made this fiddle with comment where I am facing the issue, it would be great help if someone can take a look and suggest me something, here is the fiddle : jsfiddle.net/baozkrw1/1

Also, here is the fiddle code for quick view :

HTML :

<h1>Chart One</h1>
<canvas id="LineChart" height="100"></canvas>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.min.js"
    integrity="sha512-asxKqQghC1oBShyhiBwA+YgotaSYKxGP1rcSYTDrB0U6DxwlJjU59B67U8+5/++uFjcuVM8Hh5cokLjZlhm3Vg=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>


JS :

var ctx = document.getElementById('LineChart').getContext("2d");
let width, height, gradient;

function getGradient(ctx, chartArea) {
  const chartWidth = chartArea.right - chartArea.left;
  const chartHeight = chartArea.bottom - chartArea.top;

  if (gradient === null || width !== chartWidth || height !== chartHeight) {
    width = chartWidth;
    height = chartHeight;
    let y = 27; // I want this to be the pixel for value I pass, instead of hard value

    // but I can't select y-axis like this:
    // var yaxis=y.scales[ 'y-axis-0' ];
    // as it says undefined, and if I do like this : 
    //  const { ctx, canvas, scales: { x, y } }=chart;
    // var xaxis=x.scales[ 'x-axis-0' ];
    // It says chart not defined, which I think is because I am not making this a function and passing chart, and I can't figure out how to do it right.


    gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
    gradient.addColorStop(y / chartHeight, 'black');
    gradient.addColorStop(0.6, 'yellow');

    gradient.addColorStop(0.9, 'red');

    gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
    gradient.addColorStop(y / chartHeight, 'black');
    gradient.addColorStop(0.6, 'yellow');

    gradient.addColorStop(0.9, 'red');
  }
  return gradient;
}
var myChart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['10', '20', '30', '40', '50'],
    datasets: [{
      label: 'Hello',
      data: ['1', '2', '3', '4', '5'],
      backgroundColor: 'rgba(255, 255, 78, 1)',
      borderColor: function(context, options) {
        const chart = context.chart;
        const {
          ctx,
          chartArea
        } = chart;
        if (!chartArea) {
          return null;
        }
        return getGradient(ctx, chartArea);
      },
    }]
  },
});

1 Answers

You will get the both points on the start and end of the line segment the function is evaluating for as p0 and p1 in here you can access the y value like this: val = ctx.p0.parsed.y;

Live example:

var options = {
  type: 'line',
  data: {
    labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
    datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      segment: {
        borderColor: (ctx) => {
          val = ctx.p0.parsed.y;
          return val >= 15 ? 'green' : val >= 10 ? 'blue' : val >= 5 ? 'pink' : 'purple'
        }
      }
    }]
  },
  options: {}
}

var ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
  <canvas id="chartJSContainer" width="600" height="400"></canvas>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.js"></script>
</body>

EDIT:

As per my comment you were pretty close with your pixelForValue, you just had to pass the context also to your gradiant function and dont use the v2 default ID's for the scales:

borderColor: function(context, options) {
  const chart = context.chart;
  const {
    ctx,
    chartArea
  } = chart;
  if (!chartArea) {
    return null;
  }
  return getGradient(ctx, chartArea, chart);
}

function getGradient(ctx, chartArea, chart) {
  const chartWidth = chartArea.right - chartArea.left;
  const chartHeight = chartArea.bottom - chartArea.top;

  if (gradient === null || width !== chartWidth || height !== chartHeight) {
    width = chartWidth;
    height = chartHeight;
    let y2 = 27; // I want this to be the pixel for value I pass, instead of hard value
    
    const { scales: { x, y } } = chart;

    console.log(x.getPixelForValue(20), x.getPixelForValue(4))
    


    gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
    gradient.addColorStop(y2 / chartHeight, 'black');
    gradient.addColorStop(0.6, 'yellow');

    gradient.addColorStop(0.9, 'red');

    gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
    gradient.addColorStop(y2 / chartHeight, 'black');
    gradient.addColorStop(0.6, 'yellow');

    gradient.addColorStop(0.9, 'red');
  }
  return gradient;
}

But you can also choose to make an array of objects with the value and color and use that to make your gradiant:

var ctx = document.getElementById('LineChart').getContext("2d");
let width, height, gradient;

const getcolorStop = (val, max) => (val / max);
const colors = [{
    val: 2,
    color: 'pink',
  },
  {
    val: 12,
    color: 'purple',
  },
  {
    val: 20,
    color: 'green',
  }
]

function getGradient(ctx, chartArea, chart) {
  const chartWidth = chartArea.right - chartArea.left;
  const chartHeight = chartArea.bottom - chartArea.top;

  if (gradient === null || width !== chartWidth || height !== chartHeight) {
    width = chartWidth;
    height = chartHeight;

    const {
      scales: {
        y: {
          max
        }
      }
    } = chart;

    gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);

    colors.forEach((color) => {
      if (color.val > max) {
        color.val = max;
      }

      gradient.addColorStop(getcolorStop(color.val, max), color.color);
    });
  }
  return gradient;
}
var myChart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['10', '20', '30', '40', '50'],
    datasets: [{
      label: 'Hello',
      data: [12, 19, 3, 5, 2, 3],
      backgroundColor: 'rgba(255, 255, 78, 1)',
      borderColor: function(context, options) {
        const chart = context.chart;
        const {
          ctx,
          chartArea
        } = chart;
        if (!chartArea) {
          return null;
        }
        return getGradient(ctx, chartArea, chart);
      },
    }]
  },
});
<h1>Chart One</h1>
<canvas id="LineChart" height="100"></canvas>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>

Related