Skip to content

Instantly share code, notes, and snippets.

@alexbordewyk
Forked from brattonc/README.md
Last active August 24, 2017 17:57
Show Gist options
  • Select an option

  • Save alexbordewyk/8523bad1e75c2cb32dffca64ac882b29 to your computer and use it in GitHub Desktop.

Select an option

Save alexbordewyk/8523bad1e75c2cb32dffca64ac882b29 to your computer and use it in GitHub Desktop.
D3v4 Liquid Fill Gauge

Liquid Fill Gauge v2.1 - 8/24/2017

Changes:

  • Updated to support D3 v4
  • Updated to ES6

Configurable features include:

  • Changeable min/max values.
  • All colors.
  • Outer circle thickness.
  • Gap between the outer circle and inner fill area.
  • Wave height.
  • Wave speed.
  • Wave count.
  • Wave rise time.
  • Wave height scaling on/off. Reduces the wave height near the min/max values so that the wave won't make the fill area appear total full or totally empty.
  • Wave starting offset. Most useful when wave animation is turned off and you want the wave min or max at a specific horizontal position in the fill area.
  • Wave rising upon load on/off.
  • Wave animation on/off.
  • Text height.
  • Text vertical position.
  • Text increment from min value upon loading.
  • Display of % symbol on/off.

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

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="http://d3js.org/d3.v4.min.js" language="JavaScript"></script>
<script src="liquidFillGauge.js" language="JavaScript"></script>
<style>
.liquidFillGaugeText { font-family: Helvetica; font-weight: bold; }
</style>
</head>
<body>
<svg id="fillgauge1" width="97%" height="250" onclick="gauge1.update(NewValue());"></svg>
<svg id="fillgauge2" width="19%" height="200" onclick="gauge2.update(NewValue());"></svg>
<svg id="fillgauge3" width="19%" height="200" onclick="gauge3.update(NewValue());"></svg>
<svg id="fillgauge4" width="19%" height="200" onclick="gauge4.update(NewValue());"></svg>
<svg id="fillgauge5" width="19%" height="200" onclick="gauge5.update(NewValue());"></svg>
<svg id="fillgauge6" width="19%" height="200" onclick="gauge6.update(NewValue());"></svg>
<script language="JavaScript">
var gauge1 = loadLiquidFillGauge("fillgauge1", 55);
var config1 = liquidFillGaugeDefaultSettings();
config1.circleColor = "#FF7777";
config1.textColor = "#FF4444";
config1.waveTextColor = "#FFAAAA";
config1.waveColor = "#FFDDDD";
config1.circleThickness = 0.2;
config1.textVertPosition = 0.2;
config1.waveAnimateTime = 1000;
var gauge2= loadLiquidFillGauge("fillgauge2", 28, config1);
var config2 = liquidFillGaugeDefaultSettings();
config2.circleColor = "#D4AB6A";
config2.textColor = "#553300";
config2.waveTextColor = "#805615";
config2.waveColor = "#AA7D39";
config2.circleThickness = 0.1;
config2.circleFillGap = 0.2;
config2.textVertPosition = 0.8;
config2.waveAnimateTime = 2000;
config2.waveHeight = 0.3;
config2.waveCount = 1;
var gauge3 = loadLiquidFillGauge("fillgauge3", 60.1, config2);
var config3 = liquidFillGaugeDefaultSettings();
config3.textVertPosition = 0.8;
config3.waveAnimateTime = 5000;
config3.waveHeight = 0.15;
config3.waveAnimate = false;
config3.waveOffset = 0.25;
config3.valueCountUp = false;
config3.displayPercent = false;
var gauge4 = loadLiquidFillGauge("fillgauge4", 50, config3);
var config4 = liquidFillGaugeDefaultSettings();
config4.circleThickness = 0.15;
config4.circleColor = "#808015";
config4.textColor = "#555500";
config4.waveTextColor = "#FFFFAA";
config4.waveColor = "#AAAA39";
config4.textVertPosition = 0.8;
config4.waveAnimateTime = 1000;
config4.waveHeight = 0.05;
config4.waveAnimate = true;
config4.waveRise = false;
config4.waveHeightScaling = false;
config4.waveOffset = 0.25;
config4.textSize = 0.75;
config4.waveCount = 3;
var gauge5 = loadLiquidFillGauge("fillgauge5", 60.44, config4);
var config5 = liquidFillGaugeDefaultSettings();
config5.circleThickness = 0.4;
config5.circleColor = "#6DA398";
config5.textColor = "#0E5144";
config5.waveTextColor = "#6DA398";
config5.waveColor = "#246D5F";
config5.textVertPosition = 0.52;
config5.waveAnimateTime = 5000;
config5.waveHeight = 0;
config5.waveAnimate = false;
config5.waveCount = 2;
config5.waveOffset = 0.25;
config5.textSize = 1.2;
config5.minValue = 30;
config5.maxValue = 150
config5.displayPercent = false;
var gauge6 = loadLiquidFillGauge("fillgauge6", 120, config5);
function NewValue(){
if(Math.random() > .5){
return Math.round(Math.random()*100);
} else {
return (Math.random()*100).toFixed(1);
}
}
</script>
</body>
</html>
/*!
* @license Open source under BSD 2-clause (http://choosealicense.com/licenses/bsd-2-clause/)
* Copyright (c) 2015, Curtis Bratton
* All rights reserved.
*
* Liquid Fill Gauge v2.1
* Updated by Alex Bordewyk
*/
function liquidFillGaugeDefaultSettings(){
return {
minValue: 0, // The gauge minimum value.
maxValue: 100, // The gauge maximum value.
circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius.
circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius.
circleColor: '#178BCA', // The color of the outer circle.
waveHeight: 0.05, // The wave height as a percentage of the radius of the wave circle.
waveCount: 1, // The number of full waves per width of the wave circle.
waveRiseTime: 1000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
waveAnimateTime: 18000, // The amount of time in milliseconds for a full wave to enter the wave circle.
waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height.
waveHeightScaling: true, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill.
waveAnimate: true, // Controls if the wave scrolls or is static.
waveColor: '#178BCA', // The color of the fill wave.
waveOffset: 0, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave.
textVertPosition: .5, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top.
textSize: 1, // The relative height of the text to display in the wave circle. 1 = 50%
valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed.
displayPercent: true, // If true, a % symbol is displayed after the value.
textColor: '#045681', // The color of the value text when the wave does not overlap it.
waveTextColor: '#A4DBf8' // The color of the value text when the wave overlaps it.
};
}
function loadLiquidFillGauge(elementId, value, config) {
if (config == null) config = liquidFillGaugeDefaultSettings();
const gauge = d3.select(`#${elementId}`);
const radius = Math.min(parseInt(gauge.style('width')), parseInt(gauge.style('height'))) / 2;
const locationX = (parseInt(gauge.style('width')) / 2) - radius;
const locationY = (parseInt(gauge.style('height')) / 2) - radius;
const fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
let waveHeightScale;
if (config.waveHeightScaling) {
waveHeightScale = d3.scaleLinear()
.range([0, config.waveHeight, 0])
.domain([0, 50, 100]);
} else {
waveHeightScale = d3.scaleLinear()
.range([config.waveHeight, config.waveHeight])
.domain([0, 100]);
}
const textPixels = (config.textSize * radius) / 2;
const textFinalValue = parseFloat(value).toFixed(2);
const textStartValue = config.valueCountUp ? config.minValue : textFinalValue;
const percentText = config.displayPercent ? '%' : '';
const circleThickness = config.circleThickness * radius;
const circleFillGap = config.circleFillGap * radius;
const fillCircleMargin = circleThickness + circleFillGap;
const fillCircleRadius = radius - fillCircleMargin;
const waveHeight = fillCircleRadius * waveHeightScale(fillPercent * 100);
const waveLength = (fillCircleRadius * 2) / config.waveCount;
const waveClipCount = 1 + config.waveCount;
const waveClipWidth = waveLength * waveClipCount;
// Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
let textRounder = value => Math.round(value);
if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
textRounder = value => parseFloat(value).toFixed(1);
}
if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
textRounder = value => parseFloat(value).toFixed(2);
}
// Data for building the clip wave area.
let data = [];
for (let i = 0; i <= 40 * waveClipCount; i++) {
data.push({ x: i / (40 * waveClipCount), y: i / 40 });
}
// Scales for drawing the outer circle.
const gaugeCircleX = d3.scaleLinear().range([0, 2 * Math.PI]).domain([0, 1]);
const gaugeCircleY = d3.scaleLinear().range([0, radius]).domain([0, radius]);
// Scales for controlling the size of the clipping path.
const waveScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
const waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);
// Scales for controlling the position of the clipping path.
const waveRiseScale = d3.scaleLinear()
// The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
// such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
// circle at 100%.
.range([(fillCircleMargin + waveHeight) + (fillCircleRadius * 2), fillCircleMargin - waveHeight])
.domain([0, 1]);
const waveAnimateScale = d3.scaleLinear()
.range([0, waveClipWidth - (fillCircleRadius * 2)]) // Push the clip area one full wave then snap back.
.domain([0, 1]);
// Scale for controlling the position of the text within the gauge.
const textRiseScaleY = d3.scaleLinear()
.range([fillCircleMargin + (fillCircleRadius * 2), fillCircleMargin + (textPixels * 0.7)])
.domain([0, 1]);
// Center the gauge within the parent SVG.
const gaugeGroup = gauge.append('g')
.attr('transform', `translate(${locationX},${locationY})`);
// Draw the outer circle.
const gaugeCircleArc = d3.arc()
.startAngle(gaugeCircleX(0))
.endAngle(gaugeCircleX(1))
.outerRadius(gaugeCircleY(radius))
.innerRadius(gaugeCircleY(radius-circleThickness));
gaugeGroup.append('path')
.attr('d', gaugeCircleArc)
.style('fill', config.circleColor)
.attr('transform', `translate(${radius},${radius})`);
// Text where the wave does not overlap.
const text1 = gaugeGroup.append('text')
.text(textRounder(textStartValue) + percentText)
.attr('class', 'liquidFillGaugeText')
.attr('id', 'text1')
.attr('text-anchor', 'middle')
.attr('font-size', `${textPixels}px`)
.style('fill', config.textColor)
.attr('transform', `translate(${radius},${textRiseScaleY(config.textVertPosition)})`);
// The clipping wave area.
const clipArea = d3.area()
.x(d => waveScaleX(d.x))
.y0(d => waveScaleY(Math.sin((Math.PI * 2 * config.waveOffset * -1) + (Math.PI * 2 * (1 - config.waveCount)) + (d.y * 2 * Math.PI))))
.y1(d => (fillCircleRadius * 2) + waveHeight);
const waveGroup = gaugeGroup.append('defs')
.append('clipPath')
.attr('id', `clipWave${elementId}`);
const wave = waveGroup.append('path')
.datum(data)
.attr('d', clipArea)
.attr('T', 0);
// The inner circle with the clipping wave attached.
const fillCircleGroup = gaugeGroup.append('g')
.attr('clip-path', `url(#clipWave${elementId})`);
fillCircleGroup.append('circle')
.attr('cx', radius)
.attr('cy', radius)
.attr('r', fillCircleRadius)
.style('fill', config.waveColor);
// Text where the wave does overlap.
const text2 = fillCircleGroup.append('text')
.text(textRounder(textStartValue) + percentText)
.attr('class', 'liquidFillGaugeText')
.attr('id', 'text2')
.attr('text-anchor', 'middle')
.attr('font-size', `${textPixels}px`)
.style('fill', config.waveTextColor)
.attr('transform', `translate(${radius},${textRiseScaleY(config.textVertPosition)})`);
// Make the value count up.
if (config.valueCountUp) {
function raiseTextAnimation() {
d3.active(this)
.tween('text', () => {
const that = d3.select(this);
const i = d3.interpolateString(that.text(), textFinalValue);
return (t) => {
that.text(textRounder(i(t)) + percentText);
}
})
}
text1.transition()
.duration(config.waveRiseTime)
.on('start', raiseTextAnimation);
text2.transition()
.duration(config.waveRiseTime)
.on('start', raiseTextAnimation);
}
// Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
const waveGroupXPosition = fillCircleMargin + (fillCircleRadius * 2) - waveClipWidth;
if (config.waveRise) {
waveGroup.attr('transform', `translate(${waveGroupXPosition},${waveRiseScale(0)})`)
.transition()
.duration(config.waveRiseTime)
.attr('transform', `translate(${waveGroupXPosition},${waveRiseScale(fillPercent)})`)
.on('start', () => wave.attr('transform','translate(1,0)'));
} else {
waveGroup.attr('transform', `translate(${waveGroupXPosition},${waveRiseScale(fillPercent)})`);
}
if (config.waveAnimate) animateWave();
function animateWave() {
wave.attr('transform', `translate(${waveAnimateScale(wave.attr('T'))}, 0)`);
wave.transition()
.duration(config.waveAnimateTime * ( 1 - wave.attr('T')))
.attr('transform', `translate(${waveAnimateScale(1)}, 0)`)
.attr('T', 1)
.ease(d3.easeLinear)
.on('end', () => {
wave.attr('T', 0);
animateWave();
});
}
function GaugeUpdater(){
this.update = (value) => {
const newFinalValue = parseFloat(value).toFixed(2);
let textRounderUpdater = value => Math.round(value);
if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
textRounderUpdater = value => parseFloat(value).toFixed(1);
}
if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
textRounderUpdater = value => parseFloat(value).toFixed(2);
}
function updateText() {
d3.active(this)
.tween('text', () => {
const that = d3.select(this);
const i = d3.interpolateString(that.text(), newFinalValue);
return (t) => {
that.text(textRounder(i(t)) + percentText);
}
})
}
text1.transition()
.duration(config.waveRiseTime)
.on('start', updateText);
text2.transition()
.duration(config.waveRiseTime)
.on('start', updateText);
const fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
easeLinearHeight = fillCircleRadius * waveHeightScale(fillPercent * 100);
easeLinearRiseScale = d3.scaleLinear()
// The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
// such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
// circle at 100%.
.range([(fillCircleMargin + (fillCircleRadius * 2) + waveHeight), (fillCircleMargin - waveHeight)])
.domain([0, 1]);
const newHeight = waveRiseScale(fillPercent);
easeLinearScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
const waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);
let newClipArea;
if (config.waveHeightScaling) {
newClipArea = d3.area()
.x(d => waveScaleX(d.x))
.y0(d => waveScaleY(Math.sin((Math.PI * 2 * config.waveOffset * -1) + (Math.PI * 2 * (1 - config.waveCount)) + (d.y * 2 * Math.PI))))
.y1(d => (fillCircleRadius * 2) + waveHeight);
} else {
newClipArea = clipArea;
}
const newWavePosition = config.waveAnimate ? waveAnimateScale(1) : 0;
wave.transition()
.ease(d3.easeLinear)
.duration(0)
.transition()
.ease(d3.easeLinear)
.duration(config.waveAnimate ? (config.waveAnimateTime * (1 - wave.attr('T'))) : (config.waveRiseTime))
.attr('d', newClipArea)
.attr('transform', `translate(${newWavePosition},0)`)
.attr('T','1')
.on('end', () => {
if (config.waveAnimate) {
wave.attr('transform', `translate(${waveAnimateScale(0)}, 0)`);
animateWave();
}
});
waveGroup.transition()
.duration(config.waveRiseTime)
.attr('transform', `translate(${waveGroupXPosition}, ${newHeight})`);
}
}
return new GaugeUpdater();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment