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.
- 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 + " % "; | |
}; | |
}; |