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