Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to set different line colors based on marker values and color in chart js line graph #11204

Closed
ttvettuparambil opened this issue Mar 28, 2023 · 31 comments · Fixed by #11217

Comments

@ttvettuparambil
Copy link

ttvettuparambil commented Mar 28, 2023

Expected behavior

The line chart should render with the line color changing based on the marker colors.
This is an example screenshot of what the output should be.
https://pasteboard.co/6PKRCJzsaKa6.png
Currently the code that I have written using chart JS (v 4.2.1) is not rendering in the browser.
I am trying to do this using vanilla javascript. I am welcome to any suggestions. If there is an alternate method, kindly suggest it to me also.

Current behavior

The code is not rendering in the frontend and the there is error in console window. (screenshot attached below.)
https://pasteboard.co/Rma2XuY1jAz7.png

Reproducible sample

https://codepen.io/Thomas-1/pen/oNPVqQp

Optional extra steps/info to reproduce

No response

Possible solution

No response

Context

This is an example screenshot of what I am trying to achieve.
https://pasteboard.co/HgPrWMUziKIS.png

chart.js version

v4.2.1

Browser name and version

Microsoft Edge Version 111.0.1661.44 (Official build) (64-bit)

Link to your project

No response

@stockiNail
Copy link
Contributor

@ttvettuparambil I think you could the segment option (instead of custom plugin) in line chart: https://www.chartjs.org/docs/latest/charts/line.html#segment

@stockiNail
Copy link
Contributor

@ttvettuparambil another possible solution is to use chartjs-plugin-gradient.

@ttvettuparambil
Copy link
Author

@ttvettuparambil I think you could the segment option (instead of custom plugin) in line chart: https://www.chartjs.org/docs/latest/charts/line.html#segment

Hello stockiNail, thank you for the reply. I have managed to use segments to bring colors in different segments but still have not gotten the required result.

currently I have modified my code as can be seen in the link below.
https://pastebin.com/JdFtjE4s

I am trying to use ctx object to access p0 and p1 points and give a gradient.addColorStop to p0 and p1 points so that theoretically the gradient should appear in each segment depending on the color of the points, but unfortunately let gradient = ctx.createLinearGradient(ctx.p0.x, ctx.p0.y, ctx.p1.x, ctx.p1.y) in test() is having an error in console although I can access p1 and p1 objects just fine.
Could you please take a look into the above pastebin url and see if I am missing anything. The chart designed using my code currently looks like this (screenshot attached below).
https://pasteboard.co/QoxILiee5MVP.png

@ttvettuparambil
Copy link
Author

const test = (ctx) => {
// let gradient = ctx.createLinearGradient(ctx.p0.x, ctx.p0.y, ctx.p1.x, ctx.p1.y);
gradient.addColorStop(0, ctx.p0.options.backgroundColor);
gradient.addColorStop(1, ctx.p1.options.backgroundColor);
console.log(ctx p0 color::::>,ctx.p0.options.backgroundColor);
console.log(ctx p1 color::::>,ctx.p1.options.backgroundColor);
return gradient;
}
When I uncomment the first line in the function, I am getting "ctx.createLinearGradient is not a function" error in browser console. Could somebody tell me why it is like that.

1 similar comment
@ttvettuparambil
Copy link
Author

const test = (ctx) => {
// let gradient = ctx.createLinearGradient(ctx.p0.x, ctx.p0.y, ctx.p1.x, ctx.p1.y);
gradient.addColorStop(0, ctx.p0.options.backgroundColor);
gradient.addColorStop(1, ctx.p1.options.backgroundColor);
console.log(ctx p0 color::::>,ctx.p0.options.backgroundColor);
console.log(ctx p1 color::::>,ctx.p1.options.backgroundColor);
return gradient;
}
When I uncomment the first line in the function, I am getting "ctx.createLinearGradient is not a function" error in browser console. Could somebody tell me why it is like that.

@ttvettuparambil
Copy link
Author

On logging the ctx object in console window for both segment{ borderColor } as well as datasets { borderColor } I have found that addcolorstop property is not available in segment{ borderColor }. So I guess segment method to add gradient for line is out of the question. Can you show me as to how to implement chartjs-plugin-gradient in vanilla js.
Also related question, how to make segment line color same as that of p1 point using segments ?

@ttvettuparambil
Copy link
Author

I have found the solution (kind of.) Since gradient is not possible in ctx (context) object in borderColor in segments object, I decided to apply a solid color for each segment in the graph with the color changing based on the ctx.p1.options.backgroundColor value. This is the code that I have used to acheive this.
It now looks something like this.
https://pasteboard.co/EbJAm3uL4xji.png

function drawCustomScoreChart(panel) {
vals = setDataForReadinessScore()
var data = vals["y"];
console.log(data is:::::::>,data);
var colors = getReadinessScoreColors()
console.log(colors::::::::::>,colors);
const test = (ctx) => {
console.log(ctx object::::::>,ctx);
console.log(ctx p0 color::::>,ctx.p0.options.backgroundColor);
console.log(ctx p1 color::::>,ctx.p1.options.backgroundColor);
return ctx.p1.options.backgroundColor;
// return gradient;
}

  var myChart = new Chart(panel, {
    type: 'line',
    data: {
      labels: vals["x"],
      datasets: [{
        label: 'Sales',
        data: data,
        tension:0.4,
        borderColor:'#000',
        pointBackgroundColor:colors,
        borderWidth: 3,
        segment: {

            borderColor: ctx => test(ctx),

        }
      }],
     
    },
    options: {
        maintainAspectRatio: false,
    //   aspectRatio: 3/1,
      responsive: true,
      showScale: false,
      scales: {
        y: {
            beginAtZero: true
        },
        x: {
            beginAtZero:true
        },  
        yAxes: [{
          ticks: {
            beginAtZero: true
          }
        }],
      },
    //   extra code
    plugins: {
        legend: {
          display: false
        }
      },
    },
 
  });

}
I would like it if gradient option would be applied by default to chart.js library.

@ttvettuparambil
Copy link
Author

@ttvettuparambil another possible solution is to use chartjs-plugin-gradient.

@stockiNail @can you give me a working example of this plugin along with a quick summary on the plugin options ?

@stockiNail
Copy link
Contributor

On logging the ctx object in console window for both segment{ borderColor } as well as datasets { borderColor } I have found that addcolorstop property is not available in segment{ borderColor }. So I guess segment method to add gradient for line is out of the question. Can you show me as to how to implement chartjs-plugin-gradient in vanilla js. Also related question, how to make segment line color same as that of p1 point using segments ?

@ttvettuparambil apologize for late reply but I was busy on other stuff.
Well, the ctx that you are getting as argument is not the context of the canvas but the context of the scriptable option.
See the doc: https://www.chartjs.org/docs/latest/charts/line.html#segment

To have the canvas context, you should access to the chart instance, as following:

const canvasContext = ctx.chart.ctx;
canvasContext.createLineagGradient...

@stockiNail
Copy link
Contributor

@ttvettuparambil another possible solution is to use chartjs-plugin-gradient.

@stockiNail @can you give me a working example of this plugin along with a quick summary on the plugin options ?

Yes, I can but let me take time a bit. But having a look to your code, I see (but maybe I'm wrong) that the gradient must be applied to the single segment and based on the values of p0 and p1, therefore the plugin is not a good solution for this use case because the plugin is applied to the whole line based on values of dataset.

@stockiNail
Copy link
Contributor

const test = (ctx) => {
// let gradient = ctx.createLinearGradient(ctx.p0.x, ctx.p0.y, ctx.p1.x, ctx.p1.y);
gradient.addColorStop(0, ctx.p0.options.backgroundColor);
gradient.addColorStop(1, ctx.p1.options.backgroundColor);
console.log(ctx p0 color::::>,ctx.p0.options.backgroundColor);
console.log(ctx p1 color::::>,ctx.p1.options.backgroundColor);
return gradient;
}

I didn't test it but it should be:

const test = (context) => {
  const {chart, p0, p1} = context;
  const ctx = chart.ctx;
  let gradient = ctx.createLinearGradient(p0.x, p0.y, p1.x, p1.y);
  gradient.addColorStop(0, p0.options.backgroundColor);
  gradient.addColorStop(1, p1.options.backgroundColor);
  return gradient;
}

@ttvettuparambil
Copy link
Author

const test = (ctx) => {
// let gradient = ctx.createLinearGradient(ctx.p0.x, ctx.p0.y, ctx.p1.x, ctx.p1.y);
gradient.addColorStop(0, ctx.p0.options.backgroundColor);
gradient.addColorStop(1, ctx.p1.options.backgroundColor);
console.log(ctx p0 color::::>,ctx.p0.options.backgroundColor);
console.log(ctx p1 color::::>,ctx.p1.options.backgroundColor);
return gradient;
}

Hello @stockiNail, Thank you for your response, Seems however the snippet is not working properly. I called the function in segment: { borderColor } and put the snippet but now it is rendering only green color gradient. This is the screenshot of the output now.
https://pasteboard.co/4VDXH8dIiBag.png
If you look at the screenshot, the first 2 points have orange color. The third one is having dark green color and the fourth point having green color, but the entire gradient line is having green color.

segment: {
borderColor: ctx => test(ctx),
}
const test = (context) => {
const {chart, p0, p1} = context;
const ctx = chart.ctx;
let gradient = ctx.createLinearGradient(p0.x, p0.y, p1.x, p1.y);
gradient.addColorStop(0, p0.options.backgroundColor);
gradient.addColorStop(1, p1.options.backgroundColor);
return gradient;
}

@ttvettuparambil
Copy link
Author

@ttvettuparambil another possible solution is to use chartjs-plugin-gradient.

@stockiNail @can you give me a working example of this plugin along with a quick summary on the plugin options ?

Yes, I can but let me take time a bit. But having a look to your code, I see (but maybe I'm wrong) that the gradient must be applied to the single segment and based on the values of p0 and p1, therefore the plugin is not a good solution for this use case because the plugin is applied to the whole line based on values of dataset.

Ok thank you for clarifying this point for me.

@ttvettuparambil
Copy link
Author

On logging the const ctx = chart.ctx; I dont see any p0 and p1 objects in the output. So I am not sure how the green gradient color is even rendering.

@stockiNail
Copy link
Contributor

I dont see any p0 and p1 objects in the output.

yes, because p0 and p1 are int not chart.ctx. Anyway I have created a codepen and I have got the same weird behavior that only the last gradient is applied.

@ttvettuparambil
Copy link
Author

@stockiNail, so basically the above snippet will not satisfy the requirement. Is there any other method that you could think of that can be used to solve the requirement.

@stockiNail
Copy link
Contributor

@ttvettuparambil no at the moment, may be anything will come later to my mind. Anyway there is something weird that I cannot understand.

@ttvettuparambil
Copy link
Author

ttvettuparambil commented Mar 30, 2023

ok. Thanks a lot for the clarifications. Hope you have a nice day. Do let me know if this feature is implemented in chart.js in the future because as far as I see, no library has implemented this.

@stockiNail
Copy link
Contributor

Nice day to you too. I don't give up! I need to know why it's not working!

@stockiNail
Copy link
Contributor

@ttvettuparambil have a look to the codepen: https://codepen.io/stockinail/pen/rNZgxae
It's working as you expected.
But I think there is a bug in CHART.JS when a segment callback is returning a gradient (but also a canvas pattern).

function styleChanged(style, prevStyle) {
return prevStyle && JSON.stringify(style) !== JSON.stringify(prevStyle);
}

The changed style is applied if the JSON stringify value of prev and new values are different. But the stringify of a gradient is always and empty object string representation.

As workaround, as you can see in codepen, I set a random value in order it's different for all segments and then the JSON stringify is also different then the gradient is always applied.

Play on it if you can and let me know. In the meanwhile I have to find time to fix it.
Let me know

@ttvettuparambil
Copy link
Author

@stockiNail you are a legend. Put the function in my code and working like a charm. Can't tell you how much I appreciate this. Do let me know when the code bug is fixed. Was racking my brain for past few days to find a solution for this issue.

@ttvettuparambil
Copy link
Author

Could we leave this issue open for 1 more day. Just in case I have any doubts.

@stockiNail
Copy link
Contributor

Thank you @ttvettuparambil
We must leave this issue open because the PR will be related to this issue

@stockiNail
Copy link
Contributor

@ttvettuparambil remember that when the PR will be released you should remove the random value from gradient instance to be clean

@ttvettuparambil
Copy link
Author

@stockiNail, ok sure. will look out for the new release. Thank you so much.

@stockiNail
Copy link
Contributor

@ttvettuparambil PR submitted: #11217

@ttvettuparambil
Copy link
Author

@stockiNail that's great news. Never thought the gradient requirement would be possible. Will recommend chart js to anyone and will be my go to library for the future. One quick query: I should only remove the random gradient value; the rest of the code stays the same right?

@stockiNail
Copy link
Contributor

I should only remove the random gradient value; the rest of the code stays the same right?

YES! The rest will remain the same. To be more precise (see comment):

const test = (context) => {
  const {chart, p0, p1} = context;
  const ctx = chart.ctx;
  const {x: x0} = p0.getProps(['x'], true);
  const {x: x1} = p1.getProps(['x'], true);
  const gradient = ctx.createLinearGradient(x0, 0, x1, 0);
  gradient.addColorStop(0, p0.options.backgroundColor);
  gradient.addColorStop(1, p1.options.backgroundColor);
  gradient.random = Math.random(); /// <--- This row could be removed after PR will be released
  return gradient;
}

Is it also clear to you why the code is accessing to the p0/p1 X properties by getProps instead of using directly the properties?
Because I didn't comment and maybe it's not clear.

@ttvettuparambil
Copy link
Author

I should only remove the random gradient value; the rest of the code stays the same right?

YES! The rest will remain the same. To be more precise (see comment):

const test = (context) => {
  const {chart, p0, p1} = context;
  const ctx = chart.ctx;
  const {x: x0} = p0.getProps(['x'], true);
  const {x: x1} = p1.getProps(['x'], true);
  const gradient = ctx.createLinearGradient(x0, 0, x1, 0);
  gradient.addColorStop(0, p0.options.backgroundColor);
  gradient.addColorStop(1, p1.options.backgroundColor);
  gradient.random = Math.random(); /// <--- This row could be removed after PR will be released
  return gradient;
}

Is it also clear to you why the code is accessing to the p0/p1 X properties by getProps instead of using directly the properties? Because I didn't comment and maybe it's not clear.

@stockiNail , I confess I do not know how the above code snippet works. Could you please explain it to me?

@stockiNail
Copy link
Contributor

const test = (context) => {
  const {chart, p0, p1} = context;
  const ctx = chart.ctx;

context is the CHART.JS context for segment callback (NO the Context2d of a canvas) and is described here.
The Context2d instance (ctx) is retrievable from the chart instance.

  const {x: x0} = p0.getProps(['x'], true);
  const {x: x1} = p1.getProps(['x'], true);

p0 and p1 are data point element.
As you can see, the code is not accessing to properties of the element directly (i.e. p0.x). If you will do, you will see that at the first chart loading, the segments will not be drawn, if you enabled the animation.
The properties of the point elements are affected by animation and i.e. p0.x is providing the current position of the point in the animation cycle. Furthermore, the segment callbacks are invoked at the beginning of dataset drawing.
In this use case, it's better to use the final position of the point to create the gradient and the way to get it is to use getProps method of data element instance, passing the array of properties you need and true which means you need the final value.

  const gradient = ctx.createLinearGradient(x0, 0, x1, 0);
  gradient.addColorStop(0, p0.options.backgroundColor);
  gradient.addColorStop(1, p1.options.backgroundColor);
  return gradient;
}

I think the last part doesn't need any comment. Be aware that the code is creating a gradient where the color is ONLY horizontally distributed (see y1 and y2 set to 0).

@stockiNail
Copy link
Contributor

@ttvettuparambil just FYI, I used the same code you have as segment callback in the PR as additional test case for Chart.js:

https://github.com/chartjs/Chart.js/pull/11217/files#diff-c6a1b50ea001b54e20c01e22400f72a72c54408b1f2411c08c1f71ebb495b202

@etimberg etimberg added this to the Version 4.3.0 milestone Apr 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants