Skip to content

Instantly share code, notes, and snippets.

@steveio
Last active April 22, 2020 16:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steveio/c7018c8432710ff8df75bf5a0d5cf03f to your computer and use it in GitHub Desktop.
Save steveio/c7018c8432710ff8df75bf5a0d5cf03f to your computer and use it in GitHub Desktop.
D3.js Radial Liquid Fill Gauge Websocket Updater
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="https://d3js.org/d3.v3.min.js" language="JavaScript"></script>
<script src="wsLiquidFillGauge.js" language="JavaScript"></script>
<style>
.liquidFillGauge { }
.liquidFillGaugeText { font-family: Helvetica; font-weight: bold; }
</style>
</head>
<body>
<script language="JavaScript">
var gauge1, gauge2, gauge3, gauge4, gauge5 = null;
/*
var ws = null
var maxReconnectAttemps = 10;
var reconnectAttempts = 0;
// setup WebSocket
function setupWebSocket()
{
reconnectAttempts = 0;
ws = new WebSocket('ws://192.168.1.127:8080',[]);
ws.onopen = function () {
console.log('WebSocket Open');
};
ws.onerror = function () {
console.log('WebSocket Error ' + error);
};
ws.onmessage = function (e) {
var rawData = e.data;
if(rawData.trim().length > 1 && rawData.trim() != "undefined")
{
try {
// example JSON message object
// [{"ts":1587572880,"tempC":23.1,"tempF":73.58,"h":40,"LDR":246,"p":101518,"tc2":22.1,"a":4.653044}]
var jsonObj = JSON.parse(rawData);
jsonObj[0]['p'] = jsonObj[0]['p'] / 100;
jsonObj[0]['LDR'] = jsonObj[0]['LDR'] / 1024 * 100;
gauge1.update(jsonObj[0]['tempC']);
gauge2.update(jsonObj[0]['h']);
gauge3.update(jsonObj[0]['tempF']);
gauge4.update(jsonObj[0]['LDR']);
} catch(e) {
console.log("Invalid JSON:"+rawData.toString());
}
}
};
}
// check connection status every 60sec, upto maxReconnectAttemps, try reconnect
const interval = setInterval(function checkConnectStatus() {
if (reconnectAttempts++ < maxReconnectAttemps)
{
if (ws.readyState !== ws.OPEN) {
console.log("WS connection closed - try re-connect");
setupWebSocket();
}
}
}, 60000);
*/
document.addEventListener("DOMContentLoaded", function() {
//setupWebSocket();
initGauge();
});
initGauge = function()
{
gauge1 = loadLiquidFillGauge("fillgauge1", 90);
var config2 = liquidFillGaugeDefaultSettings();
config2.width = 300;
config2.height = 400;
config2.circleColor = "#FF7777";
config2.textColor = "#FF4444";
config2.waveTextColor = "#FFAAAA";
config2.waveColorDomain = ["#e34a33","#fee8c8"];
config2.waveColorDataDomain = [0,100];
config2.circleThickness = 0.2;
config2.textVertPosition = 0.5;
config2.waveAnimateTime = 1000;
gauge2= loadLiquidFillGauge("fillgauge2", 15, config2);
var config3 = liquidFillGaugeDefaultSettings();
config3.width = 300;
config3.height = 400;
config3.unitText = "hPa";
config3.textSize = 0.5;
config3.circleColor = "#fecc5c";
config3.textColor = "#bd0026";
config3.waveColorDomain = ["#fff7bc","#fec44f"];
config3.waveColorDataDomain = [0,100];
config3.waveTextColor = "#bd0026";
config3.circleThickness = 0.1;
config3.circleFillGap = 0.1;
config3.textVertPosition = 0.8;
config3.waveAnimateTime = 2000;
config3.waveHeight = 0.3;
config3.waveCount = 1;
gauge3 = loadLiquidFillGauge("fillgauge3", 25, config3);
var config4 = liquidFillGaugeDefaultSettings();
config4.width = 300;
config4.height = 400;
config4.circleThickness = 0.15;
config4.circleColor = "#a1d99b";
config4.textColor = "#808015";
config4.waveTextColor = "#31a354";
config4.waveColorDomain = ["#31a354","#e5f5e0"];
config4.waveColorDataDomain = [0,100];
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;
gauge4 = loadLiquidFillGauge("fillgauge4", 50, config4);
}
</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 v1.1
* http://bl.ocks.org/brattonc/5e5ce9beee483220e2f6
*/
function liquidFillGaugeDefaultSettings(){
return {
width: 300,
height: 400,
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.
waveColorDomain: ["#08519c","#6baed6"],
waveColorDataDomain: [0,100],
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.
unitText: "%", // Unit text/ symbol
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();
//var gauge = d3.select("#" + elementId).selectAll("svg");
var gauge = d3.select("body").append("svg")
.attr({
width: config.width,
height: config.height
});
//var gauge = d3.select("#" + elementId);
var radius = Math.min(parseInt(gauge.style("width")), parseInt(gauge.style("height")))/2;
var locationX = parseInt(gauge.style("width"))/2 - radius;
var locationY = parseInt(gauge.style("height"))/2 - radius;
var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value))/config.maxValue;
var colorScale = d3.scale.linear().domain(config.waveColorDataDomain).range(config.waveColorDomain);
var waveFillColor = colorScale(fillPercent * 100);
var waveHeightScale;
if(config.waveHeightScaling){
waveHeightScale = d3.scale.linear()
.range([0,config.waveHeight,0])
.domain([0,50,100]);
} else {
waveHeightScale = d3.scale.linear()
.range([config.waveHeight,config.waveHeight])
.domain([0,100]);
}
var textPixels = (config.textSize*radius/2);
var textFinalValue = parseFloat(value).toFixed(2);
var textStartValue = config.valueCountUp?config.minValue:textFinalValue;
var unitText = config.unitText;
var circleThickness = config.circleThickness * radius;
var circleFillGap = config.circleFillGap * radius;
var fillCircleMargin = circleThickness + circleFillGap;
var fillCircleRadius = radius - fillCircleMargin;
var waveHeight = fillCircleRadius*waveHeightScale(fillPercent*100);
var waveLength = fillCircleRadius*2/config.waveCount;
var waveClipCount = 1+config.waveCount;
var waveClipWidth = waveLength*waveClipCount;
// Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
var textRounder = function(value){ return Math.round(value); };
if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){
textRounder = function(value){ return parseFloat(value).toFixed(1); };
}
if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){
textRounder = function(value){ return parseFloat(value).toFixed(2); };
}
// Data for building the clip wave area.
var data = [];
for(var i = 0; i <= 40*waveClipCount; i++){
data.push({x: i/(40*waveClipCount), y: (i/(40))});
}
// Scales for drawing the outer circle.
var gaugeCircleX = d3.scale.linear().range([0,2*Math.PI]).domain([0,1]);
var gaugeCircleY = d3.scale.linear().range([0,radius]).domain([0,radius]);
// Scales for controlling the size of the clipping path.
var waveScaleX = d3.scale.linear().range([0,waveClipWidth]).domain([0,1]);
var waveScaleY = d3.scale.linear().range([0,waveHeight]).domain([0,1]);
// Scales for controlling the position of the clipping path.
var waveRiseScale = d3.scale.linear()
// 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]);
var waveAnimateScale = d3.scale.linear()
.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.
var textRiseScaleY = d3.scale.linear()
.range([fillCircleMargin+fillCircleRadius*2,(fillCircleMargin+textPixels*0.7)])
.domain([0,1]);
// Center the gauge within the parent SVG.
var gaugeGroup = gauge.append("g")
.attr('transform','translate('+locationX+','+locationY+')');
// Draw the outer circle.
var gaugeCircleArc = d3.svg.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.
var text1 = gaugeGroup.append("text")
.text(textRounder(textStartValue) + unitText)
.attr("class", "liquidFillGaugeText")
.attr("text-anchor", "middle")
.attr("font-size", textPixels + "px")
.style("fill", config.textColor)
.attr('transform','translate('+radius+','+textRiseScaleY(config.textVertPosition)+')');
// The clipping wave area.
var clipArea = d3.svg.area()
.x(function(d) { return waveScaleX(d.x); } )
.y0(function(d) { return waveScaleY(Math.sin(Math.PI*2*config.waveOffset*-1 + Math.PI*2*(1-config.waveCount) + d.y*2*Math.PI));} )
.y1(function(d) { return (fillCircleRadius*2 + waveHeight); } );
var waveGroup = gaugeGroup.append("defs")
.append("clipPath")
.attr("id", "clipWave" + elementId);
var wave = waveGroup.append("path")
.datum(data)
.attr("d", clipArea)
.attr("T", 0);
// The inner circle with the clipping wave attached.
var fillCircleGroup = gaugeGroup.append("g")
.attr("clip-path", "url(#clipWave" + elementId + ")");
fillCircleGroup.append("circle")
.attr("cx", radius)
.attr("cy", radius)
.attr("r", fillCircleRadius)
.style("fill", waveFillColor);
// Text where the wave does overlap.
var text2 = fillCircleGroup.append("text")
.text(textRounder(textStartValue) + unitText)
.attr("class", "liquidFillGaugeText")
.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){
var textTween = function(){
var i = d3.interpolate(this.textContent, textFinalValue);
return function(t) { this.textContent = textRounder(i(t)) + unitText; }
};
text1.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
text2.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
}
// Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
var 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)+')')
.each("start", function(){ wave.attr('transform','translate(1,0)'); }); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary.
} 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')))
.ease('linear')
.attr('transform','translate('+waveAnimateScale(1)+',0)')
.attr('T', 1)
.each('end', function(){
wave.attr('T', 0);
animateWave(config.waveAnimateTime);
});
}
function GaugeUpdater(){
this.update = function(value){
var newFinalValue = parseFloat(value).toFixed(2);
var textRounderUpdater = function(value){ return Math.round(value); };
if(parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))){
textRounderUpdater = function(value){ return parseFloat(value).toFixed(1); };
}
if(parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))){
textRounderUpdater = function(value){ return parseFloat(value).toFixed(2); };
}
var textTween = function(){
var i = d3.interpolate(this.textContent, parseFloat(value).toFixed(2));
return function(t) { this.textContent = textRounderUpdater(i(t)) + unitText; }
};
text1.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
text2.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value))/config.maxValue;
var waveHeight = fillCircleRadius*waveHeightScale(fillPercent*100);
var waveRiseScale = d3.scale.linear()
// 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]);
var newHeight = waveRiseScale(fillPercent);
var waveScaleX = d3.scale.linear().range([0,waveClipWidth]).domain([0,1]);
var waveScaleY = d3.scale.linear().range([0,waveHeight]).domain([0,1]);
var newClipArea;
if(config.waveHeightScaling){
newClipArea = d3.svg.area()
.x(function(d) { return waveScaleX(d.x); } )
.y0(function(d) { return waveScaleY(Math.sin(Math.PI*2*config.waveOffset*-1 + Math.PI*2*(1-config.waveCount) + d.y*2*Math.PI));} )
.y1(function(d) { return (fillCircleRadius*2 + waveHeight); } );
} else {
newClipArea = clipArea;
}
var newWavePosition = config.waveAnimate?waveAnimateScale(1):0;
wave.transition()
.duration(0)
.transition()
.duration(config.waveAnimate?(config.waveAnimateTime * (1-wave.attr('T'))):(config.waveRiseTime))
.ease('linear')
.attr('d', newClipArea)
.attr('transform','translate('+newWavePosition+',0)')
.attr('T','1')
.each("end", function(){
if(config.waveAnimate){
wave.attr('transform','translate('+waveAnimateScale(0)+',0)');
animateWave(config.waveAnimateTime);
}
});
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