|
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); |
|
} |
Absolutely brilliant! Thank you for providing this to the community! 👍