Chartjs: Is it possible to add phases in line chart? - charts

Hello I want to create a dynamic chart that looks like this: phases in line chart
Is it possible with ChartJs to create vertical lines at certain milestones? (If possible also give each phase another color)
If not, what other libraries can handle dynamic line chart libraries that support phases?

Using Chart.js plugins can help you with this. They let you handle specific events that are triggered during the chart creation like beforeInit, afterUpdate or afterDraw and are also easy to implement :
var myPlugin = {
afterDraw: function(chart) {
// This will be triggered after the chart is drawn
}
};
Chart.pluginService.register(myPlugin);
A plugin doing what you are looking for will be linked below, but first let me explain how it works.
In your chart options, you'll need to add a new property, called phases, which is an array :
// The following is an example that won't work with every chart
options: {
scales: {
xAxes: [{
// `from` & `to` must exist in your xAxe ticks
phases: [{
from: "January",
to: "March",
// You can also define the color here
color: "blue"
},
{
from: "May",
to: "June",
color: "red"
}]
}]
}
}
And then, here is the plugin that use this information :
var phasePlugin = {
// You need to handle the `afterDraw` event since you draw phases edges
// after everything is drawn
afterDraw: function(chart)
{
// All the variables the plugin needs follow ...
var ctx = chart.chart.ctx;
var xAxeId = chart.config.options.scales.xAxes[0].id;
var xAxe = chart.scales[xAxeId];
var yAxeId = chart.config.options.scales.yAxes[0].id;
var yAxe = chart.scales[yAxeId];
var ticks = xAxe.ticks;
var phases = chart.config.options.scales.xAxes[0].phases;
// For every phase ...
for (var i = 0; i < phases.length; i++) {
// We check if the tick actually exists
if (ticks.indexOf(phases[i].from) == -1 || (ticks.indexOf(phases[i].to) == -1)) {
// Else, we skip to the next phase
continue;
}
// We get the x position of the first limit tick
var xPos = ((xAxe.width - xAxe.paddingRight) / xAxe.maxIndex) * ticks.indexOf(phases[i].from) + xAxe.left + 1;
// And draw the dashed line
ctx.setLineDash([8, 8]);
ctx.strokeStyle = phases[i].color;
ctx.strokeRect(xPos, yAxe.top, 0, yAxe.height);
// Same for the other limit
xPos = ((xAxe.width - xAxe.paddingRight) / xAxe.maxIndex) * ticks.indexOf(phases[i].to) + xAxe.left + 1;
ctx.strokeStyle = phases[i].color;
ctx.strokeRect(xPos, yAxe.top, 0, yAxe.height);
ctx.setLineDash([1,0]);
}
}
};
You can see a fully working example in this jsFiddle and here is its result :

Related

References in axis using chart.js (or another library)

Im trying to make a graph like this:
https://www.google.com/finance?q=BCBA:PAMP
I have a line chart in chart.js, now I want to add labels (like the letters A, B, C) for certain dates.
Can't find a doc/example to start from. Any idea?
If its more simple to do with another library a recommendation is more than welcome.
Thanks!
Unfortunately, there is no native support in chart.js for what you are wanting. However, you can certainly add this capability using the plugin interface. This requires that you implement your own logic to draw the canvas pixels at the locations that you want them. It might sound challenging, but its easier than it sounds.
Here is an example plugin that will add a value above specific points in the chart (based upon configuration).
Chart.plugins.register({
afterDraw: function(chartInstance) {
if (chartInstance.config.options.showDatapoints || chartInstance.config.options.showDatapoints.display) {
var showOnly = chartInstance.config.options.showDatapoints.showOnly || [];
var helpers = Chart.helpers;
var ctx = chartInstance.chart.ctx;
var fontColor = helpers.getValueOrDefault(chartInstance.config.options.showDatapoints.fontColor, chartInstance.config.options.defaultFontColor);
// render the value of the chart above the bar
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize + 5, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = fontColor;
chartInstance.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
if (showOnly.includes(dataset.data[i])) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model;
var scaleMax = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
var yPos = (scaleMax - model.y) / scaleMax >= 0.93 ? model.y + 20 : model.y - 5;
ctx.fillText(dataset.data[i], model.x, yPos);
}
}
});
}
}
});
It allows you to configure which points you want to annotate using this new configuration. The showOnly option contains the points that you want to label.
options: {
showDatapoints: {
display: true,
showOnly: [3, 10, 9]
},
}
Obviously, this only adds the datapoint value at the specified points, but you can just change the plugin to paint whatever you want to show instead. Simply replace ctx.fillText(dataset.data[i], model.x, yPos) with different code to render something different on the canvas.
Here is a codepen example to show you want it looks like.

Chartjs: display different average line while grouping

Currently I'm working on chartjs and I found that is extremely fast to learn(at least for normal task).Currently I'm facing a problem: I was asked to display a grouped bar chart. like in figure.
grouped bar char
as you can see for the date 30-08-2016 there will be 3 distinct values for B,C,D and for 31-09-2016 same group of data but with different values.
I was asked also to add an average line for each different group in the chart
to look like this:
grouped bar chart with averages
I need to bind the start of one average line with the associated bar group.
I serached on internet but i couldn't find any example.Can you tell me if there is an example or give some suggestion? thanks in advance
this was my solution: i created a plugin and than tried to draw a line for each component of the chart. the code is quite messy (i'll try to clean)
// Define a plugin to provide average for different groups of data
Chart.plugins.register({
afterDatasetsDraw: function(chartInstance, easing) {
// To only draw at the end of animation, check for easing === 1
{
var ctx = chartInstance.chart.ctx;
var mapAverageLinePoints = {};
chartInstance.data.datasets.forEach(function (dataset, i) {
var meta = chartInstance.getDatasetMeta(i);
if (!meta.hidden) {
meta.data.forEach(function(element, index) {
var dataString = dataset.label;
var groupAverageLine = mapAverageLinePoints[dataString];
if(groupAverageLine==null)
{
groupAverageLine = [];
}
//store the point coordinate and the value
var linePoint =
{
x : position.x,
y : position.y,
value: dataset.data[index],
avg : 0
}
//adding the point to the array going to be stored in the map that group the point by the label
groupAverageLine.push(linePoint);
mapAverageLinePoints[dataString]=groupAverageLine;
}
);
}
});
for (var type in mapAverageLinePoints) {
var avgLinePoints = mapAverageLinePoints[type];
//NON E' in valore bensì rispecchia la sommatoria dei posY utilizzati nella rappresentazione
var totalYAxis=0;
var totale=0;
var labelNumero=0;
for(var k=0;k<avgLinePoints.length;k++)
{
var point = avgLinePoints[k];
totalYAxis+=point.y;
totale+=point.value;
//jump the first one
if(k>=1)
{
var prevPoint = avgLinePoints[k-1];
//k start from 0!!!!
var avgYAxis = (totalYAxis/(k+1));
var avg = (totale/(k+1));
// here i draw the line starting from the previous average
ctx.beginPath();
ctx.moveTo(point.x, avgYAxis);
ctx.strokeStyle = '#979797';
ctx.lineTo((prevPoint.x), prevPoint.avg);
ctx.stroke();
point.avg=avgYAxis;
//this one is for drawing a "o" where two segments collide
var fontSize = 12;
var fontStyle = 'normal';
var fontFamily = 'Helvetica Neue';
ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.fillText("avg: "+(avg/range).toLocaleString() + rangeSuffix + ' €', point.x, point.y+(point.value>0?-30:+10));
}
else{
//for the first one only record the y as the avg
point.avg=point.y;
}
var fontSize = 10;
var fontStyle = 'normal';
var fontFamily = 'Helvetica Neue';
ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.fillText("o", point.x, point.avg);
}
labelNumero=labelNumero+1;
}
}
}
});
the result is this one:
chart result

Display values outside of pie chart in chartjs

When I hover on pie chart, the values are displayed in tooltip. However, I want to display values outside of pie chart. I want to make chart like this image:
How to do this?
I was able to get something similar working using chart.js v2.3.0 using both the plugin API and extending chart types API. You should be able to take this as a starting point and tweak it to your needs.
Here is how it looks after being rendered.
Note, this requires digging deep into chart.js internals and could break if they change the way tooltips are positioned or rendered in the future. I also added a new configuration option called showAllTooltips to enable selectively using the plugin on certain charts. This should work for all chart types, but I am currently only using it for pie, doughnut, bar, and line charts so far.
With that said, here is a working solution for the image above.
Chart.plugins.register({
beforeRender: function (chart) {
if (chart.config.options.showAllTooltips) {
// create a namespace to persist plugin state (which unfortunately we have to do)
if (!chart.showAllTooltipsPlugin) {
chart.showAllTooltipsPlugin = {};
}
// turn off normal tooltips in case it was also enabled (which is the global default)
chart.options.tooltips.enabled = false;
// we can't use the chart tooltip because there is only one tooltip per chart which gets
// re-positioned via animation steps.....so let's create a place to hold our tooltips
chart.showAllTooltipsPlugin.tooltipsCollection = [];
// create a tooltip for each plot on the chart
chart.config.data.datasets.forEach(function (dataset, i) {
chart.getDatasetMeta(i).data.forEach(function (sector, j) {
// but only create one for pie and doughnut charts if the plot is large enough to even see
if (!_.contains(['doughnut', 'pie'], sector._chart.config.type) || sector._model.circumference > 0.1) {
var tooltip;
// create a new tooltip based upon configuration
if (chart.config.options.showAllTooltips.extendOut) {
// this tooltip reverses the location of the carets from the default
tooltip = new Chart.TooltipReversed({
_chart: chart.chart,
_chartInstance: chart,
_data: chart.data,
_options: chart.options.tooltips,
_active: [sector]
}, chart);
} else {
tooltip = new Chart.Tooltip({
_chart: chart.chart,
_chartInstance: chart,
_data: chart.data,
_options: chart.options.tooltips,
_active: [sector]
}, chart);
}
// might as well initialize this now...it would be a waste to do it once we are looping over our tooltips
tooltip.initialize();
// save the tooltips so they can be rendered later
chart.showAllTooltipsPlugin.tooltipsCollection.push(tooltip);
}
});
});
}
},
afterDraw: function (chart, easing) {
if (chart.config.options.showAllTooltips) {
// we want to wait until everything on the chart has been rendered before showing the
// tooltips for the first time...otherwise it looks weird
if (!chart.showAllTooltipsPlugin.initialRenderComplete) {
// still animating until easing === 1
if (easing !== 1) {
return;
}
// animation is complete, let's remember that fact
chart.showAllTooltipsPlugin.initialRenderComplete = true;
}
// at this point the chart has been fully rendered for the first time so start rendering tooltips
Chart.helpers.each(chart.showAllTooltipsPlugin.tooltipsCollection, function (tooltip) {
// create a namespace to persist plugin state within this tooltip (which unfortunately we have to do)
if (!tooltip.showAllTooltipsPlugin) {
tooltip.showAllTooltipsPlugin = {};
}
// re-enable this tooltip otherise it won't be drawn (remember we disabled all tooltips in beforeRender)
tooltip._options.enabled = true;
// perform standard tooltip setup (which determines it's alignment and x, y coordinates)
tooltip.update(); // determines alignment/position and stores in _view
tooltip.pivot(); // we don't actually need this since we are not animating tooltips, but let's be consistent
tooltip.transition(easing).draw(); // render and animate the tooltip
// disable this tooltip in case something else tries to do something with it later
tooltip._options.enabled = false;
});
}
},
});
// A 'reversed' tooltip places the caret on the opposite side from the current default.
// In order to do this we just need to change the 'alignment' logic
Chart.TooltipReversed = Chart.Tooltip.extend({
// Note: tooltipSize is the size of the box (not including the caret)
determineAlignment: function(tooltipSize) {
var me = this;
var model = me._model;
var chart = me._chart;
var chartArea = me._chartInstance.chartArea;
// set caret position to top or bottom if tooltip y position will extend outsite the chart top/bottom
if (model.y < tooltipSize.height) {
model.yAlign = 'top';
} else if (model.y > (chart.height - tooltipSize.height)) {
model.yAlign = 'bottom';
}
var leftAlign, rightAlign; // functions to determine left, right alignment
var overflowLeft, overflowRight; // functions to determine if left/right alignment causes tooltip to go outside chart
var yAlign; // function to get the y alignment if the tooltip goes outside of the left or right edges
var midX = (chartArea.left + chartArea.right) / 2;
var midY = (chartArea.top + chartArea.bottom) / 2;
if (model.yAlign === 'center') {
leftAlign = function(x) {
return x >= midX;
};
rightAlign = function(x) {
return x < midX;
};
} else {
leftAlign = function(x) {
return x <= (tooltipSize.width / 2);
};
rightAlign = function(x) {
return x >= (chart.width - (tooltipSize.width / 2));
};
}
overflowLeft = function(x) {
return x - tooltipSize.width < 0;
};
overflowRight = function(x) {
return x + tooltipSize.width > chart.width;
};
yAlign = function(y) {
return y <= midY ? 'bottom' : 'top';
};
if (leftAlign(model.x)) {
model.xAlign = 'left';
// Is tooltip too wide and goes over the right side of the chart.?
if (overflowLeft(model.x)) {
model.xAlign = 'center';
model.yAlign = yAlign(model.y);
}
} else if (rightAlign(model.x)) {
model.xAlign = 'right';
// Is tooltip too wide and goes outside left edge of canvas?
if (overflowRight(model.x)) {
model.xAlign = 'center';
model.yAlign = yAlign(model.y);
}
}
}
});

Chart.js and long labels

I use Chart.js to display a Radar Chart. My problem is that some labels are very long :
the chart can't be display or it appears very small.
So, is there a way to break lines or to assign a max-width to the labels?
Thank you for your help!
For Chart.js 2.0+ you can use an array as label:
Quoting the DOCs:
"Usage: If a label is an array as opposed to a string i.e. [["June","2015"], "July"] then each element is treated as a seperate line."
var data = {
labels: [["My", "long", "long", "long", "label"], "another label",...],
...
}
With ChartJS 2.1.6 and using #ArivanBastos answer
Just pass your long label to the following function, it will return your label in an array form, each element respecting your assigned maxWidth.
/**
* Takes a string phrase and breaks it into separate phrases
* no bigger than 'maxwidth', breaks are made at complete words.
*/
function formatLabel(str, maxwidth){
var sections = [];
var words = str.split(" ");
var temp = "";
words.forEach(function(item, index){
if(temp.length > 0)
{
var concat = temp + ' ' + item;
if(concat.length > maxwidth){
sections.push(temp);
temp = "";
}
else{
if(index == (words.length-1)) {
sections.push(concat);
return;
}
else {
temp = concat;
return;
}
}
}
if(index == (words.length-1)) {
sections.push(item);
return;
}
if(item.length < maxwidth) {
temp = item;
}
else {
sections.push(item);
}
});
return sections;
}
console.log(formatLabel("This string is a bit on the longer side, and contains the long word Supercalifragilisticexpialidocious for good measure.", 10))
To wrap the xAxes label, put the following code into optoins. (this will split from white space and wrap into multiple lines)
scales: {
xAxes: [
{
ticks: {
callback: function(label) {
if (/\s/.test(label)) {
return label.split(" ");
}else{
return label;
}
}
}
}
]
}
You can write a javascript function to customize the label:
// Interpolated JS string - can access value
scaleLabel: "<%=value%>",
See http://www.chartjs.org/docs/#getting-started-global-chart-configuration
Unfortunately there is no solution for this until now (April 5th 2016).
There are multiple issues on Chart.js to deal with this:
https://github.com/nnnick/Chart.js/issues/358 (closed with fix)
https://github.com/nnnick/Chart.js/issues/608 (closed with no fix)
https://github.com/nnnick/Chart.js/issues/358 (closed with no fix)
https://github.com/nnnick/Chart.js/issues/780 (closed with no fix)
https://github.com/nnnick/Chart.js/issues/752 (closed with no fix)
This is a workaround: Remove x-axis label/text in chart.js
It seems you might be actually be talking about data labels and not the scale labels. In this case you'd want to use the pointLabelFontSize option. See below example:
var ctx = $("#myChart").get(0).getContext("2d");
var data = {
labels: ["Eating", "Sleeping", "Coding"],
datasets: [
{
label: "First",
strokeColor: "#f00",
pointColor: "#f00",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "#ccc",
data: [45, 59, 90]
},
{
label: "Second",
strokeColor: "#00f",
pointColor: "#00f",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "#ccc",
data: [68, 48, 40]
}
]
};
// This is the important part
var options = {
pointLabelFontSize : 20
};
var myRadarChart = new Chart(ctx).Radar(data, options);
Finally you may want to play with the dimensions of your < canvas > element as I've found sometimes giving the Radar chart more height helps the auto scaling of everything.
I found the best way to manipulate the labels on the radar chart was by using the pointlabels configuration from Chartjs.
let skillChartOptions = {
scale: {
pointLabels: {
callback: (label: any) => {
return label.length > 5 ? label.substr(0, 5) + '...' : label;
},
}, ...
}, ...
}
I'd like to further extend on Fermin's answer with a slightly more readable version. As previously pointed out, it's possible to give Chart.js an array of strings to make it wrap the text. To make this array of strings from a longer string, I propose this function:
function chunkString(str, maxWidth){
const sections = [];
const words = str.split(" ");
let builder = "";
for (const word of words) {
if(word.length > maxWidth) {
sections.push(builder.trim())
builder = ""
sections.push(word.trim())
continue
}
let temp = `${builder} ${word}`
if(temp.length > maxWidth) {
sections.push(builder.trim())
builder = word
continue
}
builder = temp
}
sections.push(builder.trim())
return sections;
}
const str = "This string is a bit on the longer side, and contains the long word Supercalifragilisticexpialidocious for good measure."
console.log(str)
console.log(chunkString(str, 10))
.as-console-wrapper {
max-height: 100vh!important;
}
For most of the recent versions of chart.js, the labels can be mentioned as array of arrays. That's your labels can be:
labels = [['a', 'label1'],['the', 'lable2'],label3] '$'
You can use following function which is fast and compatible across all versions for converting your labels array into array of array in case the labels contain multiple words:
function splitLongLabels(labels){
//labels = ["ABC PQR", "XYZ"];
var i = 0, len = labels.length;
var splitlabels = labels;
while (i < len) {
var words = labels[i].trim().split(' ');
if(words.length>1){
for(var j=0; j<words.length; j++){
}
splitlabels[i] = words;
}
i++
}
return splitlabels;
}

Add a vertical line marker to Google Visualization's LineChart that moves when mouse move?

Is it possible to display a vertical Line marker showing the current x-axis value on LineChart, and moving when mouse moves ?
Thanks in advance.
While this was difficult before, a recent update to the API makes it much easier! You need to use a mouseover event handler to get the mouse coordinates and the new ChartLayoutInterface to translate the coordinates into chart values. Here's some example code:
[edit - fixed cross-browser compatibility issue]
*[edit - updated to get the value of points near the annotation line]*
function drawChart() {
// Create and populate the data table.
var data = new google.visualization.DataTable();
data.addColumn('number', 'x');
// add an "annotation" role column to the domain column
data.addColumn({type: 'string', role: 'annotation'});
data.addColumn('number', 'y');
// add 100 rows of pseudorandom data
var y = 50;
for (var i = 0; i < 100; i++) {
y += Math.floor(Math.random() * 5) * Math.pow(-1, Math.floor(Math.random() * 2));
data.addRow([i, null, y]);
}
// add a blank row to the end of the DataTable to hold the annotations
data.addRow([null, null, null]);
// get the index of the row used for the annotations
var annotationRowIndex = data.getNumberOfRows() - 1;
var options = {
annotation: {
1: {
// set the style of the domain column annotations to "line"
style: 'line'
}
},
height: 400,
width: 600
};
// create the chart
var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
// create 'ready' event listener to add mousemove event listener to the chart
var runOnce = google.visualization.events.addListener(chart, 'ready', function () {
google.visualization.events.removeListener(runOnce);
// create mousemove event listener in the chart's container
// I use jQuery, but you can use whatever works best for you
$('#chart_div').mousemove(function (e) {
var xPos = e.pageX - container.offsetLeft;
var yPos = e.pageY - container.offsetTop;
var cli = chart.getChartLayoutInterface();
var xBounds = cli.getBoundingBox('hAxis#0#gridline');
var yBounds = cli.getBoundingBox('vAxis#0#gridline');
// is the mouse inside the chart area?
if (
(xPos >= xBounds.left || xPos <= xBounds.left + xBounds.width) &&
(yPos >= yBounds.top || yPos <= yBounds.top + yBounds.height)
) {
// if so, draw the vertical line here
// get the x-axis value at these coordinates
var xVal = cli.getHAxisValue(xPos);
// set the x-axis value of the annotation
data.setValue(annotationRowIndex, 0, xVal);
// set the value to display on the line, this could be any value you want
data.setValue(annotationRowIndex, 1, xVal.toFixed(2));
// get the data value (if any) at the line
// truncating xVal to one decimal place,
// since it is unlikely to find an annotation like that aligns precisely with the data
var rows = data.getFilteredRows([{column: 0, value: parseFloat(xVal.toFixed(1))}]);
if (rows.length) {
var value = data.getValue(rows[0], 2);
// do something with value
}
// draw the chart with the new annotation
chart.draw(data, options);
}
});
});
// draw the chart
chart.draw(data, options);
}
See it working here: http://jsfiddle.net/asgallant/tVCv9/12