I use Chart.js and stack groups bar chart.
The problem is that I'd like to show the data from the biggest (higher) to the smallest (lower) to make it more readable.
Screenshot:
Use external tooltips, in the var bodyLines from the example are the items of the tooltip, if you process the array beforehand that it is sorted correctly you will have a sorted tooltip:
https://www.chartjs.org/docs/latest/configuration/tooltip.html#external-custom-tooltips
Using external tooltip as base from chartjs samples:
Chart.defaults.pointHitDetectionRadius = 1;
var getOrCreateTooltip = function(chart) {
var tooltipEl = chart.canvas.parentNode.querySelector('div');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.classList.add('chartjs-tooltip');
tooltipEl.innerHTML = '<table></table>';
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
var customTooltips = function(context) {
// Tooltip Element
var chart = context.chart;
var tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
var tooltip = context.tooltip;
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Set caret Position
tooltipEl.classList.remove('above', 'below', 'no-transform');
if (tooltip.yAlign) {
tooltipEl.classList.add(tooltip.yAlign);
} else {
tooltipEl.classList.add('no-transform');
}
function getBody(bodyItem) {
return bodyItem.lines;
}
// Set Text
if (tooltip.body) {
var titleLines = tooltip.title || [];
var bodyLines = tooltip.body.map(getBody);
// make object by parsing the last number with regex (the value) and then sort it, add index to it for color later on
var objectBodyLines = bodyLines.map((input, index) => ({number: /(\d+)(?!.*\d)/.exec(input)[0], whole: input[0], index})).sort((a,b) => {return (a.number > b.number) ? 1 : ((b.number > a.number) ? -1 : 0)});
var innerHtml = '<thead>';
titleLines.forEach(function(title) {
innerHtml += '<tr><th>' + title + '</th></tr>';
});
innerHtml += '</thead><tbody>';
objectBodyLines.forEach(function(body, i) {
var colors = tooltip.labelColors[body.index];
var style = 'background:' + colors.backgroundColor;
style += '; border-color:' + colors.borderColor;
style += '; border-width: 2px';
var span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
innerHtml += '<tr><td>' + span + body.whole + '</td></tr>';
});
innerHtml += '</tbody>';
var tableRoot = tooltipEl.querySelector('table');
tableRoot.innerHTML = innerHtml;
}
var positionY = chart.canvas.offsetTop;
var positionX = chart.canvas.offsetLeft;
// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
};
var lineChartData = {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'My First dataset',
borderColor: window.chartColors.red,
pointBackgroundColor: window.chartColors.red,
fill: false,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}, {
label: 'My Second dataset',
borderColor: window.chartColors.blue,
pointBackgroundColor: window.chartColors.blue,
fill: false,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}]
};
window.onload = function() {
var chartEl = document.getElementById('chart');
window.myLine = new Chart(chartEl, {
type: 'line',
data: lineChartData,
options: {
plugins: {
title: {
display: true,
text: 'Chart.js Line Chart - Custom Tooltips'
},
tooltip: {
enabled: false,
mode: 'index',
intersect: false,
position: 'nearest',
custom: customTooltips
}
}
}
});
};
Only adjustments I made is transform the bodylines into a new array with info I could use (can do this also straigt on the bodylines var), in the objectBodyLines.forEach loop pick the right part from my object to fill the tooltip and set the color.
After quick extra test its not totally working, guess I made a mistake somewhere in the sort I guess but it should still give you a good starting point to work from
In Chart JS 3.4.1 you have the tooltip.itemSort option. I suspect you have a stacked bar chart. So this will work fine for ASC (lowest to highest) or DESC (highest to lowest) order of the tooltip.
That is the one you can use to sort the tooltip without too much hassle. I have added a complete video covering this topic as well which explains the process in depth: how to sort tooltip in Chart JS.
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// setup
const labels = ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'];
const datapoint = [12, 19, 3, 5, 2, 3];
const backgroundColor = ['rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)'];
const borderColor = ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'];
const labels2 = ['green', 'blue', 'black', 'brown', 'purple', 'pink'];
const datapoint2 = [3, 7, 13, 9, 8, 13];
const backgroundColor2 = ['green', 'blue', 'black', 'brown', 'purple', 'pink'];
const borderColor2 = ['green', 'blue', 'black', 'brown', 'purple', 'pink'];
const data = {
labels: labels,
datasets: [{
label: '# of Votes',
data: datapoint,
backgroundColor: backgroundColor,
borderColor: borderColor,
borderWidth: 1
}, {
label: '# of Sales',
data: datapoint2,
backgroundColor: backgroundColor2,
borderColor: borderColor2,
borderWidth: 1
}]
};
// config
const config = {
type: 'bar',
data,
options: {
plugins: {
tooltip: {
itemSort: function (a, b) {
return b.raw - a.raw;
}
}
},
scales: {
x: {
stacked: true
},
y: {
stacked: true,
beginAtZero: true
}
}
}
};
// render init block
const myChart = new Chart(
document.getElementById('myChart'),
config
);
</script>
Related
As the title suggests, I would like to create a pie chart like an attachment using chart.js in Node-RED.
It's a pie chart showing the activity record of the day, but I'm worried because I can't set the two-axis label.
You can use chartjs-plugin-datalabels and define a dataset with weight: 0 for displaying the numbers.
{
data: [...Array(24)].map(v => 1),
weight: 0,
rotation: -7.5,
borderWidth: 20,
datalabels: {
formatter: (v, ctx) => ctx.dataIndex
}
}
For improving the positioning of the numbers, please consult the chartjs-plugin-datalabels documentation here.
Please take a look at the runnable code below and see how it works.
Chart.register(ChartDataLabels);
new Chart('myChart', {
type: 'pie',
data: {
labels: ['red', 'blue', 'green', 'gray'],
datasets: [{
data: [...Array(24)].map(v => 1),
weight: 0,
rotation: -7.5,
borderWidth: 20,
datalabels: {
formatter: (v, ctx) => ctx.dataIndex
}
},
{
label: 'My Dataset',
data: [48, 56, 33, 44],
backgroundColor: ['rgba(255, 0, 0, 0.2)', 'rgba(0, 0, 255, 0.2)', 'rgba(0, 255, 0, 0.2)', 'rgba(124, 124, 124, 0.2)'],
datalabels: {
textAlign: 'center',
formatter: (v, ctx) => {
const dataset = ctx.chart.data.datasets[1];
return ctx.chart.data.labels[ctx.dataIndex] + ': ' + dataset.data[ctx.dataIndex];
}
}
}
]
},
options: {
responsive: false,
plugins: {
legend: {
display: false
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels#2"></script>
<canvas id="myChart" height="300"></canvas>
I try to display stacked line chart but it doesn't work.
Could you advise where I should change with the following code?
window.onload=function(){
var ctx = document.getElementById("myChart").getContext("2d");
var data = {
labels: {{subcategories|tojson}},
datasets: [
{
1st_sample_dataset
},
{
2nd_sample_dataset
}
]
},
options = {
scales: {
yAxes: [{
stacked: true
}]
}
};
new Chart(ctx).Line(data, {
onAnimationComplete: function () {
var sourceCanvas = this.chart.ctx.canvas;
var targetCtx = document.getElementById("myChartAxis").getContext("2d");
targetCtx.canvas.width = copyWidth;
targetCtx.drawImage(sourceCanvas, 0, 0, copyWidth, copyHeight, 0, 0, copyWidth, copyHeight);
}
});
}
And this is actual display.
From your code I presume you're using an old version of chart.js.
new Chart(ctx).Line(data, {
...
Charts are now created in a different way.
var myChart = new Chart(ctx, {
type: 'line',
...
If you switch to version 2.9.3., it works fine as the following code sample illustrates.
new Chart(document.getElementById('myChartAxis'), {
type: 'line',
data: {
labels: ['A', 'B', 'C', 'D'],
datasets: [
{
label: 'WARNINGS',
data: [1, 2, 3, 2],
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.2)'
},
{
label: 'ERRORS',
data: [1, 2, 1, 3],
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)'
}
]
},
options: {
scales: {
yAxes: [{
stacked: true
}]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
<canvas id="myChartAxis" height="90"></canvas>
Can I offset the bars in the chart.js stacked bar chart like so:
Based on this answer, I created a runnable code snippet that illustrates how it could be done. I'm not using stacked bars because the use case is not clear to me and the image from the question rather looks like a bar with shadows.
const dataset = [40, 80, 50, 60, 70];
const offset = 8;
Chart.pluginService.register({
afterUpdate: function(chart) {
var dataset = chart.config.data.datasets[1];
for (var i = 0; i < dataset._meta[0].data.length; i++) {
var model = dataset._meta[0].data[i]._model;
model.x += offset;
model.controlPointNextX += offset;
model.controlPointPreviousX += offset;
}
}
});
var data = {
labels: ["A", "B", "C", "D", "E"],
datasets: [{
backgroundColor: [
'rgba(255, 99, 132)',
'rgba(255, 206, 86)',
'rgba(54, 162, 235)',
'rgba(75, 192, 192)',
'rgba(153, 102, 255)'
],
borderWidth: 1,
data: dataset,
xAxisID: "bar-x-axis1",
categoryPercentage: 0.5,
barPercentage: 0.5,
},
{
backgroundColor: 'rgba(0, 0, 0, 0.2)',
data: dataset.map(v => v + offset),
xAxisID: "bar-x-axis2",
categoryPercentage: 0.5,
barPercentage: 0.5
}
]
};
var options = {
legend: {
display: false
},
tooltips: {
enabled: false
},
scales: {
xAxes: [
{
id: "bar-x-axis2"
},
{
id: "bar-x-axis1",
offset: true,
ticks: {
display: false
}
}
],
yAxes: [{
id: "bar-y-axis1",
ticks: {
beginAtZero: true,
stepSize: 50
}
}]
}
};
var ctx = document.getElementById("myChart").getContext("2d");
var myBarChart = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<canvas id="myChart" height="60"></canvas>
For Chart.js v3, you can use a different approach.
In the beforeDatasetsDraw, you save the define the shadow properties on the canvas through CanvasRenderingContext2D. Make sure to first save the state of the rendering context by invoking ctx.save().
In the afterDatasetsDraw hook, you need to restore the state of the rendering context by invoking ctx.restore().
Convert the bars into floating bars through data: data.map(v => [-20, v]) to makes sure, the shadows appear from the base of the bars.
Define a tooltip.callbacks.label function that provides the initial values in the tooltips.
Please take a look at below runnable code and see how it works.
const data = [40, 80, 50, 60, 70];
const offset = 10;
new Chart('myChart', {
type: 'bar',
plugins: [{
beforeDatasetsDraw: chart => {
const ctx = chart.ctx;
ctx.save();
ctx.shadowOffsetX = offset;
ctx.shadowOffsetY = -offset;
ctx.shadowBlur = 5;
ctx.shadowColor = 'rgb(220, 220, 220)';
},
afterDatasetsDraw: chart => chart.ctx.restore()
}],
data: {
labels: ["A", "B", "C", "D", "E"],
datasets: [{
label: 'My Data',
data: data.map(v => [-20, v]),
backgroundColor: [
'rgba(255, 99, 132)',
'rgba(255, 206, 86)',
'rgba(54, 162, 235)',
'rgba(75, 192, 192)',
'rgba(153, 102, 255)'
],
categoryPercentage: 0.8
}]
},
options: {
plugins: {
tooltip: {
callbacks: {
label: ctx => data[ctx.dataIndex]
}
}
},
scales: {
y: {
min: 0,
max: Math.max(...data) + offset
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
<canvas id="myChart" height="100"></canvas>
I'm using chart.js and chartjs-plugin-annotation to highlight areas on a graph:
The following config generates the result above:
annotation: {
events: ["click"],
annotations: [
{
type: "box",
drawTime: "beforeDatasetsDraw",
xScaleID: 'x-axis-0',
yScaleID: 'y-axis-0',
xMin: "2019-08-09T07:00:00Z",
xMax: "2019-08-09T09:00:00Z",
yMin: 0,
yMax: 7,
borderWidth: 0,
backgroundColor: "rgba(230,97,79,0.3)",
onClick: function (e) {
console.log("Annotation", e.type, this);
}
}
]
}
I want to be able to highlight a single hour, though:
Anyone if this is possible using the annotations plugin?
To be able to reproduce your request i used annotation type line instead of box.
Annotation type line only accepts borderWidth for thickness of the line.
Because the borderWidth needs to match the column size, i got the width of the chart chart.width, and i divided it to the number of columns chart.data.datasets[0].data.length. Of course i used Chart.js Plugin extension hooks for that:
afterInit (Called after chart has been initialized and before the first update.)
resize (Called after the chart as been resized.)
Code:
var ctx = document.getElementById("myChart");
var dataAnnotation = [
{
type: 'line',
mode: 'vertical',
scaleID: 'x-axis-0',
value: "2019-08-09T08:00:00Z",
borderColor: 'rgba(255, 99, 132, 0.2)',
borderWidth: null,
onClick: function (e) {
console.log("Annotation", e.type, this);
}
},
{
type: 'line',
mode: 'vertical',
scaleID: 'x-axis-0',
value: "2019-08-09T11:00:00Z",
borderColor: 'rgba(255, 99, 132, 0.2)',
borderWidth: null,
onClick: function (e) {
console.log("Annotation", e.type, this);
}
}
];
Chart.plugins.register({
resize: function(chart) {
columns = chart.data.datasets[0].data.length;
chartWidth = chart.width - 40;
setBorderWidth = chartWidth / columns;
annotationsData = chart.options.annotation.annotations.map(function(item){
item.borderWidth = setBorderWidth;
return item;
});
chart.options.annotation.annotations = annotationsData;
chart.update();
},
afterInit: function(chart) {
columns = chart.data.datasets[0].data.length;
chartWidth = chart.width - 40;
setBorderWidth = chartWidth / columns;
annotationsData = dataAnnotation.map(function(item){
item.borderWidth = setBorderWidth;
return item;
});
chart.options.annotation.annotations = annotationsData;
chart.update();
}
});
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: [
"2019-08-09T07:00:00Z",
"2019-08-09T08:00:00Z",
"2019-08-09T09:00:00Z",
"2019-08-09T10:00:00Z",
"2019-08-09T11:00:00Z",
"2019-08-09T12:00:00Z"
],
datasets: [{
label: '# of Tomatoes',
data: [2, 10, 3, 5, 12, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(239, 169, 249, 0.2)',
'rgba(255, 133, 27, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(239, 169, 249, 1)',
'rgba(255, 133, 27, 1)'
],
borderWidth: 1,
fill: false,
lineTension: 0
}]
},
options: {
responsive: true,
scales: {
xAxes: [{
ticks: {
beginAtZero: true,
stepSize: 1,
callback: function(label, index, labels) {
var local = moment.utc(label).toDate();
var format = moment(local).format("dd HH:mm");
return format;
}
}
}],
yAxes: [{
ticks: {
beginAtZero: true,
stepSize: 1
}
}]
},
annotation: {
drawTime: 'afterDraw',
events: ["click"]
}
}
});
Online demo:
https://jsfiddle.net/ZsharE/8nec704L/
Known bugs: click handler works outside annotations !
I have the below stacked group chart.
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
<script>
var barChartData = {
labels: ['January', 'March', 'June', 'September', 'December'],
datasets: [{
label: 'Apple',
borderColor: 'rgba(255, 50, 50,1)',
backgroundColor: 'rgba(255, 50, 50,0.5)',
stack: 'Stack 0',
data: [3,6,4,8,2]
}, {
label: 'Orange',
borderColor: 'rgba(255, 160, 242,1)',
backgroundColor: 'rgba(255, 160, 242,0.5)',
stack: 'Stack 0',
data: [2,1,3,0,1]
}, {
label: 'Pear',
borderColor: 'rgba(79, 158, 255,1)',
backgroundColor: 'rgba(79, 158, 255,0.5)',
stack: 'Stack 0',
data: [3,4,1,5,2]
}, {
label: 'Apple',
borderColor: 'rgba(255, 50, 50,1)',
backgroundColor: 'rgba(255, 50, 50,0.5)',
stack: 'Stack 1',
data: [7,16,10,8,5]
},{
label: 'Mango',
borderColor: 'rgba(100, 244, 97,1)',
backgroundColor: 'rgba(100, 244, 97,0.5)',
stack: 'Stack 1',
data: [4,9,12,5,7]
}, {
label: 'Banana',
borderColor: 'rgba(255, 252, 91,1)',
backgroundColor: 'rgba(255, 252, 91,0.5)',
stack: 'Stack 1',
data: [3,6,13,14,5]
}, {
label: 'Orange',
borderColor: 'rgba(255, 160, 242,1)',
backgroundColor: 'rgba(255, 160, 242,0.5)',
stack: 'Stack 1',
data: [7,12,3,0,2]
}, {
label: 'Cherry',
borderColor: 'rgba(255, 132, 38,1)',
backgroundColor: 'rgba(255, 132, 38,0.5)',
stack: 'Stack 1',
data: [8,4,7,11,6]
}, {
label: 'Pear',
borderColor: 'rgba(79, 158, 255,1)',
backgroundColor: 'rgba(79, 158, 255,0.5)',
stack: 'Stack 1',
data: [8,14,9,12,16]
}]
};
var ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
legend: {
display:true,
},
tooltips: {
mode: 'x',
intersect: false
},
responsive: true,
scales: {
xAxes: [{
stacked: true,
}],
yAxes: [{
stacked: true
}]
}
}
});
</script>
I would like to do two things:
1) Remove the redundant Apple, Orange and Pear labels
2) Highlight/Darken just the border on the individual stacks in the graph with the colors of those individual stacks. Like how the legend is.
Any help would be greatly appreciated.
Image of my graph
You can add custom logic to legend options like:
options: {
legend: {
display: true,
labels: {
generateLabels: function (chart) { ...
You can reach the labels here, customize their color, text, you can group them etc. So in this case you have to group the labels by their text first like
const labelTexts = []
const newLabels = []
labels = Chart.defaults.global.legend.labels.generateLabels(chart);
// group labels
labels.map((label) => {
if (labelTexts.indexOf(label.text) === -1) {
labelTexts.push(label.text)
}
})
labelTexts.map((text) => {
labels.map((label) => {
if (label.text === text) {
newLabels.push(label)
}
})
})
Then you can remove the redundant labels like:
for (var i = 0; i < newLabels.length - 2; i++) {
if (newLabels[i].text !== newLabels[i + 1].text) {
newLabels2.push(newLabels[i])
}
}
Check a working example here