Skip to content

Instantly share code, notes, and snippets.

@brattonc
Last active March 28, 2021 12:00
Show Gist options
  • Save brattonc/d54d1c9d33aa13491279 to your computer and use it in GitHub Desktop.
Save brattonc/d54d1c9d33aa13491279 to your computer and use it in GitHub Desktop.
D3 Bar Stacker Gauge

A D3 Bar Stacker Gauge with animated filling.

Configurable features include:

  • Configurable minimum and maximum values.
  • Configurable corner rounding.
  • Padding.
  • Color.
  • Horizontal or verticle layout.
  • Text size.
  • Text position.
  • Value text prefix/postfix.
  • Bar thickness.
  • Animation speed.

Open source under BSD 2-clause
Copyright (c) 2015, Curtis Bratton
All rights reserved.

function barStackerDefaultSettings(){
return {
minValue: 0, // The gauge minimum value.
maxValue: 100, // The gauge maximum value.
cornerRoundingX: 20,
cornerRoundingY: 20,
barBoxPadding: 6,
barPadding: 6,
color: "#222222", // The color of the outer circle.
vertical: true,
textLeftTop: true,
textPx: 20,
barThickness: 3,
valuePrefix: "",
valuePostfix: "",
valueAnimateTime: 1000
};
}
function loadBarStacker(elementId, label, value, config) {
if(config == null) config = barStackerDefaultSettings();
if(value > config.maxValue)
value = config.maxValue;
if(value < config.minValue)
value = config.minValue;
var stacker = d3.select("#" + elementId);
var stackerWidth = parseInt(stacker.style("width"));
var stackerHeight = parseInt(stacker.style("height"));
var barBoxX = config.vertical ? (config.textLeftTop ? config.barBoxPadding + config.textPx : config.barBoxPadding) : config.barBoxPadding;
var barBoxY = config.vertical ? config.barBoxPadding : (config.textLeftTop ? config.barBoxPadding + config.textPx : config.barBoxPadding);
var barBoxHeight = config.vertical ? (stackerHeight - config.barBoxPadding * 2) : (stackerHeight - config.textPx - config.barBoxPadding * 2);
var barBoxWidth = config.vertical ? (stackerWidth - config.textPx - config.barBoxPadding * 2) : (stackerWidth - config.barBoxPadding * 2);
var barClipPathBoxX = barBoxX + config.barPadding;
var barClipPathBoxY = barBoxY + config.barPadding;
var barClipPathBoxHeight = barBoxHeight - config.barPadding * 2;
var barClipPathBoxWidth = barBoxWidth - config.barPadding * 2;
var textRightBottomPaddingMultiplier = 0.25;
var textRotation = config.vertical ? -90 : 0;
var labelTextX = config.vertical ? (config.textLeftTop ? config.textPx : stackerWidth - (config.textPx*textRightBottomPaddingMultiplier)) : config.cornerRoundingX;
var labelTextY = config.vertical ? stackerHeight - config.cornerRoundingY : (config.textLeftTop ? config.textPx : stackerHeight - (config.textPx*textRightBottomPaddingMultiplier));
var valueTextX = config.vertical ? (config.textLeftTop ? config.textPx : stackerWidth - (config.textPx*textRightBottomPaddingMultiplier)) : stackerWidth - config.cornerRoundingX;
var valueTextY = config.vertical ? config.cornerRoundingY : (config.textLeftTop ? config.textPx : stackerHeight - (config.textPx*textRightBottomPaddingMultiplier));
var defs = stacker.append("defs");
var mask = defs.append("mask")
.attr("id", "barboxMask_" + elementId);
mask.append("rect")
.attr("height", stackerHeight)
.attr("width", stackerWidth)
.attr("rx", config.cornerRoundingX)
.attr("ry", config.cornerRoundingY)
.style("fill", "white");
mask.append("rect")
.attr("x", barBoxX)
.attr("y", barBoxY)
.attr("rx", config.cornerRoundingX)
.attr("ry", config.cornerRoundingY)
.attr("height", barBoxHeight)
.attr("width", barBoxWidth)
.style("fill", "black");
mask.append("text")
.text(label)
.attr("text-anchor", "start")
.attr("font-size", config.textPx + "px")
.attr("x", labelTextX)
.attr("y", labelTextY)
.style("fill", "black")
.attr("transform","rotate("+textRotation+" "+labelTextX+" "+labelTextY+")");
var valueText = mask.append("text")
.text(config.valuePrefix + 0 + config.valuePostfix)
.attr("V", 0)
.attr("text-anchor", "end")
.attr("font-size", config.textPx + "px")
.attr("x", valueTextX)
.attr("y", valueTextY)
.style("fill", "black")
.attr("transform","rotate("+textRotation+" "+valueTextX+" "+valueTextY+")");
defs.append("clipPath")
.attr("id", "barClipPath_" + elementId)
.append("rect")
.attr("x", barClipPathBoxX)
.attr("y", barClipPathBoxY)
.attr("rx", config.cornerRoundingX)
.attr("ry", config.cornerRoundingY)
.attr("height", barClipPathBoxHeight)
.attr("width", barClipPathBoxWidth);
stacker.append("rect")
.attr("height", stackerHeight)
.attr("width", stackerWidth)
.attr("rx", config.cornerRoundingX)
.attr("ry", config.cornerRoundingY)
.style("fill", config.color)
.attr("mask", "url(#barboxMask_" + elementId + ")");
var barGroup = stacker.append("g")
.attr("clip-path", "url(#barClipPath_" + elementId + ")")
.attr("T", 0);
var barCount = config.vertical ? barClipPathBoxHeight / (config.barThickness * 2) : barClipPathBoxWidth / (config.barThickness * 2);
var bars = [];
//Draw all the bars.
for(var i = 0; i < barCount; i++){
if(config.vertical){
bars[i] = barGroup.append("rect")
.attr("x", barClipPathBoxX)
.attr("y", (barClipPathBoxY + barClipPathBoxHeight - config.barThickness) - (i * config.barThickness * 2))
.attr("height", config.barThickness)
.attr("width", barClipPathBoxWidth)
.style("fill", config.color)
.style("visibility", "hidden");
} else {
bars[i] = barGroup.append("rect")
.attr("x", barClipPathBoxX + i * config.barThickness * 2)
.attr("y", barClipPathBoxY)
.attr("height", barClipPathBoxHeight)
.attr("width", config.barThickness)
.style("fill", config.color)
.style("visibility", "hidden");
}
}
valueText.transition()
.duration(config.valueAnimateTime)
.tween("text", valueTextAnimator(value));
barGroup.transition()
.duration(config.valueAnimateTime)
.attrTween("T", new BarTweener(value, bars).tween);
//A tweener for revealing or hiding bars one at a time.
// This is done instead of clipping or using a mask to prevent displaying partial bars. If a bar has a thickness
// of 5 pixels for example, using these methods could cause only a couple pixels of the bar to display as the clip
// or mask slides around.
function BarTweener(value, bars){
var _bars = bars;
//The new maximum bar to display
var endBar = calcBarValue(value)-1;
var newMaxBar;
this.tween = function(d,i,a){
var startBar = parseInt(a);
var ascend = endBar > startBar; //Are we animating up or down...
var barId = d3.interpolateRound(startBar, endBar);
return function(t) {
newMaxBar = barId(t); //The maximum bar to display at this point in the tween.
barGroup.attr("T", newMaxBar); //Keep track of the new max bar incase the animation gets interrupted.
if(ascend){
//If we're going up, find the highest bar below newMaxBar that's currently visible...
while(newMaxBar > 0 && _bars[newMaxBar].style("visibility") == "hidden")
newMaxBar--;
//From there going up, make all bars visible until newMaxBar.
for(var i = newMaxBar; i <= barId(t); i++){
_bars[i].style("visibility", "visible");
}
} else {
//If we're going down, find highest bar above newMaxBar that's currently visible...
while(newMaxBar < startBar && (newMaxBar < 0 ||_bars[newMaxBar].style("visibility") == "visible"))
newMaxBar++;
//From there going down, make all bars hidden until newMaxBar.
for(var i = newMaxBar; i > barId(t) && i >= 0; i--){
_bars[i].style("visibility", "hidden");
}
}
return endBar;
};
}
this.interrupt = function(){
//And our animation was going so well...
barGroup.attr("T", newMaxBar);
for(var i = newMaxBar; i < barCount; i++){
_bars[i].style("visibility", "hidden");
}
}
}
function calcBarValue(value){
return Math.ceil(barCount * ((value - config.minValue) / (config.maxValue - config.minValue)));
}
function valueTextAnimator(value){
return function(){
var i = d3.interpolate(d3.select(this).attr("V"), value);
return function(t) {
var newValue = Math.round(i(t));
this.textContent = config.valuePrefix + newValue + config.valuePostfix;
//Store the current value in V as this is easier than parsing the current value of textContent which
//may contain a prefix or postfix. This is needed so that, if the text tween gets interrupted, the
//next animation will start from whatever the current text value is.
d3.select(this).attr("V", newValue);
}
}
}
function BarStackerUpdater(bars){
var _bars = bars;
this.update = function(value){
if(value > config.maxValue)
value = config.maxValue;
if(value < config.minValue)
value = config.minValue;
var barTweener = new BarTweener(value, _bars);
barGroup.transition()
.duration(config.valueAnimateTime)
.attrTween("T", barTweener.tween)
.each("interrupt", barTweener.interrupt);
valueText.transition()
.duration(config.valueAnimateTime)
.tween("text", valueTextAnimator(value));
}
}
return new BarStackerUpdater(bars);
}
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="http://d3js.org/d3.v3.min.js" language="JavaScript"></script>
<script src="barstacker.js" language="JavaScript"></script>
</head>
<body>
<div align="center" style="width: 600px;">
<svg id="stacker1" width="100" height="300" style="font-family: Helvetica; font-weight: bold;" onclick="stacker1.update((Math.random()*100));"></svg>
<svg id="stacker2" width="200" height="300" style="font-family: Helvetica; font-weight: bold;" onclick="stacker2.update((Math.random()*100));"></svg>
<svg id="stacker3" width="150" height="300" style="font-family: Helvetica; font-weight: bold;" onclick="stacker3.update((Math.random()*100));"></svg>
<svg id="stacker4" width="100" height="300" style="font-family: Helvetica; font-weight: bold;" onclick="stacker4.update((Math.random()*1200));"></svg><br/>
<svg id="stacker5" width="560" height="100" style="font-family: Helvetica; font-weight: bold;" onclick="stacker5.update((Math.random()*100));"></svg><br/>
<svg id="stacker6" width="560" height="50" style="font-family: Helvetica; font-weight: bold;" onclick="stacker6.update((Math.random()*750));"></svg>
</div>
<script language="JavaScript">
var stacker1 = loadBarStacker("stacker1", "Fuel", 100);
var settings2 = barStackerDefaultSettings();
settings2.barThickness = 6;
settings2.valueAnimateTime = 2000;
settings2.textPx = 15;
settings2.valuePostfix = " Liters";
settings2.maxValue = 40;
settings2.color = "#2B1600";
settings2.cornerRoundingX = 70;
settings2.cornerRoundingY = 50;
var stacker2 = loadBarStacker("stacker2", "Coffee Remaining", 20, settings2);
var settings3 = barStackerDefaultSettings();
settings3.textLeftTop = false;
settings3.barBoxPadding = 3;
settings3.barPadding = 0;
settings3.valueAnimateTime = 350;
settings3.cornerRoundingX = 5;
settings3.cornerRoundingY = 30;
var stacker3 = loadBarStacker("stacker3", "Queue Length", 56, settings3);
var settings4 = barStackerDefaultSettings();
settings4.barThickness = 1;
settings4.barBoxPadding = 12;
settings4.barPadding = 1;
settings4.textPx = 30;
settings4.maxValue = 1500;
settings4.color = "#0066FF";
settings4.cornerRoundingX = 5;
settings4.cornerRoundingY = 5;
var stacker4 = loadBarStacker("stacker4", "Minerals",650, settings4);
var settings5 = barStackerDefaultSettings();
settings5.vertical = false;
settings5.valuePrefix = "$";
settings5.color = "#337733";
var stacker5 = loadBarStacker("stacker5", "Balance", 45, settings5);
var settings6 = barStackerDefaultSettings();
settings6.vertical = false;
settings6.textLeftTop = false;
settings6.textPx = 16;
settings6.valuePostfix = "gb";
settings6.maxValue = 750;
settings6.barThickness = 2;
settings6.barPadding = 3;
settings6.barBoxPadding = 12;
settings6.cornerRoundingX = 8;
settings6.cornerRoundingY = 20;
var stacker6 = loadBarStacker("stacker6", "Storage Capacity Available", 435, settings6);
</script>
</body>
</html>
@rlugojr
Copy link

rlugojr commented Jul 4, 2016

Absolutely brilliant! Thank you for providing this to the community! 👍

@ashley-solanoh
Copy link

Thanks for contributing this awesome graphs. Do you happen to have a version using d3 V4?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment