Related
Hi I am trying to attach click event on datapoints with Chart.js line graph.
However, the official document does not have such an information. I found some source which make custom events on tooltip, but they looks like using version 1.X.(add custom event on chartjs) - at version 2.x, can not access Chart.defaults -
So what I have to do is something like below.
I will add click event on data points of Chart.js line graph
hover event will be remain.
FYI, the codes I am trying to write is here.
function makeGraphSuitableData(items, type){
var object = {
labels : []
, data : []
}
if(type == 'month') {
object.labels = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"];
object.data = new Array(12);
for(var i = 0; i < object.labels.length; i++) {
var datas = items.filter(function(item){
return (item.reg_month == object.labels[i]);
})
console.log('datas', datas);
if(datas && datas.length > 0){
var single = datas[0];
object.data[(object.labels[i] * 1) - 1] = single.frequency;
} else {
object.data[(object.labels[i] * 1) - 1] = 0;
}
}
object.labels = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
}
return object;
}
function drawGraph(holder, statistics){
var statistics = [
{
labels : "01"
, frequency : "100"
}, {
labels : "02"
, frequency : "150"
}
]
var canvas = holder.querySelector('canvas');
if(!canvas){
var canvas = document.createElement('canvas');
canvas.width = 400; canvas.height = 150;
holder.appendChild(canvas);
}
var graphObject = makeGraphSuitableData(statistics, 'month');
// Bar-Graph
var type = 'line'; //bar, line
var displayLabel = 'Expose counts';
switch (type){
case 'line':
var myChart = new Chart(canvas, {
type: type,
data: {
labels: graphObject.labels,
datasets: [{
label: displayLabel,
data: graphObject.data,
fill: false,
lineTension: 0,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(75,192,192,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(75,192,192,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
spanGaps: false
}]
}
});
canvas.addEventListener('click', function(e){
var x = myChart;
window.x = x;
}, false);
break;
}
}
Thanks for reading and your answers. getPointsAtEvent looks disappeared in version 2.X.
====== UPDATES 24MAR ======
To clear my point, I updated the post.
In my thought, in order to add click event on datapoints, need to get datapoints objects.
But I can not see how to access those points.
I have examined the Chart object but could not find getDataPoints() or getTooltips(). Access to those object should be a solution, I guess.
FIDDLE
The correct (documented) way to do this, per the api, is to use the .getElementAtEvent() prototype method.
The other answer kind of works, but does not work if your chart has more than 1 dataset (e.g. line). Plus, it relies on undocumented objects/properties in the chart.js object that could change at any time with a new release.
document.getElementById("canvas").onclick = function(evt){
var activePoint = myChart.getElementAtEvent(event);
// make sure click was on an actual point
if (activePoint.length > 0) {
var clickedDatasetIndex = activePoint[0]._datasetIndex;
var clickedElementindex = activePoint[0]._index;
var label = myChart.data.labels[clickedElementindex];
var value = myChart.data.datasets[clickedDatasetIndex].data[clickedElementindex];
alert("Clicked: " + label + " - " + value);
}
};
Here is a codepen example demonstrating the above code.
There is always a way to solve any issue. It took about half an hour but I found it.
in your jsfiddle, just change below:
canvas.addEventListener('click', function(e){
var x = myChart;
}, false);
break;
to
canvas.addEventListener('click', function(e){
var x = myChart;
if(x.active[0]){
var value= x.tooltip._data.datasets["0"].data[x.active["0"]._index];
var label = x.tooltip._data.labels[x.active["0"]._index];
console.log(value + " at "+ label);
}
}, false);
break;
You will have access to the point via label and value. enjoy :)
I have got this data for my chart:
datasets: [
{
label: "Price Compliant",
backgroundColor: "rgba(34,139,34,0.5)",
hoverBackgroundColor: "rgba(34,139,34,1)",
data: [99.0, 99.2, 99.4, 98.9, 99.1, 99.5, 99.6, 99.2, 99.7]
},
{
label: "Non-Compliant",
backgroundColor: "rgba(255, 0, 0, 0.5)",
hoverBackgroundColor: "rgba(255, 0, 0, 1)",
data: [1.0, 0.8, 0.6, 1.1, 0.9, 0.5, 0.4, 0.8, 0.3]
}
]
...which looks like so:
As you can see, the first data point (99.0) displays as 99, truncating the ".0" portion.
There is, of course, some logic to this, but the GUI nazis want that ".0" to be retained.
What do I need to do to force display of even "meaningless" portions of data?
UPDATE
afterDraw() event, for Jaromanda X:
Chart.pluginService.register({
afterDraw: function (chartInstance) {
if (chartInstance.id !== 1) return; // affect this one only
var ctx = chartInstance.chart.ctx;
// render the value of the chart above the bar
ctx.font = Chart.helpers.fontString(14, 'bold', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
chartInstance.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model;
ctx.fillText(dataset.data[i] + "%", model.base + 180, model.y + 6);
//ctx.fillText(dataset.data[i], model.base + 20, model.y + 6);
}
});
}
});
As mentioned by #machineghost in his comment, this is a known issue.
But you have still several workarounds to make it work :
Simply change your data into string : (fiddle link)
For example you data will be like this :
{
label: "Price Compliant",
backgroundColor: "rgba(34,139,34,0.5)",
hoverBackgroundColor: "rgba(34,139,34,1)",
data: ["99.0", "99.2", "99.4", "98.9", "99.1", "99.5", "99.6", "99.2", "99.7"]
}
Chart.js will handle it as a classic string and then won't remove the ".0".
Add the ".0" in your code, using plugins : (fiddle link)
A small condition (using ternaries) can make it easy to write & read :
ctx.fillText(dataset.data[i] + (Number.isInteger(dataset.data[i]) ? ".0" : "") + "%", ((model.x + model.base) / 2 ), model.y + (model.height / 3));
It is basically checking if the value is an integer (no decimals) and add a ".0" in the string if it is.
Both codes will give you the following result :
I need to change the fill color (internal area) in a Line Chart.js when the point is negative.
The code is simple and basic:
$(document).ready(function(){
var ctx = $("#myChart").get(0).getContext("2d");
var data = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
//fillColor : "rgba(60,91,87,1)",
// String - the color to fill the area under the line with if fill is true
backgroundColor: "rgba(75,192,192,0.4)",
strokeColor : "rgba(60,91,87,1)",
pointColor : "rgba(60,91,87,1)",
pointStrokeColor : "#58606d",
// The actual data
data: [65, 59, 80, -81, 56, 55, -40],
// String - If specified, binds the dataset to a certain y-axis. If not specified, the first y-axis is used. First id is y-axis-0
yAxisID: "y-axis-0",
}
]
};
var options = {
scales: {
yAxes: [{
display: true,
ticks: {
suggestedMin: 0, // minimum will be 0, unless there is a lower value.
// OR //
beginAtZero: true // minimum value will be 0.
}
}]
}
};
var myLineChart = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
// myLineChart.data.datasets[0].metaDataset._points[3]._model.backgroundColor = "red";
// if (myLineChart.datasets[0].points[4].value < 0) {
// myLineChart.datasets[0].points[4].fillColor = "red";
// myLineChart.update();
// }
})
I'm trying to get this result:
You can extend the line chart to do this.
Preview
Script
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[0].data);
var max = Math.max.apply(null, this.chart.data.datasets[0].data);
var yScale = this.getScaleForId(this.getDataset().yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(75,192,192,0.4)');
gradient.addColorStop(ratio, 'rgba(75,192,192,0.4)');
gradient.addColorStop(ratio, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
this.chart.data.datasets[0].backgroundColor = gradient;
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
and then
...
var myLineChart = new Chart(ctx, {
type: 'NegativeTransparentLine',
data: {
...
Fiddle - http://jsfiddle.net/g2r2q5Lu/
To get #potatopeelings code above to work with chart.js 2.5.x you need to add yAxisID : 'y-axis-0' into your datasets, as below.
var myLineChart = new Chart(ctx, {
type: 'NegativeTransparentLine',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
yAxisID : 'y-axis-0',
....
i update the method to work with multiple datasets.
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
for(let i=0; i< this.chart.data.datasets.length; i++) {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[i].data);
var max = Math.max.apply(null, this.chart.data.datasets[i].data);
var yScale = this.getScaleForId(this.chart.data.datasets[i].yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(247,100,120,0.4)');
gradient.addColorStop(1, 'rgba(247,100,120,0.4)');
this.chart.data.datasets[i].backgroundColor = gradient;
}
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
Tested on chart.js 2.8.0 on Angular 8
import { Component, OnInit, ViewChild } from '#angular/core';
import { Chart, ChartDataSets, ChartOptions } from 'chart.js';
import { Color, Label } from 'ng2-charts';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public lineChartData: ChartDataSets[] = [
{ data: [89, 0, -80, 81, 56, -55, 40], label: 'Series A', yAxisID: 'y-axis-0' },
{ data: [-890, 0, 800, -810, -560, 550, -400], label: 'Series B', yAxisID: 'y-axis-0' },
];
public lineChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
public lineChartOptions: (ChartOptions & { annotation: any }) = {
responsive: true,
};
public lineChartColors: Color[] = [
{
backgroundColor: 'rgba(255,0,0,0.3)',
},
{
backgroundColor: 'rgba(0,255,0,0.3)',
},
];
public lineChartLegend = true;
public lineChartType = 'line';
public lineChartPlugins = [];
constructor() {
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
for(let i=0; i< this.chart.data.datasets.length; i++) {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[i].data);
var max = Math.max.apply(null, this.chart.data.datasets[i].data);
var yScale = this.getScaleForId(this.chart.data.datasets[i].yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(247,100,120,0.4)');
gradient.addColorStop(1, 'rgba(247,100,120,0.4)');
this.chart.data.datasets[i].backgroundColor = gradient;
}
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
this.lineChartType = 'NegativeTransparentLine';
}
ngOnInit() {
}
}
<div style="display: block;">
<canvas baseChart width="400" height="400"
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[colors]="lineChartColors"
[legend]="lineChartLegend"
[chartType]="lineChartType"
[plugins]="lineChartPlugins">
</canvas>
</div>
This is derived from this post. It works for Chart.js v2.9.4 and doesn't require any external code or creating a custom chart type. Simply add this plugins object to your chart options. (note that the plugins object is separate from the options object. If you put the plugins object inside of the options object, it won't work.)
new Chart(document.querySelector(`canvas`), {
type: 'line',
data: {
labels: your_labels,
datasets: [{
data: your_data
}]
},
options: {
maintainAspectRatio: false, //allow the graph to resize to its container
scales: {
yAxes: [{
ticks: {
beginAtZero: true //make sure zero line exists on the graph
}
}]
}
}, //<-make sure plugins is outside of the options object
plugins: [{
beforeRender: function(graph) {
let gradient = graph.ctx.createLinearGradient(0, 0, 0, graph.height), //create a gradient for the background
zero_line = graph.scales[`y-axis-0`].getPixelForValue(0) / graph.height; //calculate where the zero line is plotted on the graph
gradient.addColorStop(0, `rgba(0,200,0,.2)`); //good color faded out
gradient.addColorStop(zero_line, `rgba(0,200,0,.8)`); //good color at zero line
gradient.addColorStop(zero_line, `rgba(200,0,0,.8)`); //bad color at zero line
gradient.addColorStop(1, `rgba(200,0,0,.2)`); //bad color faded out
graph.data.datasets[0]._meta[0].$filler.el._model.backgroundColor = gradient; //set the graphs background to the gradient we just made
}
}]
});
Obviously for more complex graphs you'll need to update dataset indexes and axis names, but for simple graphs, it's this simple.
#potatopeelings code will work if your dataset data format is in [1,2,3,...] form
If your data format is in [{x: 1 , y: 1},...] form, you need to change var min and var max to:
var min = this.chart.data.datasets[0].data.reduce((min, p) => p.y < min ? p.y : min, this.chart.data.datasets[0].data[0].y);
var max = this.chart.data.datasets[0].data.reduce((max, p) => p.y > max ? p.y : max, this.chart.data.datasets[0].data[0].y);
Tested on ChartJS 2.7.3
#potatopeelings The gradient messed up if all data was negative or positive, here's how I fixed it. (Changed the gradient colours but the fix is still there)
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[0].data);
var max = Math.max.apply(null, this.chart.data.datasets[0].data);
var yScale = this.getScaleForId(this.getDataset().yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
if(ratio < 0){
ratio = 0;
gradient.addColorStop(1, 'rgba(0,255,0,1)');
}else if(ratio == 1){
gradient.addColorStop(1, 'rgba(255,0,0,1)');
}else{
gradient.addColorStop(0, 'rgba(255,0,0,1)');
gradient.addColorStop(ratio, 'rgba(255,0,0,1)');
gradient.addColorStop(ratio, 'rgba(0,255,0,1)');
gradient.addColorStop(1, 'rgba(0,255,0,1)');
}
console.log(ratio)
this.chart.data.datasets[0].backgroundColor = gradient;
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
I just found this little bug when I wanted to show 1 single point using line chart.. I don't know why it didn't show the point. Here is the screenshot:
Here is how I created my object:
avg_payment = {
labels: ["Jan"]
datasets: [
{
label: "Average_payment"
fillColor: "rgba(220,220,220,0.5)"
strokeColor: "rgba(220,220,220,0.8)"
highlightFill: "rgba(220,220,220,0.75)"
highlightStroke: "rgba(220,220,220,1)"
data: [65]
}
]
}
This is my current workaround, eventhough it still gives me the same result:
if avg_payment.labels.length is 1
max_val = Math.max(avg_payment.datasets[0].data)
opt = {
scaleOverride : true
scaleSteps : 2
scaleStepWidth : 1
scaleStartValue : max_val - 1
}
myLineChart = new Chart(ctx1).Line(avg_payment, opt)
Is there any workaround for this issue ?
This issues was caused by a variable becoming infinity when chartjs is trying to draw the x axis. The fix for this has to go into the core of Chartjs's scale so you could either extend scale like below or I have added this fix to my custom build of chartjs https://github.com/leighquince/Chart.js
Chart.Scale = Chart.Scale.extend({
calculateX: function(index) {
//check to ensure data is in chart otherwise we will get infinity
if (!(this.valuesCount)) {
return 0;
}
var isRotated = (this.xLabelRotation > 0),
// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
//if we only have one data point take nothing off the count otherwise we get infinity
valueWidth = innerWidth / (this.valuesCount - ((this.offsetGridLines) || this.valuesCount === 1 ? 0 : 1)),
valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
if (this.offsetGridLines) {
valueOffset += (valueWidth / 2);
}
return Math.round(valueOffset);
},
});
var line_chart_data = {
labels: ["Jan"],
datasets: [{
label: "Average_payment",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.75)",
highlightStroke: "rgba(220,220,220,1)",
data: [65]
}]
};
var ctx = $("#line-chart").get(0).getContext("2d");
var lineChart = new Chart(ctx).Line(line_chart_data);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://rawgit.com/nnnick/Chart.js/master/Chart.min.js"></script>
<canvas id="line-chart" width="100" height="100"></canvas>
Here you can see a chart created using graphael. http://jsfiddle.net/aNJxf/4/
It is shown with it's y axis correctly.
The first y value is 0.03100 and the maximum value at y axis is at 0.031
If we change the value to 0.03104 the maximum value at y axis becomes 1.03 and now all our points are in the bottom.
If we add another 0.00001, which makes that value 0.03105, the maximum at the axis y becomes 0.531 and now our points are shown at the wrong position of the chart.
It seems that something is going wrong while graphael calculates the maximum y axis value.
Why this happens? And how we can fix that?
The code that I have pasted there is
var r = Raphael("holder"),
txtattr = { font: "12px sans-serif" };
var x = [], y = [], y2 = [], y3 = [];
for (var i = 0; i < 1e6; i++) {
x[i] = i * 10;
y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3;
}
var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]];
var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]];
var xVals =[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58];
var yVals = [0.03100,0.02259,0.02623,0.01967,0.01967,0.00788,0.02217,0.0137,0.01237,0.01764,0.0131,0.00942,0.0076,0.01463,0.02882,0.02093,0.02502,0.01961,0.01551,0.02227,0.0164,0.0191,0.00774,0.03076,0.0281,0.01338,0.02763,0.02334,0.00557,0.00023,0.01523,0.0263,0.03077,0.02404,0.02492,0.01954,0.01954,0.02337,0.01715,0.02271,0.00815,0.01343,0.00985,0.01837,0.00749,0.02967,0.01156,0.0083,0.00209,0.01538,0.01348,0.01353];
//r.text(160, 10, "Symbols, axis and hover effect").attr(txtattr);
var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines.symbols.attr({ r: 3 });
Thanks
Sorry, I'm not real familiar with gRaphael, but I did find that converting your yVals into whole numbers (by multiplying each by 1e5) seemed to rid your chart of the awkward behavior.
This suggests that it could be related to the algorithm gRaphael uses to find the max axis value (as you ask in your related question) when your values are small decimal values (and alter at even more significant digits).
I know there are inherent issues with float precision, but I can't be sure that applies to your case, or that your values are low enough to consider this.
Not the best workaround, but if it would be feasible for you, you could display the yValues in an order of magnitude larger, and remind the viewer that they are actually smaller than presented. For example, your chart could go from 0 to 3100 and remind your viewer that the scale is scale * 1e-5.