Chartjs how to add animations to legend labels, ticks and other elements - chart.js

In the same way the bars / datasets animates "Out of the box", how can I also add animations to the labels, ticks and the grid when the chart first loads?
I looked through the docs but don't see anything about animating these elements? However on their landing page is claims "Animate everything!"
I managed to get one element animated doing the following:
new Chart(ctx, {
type: 'bar',
data: chart_data,
options: {
animation: {
onComplete: () => {
delayed = true;
},
onProgress: function(animation) {
if(!delayed) {
//this works
this.options.scales.x.grid.borderWidth = animation.currentStep / animation.numSteps;
//this needs to work but doesn't
this.options.scales.x.grid.borderDash = [animation.currentStep / animation.numSteps * 10, 20];
//I've tried other elements here, but none of them change, only this.options.scales.x.grid.borderWidth for some reason
}
},
delay: (context) => {
let delay = 0;
if (context.type === 'data' && context.mode === 'default' && !delayed) {
delay = context.dataIndex * 300 + context.datasetIndex * 100;
}
return delay;
},
}...`
However it doesn't seem to work on anything else.

Related

Add custom JavaScript function callback in Blazor

I'm creating a Razor component to encapsulate Chart.js. The source code is on GitHub.
At the moment I create nice chart but the problem I'm facing is about the callback. To create a new chart is quite straight forward. In the page I add the component
<Chart Config="_config1" #ref="_chart1" Id="#Id1"></Chart>
and in the code, I add the configuration for a bar chart. The object BarChartConfig is converted in a json to create the chart.
_config1 = new BarChartConfig()
{
CanvasId = Id1,
Type = ChartType.Bar,
Options = new Options()
{
Plugins = new Plugins()
{
Legend = new Legend()
{
Align = LegendAlign.Center,
Display = false,
Position = LegendPosition.Right
}
},
Scales = new Scales()
{
X = new XAxes()
{
Stacked = true,
Ticks = new Ticks()
{
MaxRotation = 0,
MinRotation = 0
}
},
Y = new YAxes()
{
Stacked = true
}
}
}
};
What I try to do now is to add some callback or function. For example, I like to add a custom function like this one in the ticks part.
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Chart with Tick Configuration'
}
},
scales: {
x: {
ticks: {
// For a category axis, the val is the index so the lookup via getLabelForValue is needed
callback: function(val, index) {
// Hide every 2nd tick label
return index % 2 === 0 ? this.getLabelForValue(val) : '';
},
color: 'red',
}
}
}
},
};
I'm pretty sure I can use JSInterop in some way to achieve that but I don't know how.
For example, how can I bind/add a JavaScript function at runtime?
Another nice thing I found is the user can click on the legend to exclude a category for the chart. How can I add a callback for this event?

ChartJs: Is there a way to control the font options per line for a multiline axis label

I am open to learning that there is already a way (via configuration, or developing a plugin) to hook into the rendering of the label of an axis, such that I could control aspects of the font used to render each line of a multiline label (e.g., what I need to render would be similar visually to a label and sub-label below it, with the primary label being bolded and a larger font size, while the sub-label directly beneath it would be normal font weight and a smaller size).
I am using ChartJs version 3.5.1 to render a horizontal barchart (meaning that the dataset labels on the left are really configured under the y axis), and have tried a few different things already:
Hooking into the tick callback - but I can't even use this function to duplicate default functionality (the value coming into that function isn't the label text; instead it is the index/ordinal of the data row?). Even if I could get this to work as shown in examples, it appears like this would be more for the content of the label than any of the configuration options themselves.
Setting the font configuration for ticks to be an array - but this only serves to allow me to change the font between data rows (e.g., I can make the label of the top row in my horizontal bar chart be size 22, the second label 10, etc. - but not change font attributes within lines of a given label)
Using a plugin like afterDraw to try to go tweak things - but again, the configuration at that point seems to only consider all of the lines together as one label.
Tried looking through past PRs to the project (mostly centered around adding multiline label support, as well as bug fixes specific to that area) to get any additional insight
If there isn't a way currently (via plugins or existing configuration), does anyone have a good feel for where to start attacking this sort of a change as a new PR?
UPDATE
As was shared as a response to my corresponding ChartJs feature request and as the accepted answer below, a custom plugin seems to be the only way currently to accomplish what I wanted for now.
Here are the key bits from my configuration (admittedly much more "one time use only" than the accepted answer, as I moved some of the configuration inside of the plugin as hard-coded values given my relatively narrow use case):
// this will be passed into the chart constructor...
const options = {
//...
scales: {
//...
// I wanted to impact the lefthand side of a horizontal bar chart
y: {
ticks: {
// make the original labels white for later painting over with custom sub-labels
color: "white",
// we still want this here to be able to take up the same space as the eventual label we will stick here
font: {
size: 22,
weight: "bold"
}
}
},
//...
}
};
// This is my plugin, also later passed into the chart constructor
const customSubLabelsPlugin = {
id: "customSubLabels",
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
// I only cared about altering one specific axis
scales: { y }
} = chart;
const labelItems = y._labelItems;
const fontStringSubTitle = "16px Helvetica,Arial,sans-serif";
const fontStringMain = "bold 22px Helvetica,Arial,sans-serif";
// loop over each dataset label
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
// For purposes of redrawing, we are going to always assume that each label is an array - because we make it that way if we need to
const label = Array.isArray(labelItem.label)
? labelItem.label
: [labelItem.label];
// Draw new text on canvas
let offset = 0;
label.forEach((el) => {
let elTextMetrics = ctx.measureText(el);
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain;
} else {
ctx.font = fontStringSubTitle;
}
ctx.save();
ctx.fillStyle = "#546a6f";
ctx.fillText(
el,
labelItem.translation[0],
labelItem.translation[1] + labelItem.textOffset + offset
);
ctx.restore();
offset +=
elTextMetrics.fontBoundingBoxAscent +
elTextMetrics.fontBoundingBoxDescent;
});
}
}
};
You can use a plugin to redraw the ticks for you, might need some finetuning for your specific needs:
var options = {
type: 'line',
data: {
labels: [
["Red", "subTitle"],
["Blue", "subTitle"],
["Yellow", "subTitle"],
["Green", "subTitle"],
["Purple", "subTitle"],
["Orange", "subTitle"]
],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'red',
backgroundColor: 'red'
}]
},
options: {
plugins: {
customTextColor: {
color: 'blue',
boxColor: 'white',
fontStringSubTitle: 'italic 12px Comic Sans MS',
fontStringMain: ''
}
}
},
plugins: [{
id: 'customTextColor',
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
scales: {
y,
x
}
} = chart;
const labelItems = x._labelItems;
const {
color,
boxColor,
fontStringMain,
fontStringSubTitle
} = opts;
const defaultFontString = '12px "Helvetica Neue", Helvetica, Arial, sans-serif';
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
if (!Array.isArray(labelItem.label)) {
continue;
}
let metrics = ctx.measureText(labelItem.label);
let labelWidth = metrics.width;
let labelHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
//Draw box over old labels so they are inviseble
ctx.save();
ctx.fillStyle = boxColor || '#FFFFFF';
ctx.fillRect((labelItem.translation[0] - labelWidth / 2), labelItem.translation[1], labelWidth, labelHeight * labelItem.label.length);
ctx.restore();
// Draw new text on canvas
let offset = 0;
labelItem.label.forEach(el => {
let elTextMetrics = ctx.measureText(el);
let elWidth = elTextMetrics.width;
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain || defaultFontString;
} else {
ctx.font = fontStringSubTitle || defaultFontString;
}
ctx.save();
ctx.fillStyle = color || Chart.defaults.color
ctx.fillText(el, (labelItem.translation[0] - elWidth / 2), labelItem.translation[1] + labelItem.textOffset + offset);
ctx.restore();
offset += elTextMetrics.fontBoundingBoxAscent + elTextMetrics.fontBoundingBoxDescent;
});
}
// Draw white box over old label
}
}]
}
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.1/chart.js"></script>
</body>

Chart.js sync legend toggle on multiple charts

There are solutions to using legend onClick to toggle on/off visibility of (all other) datasets on the clicked chart, but I needed a way to sync this toggle on multiple charts if they have the same label/legend. For example, I have 6 charts presenting different information about the same data. However, not all the charts have all the datasets. One may have 5 datasets, another has 3 and so on. And they may not show up in the same order either.
The goal was to be able to click a legend item on one chart, and that same item be toggled on all the charts.
Since I did not find an existing solution, I'm posting this.
To do this, I put all the charts in a global var and loop through them to match dataset by legendItem.text instead of legendItem.datasetindex, since the label may or may not exist or even be in the same index position on other charts.
Here's how I create/replace the multiple charts: https://stackoverflow.com/a/51882403/1181367
And here's the legend onClick toggle solution:
var config = {
type: type,
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
}
}]
},
legend: {
position: 'right',
onClick: function (e, legendItem) {
var text = legendItem.text;
Object.keys(charts).forEach(function (id) {
// loop through the charts
var ci = charts[id].chart
var cindex = (function () {
var match = null;
ci.legend.legendItems.forEach(function (item) {
if (item.text == text) {
// get index for legend.text that matches clicked legend.text
match = item.datasetIndex;
}
});
return match;
})();
if (cindex !== null) {
// if there's a match
var alreadyHidden = (ci.getDatasetMeta(cindex).hidden === null) ? false : ci.getDatasetMeta(cindex).hidden;
ci.data.datasets.forEach(function (e, i) {
var meta = ci.getDatasetMeta(i);
if (i !== cindex) {
if (!alreadyHidden) {
meta.hidden = meta.hidden === null ? !meta.hidden : null;
} else if (meta.hidden === null) {
meta.hidden = true;
}
} else if (i === cindex) {
meta.hidden = null;
}
});
ci.update();
}
});
}
}
}
};
After using Chris's answer here https://stackoverflow.com/a/51920456/671140 I created a simplified solution.
var config = {
type: type,
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
}
}]
},
legend: {
position: 'right',
onClick: function (e, legendItem) {
var text = legendItem.text;
Object.keys(charts).forEach(function (id) {
// loop through the charts
var ci = charts[id].chart
ci.legend.legendItems.forEach(function (item) {
if (item.text == text) {
ci.options.legend.onClick.call(chart.legend, null, item);
ci.update();
}
});
});
}
}
}
};
The benefit of using this solution is that it will call any custom onClick() handlers that have been added to any of the chart legends.
If anyone's looking for a way to sync a pie chart legend with a line chart legend, try this (it should also work just fine if the charts are the same type, too):
onClick: function(e, legendItem) {
// Save name of clicked label, for later comparison
var legendName = legendItem.text;
// Iterate through global charts array
Object.keys(myCharts).forEach(function(id) {
// Assign shorthand variable to address chart easier
var chrt = myCharts[id];
// Determine chart type
var chartType = chrt.config.type;
// Iterate through each legend in the chart
chrt.legend.legendItems.forEach(function(item) {
// If legend name matches clicked label
if (item.text == legendName) {
if (chartType == 'pie') { // If pie chart
if (chrt.getDatasetMeta(0).data[item.index].hidden === true) chrt.getDatasetMeta(0).data[item.index].hidden = false;
else if (chrt.getDatasetMeta(0).data[item.index].hidden === false) chrt.getDatasetMeta(0).data[item.index].hidden = true;
} else if (chartType == 'line') { // If line chart
if (chrt.getDatasetMeta(item.datasetIndex).hidden === true) chrt.getDatasetMeta(item.datasetIndex).hidden = null;
else if (chrt.getDatasetMeta(item.datasetIndex).hidden === null) chrt.getDatasetMeta(item.datasetIndex).hidden = true;
}
// Trigger chart update
chrt.update();
}
});
});
Place this onClick function in the legend section of the options, just like you can see in the other answers.
My line chart was a day-to-day trend, while the pie chart compared totals across the entire range.
The chart's labels must be identically named for this to work, of course.
Like the other solutions, you must store both charts in one, global variable, like:
window.myCharts['pieChart1']
window.myCharts['lineChart1']
Since hiding datasets, and getting their indexes within the whole dataset, is different depending on the chart type, this function will check the chart type and act accordingly.
Also notice that for pie charts, the "hidden" setting is either true or false, but for line charts, it's either true or null (thanks, chart.js).
I'm sure you can expand this for other chart types, but I've only bothered setting it up for 'line' and 'pie' charts.
If someone is looking for this, here is the working solution:
onClick: function (e, legendItem, legend) {
var text = legendItem.text;
Object.keys(charts).forEach(function (id) {
var ci = charts[id]
ci.legend.legendItems.forEach(function (item) {
if (item.text == text) {
if (ci.data.datasets[item.datasetIndex].hidden == true) {
ci.data.datasets[item.datasetIndex].hidden = false;
} else {
ci.data.datasets[item.datasetIndex].hidden = true;
}
}
});
ci.update();
});
}

Pie Chart Legend - Chart.js

I need help to put the number of the pie chart in the legend
Chart Image
If i hover the chart with mouse i can see the number relative to each item
i want to display it in the legend either
the important code so far:
var tempData = {
labels: Status,
datasets: [
{
label: "Status",
data: Qtd,
backgroundColor: randColor
},
]
};
var ctx = $("#pieStatus").get(0).getContext("2d");
var chartInstance = new Chart(ctx, {
type: 'pie',
data: tempData,
options: {
title: {
display: true,
fontsize: 14,
text: 'Total de Pedidos por Situação'
},
legend: {
display: true,
position: 'bottom',
},
responsive: false
}
});
"Qtd","randColor" are "var" already with values
You need to edit the generateLabels property in your options :
options: {
legend: {
labels: {
generateLabels: function(chart) {
// Here
}
}
}
}
Since it is quite a mess to create on your own a great template. I suggest using the same function as in the source code and then edit what is needed.
Here are a small jsFiddle, where you can see how it works (edited lines - from 38 - are commented), and its result :
Maybe this is a hacky solution, but for me seems simpler.
The filter parameter
ChartJS legend options have a filter parameter. This is a function that is called for each legend item, and that returns true/false whether you want to show this item in the legend or not.
filter has 2 arguments:
legendItem : The legend item to show/omit. Its properties are described here
data : The data object passed to the chart.
The hack
Since JS passes objects by reference, and filter is called for each legend item, then you can mutate the legendItem object to show the text that you want.
legend : {
labels: {
filter: (legendItem, data) => {
// First, retrieve the data corresponding to that label
const label = legendItem.text
const labelIndex = _.findIndex(data.labels, (labelName) => labelName === label) // I'm using lodash here
const qtd = data.datasets[0].data[labelIndex]
// Second, mutate the legendItem to include the new text
legendItem.text = `${legendItem.text} : ${qtd}`
// Third, the filter method expects a bool, so return true to show the modified legendItem in the legend
return true
}
}
}
Following on from tektiv's answer, I've modified it for ES6 which my linter requires;
options: {
legend: {
labels: {
generateLabels: (chart) => {
const { data } = chart;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const meta = chart.getDatasetMeta(0);
const ds = data.datasets[0];
const arc = meta.data[i];
const custom = (arc && arc.custom) || {};
const { getValueAtIndexOrDefault } = Chart.helpers;
const arcOpts = chart.options.elements.arc;
const fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
const stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
const bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
const value = chart.config.data.datasets[arc._datasetIndex].data[arc._index];
return {
text: `${label}: ${value}`,
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: Number.isNaN(ds.data[i]) || meta.data[i].hidden,
index: i,
};
});
}
return [];
},
},
},
},
I wanted to let the user select from 100+ data sets, but rather than adding/removing them from my Chart I decided to set the showLine: false on any dataset that I want hidden. Unfortunately the default legend would show all 100+. So in my solution I generate the legend manually, filtering out any dataset that has showLine: false.
Your settings will have this:
legend: {
labels: {
generateLabels: (a) => {
return a.data.labels
}
}
And you'll generate your own labels with a helper function:
function updateAllLabels() {
const myNewLabels = [];
myChart.data.datasets.forEach((element) => {
if (element.showLine) {
myNewLabels.push(generateLabel(element));
}
});
myChart.data.labels = myNewLabels;
}
And you'll generate the label with another function:
function generateLabel(data) {
return {
fillStyle: data.borderColor,
lineWidth: 1,
strokeStyle: data.borderColor,
text: data.countyName, // I attach countryName to my datasets for convenience
}
}
Now just don't forget to call the function whenever updating your chart:
updateAllLabels();
myChart.update();
Happy graphing!

Can the colors of bars in a bar chart be varied based on their value?

Using chart.js 2, can the colors of the bars in a bar-cart be varied based on their value?
For example, if the scale is 0 - 100, columns with 50% and above could be green, while 0-49% could be red.
As far as I know there is no configuration or callback for each individual point being drawn. The best way I can think of to do this would be to create a function that would modify your chart config/data object. This isn't the most elegant way to deal with the problem, but it would work.
The Fix
Pass your chart config/data object to a function that will add the background color.
Main Point of the example is function AddBackgroundColors(chartConfig)
Example:
function AddBackgroundColors(chartConfig) {
var min = 1; // Min value
var max = 100; // Max value
var datasets;
var dataset;
var value;
var range = (max - min);
var percentage;
var backgroundColor;
// Make sure the data exists
if (chartConfig &&
chartConfig.data &&
chartConfig.data.datasets) {
// Loop through all the datasets
datasets = chartConfig.data.datasets;
for (var i = 0; i < datasets.length; i++) {
// Get the values percentage for the value range
dataset = datasets[i];
value = dataset.data[0];
percentage = value / range * 100;
// Change the background color for this dataset based on its percentage
if (percentage > 100) {
// > 100%
backgroundColor = '#0000ff';
} else if (percentage >= 50) {
// 50% - 100%
backgroundColor = '#00ff00';
} else {
// < 50%
backgroundColor = '#ff0000';
}
dataset.backgroundColor = backgroundColor;
}
}
// Return the chart config object with the new background colors
return chartConfig;
}
var chartConfig = {
type: 'bar',
data: {
labels: ["percentage"],
datasets: [{
label: '100%',
data: [100]
}, {
label: '50%',
data: [50]
}, {
label: '49%',
data: [49]
}, {
label: '5%',
data: [5]
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
};
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
chartConfig = AddBackgroundColors(chartConfig);
var myChart = new Chart(ctx, chartConfig);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.2/Chart.bundle.min.js"></script>
<canvas id="canvas" width="400" height="200"></canvas>
In Chart.js 2 it is possible to set multiple colors with an array.
So you can define the backgroundColor as an array of color strings, matching the datasets data.
var myChart = new Chart(ctx, {
datasets: [{
label: 'Votes',
data: [1, 2, 3],
// Make the first bar red, the second one green and the last one blue
backgroundColor: ['#f00', '#0f0', '#00f']
}]
});
You can easily generate an array based on the values in data:
function getColorArray(data, threshold, colorLow, colorHigh) {
var colors = [];
for(var i = 0; i < data.length; i++) {
if(data[i] > threshold) {
colors.push(colorHigh);
} else {
colors.push(colorLow);
}
}
return colors;
}
See this fiddle for a working demo