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); }