Tuesday, 23 June 2020

Red, Amber and Green progress bar

I've been doing some Moodle theming over the past few weeks and ended up discussing progress bars with the designer involved. Little progress is bad whereas finishing a Moodle course is good so I, of course, started waxing lyrical about colours. If red means bad and green means good, what about the middle ground? Then I remembered a traffic light visualisation I was working on ages ago and the joys of amber. Interestingly that post is ten years old! I must've been thinking about colours all these years - guess that's a good (green?) thing for a front-end developer!

Anyway, the project I'm working on, as I said, it based within Moodle and writing JS for that is something of a challenge. I can't get my head around the module system they use, and I don't want to learn it enough to get access to jQuery within a theme's script, so I wrote it in Vanilla JS.

I had CSS variables for the colours, so first I clocked I needed those within the script. Still, I also needed them in terms of the RGB values - as an array would work fine, after some research I came up with hexToRGB, which produces a simple collection with the RGB values as three distinct integer values in an array. One possible issue with using JS to get the computed style from a CSS variable is that it returns everything from the colon to the semi-colon as a string. That's good and all, but that left an extraneous space which needed trimming.

const hexToRGB = hex => hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => '#' + r + r + g + g + b + b).substring(1).match(/.{2}/g).map(x => parseInt(x, 16))
const red = hexToRGB(getComputedStyle(document.documentElement).getPropertyValue('--under-third').trim())
const amber = hexToRGB(getComputedStyle(document.documentElement).getPropertyValue('--third-to-two-thirds').trim())
const green = hexToRGB(getComputedStyle(document.documentElement).getPropertyValue('--over-two-thirds').trim())

I also needed to mix the colours, which wasn't much of a bother. Though I did discover there were multiple ways of mixing colours, I chose to go the most straightforward route with mixColour (hey, what can I say? I know I'm supposed to use `color` when I code HTML and CSS, but I much prefer `colour`). mixColour uses a further function, imaginatively called mix, where the heavy lifting occurs and takes into account the percentage of each colour needing mixing, we pass this as a float between 0 and 1.

const mixColour = (c1, c2, pc) => RGBToHex(Math.round(mix(c1[0], c2[0], pc)), Math.round(mix(c1[1], c2[1], pc)), Math.round(mix(c1[2], c2[2], pc)))
const mix = (s, e, pc) => s + ((pc) * (e - s))

Once mixed, I needed to produce RGB values again, so I worked up RGBToHex which uses some smart bit-wise operators - I wish I knew what they did. Still, I use bit-wise operators so infrequently that I can't seem to work up the enthusiasm to learn them properly.

const RGBToHex = (r, g, b) => `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`

We had values between 0 and 100 for the progress bar, so working out the float value was the only thing that caused me any head-scratching. If the amount was less than 50, then red and amber needed to be mixed by the value divided by 50; if the value was over 50 then we use amber and green: we need to subtract 50 from the value and then, again, divide the result by 50.

let value = 0;
const container = document.getElementById('bar');
const bar = container.querySelector('.progress-bar-filler');
const interval = setInterval(
  () => {
    value = value === 100 ? 0 : value + 1;
    container.setAttribute('data-value', value);
    bar.style.width = value + "%";
    bar.style.backgroundColor = value < 50
      ? mixColour(red, amber, (value / 50))
      : mixColour(amber, green, ((value - 50) / 50))
  }, 100);

Simples eh? Once it's all broken down, then it's just a collection of function, and it works a treat too. There are again some dodgy colours produced between amber and green - but that's only for a very little time, something like 5% I think.

All that was left was to work up a demo.

No comments:

Post a Comment