Skip to content

Instantly share code, notes, and snippets.

@royashbrook
Last active September 4, 2020 19:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save royashbrook/c19b5ed165ff1a93791cc6ddc7157234 to your computer and use it in GitHub Desktop.
Save royashbrook/c19b5ed165ff1a93791cc6ddc7157234 to your computer and use it in GitHub Desktop.
ChartJS Bar Chart Label Plugin
//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)
})
})
},
}
// 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)
})
})
},
}
// 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