Skip to content

Instantly share code, notes, and snippets.

@ypetya
Last active August 31, 2015 12:44
Show Gist options
  • Save ypetya/d5d572844f3108b632be to your computer and use it in GitHub Desktop.
Save ypetya/d5d572844f3108b632be to your computer and use it in GitHub Desktop.
Temperature experiment. http://j.mp/home_sensors

Temperature and humidity monitoring

It shows two diagrams, one for humidity and one for temperature. The first is a blue/cold one and the second is a red/warm one.

Features to implement in the future

  • Render d3 axis, to display exact values
  • Add a faster back-end
var Graph = function () {
var attrs = {
height: 200,
width: 400,
resolution: 200,
dataSource: null
}, instance = this,
scaleHumidity,
scaleTemperature;
function getterSetter(attr, value) {
if (!value) return attrs[attr];
attrs[attr] = value;
return instance;
}
Object.keys(attrs).forEach(function (attr) {
instance[attr] = getterSetter.bind(instance, attr);
});
this.draw = function (data) {
calculateScale(data);
createSkeletonIfNotExist();
drawData('path.temp',scaleTemperature);
drawData('path.humidity',scaleHumidity);
drawAxis();
bindPaths(data);
loadMore(data);
};
function createSkeletonIfNotExist() {
if (d3.select('body svg g').empty()) {
var g = d3.select('body').append('svg')
.attr('height', instance.height())
.attr('width', instance.width())
.append('g');
g.append('path').attr('style','opacity:0.5;stroke:crimson;fill:pink;')
.classed('temp', true);
g.append('path').attr('style','opacity:0.3;stroke:navy;fill:darkcyan;')
.classed('humidity', true);
g.append('g')
.classed('axis', true);
}
}
function calculateScale(data) {
var temperatures = data.map(function(d){ return d.temperature; }),
humidities = data.map(function(d){ return d.humidity; });
scaleHumidity = createScaleForArray(humidities);
scaleTemperature = createScaleForArray(temperatures);
function min(arr){
return arr.reduce(function(c,p){
return c < p ? c : p
}, 100);
}
function max(arr){
return arr.reduce(function(c,p){
return c > p ? c : p
}, -100);
}
function createScaleForArray(array) {
return d3.scale.linear()
.domain([min(array), max(array)])
.range([0, instance.height()]);
}
}
function drawData(selector, scale) {
d3.select( selector )
.attr('d', function (d) {
if (!Array.isArray(d)) return;
var pathInfo='M ' + instance.width() + ',' + instance.height(),
dx = instance.width() / instance.resolution();
var x = instance.width()-dx;
for (var i= 0,y;
x > 0 && i < d.length;
x -= dx,
i++) {
y = instance.height() - scale(d[i]),
pathInfo += ' L ' + x + ',' + y;
}
pathInfo += ' L ' + x + ',' + instance.height() + ' Z';
return pathInfo;
});
return instance;
}
function drawAxis() {
var dx = instance.width() / instance.resolution(),
timeFormat = d3.time.format('%Y-%m-%d %H:%M');
d3.select('g.axis')
.attr('d', function(originalData){
if (!Array.isArray(originalData)) return;
var xScale = d3.scale.ordinal()
.domain( originalData )
.rangeBands([instance.width(), instance.width() - originalData.length * dx ]),
axis = d3.svg.axis()
.scale(xScale)
.orient(['bottom'])
.tickSize(1,1)
.tickPadding(0);
d3.select(this).call(axis);
});
}
this.stopDataTransfer = function () {
attrs['dataSource'] = null;
};
function bindPaths(data) {
d3.selectAll('body svg g path').data([
data.map(function (d) {
return d.temperature
}),
data.map(function (d) {
return d.humidity
})]);
d3.select('g.axis').datum(data.map(function (d) {
return d.timestamp
}));
}
function loadMore(data) {
if (instance.dataSource() ) {
setTimeout(instance.dataSource(), 10);
}
}
};
<!DOCTYPE html>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache">
<style>
.axis {
opacity: 0;
}
.axis:hover{
opacity: 1;
transition-duration: 2s;
}
.text {
transform:translateY(5px);
rotate(90deg);
color:whitesmoke;
font-size:1.5em;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var HomeVersion= false; //(/localhost|file/i).test(document.location);
</script>
<script src="measure.js"></script>
<script src="graph.js"></script>
<script src="loader.js"></script>
</body>
var Service = function (Measure, Graph, HomeVersion) {
var keys = [],
dateFormat = d3.time.format("%Y%m%d"),
now = Date.now(),
parseAndStoreReceivedData = function (dataReceived) {
if (dataReceived.array[0]) {
var key = dataReceived.array[0].date;
if(!localStorage[key]) {
localStorage[key] = JSON.stringify(dataReceived);
localStorage[key + "_len"] = dataReceived.array.length;
console.log('stored ' + dataReceived.array.length + ' records for key : ' + key + ' in localStorage');
}
}
for(var i= 0, lastM;i<dataReceived.array.length;i++){
var measurement = new Measure(dataReceived.array[i]);
putCache(measurement);
}
function putCache(measurement){
if (!localStorage[measurement.key]) {
localStorage[measurement.key] = JSON.stringify(measurement);
}
if (keys.indexOf(measurement.key) < 0) {
keys.push(measurement.key);
}
lastItemTS = measurement.key;
}
if(dataReceived.array.length === 0){
pageCounter = -1;
}
},
getServerHost = function () {
return HomeVersion ? '192.168.1.66' : 'ypetya.no-ip.biz';
},
alreadyHasHistoricalDataForDate = function(date){
var len;
return localStorage[date] && (len = localStorage[date + "_len"]) && parseInt(len) > 143;
},
loadSetOfDate = function (date) {
if (alreadyHasHistoricalDataForDate(date)) {
var data = JSON.parse(localStorage[date]);
Service.callback(data);
} else {
downloadJsonp("http://" + getServerHost() + "/temp/Service.callback/date/" + date);
}
},
downloadJsonp = function(url){
var script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
},
pageCounter = 0,
PAGE_SIZE = 10,
loadSetOfPage = function () {
pageCounter >= 0 && downloadJsonp("http://" + getServerHost() + "/temp/Service.callback/page/" + pageCounter);
},
A_DAY = 24 * 60 * 60 * 1000,
loadDayDataTS = now,
obsoleteDataTS = now - A_DAY * 6,
lastItemTS = now,
calculatePreviousDayToGet = function () {
var newDay = loadDayDataTS - A_DAY;
if (newDay >= obsoleteDataTS) {
loadDayDataTS = newDay;
return true;
}
return false;
},
getQueryParam = function() {
return dateFormat(new Date(loadDayDataTS));
},
graph = new Graph(),
screenWidth = parseInt(d3.select('body').style('width')),
render = function () {
keys.sort().reverse();
var data = [];
for (var i = 0, lastKnownGoodMeasurement; i < keys.length; i++) {
var measurement = JSON.parse(localStorage[keys[i]]);
if (measurement.valid) {
if(lastKnownGoodMeasurement) {
// filtering out more than 15 celsius or 50% jumps, which can be originated from wrong data
if( Math.abs(measurement.temperature - lastKnownGoodMeasurement.temperature ) > 15
|| Math.abs(measurement.humidity - lastKnownGoodMeasurement.humidity ) > 50 ) {
continue;
}
}
data.push(measurement);
lastKnownGoodMeasurement=measurement;
}
}
if(lastItemTS > (now - A_DAY)) {
// get a page earlier
if(pageCounter>=0) {
pageCounter += PAGE_SIZE;
graph.dataSource(loadSetOfPage.bind(this));
} else {
graph.stopDataTransfer();
}
} else {
// get a day earlier
if (calculatePreviousDayToGet()) {
graph.dataSource(loadSetOfDate.bind(this, getQueryParam()));
} else {
graph.stopDataTransfer();
}
}
graph.draw(data);
},
jsonpCallback = function(data) {
parseAndStoreReceivedData(data);
render();
};
graph.height(200);
graph.width(screenWidth);
graph.resolution(screenWidth / 3);
loadSetOfPage();
return {
callback: jsonpCallback
};
function getHost() {
return HomeVersion ? '192.168.1.66' : 'ypetya.no-ip.biz';
}
}(Measure, Graph, HomeVersion);
var Measure = function (json) {
var validNumber = /^[0-9\. ]+$/,
parseTimeFormat = d3.time.format("%Y%m%d%H%M"),
outputTimeFormat = d3.time.format("%a %b %e. %H:%M"),
instance = this;
this.status = json.status && json.status.trim();
this.valid = ( this.status === "OK" ? true : false);
['humidity', 'temperature', 'timestamp', 'date'].forEach(function (attr) {
if (validNumber.test(json[attr])) {
instance[attr] = Number(json[attr]);
} else {
instance.valid = false;
}
});
this.sensor = json.sensor_id.trim();
this.timestamp = parseTimeFormat.parse(json.timestamp);
this.key = this.timestampInMillis = Number(this.timestamp);
this.toString = function () {
return this.sensor + " " + outputTimeFormat(this.timestamp) + " " + this.temperature + " C " + this.humidity + " % ";
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment