Last active
September 4, 2020 19:38
-
-
Save royashbrook/c19b5ed165ff1a93791cc6ddc7157234 to your computer and use it in GitHub Desktop.
ChartJS Bar Chart Label Plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//comments in blogpost | |
myoptions.events = [] | |
myoptions.animation = { | |
duration: 0, | |
onComplete: function () { | |
let chartInstance = this.chart | |
let ctx = chartInstance.chart.ctx | |
ctx.textAlign = 'center' | |
function formatLabel(value) { | |
let retval = '$' | |
let absvalue = Math.abs(value) | |
if (absvalue >= 1000000) { | |
retval += (absvalue / 1000 / 1000).toFixed(2) + 'M' | |
} else { | |
retval += +number_format(absvalue / 1000) + 'K' | |
} | |
if (value < 0) { | |
retval = `(${retval})` | |
} | |
return retval | |
} | |
this.data.datasets | |
.filter((x) => x.label === 'Total') | |
.forEach(function (dataset, i) { | |
let meta = dataset._meta[3] | |
meta.data.forEach(function (bar, index) { | |
//get label string | |
let data = formatLabel(dataset.data[index]) | |
// console.log(data,bar._model.x, bar._model.y); | |
//reposition based on positive/negative | |
ctx.fillStyle = '#000000' | |
//scale spacing and font based on the size of the bar | |
let width = bar._model.width / 4 | |
let yoffset = 5 //(width * (isNeg ? -2 : 1)) / 3 | |
let fontsize = 15 //width > 15 ? 15 : width | |
ctx.font = `${fontsize}px Arial` | |
//add label to bar | |
ctx.fillText(data, bar._model.x, bar._model.y - yoffset) | |
}) | |
}) | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// i have this in another file and am actually using svelte on this site, | |
//so i import this, but you could also just include the object value in your | |
//plugin value in options. I'm using inline plugins in this case because i | |
//have some customizations for each one for formatting. | |
export const BarChartLabels = { | |
afterRender: function (chart, options) { | |
// this is some item i had in here for looking at mobile, i figured | |
//i'd leave it here as a comment in case anyone wanted to see how you | |
//could check the screen width. The challenge on this seemed to be that | |
//the 'screen' would look at your device screen, so if you were shrinking | |
//the width vs changing the device in chrome, this will still show the same | |
//thing. But since most css rules are going to use the screen width to | |
//determine things, I wanted to do that the same way. | |
// if (window.screen.width < 575.99) { | |
// console.log('too small for labels', window.screen.width) | |
// return | |
// } | |
// i am not using any options, but plugins will get passed a chart instance by default | |
// i think you can skip the argument and just use 'this' but i normally try and avoid that | |
let chartInstance = chart | |
let ctx = chartInstance.chart.ctx | |
// aligning the text in the center based on the point we'll put it at. | |
ctx.textAlign = 'center' | |
// this is just a function to format the label. in this case, i have bar | |
//charts that are positive and negative and have various dollar amounts. | |
//i want to just abbreviate things to 2.33M for millions and 120k for | |
//thousands. i just leave it alone if it's bigger or smaller. I also wrap | |
//it in parentheses if it's negative and remove the - symbol. | |
// NOTE!!!! I am using a function i didn't include here called number_format. | |
//That's just a utility function that formats a number to a specific string | |
//format. you don't need it here, but i have it here as part of some common | |
//practice with some other labels so i left it. | |
function formatLabel(value) { | |
let retval = '$' | |
let absvalue = Math.abs(value) | |
if (absvalue >= 1000000) { | |
retval += (absvalue / 1000 / 1000).toFixed(2) + 'M' | |
} else { | |
retval += +number_format(absvalue / 1000) + 'K' | |
} | |
if (value < 0) { | |
retval = `(${retval})` | |
} | |
return retval | |
} | |
// so i am processing all of the datasets in the chart. But in reality I | |
//am only using this for one data set so I could just look for the first. | |
//But just in case you have a non stacked bar chart, processing them | |
//all should work this way. | |
chartInstance.data.datasets.forEach(function (dataset, i) { | |
// you need to get the meta for each item so you can get positions, | |
//the x and y are in there. there are other ways to access the meta as you | |
//can actually just walk down the tree, but this was in one of the examples | |
//I used and it seemed to work quite well for me so there you go. we'll get | |
//the meta for the index of the dataset in question passed in above | |
let meta = chartInstance.controller.getDatasetMeta(i) | |
// walk through our meta as this will have each of the points in it. i'm | |
//not sure what object this is, but we're calling it bar here as some | |
//example i found that did stuff to bar charts did this. again we are | |
//passing in the index for this item as well since we'll need to go back | |
//to the dataset, to it's data, and get the actual value out using this index. | |
meta.data.forEach(function (bar, index) { | |
// get label string, we go into the dataset that has this meta and get the | |
//index for this particular data element, now we have the value to put on | |
//our label. i'm calling the formatter here, but you could just use the value | |
//directly. i figured i'd include it for completeness. you could also use | |
//options and say pass in a formatter instead of including it here. that would | |
//make it more reusable, but the graphs i am using this on are basically | |
//all the same so i went with this route. | |
let data = formatLabel(dataset.data[index]) | |
// for me, i have some negative values, so a chart with a 0 in the middle | |
//and things fall above and below the line. i'll need to know that as the | |
//offset for something on the bottom will be different than something on the top. | |
const isNeg = dataset.data[index] < 0 | |
// set the baseline for the text to top or bottom based on whether it's positive or negative | |
ctx.textBaseline = isNeg ? 'top' : 'bottom' | |
// change color to red if it's negative, otherwise make it black | |
ctx.fillStyle = isNeg ? '#ff0000' : '#000000' | |
// here we scale spacing and font based on the size of the bar. there may | |
//be other ways to do this, but chartjs seems to handle resizing the width | |
//of bar charts just fine, so i am just using some relative numbers based | |
//on the width of the bar itself. Now, since they all resize, you can technically | |
//grab *any* of the bars on the chart and check the width because they are all | |
//the same. the name 'width' is a little decieving here because i'm really just | |
//getting something like a custom ratio multiplier or something. The idea was | |
//just to use some ratio of the bar width for font sizes so things looked more | |
//consistent vs having a giant bar and tiny text or having it the same size. | |
//this app runs on desktop and mobile so some scaling helps. may be a better | |
//way to do this, but when i played around with some of the responsive stuff | |
//with chartjs, it didn't let me fine tune it where i wanted to be and this worked just fine. | |
// these numbers were just trial and error, but just trying a size and then tuning it a bit worked fine. | |
let width = bar._model.width / 4 | |
// if we have a negative value, modify the y offset * 2 and make it negative. | |
//then we divice whatever the value is by 3 so whatever ratio we came up with | |
//for the font size using width, we are spacing the label itself up or down on | |
//the chart by 1/3 of that. | |
let yoffset = (width * (isNeg ? -2 : 1)) / 3 | |
// the intention was always for the 'width' to be the font size. ultimately i | |
//was shooting to make the font size scale. but i wanted the spacing to scale | |
//and wanted the font to have a maximum like i am setting here, so i changed | |
//the original 'fontsize' to width since it was just a fraction of the bar | |
//width, and then i set font size here seperately. fontsize in this case | |
//is going to have a maximum of 15 pt. | |
let fontsize = width > 15 ? 15 : width | |
// worth noting i tried using rem, em, ch, vw, etc and the only thing that | |
//seemed to work with chartjs was an actual px measurement. maybe i was doing | |
//something wrong, but i just kind of moved on and this seemed fine from my testing. | |
ctx.font = `${fontsize}px Arial` | |
// and finally, add the data. x seems to be the middle of the bar itself and then we | |
//offset up or down based on sizing info we figured out above. | |
ctx.fillText(data, bar._model.x, bar._model.y - yoffset) | |
}) | |
}) | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// so this one gets a little funny and i am going to refactor it a bit and may update | |
//this, but it does work just fine and is running in production currently. | |
export const StackedBarChartLabels = { | |
afterRender: function (chart, options) { | |
// this is the same as above | |
let chartInstance = chart | |
let ctx = chartInstance.chart.ctx | |
ctx.textAlign = 'center' | |
// this is the same as above, although there are some differences in | |
//formatting down below i'll talk about | |
function formatLabel(value) { | |
let retval = '$' | |
let absvalue = Math.abs(value) | |
if (absvalue >= 1000000) { | |
retval += (absvalue / 1000 / 1000).toFixed(2) + 'M' | |
} else { | |
retval += +number_format(absvalue / 1000) + 'K' | |
} | |
if (value < 0) { | |
retval = `(${retval})` | |
} | |
return retval | |
} | |
//so for 'stacked' bar charts, i did something different. When I am setting | |
//up the datasets for this chart, i add one with a total on it. | |
// let's assume i have a set of datasets i will turn into charts. each of | |
//these datasets has a label, and data at least. so there is just an array | |
//of values in the data element. you can see what i'm talking about if you | |
//just look at a chartjs example of a stacked bar chart. there are multiple | |
//datasets and they each have some set of data then you set the type to | |
//bar and set the axis values to stacked. | |
// good? ok so we have our datasets already for our stacked chart and it is | |
//working as a stacked chart, we just want to get a sum of all of the data | |
//for each of the values in our stacked chart. so to do this, i create another | |
//dataset with the sum of these values and that is passed in. I push it onto | |
//the datasets array so it is always the 'last' set of values in that array. | |
//this matters because i'll be getting some specific data directly from | |
//only that dataset down below. | |
// get the datasets for the chart | |
let datasets = chart.data.datasets; | |
//it seems like the metakey is always the same, at least on the charts i was | |
//working with, so basically this is getting the first meta key for the first | |
//element in datasets. in the regular bar chart, we iterate, but in here we | |
//are just going to grab the first one because the box sizes are all going | |
//to be the same size. so here we get the datasets, get the _meta, get the | |
//keys on that which gives us the index we need, but only get the first one, | |
//and then we need the first value from there to get the key. not sure | |
//why it's nested like this, but it is. | |
let firstmetakey = Object.keys(datasets.map(x => x._meta)[0])[0] | |
// now we just get the first width because we only need this | |
//one as they'll all be sized the same | |
let width = datasets.map(x => x._meta[firstmetakey]).map(a => a.data)[0][0]._model.width / 4 | |
// now we need the index for the totals dataset. This will always be the | |
//last one because we pushed it on as part of building the config | |
//for any charts that work like this | |
let totalsIdx = datasets.length - 1; //should always be last dataset | |
//get the totals dataset so we have info | |
let dataset = datasets[totalsIdx] | |
//get the first meta for this dataset | |
let meta = dataset._meta[firstmetakey] | |
//go through each bar and put a value in there. note that this is not actually | |
//a 'bar' this is actually a line chart that has showline: false in it's config | |
//and a point radius of 0. this is what was pushed along with the total data | |
//into datasets. so without doing anything, this won't appear. and if we did | |
//not turn off hover, when you hovered over this area you'd get a highlighted | |
//point at the top of each graph. since the top of each stacked bar will be | |
//at the same level as the line because the line is a total of the bars | |
//anyway. so we are really putting labels on the line, not the bars, but the | |
//line is invisible and the effect is having a floating label over your stacked bars | |
meta.data.forEach(function (bar, index) { | |
// in this case, i have some bars that have small numbers. Those charts are not | |
//money, so i just return them if not. i could make this number bigger or move | |
//it into the format label itself, but this is how it's done now. will be cleaned | |
//up in next refactor, but figured i'd leave it as is for now. | |
let data = dataset.data[index] < 100 ? dataset.data[index] : formatLabel(dataset.data[index]) | |
// no negative numbers in these graphs, so we're always black. this is set | |
//here because the default color is actually different for the charts, but | |
//i'm just using black for the bar chart labels | |
ctx.fillStyle = '#000000' | |
//offsets and sizing are slightly different here. i'm not sure why this is, but | |
//when i used the exact same values it didn't work. after reviewing this prior | |
//to refactor i'm guessing this is due to setting the textbaseline in the regular | |
//and not in stacked. but regardless these numbers were just tweaked slightly and | |
//it looked identical. again, they'll be standardized in the next refactor, | |
//but i thought i'd include it here. | |
let yoffset = (width * (1)) / 2 | |
let fontsize = width > 15 ? 15 : width | |
// in this case, we have some additional adjustments because single/double digit | |
//numbers looked too tiny, so we are boosting their size and the offset if | |
//they are single/double digit. | |
if (`${data}`.length < 3) { | |
fontsize = fontsize * 1.5 | |
yoffset = yoffset * 1.5 | |
} | |
//the rest is the same | |
ctx.font = `${fontsize}px Arial` | |
ctx.fillText(data, bar._model.x, bar._model.y - yoffset) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment