custom ticks in ChartsJS line plot - chart.js

Take the ChartsJS line plot example (https://www.chartjs.org/docs/latest/charts/line.html). How can I change Y axis so that it simply shows two labels only: 'less' at 25% of the max Y value and 'more' at 75% of it?

This can be done by defining an afterBuildTicks together with a ticks.callback function on the y-axis.
Please take a look at below runnable code and see how it works.
new Chart('my-chart', {
type: 'line',
data: {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [{
label: 'My Data',
data: [65, 59, 80, 81, 56, 55, 40],
borderColor: '#1f77b4'
}]
},
options: {
scales: {
y: {
afterBuildTicks: axis => {
let values = axis.ticks.map(t => t.value);
let max = Math.max(...values);
let min = Math.min(...values);
axis.ticks = [
{ value: min + (max - min) * 0.25 },
{ value: min + (max - min) * 0.75 }
]
},
ticks: {
callback: (v, i) => i ? 'more' : 'less'
}
}
}
}
});
canvas {
max-height: 200px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js"></script>
<canvas id="my-chart"></canvas>

Related

How to use Chartjs to plot a single row of colored bars with a time based x axis

I have some time based data I want a graphical representation of, and was hoping to use Chartjs to plot this.
The data looks something like the following:
Time State
--------------
7am up
9am down
10.45am out
17.35 up
Also, each "state" will have its own color, so I would use this as a bar color when using a bar graph
up = red
down = yellow
out = green
The end result I am after is a simple one row bar like the following...
I thought I may be able to use a Chartjs horizontal stacked bar chart (https://www.chartjs.org/docs/latest/charts/bar.html#horizontal-bar-chart) to do this somehow, but I just can't work out how to get this working.
Some (not working) experimental code is as follows:
private createChart(): void {
if (this.chart !== undefined) {
return;
}
Chart.register(BarController, PointElement, Tooltip, Legend, TimeScale, LinearScale, CategoryScale, LinearScale, BarElement);
const options: ChartOptions = {
plugins: {
legend: {
display: false,
},
title: {
display: false,
},
},
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
stacked: true,
type: 'time',
display: true,
// position: 'bottom',
time: {
unit: 'minute',
displayFormats: {
minute: 'h:mm a'
}
}
},
x: {
stacked: false,
}
}
};
this.chart = new Chart(this.canvasRef.nativeElement, {
type: 'bar',
data: this.chartData,
options
});
this.chart.config.options.scales.y.min = new Date('2023-02-13T06:19:31.842Z').getTime();
const labels = [
'up',
'down'
// 'Dataset 2'
];
const d1 = new Date('2023-02-13T06:20:32.842Z').getTime();
const d2 = new Date('2023-02-13T06:21:33.842Z').getTime();
this.chartData = {
labels,
datasets: [{
label: 'up',
data: [{x: 10, y: d1}],
backgroundColor: 'red',
},
{
label: 'down',
data: [{x: 20, y: d2}],
backgroundColor: 'green',
}
]
};
this.chart.update();
}
In the above I have tried various combinations of labels, x values, y values, data shapes, but I only even get an empty graph.
Perhaps this is not really possible (I am trying to use the wrong component).
How can I achieve this using chartjs?
Update
Using example from #winner_joiner below, I have put a copy of it at plunkr and have tried to use the time in the x axis, but can see it is still not plotting the bars using the dates as the length
Well your code basically works, here a slightly modified version of your code.
After your comments and updated question, I reworke the example (seen below). Although it is possible to do with chart.js the question is, maybe for this specific task a different library or solution would be better/more convenient.
Update Chart, with some similar values from your question:
(I'm using here momentjs, since it is recommend usually needed form date/time actions in chartjs, as mentioned in the documentation)
const d0 = moment.duration('07:00:00').asMinutes();
const d1 = moment.duration('09:00:00').asMinutes();
const d2 = moment.duration('10:45:00').asMinutes();
const d3 = moment.duration('17:35:00').asMinutes();
const d4 = moment.duration('19:00:00').asMinutes();
let values = [d0, d1, d2, d3, d4];
let data = {
labels: [''],
datasets: [{
label: 'up',
axis: 'y',
data: [d1],
backgroundColor: 'red',
},{
label: 'down',
axis: 'y',
data: [d2],
backgroundColor: 'yellow',
},{
label: 'out',
axis: 'y',
data: [d3],
backgroundColor: 'green',
},{
label: 'up',
axis: 'y',
data: [d4],
backgroundColor: 'red',
}
]
};
const config = {
data,
type: 'bar',
options:{
plugins: {
tooltip: {
mode: 'dataset',
callbacks: {
label: function(item){
return moment().startOf('day').add({ minute: item.raw}).format('HH:mm');
}
}
},
legend: {
display: false,
},
title: {
display: false,
},
},
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
min: d0,
ticks: {
callback: function(value, index, ticks) {
return moment().startOf('day').add({ minute: value}).format('HH:mm');
}
},
afterBuildTicks: axis => axis.ticks = values.map(v => ({ value: v }))
},
y: {
stacked: true
},
}
}};
new Chart(document.getElementById("chart"), config);
<script src="//cdn.jsdelivr.net/npm/chart.js"></script>
<script src="//cdn.jsdelivr.net/npm/moment#^2"></script>
<script src="//cdn.jsdelivr.net/npm/chartjs-adapter-moment#^1"></script>
<div class="chart" style="height:184px; width:350px;">
<canvas id="chart" ></canvas>
</div>

Horizontal overlapping of bars, not whole graph

I have been asked to create a bar graph similar to the above and I want to know if it is possible within chart.js?? I have had a look at a number of sites (such as Overlapping Bar Chart using Chart.js and https://github.com/chartjs/Chart.js/issues/5224) but the idea there is to have two graphs behind each other directly - I need to be able to set the position of the blocks (i.e. almost go something like margin-left:-5px as I would in CSS) - or at least have some overlap (or about 40% overlap). If not I might have to build it in CSS but it would be great to use Chart JS.
The Plugin Core API offers a range of hooks that may be used for performing custom code. You can use the afterUpdate hook to shift the bars of individual datasets by the desired number of pixels as follows:
afterUpdate: function(chart) {
let datasets = chart.config.data.datasets;
for (let iDs = 1; iDs < datasets.length; iDs++) {
let dataset = datasets[iDs];
for (var i = 0; i < dataset._meta[0].data.length; i++) {
let model = dataset._meta[0].data[i]._model;
model.x += iDs * offset;
model.controlPointNextX += iDs * offset;
model.controlPointPreviousX += iDs * offset;
}
}
}
Please take a look at below runnable code snippet and see how it works.
const offset = 12;
new Chart('myChart', {
type: 'bar',
plugins: [{
afterUpdate: function(chart) {
let datasets = chart.config.data.datasets;
for (let iDs = 1; iDs < datasets.length; iDs++) {
let dataset = datasets[iDs];
for (var i = 0; i < dataset._meta[0].data.length; i++) {
let model = dataset._meta[0].data[i]._model;
model.x += iDs * offset;
model.controlPointNextX += iDs * offset;
model.controlPointPreviousX += iDs * offset;
}
}
}
}],
data: {
labels: ["A"],
datasets: [{
label: 'X',
backgroundColor: 'orange',
borderWidth: 1,
data: [5],
xAxisID: "bar-x-axis1",
barThickness: 30,
},
{
label: 'Y',
backgroundColor: 'red',
data: [10],
xAxisID: "bar-x-axis2",
barThickness: 30,
},
{
label: 'Z',
backgroundColor: 'lightblue',
data: [15],
xAxisID: "bar-x-axis3",
barThickness: 30,
}
]
},
options: {
legend: {
display: false
},
scales: {
xAxes: [{
id: "bar-x-axis3",
display: false
},
{
id: "bar-x-axis2",
offset: true,
display: false
},
{
id: "bar-x-axis1",
offset: true,
ticks: {
display: false
}
}
],
yAxes: [{
id: "bar-y-axis1",
ticks: {
beginAtZero: true,
stepSize: 5
}
}]
}
}
});
canvas {
max-width: 120px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<canvas id="myChart" height="150"></canvas>
UPDATE
Please also check this answer that provides a slightly improved solution.

ChartJS line-chart set background-opacity

I've tried multiple examples but i'm unable to get my line-chart to have background-transparency, as of now, it's blue, but the color i'm trying to set is orange (rgba(255, 145, 68, 0.2)).
TL;DR: How do I set the fill-opacity on my line-chart graph?
I've followed countless of tutorials, but all of them seem to set the background-color in dataset, which is not what I want, since i'm using dynamic data. The code is in a TypeScript component with tons of functionality built upon it, so it's quite hard to extract the code and make it work in a jsfiddle.
When i set fill:true under elements: { line: { the graph will get the fill as shown in the image below. But i'm unable to set the background-opacity here,
I've tried these attributes: backgroundColor, fillColor
Here's the code atleast:
import {
Component
} from '../../decorators/component.decorator';
import * as EsChart from '../../../../node_modules/chart.js/dist/Chart.bundle.js';
import * as angular from 'angular';
#Component({
templateUrl: 'app/dashboard-components/es-line-chart/es-line-chart.component.html',
bindings: {
esLineChartId: '#',
chartTitle: '#',
labels: '#',
esScaleMax: '#',
esScaleMaxSecond: '#',
esScaleStepW: '#',
esScaleStepWSecond: '#',
esScaleMin: '#',
esScaleMinSecond: '#',
lowerOkValue: '#',
targetValue: '#',
upperOkValue: '#',
chartDataValueSuffix: '#',
datasets: '#',
esXLabelSkips: '#'
}
})
export default class LineChartComponent {
$ctrl: any = this;
getChartOptions(lineChartData, secondYAxis, xLabelSkips) {
return {
type: 'line',
data: lineChartData,
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: this.$ctrl.chartTitle
},
legend: {
display: true,
labels: {
boxWidth: 12
}
},
tooltips: {
enabled: true,
mode: 'index',
intersect: false,
filter: function(tooltipItem, data) {
return !data.datasets[tooltipItem.datasetIndex].tooltipHidden;
},
callbacks: {
label: function(tooltipItem, data) {
var currentDataset = data.datasets[tooltipItem.datasetIndex];
var label = currentDataset.label + ': ';
label += currentDataset.data[tooltipItem.index] + (currentDataset.dataSuffix ? " " + currentDataset.dataSuffix : "");
return label;
}
}
},
elements: {
line: {
tension: 0, // disables bezier curves
borderWidth: 3, // Line stroke width in pixels
fill: true,
fillColor: "rgba(255, 145, 68, 0.2)",
strokeColor: "rgba(255, 145, 68, 0.2)",
pointColor: "rgba(255, 145, 68, 1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(255, 145, 68, 1)"
},
point: { // disable the circles/points in the chart lines
radius: 0,
hitRadius: 0,
hoverRadius: 0
},
},
scales: {
xAxes: [{
display: true,
type: 'category',
ticks: {
autoSkip: xLabelSkips,
maxRotation: 45
},
afterFit: (scale) => {
scale.height = 100;
}
}],
yAxes: [{
id: 'y-axis-0',
display: true,
position: 'left',
ticks: {
stepSize: this.$ctrl.esScaleStepW ? eval(this.$ctrl.esScaleStepW) : 0.5, // grid lines on yAxis with value distance of stepSize
suggestedMin: (eval(this.$ctrl.esScaleMin)),
suggestedMax: (eval(this.$ctrl.esScaleMax))
}
},
{
id: 'y-axis-1',
display: secondYAxis,
position: 'right',
ticks: {
stepSize: this.$ctrl.esScaleStepWSecond ? eval(this.$ctrl.esScaleStepWSecond) : 0.5, // grid lines on yAxis with value distance of stepSize
suggestedMin: (eval(this.$ctrl.esScaleMinSecond)),
suggestedMax: (eval(this.$ctrl.esScaleMaxSecond))
}
}
]
}
}
};
}
getExtraDatasets(datasetType: string, dataLabel: string, fillValue: number, chartSpan: number) {
var dataFill = [];
for (var i = 0; i < chartSpan; i++) {
dataFill.push(fillValue);
}
return {
label: dataLabel,
borderColor: datasetType === "limit" ? 'rgba(205, 8, 4, 1)' : datasetType === "target" ? 'rgba(20, 180, 2, 1)' : 'rgba(0, 0, 0, 0.5)',
borderWidth: 0.5,
//tooltipHidden: true,
data: dataFill
};
}
removeEmptyDatasets() {
//var test = "[{label: 'Verklig cykeltid',borderColor: 'rgba(60, 141, 188, 0.75)',borderWidth: 4,data: },{label: 'Kalkylerad cykeltid',borderColor: 'rgba(205, 108, 104, 0.75)',borderWidth: 4,data: [59]}]";
this.$ctrl.datasets = this.$ctrl.datasets.replace(/data: *,/g, "data: [],");
this.$ctrl.datasets = this.$ctrl.datasets.replace(/data: *}/g, "data: []}");
this.$ctrl.datasets = this.$ctrl.datasets.replace(/: *,/g, ": 0,");
this.$ctrl.datasets = this.$ctrl.datasets.replace(/: *}/g, ": 0}");
var sets = this.$ctrl.datasets.match(/\{[^\{\}]*\}/g);
for (var i = 0; i < sets.length; i++) {
if (sets[i].match(/data: *\[\]/g)) {
//console.log("no data", sets[i]);
sets[i] = "";
}
}
this.$ctrl.datasets = ("[" + (sets.join()).replace(/^,|,$/g, "") + "]");
}
$onInit() {
var canvas = $("#" + this.$ctrl.esLineChartId + " .lineChart").get(0).getContext("2d");
this.removeEmptyDatasets();
//console.log(this.$ctrl.datasets);
var lineChartData = {
labels: this.$ctrl.labels ? eval(this.$ctrl.labels) : [],
datasets: this.$ctrl.datasets ? eval(this.$ctrl.datasets) : []
};
if (this.$ctrl.upperOkValue) {
lineChartData.datasets = lineChartData.datasets.concat(this.getExtraDatasets("limit", "Övre gräns", eval(this.$ctrl.upperOkValue), (lineChartData.labels).length));
}
if (this.$ctrl.targetValue) {
lineChartData.datasets = lineChartData.datasets.concat(this.getExtraDatasets("target", "Målvärde", eval(this.$ctrl.targetValue), (lineChartData.labels).length));
}
if (this.$ctrl.lowerOkValue) {
lineChartData.datasets = lineChartData.datasets.concat(this.getExtraDatasets("limit", "Undre gräns", eval(this.$ctrl.lowerOkValue), (lineChartData.labels).length));
}
var secondYAxis = false;
for (var i = 0; i < (lineChartData.datasets).length; i++) {
// set backgroundColor to be the same as borderColor so that tooltip and legend items look better
(lineChartData.datasets[i]).backgroundColor = (lineChartData.datasets[i]).borderColor;
// Activate the second y axis if a dataset uses it
if ((lineChartData.datasets[i]).yAxisID == 'y-axis-1') {
secondYAxis = true;
}
// Use standard data suffix if defined and no special data suffix is defined for the dataset
if (!((lineChartData.datasets[i]).dataSuffix) && (this.$ctrl.chartDataValueSuffix)) {
(lineChartData.datasets[i]).dataSuffix = this.$ctrl.chartDataValueSuffix;
}
}
var xLabelSkips = this.$ctrl.esXLabelSkips ? eval(this.$ctrl.esXLabelSkips) : false;
var chartOptions = this.getChartOptions(lineChartData, secondYAxis, xLabelSkips);
var lineChart = new EsChart(canvas, chartOptions);
}
}
resources i've followed:
http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/
https://www.chartjs.org/docs/latest/configuration/elements.html
https://canvasjs.com/docs/charts/chart-options/data/fill-opacity/
Image:

Range at the end of the line chart

Is it possible to put a range at the right side of the line chart to compare the distance between the last 2 points of the 2 lines?
This can be achieved via a custom plugin making direct draw calls to the canvas, an example of which I've included below. Note that the code makes a lot of assumptions based on your screenshot and should be considered as a starting point rather than a perfect drop-in solution.
let myChart = new Chart(document.getElementById('chart'), {
type: 'line',
data: {
labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
datasets: [{
label: 'Group1',
data: [-1000, -2000, -2000, -3000, -4000, -3000, -5000],
backgroundColor: '#F48496'
}, {
label: 'Group2',
data: [-4000, -4000, -3000, -6000, -6000, -5000, -9000],
backgroundColor: '#61B2E9'
}]
},
options: {
maintainAspectRatio: false,
layout: {
padding: {
right: 100
}
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
},
plugins: {
afterRender: function(c) {
let
// calculate difference between values of last two points in first and second datasets.
d = c.config.data.datasets[0].data[c.config.data.datasets[0].data.length - 1] - c.config.data.datasets[1].data[c.config.data.datasets[1].data.length - 1],
// position of last point in first dataset.
xy0 = c.getDatasetMeta(0).data[c.getDatasetMeta(0).data.length - 1]._model,
// position of last point in second dataset.
xy1 = c.getDatasetMeta(1).data[c.getDatasetMeta(1).data.length - 1]._model;
c.ctx.save();
// draw the line.
c.ctx.strokeStyle = 'black';
c.ctx.beginPath();
c.ctx.moveTo(xy0.x + 10, xy0.y);
c.ctx.lineTo(xy0.x + 15, xy0.y); // draw the upper horizontal line.
c.ctx.lineTo(xy0.x + 15, xy1.y); // draw the vertical line.
c.ctx.lineTo(xy1.x + 10, xy1.y); // draw the lower horizontal line.
c.ctx.stroke();
// draw the text.
c.ctx.font = '20px sans-serif';
c.ctx.fillStyle = 'black';
c.ctx.fillText(
d, // text
c.chartArea.right + 25, // text x position
xy0.y + ((xy1.y - xy0.y) / 2) // text y position
);
c.ctx.restore();
}
}
});
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
<canvas id="chart"></canvas>

ChartJS:align zeros on chart with multi axes

How to align zeros on chart with multi axes, if there are both positive and negative values in dataset?
I want zeroes to be on the same line.
I dont like this:
Graph image
link to jsfiddle
new Chart(canvas, {
type: 'line',
data: {
labels: ['1', '2', '3', '4', '5'],
datasets: [{
label: 'A',
yAxisID: 'A',
data: [-10, 96, 84, 76, 69]
}, {
label: 'B',
yAxisID: 'B',
data: [-2, 3, 5, 2, 3]
}]
},
options: {
scales: {
yAxes: [{
id: 'A',
type: 'linear',
position: 'left',
}, {
id: 'B',
type: 'linear',
position: 'right',
}]
}
}
});
actualy, The example on the official web page has the same problem. Looks so messy.
The answer above is great if you know the ranges in advance and can specify min and max for each ticks object in advance. If, like me, you're trying to produce the effect with generated data, you'll need something that computes these values for you. The following does this without recourse to hard-coded values. It does generate ugly max / min values - but there's another fix for that: Hide min and max values from y Axis in Chart.js. It may also generate less than ideal spacing in which case a simple fudge factor applied to the min and max values may be used.
function scaleDataAxesToUnifyZeroes (datasets, options) {
let axes = options.scales.yAxes
// Determine overall max/min values for each dataset
datasets.forEach(function (line) {
let axis = line.yAxisID ? axes.filter(ax => ax.id === line.yAxisID)[0] : axes[0]
axis.min_value = Math.min(...line.data, axis.min_value || 0)
axis.max_value = Math.max(...line.data, axis.max_value || 0)
})
// Which gives the overall range of each axis
axes.forEach(axis => {
axis.range = axis.max_value - axis.min_value
// Express the min / max values as a fraction of the overall range
axis.min_ratio = axis.min_value / axis.range
axis.max_ratio = axis.max_value / axis.range
})
// Find the largest of these ratios
let largest = axes.reduce((a, b) => ({
min_ratio: Math.min(a.min_ratio, b.min_ratio),
max_ratio: Math.max(a.max_ratio, b.max_ratio)
}))
// Then scale each axis accordingly
axes.forEach(axis => {
axis.ticks = axis.ticks || { }
axis.ticks.min = largest.min_ratio * axis.range
axis.ticks.max = largest.max_ratio * axis.range
})
}
You can do this by setting the ticks option on your axes to control the max, min, and stepSize. To make the zeros align, you need to make the axes symmetric. stepSize is optional if you want the gridlines to be aligned as well.
In your example:
yAxes: [{
id: 'A',
type: 'linear',
position: 'left',
ticks : {
max: 100,
min: -50,
stepSize: 50
}
}, {
id: 'B',
type: 'linear',
position: 'right',
ticks : {
max: 6,
min: -3,
stepSize: 3
}
}]
See updated fiddle: https://jsfiddle.net/x885kpe1/1/