Commits

Vassil Kalkov committed 0d70b1f

Complete mess

Comments (0)

Files changed (30)

File contents unchanged.
+express = require 'express'
+cradle = require 'cradle'
+config = require(__dirname + '/config.js').config
+couchDBMiddleware = require(__dirname + '/couchdb.js')
+date_helper = require(__dirname + '/helpers/date_helper.js')
+obj_helper = require(__dirname + '/helpers/obj_helper.js')
+view_helper = require(__dirname + '/helpers/view_helper.js')
+
+
+app = module.exports = express.createServer()
+io = require('socket.io').listen app
+_ = require 'underscore'
+SensorStream = require './sensor_stream'
+
+#db = new(cradle.Connection)('http://192.168.1.5', 5984, { 'cache': false , auth: { username: 'kalkov', password: 'parola' } }).database 'sensors'
+stream = new SensorStream()
+stream.watch()
+
+io.sockets.on 'connection', (socket) ->
+  console.log('Connected ...')
+
+  socket.on 'processed', (data) ->
+    #console.log(data.id)
+    console.log('Will Fire newDoc')
+    socket.emit 'newDoc', data
+    
+
+  socket.on 'ok?', (data) ->
+    console.log('Client JS received data')
+
+  #db.all (err, docs) ->
+  #  ids = _.map docs, (doc) ->
+  #    doc.id
+  #    db.get ids, (err, docs) ->
+  #      _.each docs, (doc) ->
+  #        socket.emit 'newDoc', { id: doc.doc._id, title: doc.doc.title }
+
+
+app.configure ->	
+  app.set 'views', __dirname + '/views'
+  app.set 'view engine', 'jade'
+  app.use express.bodyParser()
+  app.use couchDBMiddleware()
+  app.use express.methodOverride()
+  app.use app.router
+  app.use express.static __dirname + '/public'
+  app.use express.errorHandler { dumpExceptions: true, showStack: true }
+
+app.helpers({
+  time_ago_in_words: date_helper.time_ago_in_words,
+  printObj: obj_helper.printObj,
+  get_order_of_sensors: view_helper.get_order_of_sensors
+});
+
+
+
+app.get '/', (req, res) ->
+	res.render 'index', { title: 'Archive' }
+
+app.get '/archive/:id', (req, res) ->
+	db.get req.params.id, (err, doc) ->
+		#res.render 'archive', { title: doc.title, content: doc.content }
+
+require('./routes')(app).listen(config.port);
+console.log("is_it_hot_ui is listening on port %d in %s mode", config.port, app.settings.env);
+
+
+(function() {
+  var SensorStream, app, config, couchDBMiddleware, cradle, date_helper, express, io, obj_helper, stream, view_helper, _;
 
-/**
- * Module dependencies.
- */
-
-var express = require('express')
-   ,config = require(__dirname + '/config.js').config
-   ,couchDBMiddleware = require(__dirname + '/couchdb.js')
-   ,helpers = require(__dirname + '/helpers/date_helper.js')
-  // ,sio = require('../../lib/socket.io')
-   ,app = module.exports = express.createServer();
-
-// Configuration
-/*
-app.configure('kalkov.dyndns.org', function(){
-    app.use(function(req, res, next){
-        
-        };
-        next();
-    });
-});
-*/
-app.configure('development', function(){
-    app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
-});
+  express = require('express');
 
-app.configure('production', function(){
-    app.use(express.errorHandler()); 
-});
+  cradle = require('cradle');
 
-app.configure(function(){
-    app.set('views', __dirname + '/views');
-    app.set('view engine', 'jade');
-    app.use(express.bodyParser());
-    app.use(express.methodOverride());
-    app.use(couchDBMiddleware());
-    app.use(app.router);
+  config = require(__dirname + '/config.js').config;
 
-    //app.use(express.static(__dirname + '/public'));
-});
+  couchDBMiddleware = require(__dirname + '/couchdb.js');
 
-//Helpers
-app.helpers({
-  time_ago_in_words: helpers.time_ago_in_words
-});
+  date_helper = require(__dirname + '/helpers/date_helper.js');
 
+  obj_helper = require(__dirname + '/helpers/obj_helper.js');
 
-// Routes
-require('./routes')(app).listen(config.port);
-console.log("is_it_hot_ui is listening on port %d in %s mode", config.port, app.settings.env);
+  view_helper = require(__dirname + '/helpers/view_helper.js');
 
-/*
-var io = sio.listen(app)
-  , nicknames = {};
+  app = module.exports = express.createServer();
 
-io.sockets.on('connection', function (socket) {
-  socket.on('user message', function (msg) {
-    socket.broadcast.emit('user message', socket.nickname, msg);
+  io = require('socket.io').listen(app);
+
+  _ = require('underscore');
+
+  SensorStream = require('./sensor_stream');
+
+  stream = new SensorStream();
+
+  stream.watch();
+
+  io.sockets.on('connection', function(socket) {
+    console.log('Connected ...');
+    socket.on('processed', function(data) {
+      console.log('Will Fire newDoc');
+      return socket.emit('newDoc', data);
+    });
+    return socket.on('ok?', function(data) {
+      return console.log('Client JS received data');
+    });
   });
 
-  socket.on('nickname', function (nick, fn) {
-    if (nicknames[nick]) {
-      fn(true);
-    } else {
-      fn(false);
-      nicknames[nick] = socket.nickname = nick;
-      socket.broadcast.emit('announcement', nick + ' connected');
-      io.sockets.emit('nicknames', nicknames);
-    }
+  app.configure(function() {
+    app.set('views', __dirname + '/views');
+    app.set('view engine', 'jade');
+    app.use(express.bodyParser());
+    app.use(couchDBMiddleware());
+    app.use(express.methodOverride());
+    app.use(app.router);
+    app.use(express.static(__dirname + '/public'));
+    return app.use(express.errorHandler({
+      dumpExceptions: true,
+      showStack: true
+    }));
   });
 
-  socket.on('disconnect', function () {
-    if (!socket.nickname) return;
+  app.helpers({
+    time_ago_in_words: date_helper.time_ago_in_words,
+    printObj: obj_helper.printObj,
+    get_order_of_sensors: view_helper.get_order_of_sensors
+  });
 
-    delete nicknames[socket.nickname];
-    socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
-    socket.broadcast.emit('nicknames', nicknames);
+  app.get('/', function(req, res) {
+    return res.render('index', {
+      title: 'Archive'
+    });
+  });
+
+  app.get('/archive/:id', function(req, res) {
+    return db.get(req.params.id, function(err, doc) {});
   });
-});
-*/
+
+  require('./routes')(app).listen(config.port);
+
+  console.log("is_it_hot_ui is listening on port %d in %s mode", config.port, app.settings.env);
+
+}).call(this);
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express')
+   ,config = require(__dirname + '/config.js').config
+   ,couchDBMiddleware = require(__dirname + '/couchdb.js')
+   ,date_helper = require(__dirname + '/helpers/date_helper.js')
+   ,obj_helper = require(__dirname + '/helpers/obj_helper.js')
+   ,view_helper = require(__dirname + '/helpers/view_helper.js')
+  // ,sio = require('../../lib/socket.io')
+   ,app = module.exports = express.createServer();
+
+// Configuration
+/*
+app.configure('kalkov.dyndns.org', function(){
+    app.use(function(req, res, next){
+        
+        };
+        next();
+    });
+});
+*/
+app.configure('development', function(){
+    app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
+});
+
+app.configure('production', function(){
+    app.use(express.errorHandler()); 
+});
+
+app.configure(function(){
+    app.set('views', __dirname + '/views');
+    app.set('view engine', 'jade');
+    app.use(express.bodyParser());
+    app.use(express.methodOverride());
+    app.use(couchDBMiddleware());
+    app.use(app.router);
+
+    app.use(express.static(__dirname + '/public'));
+});
+
+//Helpers
+app.helpers({
+  time_ago_in_words: date_helper.time_ago_in_words,
+  printObj: obj_helper.printObj,
+  get_order_of_sensors: view_helper.get_order_of_sensors
+});
+
+
+io = require('socket.io').listen(app);
+  count = 0;
+  io.sockets.on('connection', function(socket) {
+    count++;
+    io.sockets.emit('count', {
+      number: count
+    });
+    setInterval(function() {
+      return io.sockets.emit('count', {
+        number: count
+      });
+    }, 1200);
+    return socket.on('disconnect', function() {
+      count--;
+      return io.sockets.emit('count', {
+        number: count
+      });
+    });
+  });
+
+
+// Routes
+require('./routes')(app).listen(config.port);
+console.log("is_it_hot_ui is listening on port %d in %s mode", config.port, app.settings.env);
+
+/*
+var io = sio.listen(app)
+  , nicknames = {};
+
+io.sockets.on('connection', function (socket) {
+  socket.on('user message', function (msg) {
+    socket.broadcast.emit('user message', socket.nickname, msg);
+  });
+
+  socket.on('nickname', function (nick, fn) {
+    if (nicknames[nick]) {
+      fn(true);
+    } else {
+      fn(false);
+      nicknames[nick] = socket.nickname = nick;
+      socket.broadcast.emit('announcement', nick + ' connected');
+      io.sockets.emit('nicknames', nicknames);
+    }
+  });
+
+  socket.on('disconnect', function () {
+    if (!socket.nickname) return;
+
+    delete nicknames[socket.nickname];
+    socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
+    socket.broadcast.emit('nicknames', nicknames);
+  });
+});
+*/

config_example.js

File contents unchanged.

controllers/save_design_docs.js

           
           sensor_room1: {
             map: function(doc) {
-                   var d=new Date();
-                   if(d.getDate() < 10){ var today_date = "0" + d.getDate()}
-                   else {var today_data = getDate()}
-
-                   str = d.getFullYear()+"_"+(d.getMonth()+1)+"_"+today_date+"@"+d.getHours();
-
-                   if(doc._id.search(str) != -1 && doc.name == 'room1' ) {
-                     emit(doc.time, doc);
+                   if(doc.sensors){
+                     for(var i=0;i<doc.sensors.length;i++){
+                       if(doc.sensors[i].name.search("room1") != -1){
+                             emit([doc.time,doc.sensors[i].data], [doc.sensors[i].address,doc.sensors[i].data,doc.time]);
+                       }
+                     }
                    }
-          }
+            }
+          },
+            
+          sensors_all: {
+            map: function(doc) {
+                   emit(doc.time, doc);
+                 }
+            }
            
-        }
+         
       }
     }
   , function(err){

controllers/sensors.js

 }
 
 exports.index = function (req, res) {
-  var d=new Date();
-  if(d.getDate() < 10){ var today_date = "0" + d.getDate()}
-  else {var today_data = getDate()}
-
-  var minutes_str = d.getFullYear()+"_"+(d.getMonth()+1)+"_"+today_date+"@"+d.getHours()+":"+d.getMinutes();
-
-  req.db.view('sensors/sensors_last_minute',{descending: true}, function (err, docs) {	
+  req.db.view('sensors/sensors_all',{descending: true}, function (err, docs) {	
       res.render('sensors', { sensors: docs, title: "All sensors" });
   });
 };
 exports.room1 = function (req, res) {
   var d=new Date();
   if(d.getDate() < 10){ var today_date = "0" + d.getDate()}
-  else {var today_data = getDate()}
+  else {var today_data = d.getDate()}
 
   var minutes_str = d.getFullYear()+"_"+(d.getMonth()+1)+"_"+today_date+"@"+d.getHours();
 
File contents unchanged.

helpers/date_helper.js

File contents unchanged.

helpers/obj_helper.js

+var express = require('express');
+
+exports.printObj = function printObj(obj) {
+  var result = [];
+
+  for (var key in obj) {
+    result.push(obj[key]);
+  }
+  return result;
+  // result is now an array of the input object's values.
+}
+
+
+  

helpers/view_helper.js

+var express = require('express');
+
+exports.get_order_of_sensors = function get_order_of_sensors(obj) {
+  var result = [];
+
+  for (var key in obj) {
+    result.push(obj[key]);
+  }
+  return result;
+  // result is now an array of the input object's values.
+}
+
+
+  
     , "date-utils": "latest"
     , "socket.io": "latest"
     , "forever": "latest"
+    , "stylus": ">= 0.13.4"
+    , "readability": "latest"
+    , "events": "latest"
+    , "request": "latest"
+    , "underscore": "latest"
+    
   }
 }

public/javascripts/application.coffee

+$(document).ready ->
+	
+	socket = io.connect 'http://localhost:3050'
+
+	socket.on 'newDoc', (data) ->
+		div = '<div id="1">'
+		a = '<a href="/archive/1">1</a>'
+		$('#docs').append('<p>'+div+''+a+'</div></p>')
+		socket.emit 'ok?', { text: 'OK' }
+
+	socket.on 'deleted', (data) ->
+		$('div#'+data.id).remove()
+
+	$('input[type=submit]').click (event) ->
+		event.preventDefault()
+		url = $('#url').val()
+		socket.emit 'addDoc', { url: url }
+		
+	$('#delete').live 'click', (event) ->
+		event.preventDefault()
+		id = $(this).val()
+		socket.emit 'delDoc', { id: id }

public/javascripts/application.js

+
+  $(document).ready(function() {
+    var socket;
+    socket = io.connect('http://localhost:3050');
+    socket.on('newDoc', function(data) {
+      var a, div;
+      div = '<div id="1">';
+      a = '<a href="/archive/1">1</a>';
+      $('#docs').append('<p>' + div + '' + a + '</div></p>');
+      return socket.emit('ok?', {
+        text: 'OK'
+      });
+    });
+    socket.on('deleted', function(data) {
+      return $('div#' + data.id).remove();
+    });
+    $('input[type=submit]').click(function(event) {
+      var url;
+      event.preventDefault();
+      url = $('#url').val();
+      return socket.emit('addDoc', {
+        url: url
+      });
+    });
+    return $('#delete').live('click', function(event) {
+      var id;
+      event.preventDefault();
+      id = $(this).val();
+      return socket.emit('delDoc', {
+        id: id
+      });
+    });
+  });

public/javascripts/smoothie.js

+// MIT License:
+//
+// Copyright (c) 2010, Joe Walnes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * Smoothie Charts - http://smoothiecharts.org/
+ * (c) 2010, Joe Walnes
+ *
+ * v1.0: Main charting library, by Joe Walnes
+ * v1.1: Auto scaling of axis, by Neil Dunn
+ * v1.2: fps (frames per second) option, by Mathias Petterson
+ * v1.3: Fix for divide by zero, by Paul Nikitochkin
+ * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
+ */
+
+function TimeSeries(options) {
+  options = options || {};
+  options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds
+  options.resetBounds = options.resetBounds || true; // Enable or disable the resetBounds timer
+  this.options = options;
+  this.data = [];
+  
+  this.maxValue = Number.NaN; // The maximum value ever seen in this time series.
+  this.minValue = Number.NaN; // The minimum value ever seen in this time series.
+
+  // Start a resetBounds Interval timer desired
+  if (options.resetBounds) {
+    this.boundsTimer = setInterval(function(thisObj) { thisObj.resetBounds(); }, options.resetBoundsInterval, this);
+  }
+}
+
+// Reset the min and max for this timeseries so the graph rescales itself
+TimeSeries.prototype.resetBounds = function() {
+  this.maxValue = Number.NaN;
+  this.minValue = Number.NaN;
+  for (var i = 0; i < this.data.length; i++) {
+    this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1];
+    this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1];
+  }
+};
+
+TimeSeries.prototype.append = function(timestamp, value) {
+  this.data.push([timestamp, value]);
+  this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value;
+  this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value;
+};
+
+function SmoothieChart(options) {
+  // Defaults
+  options = options || {};
+  options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 };
+  options.millisPerPixel = options.millisPerPixel || 20;
+  options.fps = options.fps || 20;
+  options.maxValueScale = options.maxValueScale || 1;
+  options.minValue = options.minValue;
+  options.labels = options.labels || { fillStyle:'#ffffff' }
+  this.options = options;
+  this.seriesSet = [];
+}
+
+SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+  this.seriesSet.push({timeSeries: timeSeries, options: options || {}});
+};
+
+SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+	this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1);
+};
+
+SmoothieChart.prototype.streamTo = function(canvas, delay) {
+  var self = this;
+  (function render() {
+    self.render(canvas, new Date().getTime() - (delay || 0));
+    setTimeout(render, 1000/self.options.fps);
+  })()
+};
+
+SmoothieChart.prototype.render = function(canvas, time) {
+  var canvasContext = canvas.getContext("2d");
+  var options = this.options;
+  var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight};
+
+  // Save the state of the canvas context, any transformations applied in this method
+  // will get removed from the stack at the end of this method when .restore() is called.
+  canvasContext.save();
+
+  // Round time down to pixel granularity, so motion appears smoother.
+  time = time - time % options.millisPerPixel;
+
+  // Move the origin.
+  canvasContext.translate(dimensions.left, dimensions.top);
+  
+  // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+  // This prevents the occasional pixels from curves near the edges overrunning and creating
+  // screen cheese (that phrase should neeed no explanation).
+  canvasContext.beginPath();
+  canvasContext.rect(0, 0, dimensions.width, dimensions.height);
+  canvasContext.clip();
+
+  // Clear the working area.
+  canvasContext.save();
+  canvasContext.fillStyle = options.grid.fillStyle;
+  canvasContext.fillRect(0, 0, dimensions.width, dimensions.height);
+  canvasContext.restore();
+
+  // Grid lines....
+  canvasContext.save();
+  canvasContext.lineWidth = options.grid.lineWidth || 1;
+  canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff';
+  // Vertical (time) dividers.
+  if (options.grid.millisPerLine > 0) {
+    for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) {
+      canvasContext.beginPath();
+      var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel));
+      canvasContext.moveTo(gx, 0);
+      canvasContext.lineTo(gx, dimensions.height);
+      canvasContext.stroke();
+      canvasContext.closePath();
+    }
+  }
+
+  // Horizontal (value) dividers.
+  for (var v = 1; v < options.grid.verticalSections; v++) {
+    var gy = Math.round(v * dimensions.height / options.grid.verticalSections);
+    canvasContext.beginPath();
+    canvasContext.moveTo(0, gy);
+    canvasContext.lineTo(dimensions.width, gy);
+    canvasContext.stroke();
+    canvasContext.closePath();
+  }
+  // Bounding rectangle.
+  canvasContext.beginPath();
+  canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height);
+  canvasContext.closePath();
+  canvasContext.restore();
+
+  // Calculate the current scale of the chart, from all time series.
+  var maxValue = Number.NaN;
+  var minValue = Number.NaN;
+
+  for (var d = 0; d < this.seriesSet.length; d++) {
+      // TODO(ndunn): We could calculate / track these values as they stream in.
+      var timeSeries = this.seriesSet[d].timeSeries;
+      if (!isNaN(timeSeries.maxValue)) {
+          maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue;
+      }
+
+      if (!isNaN(timeSeries.minValue)) {
+          minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue;
+      }
+  }
+
+  if (isNaN(maxValue) && isNaN(minValue)) {
+      return;
+  }
+
+  // Scale the maxValue to add padding at the top if required
+  maxValue = maxValue * options.maxValueScale;
+  // Set the minimum if we've specified one
+  if (options.minValue != null)
+    minValue = options.minValue;
+  var valueRange = maxValue - minValue;
+  
+  // For each data set...
+  for (var d = 0; d < this.seriesSet.length; d++) {
+    canvasContext.save();
+    var timeSeries = this.seriesSet[d].timeSeries;
+    var dataSet = timeSeries.data;
+    var seriesOptions = this.seriesSet[d].options;
+
+    // Delete old data that's moved off the left of the chart.
+    // We must always keep the last expired data point as we need this to draw the
+    // line that comes into the chart, but any points prior to that can be removed.
+    while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) {
+      dataSet.splice(0, 1);
+    }
+
+    // Set style for this dataSet.
+    canvasContext.lineWidth = seriesOptions.lineWidth || 1;
+    canvasContext.fillStyle = seriesOptions.fillStyle;
+    canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff';
+    // Draw the line...
+    canvasContext.beginPath();
+    // Retain lastX, lastY for calculating the control points of bezier curves.
+    var firstX = 0, lastX = 0, lastY = 0;
+    for (var i = 0; i < dataSet.length; i++) {
+      // TODO: Deal with dataSet.length < 2.
+      var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel));
+      var value = dataSet[i][1];
+      var offset = maxValue - value;
+      var scaledValue = valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0;
+      var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart.
+
+      if (i == 0) {
+        firstX = x;
+        canvasContext.moveTo(x, y);
+      }
+      // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves
+      //
+      // Assuming A was the last point in the line plotted and B is the new point,
+      // we draw a curve with control points P and Q as below.
+      //
+      // A---P
+      //     |
+      //     |
+      //     |
+      //     Q---B
+      //
+      // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+      // so adjacent curves appear to flow as one.
+      //
+      else {
+        canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+          Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+          Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+          x, y); // endPoint (B)
+      }
+
+      lastX = x, lastY = y;
+    }
+    if (dataSet.length > 0 && seriesOptions.fillStyle) {
+      // Close up the fill region.
+      canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+      canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+      canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+      canvasContext.fill();
+    }
+    canvasContext.stroke();
+    canvasContext.closePath();
+    canvasContext.restore();
+  }
+
+  // Draw the axis values on the chart.
+  if (!options.labels.disabled) {
+      canvasContext.fillStyle = options.labels.fillStyle;
+      var maxValueString = maxValue.toFixed(2);
+      var minValueString = minValue.toFixed(2);
+      canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10);
+      canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2);
+  }
+
+  canvasContext.restore(); // See .save() above.
+}

public/javascripts/underscore.js

+//     Underscore.js 1.1.7
+//     (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+//     Underscore is freely distributable under the MIT license.
+//     Portions of Underscore are inspired or borrowed from Prototype,
+//     Oliver Steele's Functional, and John Resig's Micro-Templating.
+//     For all details and documentation:
+//     http://documentcloud.github.com/underscore
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `global` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Establish the object that gets returned to break out of a loop iteration.
+  var breaker = {};
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var slice            = ArrayProto.slice,
+      unshift          = ArrayProto.unshift,
+      toString         = ObjProto.toString,
+      hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeForEach      = ArrayProto.forEach,
+    nativeMap          = ArrayProto.map,
+    nativeReduce       = ArrayProto.reduce,
+    nativeReduceRight  = ArrayProto.reduceRight,
+    nativeFilter       = ArrayProto.filter,
+    nativeEvery        = ArrayProto.every,
+    nativeSome         = ArrayProto.some,
+    nativeIndexOf      = ArrayProto.indexOf,
+    nativeLastIndexOf  = ArrayProto.lastIndexOf,
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) { return new wrapper(obj); };
+
+  // Export the Underscore object for **CommonJS**, with backwards-compatibility
+  // for the old `require()` API. If we're not in CommonJS, add `_` to the
+  // global object.
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = _;
+    _._ = _;
+  } else {
+    // Exported as a string, for Closure Compiler "advanced" mode.
+    root['_'] = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.1.7';
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles objects with the built-in `forEach`, arrays, and raw objects.
+  // Delegates to **ECMAScript 5**'s native `forEach` if available.
+  var each = _.each = _.forEach = function(obj, iterator, context) {
+    if (obj == null) return;
+    if (nativeForEach && obj.forEach === nativeForEach) {
+      obj.forEach(iterator, context);
+    } else if (obj.length === +obj.length) {
+      for (var i = 0, l = obj.length; i < l; i++) {
+        if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
+      }
+    } else {
+      for (var key in obj) {
+        if (hasOwnProperty.call(obj, key)) {
+          if (iterator.call(context, obj[key], key, obj) === breaker) return;
+        }
+      }
+    }
+  };
+
+  // Return the results of applying the iterator to each element.
+  // Delegates to **ECMAScript 5**'s native `map` if available.
+  _.map = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+    each(obj, function(value, index, list) {
+      results[results.length] = iterator.call(context, value, index, list);
+    });
+    return results;
+  };
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+    var initial = memo !== void 0;
+    if (obj == null) obj = [];
+    if (nativeReduce && obj.reduce === nativeReduce) {
+      if (context) iterator = _.bind(iterator, context);
+      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+    }
+    each(obj, function(value, index, list) {
+      if (!initial) {
+        memo = value;
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, value, index, list);
+      }
+    });
+    if (!initial) throw new TypeError("Reduce of empty array with no initial value");
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+    if (obj == null) obj = [];
+    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+      if (context) iterator = _.bind(iterator, context);
+      return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+    }
+    var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
+    return _.reduce(reversed, iterator, memo, context);
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, iterator, context) {
+    var result;
+    any(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Delegates to **ECMAScript 5**'s native `filter` if available.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+    each(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    each(obj, function(value, index, list) {
+      if (!iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Delegates to **ECMAScript 5**'s native `every` if available.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, iterator, context) {
+    var result = true;
+    if (obj == null) return result;
+    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+    each(obj, function(value, index, list) {
+      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+    });
+    return result;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Delegates to **ECMAScript 5**'s native `some` if available.
+  // Aliased as `any`.
+  var any = _.some = _.any = function(obj, iterator, context) {
+    iterator = iterator || _.identity;
+    var result = false;
+    if (obj == null) return result;
+    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+    each(obj, function(value, index, list) {
+      if (result |= iterator.call(context, value, index, list)) return breaker;
+    });
+    return !!result;
+  };
+
+  // Determine if a given value is included in the array or object using `===`.
+  // Aliased as `contains`.
+  _.include = _.contains = function(obj, target) {
+    var found = false;
+    if (obj == null) return found;
+    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+    any(obj, function(value) {
+      if (found = value === target) return true;
+    });
+    return found;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    return _.map(obj, function(value) {
+      return (method.call ? method || value : value[method]).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, function(value){ return value[key]; });
+  };
+
+  // Return the maximum element or (element-based computation).
+  _.max = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
+    var result = {computed : -Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed >= result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
+    var result = {computed : Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed < result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Sort the object's values by a criterion produced by an iterator.
+  _.sortBy = function(obj, iterator, context) {
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value : value,
+        criteria : iterator.call(context, value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }), 'value');
+  };
+
+  // Groups the object's values by a criterion produced by an iterator
+  _.groupBy = function(obj, iterator) {
+    var result = {};
+    each(obj, function(value, index) {
+      var key = iterator(value, index);
+      (result[key] || (result[key] = [])).push(value);
+    });
+    return result;
+  };
+
+  // Use a comparator function to figure out at what index an object should
+  // be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iterator) {
+    iterator || (iterator = _.identity);
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = (low + high) >> 1;
+      iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
+    }
+    return low;
+  };
+
+  // Safely convert anything iterable into a real, live array.
+  _.toArray = function(iterable) {
+    if (!iterable)                return [];
+    if (iterable.toArray)         return iterable.toArray();
+    if (_.isArray(iterable))      return slice.call(iterable);
+    if (_.isArguments(iterable))  return slice.call(iterable);
+    return _.values(iterable);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    return _.toArray(obj).length;
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head`. The **guard** check allows it to work
+  // with `_.map`.
+  _.first = _.head = function(array, n, guard) {
+    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail`.
+  // Especially useful on the arguments object. Passing an **index** will return
+  // the rest of the values in the array from that index onward. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = function(array, index, guard) {
+    return slice.call(array, (index == null) || guard ? 1 : index);
+  };
+
+  // Get the last element of an array.
+  _.last = function(array) {
+    return array[array.length - 1];
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, function(value){ return !!value; });
+  };
+
+  // Return a completely flattened version of an array.
+  _.flatten = function(array) {
+    return _.reduce(array, function(memo, value) {
+      if (_.isArray(value)) return memo.concat(_.flatten(value));
+      memo[memo.length] = value;
+      return memo;
+    }, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted) {
+    return _.reduce(array, function(memo, el, i) {
+      if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
+      return memo;
+    }, []);
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(_.flatten(arguments));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays. (Aliased as "intersect" for back-compat.)
+  _.intersection = _.intersect = function(array) {
+    var rest = slice.call(arguments, 1);
+    return _.filter(_.uniq(array), function(item) {
+      return _.every(rest, function(other) {
+        return _.indexOf(other, item) >= 0;
+      });
+    });
+  };
+
+  // Take the difference between one array and another.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array, other) {
+    return _.filter(array, function(value){ return !_.include(other, value); });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function() {
+    var args = slice.call(arguments);
+    var length = _.max(_.pluck(args, 'length'));
+    var results = new Array(length);
+    for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
+    return results;
+  };
+
+  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+  // we need this function. Return the position of the first occurrence of an
+  // item in an array, or -1 if the item is not included in the array.
+  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i, l;
+    if (isSorted) {
+      i = _.sortedIndex(array, item);
+      return array[i] === item ? i : -1;
+    }
+    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+    for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+
+  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+  _.lastIndexOf = function(array, item) {
+    if (array == null) return -1;
+    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
+    var i = array.length;
+    while (i--) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = arguments[2] || 1;
+
+    var len = Math.max(Math.ceil((stop - start) / step), 0);
+    var idx = 0;
+    var range = new Array(len);
+
+    while(idx < len) {
+      range[idx++] = start;
+      start += step;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Binding with arguments is also known as `curry`.
+  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
+  // We check for `func.bind` first, to fail fast when `func` is undefined.
+  _.bind = function(func, obj) {
+    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    var args = slice.call(arguments, 2);
+    return function() {
+      return func.apply(obj, args.concat(slice.call(arguments)));
+    };
+  };
+
+  // Bind all of an object's methods to that object. Useful for ensuring that
+  // all callbacks defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var funcs = slice.call(arguments, 1);
+    if (funcs.length == 0) funcs = _.functions(obj);
+    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memo = {};
+    hasher || (hasher = _.identity);
+    return function() {
+      var key = hasher.apply(this, arguments);
+      return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+    };
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){ return func.apply(func, args); }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Internal function used to implement `_.throttle` and `_.debounce`.
+  var limit = function(func, wait, debounce) {
+    var timeout;
+    return function() {
+      var context = this, args = arguments;
+      var throttler = function() {
+        timeout = null;
+        func.apply(context, args);
+      };
+      if (debounce) clearTimeout(timeout);
+      if (debounce || !timeout) timeout = setTimeout(throttler, wait);
+    };
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time.
+  _.throttle = function(func, wait) {
+    return limit(func, wait, false);
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds.
+  _.debounce = function(func, wait) {
+    return limit(func, wait, true);
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = function(func) {
+    var ran = false, memo;
+    return function() {
+      if (ran) return memo;
+      ran = true;
+      return memo = func.apply(this, arguments);
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return function() {
+      var args = [func].concat(slice.call(arguments));
+      return wrapper.apply(this, args);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var funcs = slice.call(arguments);
+    return function() {
+      var args = slice.call(arguments);
+      for (var i = funcs.length - 1; i >= 0; i--) {
+        args = [funcs[i].apply(this, args)];
+      }
+      return args[0];
+    };
+  };
+
+  // Returns a function that will only be executed after being called N times.
+  _.after = function(times, func) {
+    return function() {
+      if (--times < 1) { return func.apply(this, arguments); }
+    };
+  };
+
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = nativeKeys || function(obj) {
+    if (obj !== Object(obj)) throw new TypeError('Invalid object');
+    var keys = [];
+    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    return _.map(obj, _.identity);
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) {
+        if (source[prop] !== void 0) obj[prop] = source[prop];
+      }
+    });
+    return obj;
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) {
+        if (obj[prop] == null) obj[prop] = source[prop];
+      }
+    });
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    // Check object identity.
+    if (a === b) return true;
+    // Different types?
+    var atype = typeof(a), btype = typeof(b);
+    if (atype != btype) return false;
+    // Basic equality test (watch out for coercions).
+    if (a == b) return true;
+    // One is falsy and the other truthy.
+    if ((!a && b) || (a && !b)) return false;
+    // Unwrap any wrapped objects.
+    if (a._chain) a = a._wrapped;
+    if (b._chain) b = b._wrapped;
+    // One of them implements an isEqual()?
+    if (a.isEqual) return a.isEqual(b);
+    if (b.isEqual) return b.isEqual(a);
+    // Check dates' integer values.
+    if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
+    // Both are NaN?
+    if (_.isNaN(a) && _.isNaN(b)) return false;
+    // Compare regular expressions.
+    if (_.isRegExp(a) && _.isRegExp(b))
+      return a.source     === b.source &&
+             a.global     === b.global &&
+             a.ignoreCase === b.ignoreCase &&
+             a.multiline  === b.multiline;
+    // If a is not an object by this point, we can't handle it.
+    if (atype !== 'object') return false;
+    // Check for different array lengths before comparing contents.
+    if (a.length && (a.length !== b.length)) return false;
+    // Nothing else worked, deep compare the contents.
+    var aKeys = _.keys(a), bKeys = _.keys(b);
+    // Different object sizes?
+    if (aKeys.length != bKeys.length) return false;
+    // Recursive comparison of contents.
+    for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
+    return true;
+  };
+
+  // Is a given array or object empty?
+  _.isEmpty = function(obj) {
+    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+    for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType == 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    return obj === Object(obj);
+  };
+
+  // Is a given variable an arguments object?
+  _.isArguments = function(obj) {
+    return !!(obj && hasOwnProperty.call(obj, 'callee'));
+  };
+
+  // Is a given value a function?
+  _.isFunction = function(obj) {
+    return !!(obj && obj.constructor && obj.call && obj.apply);
+  };
+
+  // Is a given value a string?
+  _.isString = function(obj) {
+    return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
+  };
+
+  // Is a given value a number?
+  _.isNumber = function(obj) {
+    return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
+  };
+
+  // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
+  // that does not equal itself.
+  _.isNaN = function(obj) {
+    return obj !== obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false;
+  };
+
+  // Is a given value a date?
+  _.isDate = function(obj) {
+    return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
+  };
+
+  // Is the given value a regular expression?
+  _.isRegExp = function(obj) {
+    return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iterators.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Run a function **n** times.
+  _.times = function (n, iterator, context) {
+    for (var i = 0; i < n; i++) iterator.call(context, i);
+  };
+
+  // Add your own custom functions to the Underscore object, ensuring that
+  // they're correctly added to the OOP wrapper as well.
+  _.mixin = function(obj) {
+    each(_.functions(obj), function(name){
+      addToWrapper(name, _[name] = obj[name]);
+    });
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = idCounter++;
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  _.template = function(str, data) {
+    var c  = _.templateSettings;
+    var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
+      'with(obj||{}){__p.push(\'' +
+      str.replace(/\\/g, '\\\\')
+         .replace(/'/g, "\\'")
+         .replace(c.interpolate, function(match, code) {
+           return "'," + code.replace(/\\'/g, "'") + ",'";
+         })
+         .replace(c.evaluate || null, function(match, code) {
+           return "');" + code.replace(/\\'/g, "'")
+                              .replace(/[\r\n\t]/g, ' ') + "__p.push('";
+         })
+         .replace(/\r/g, '\\r')
+         .replace(/\n/g, '\\n')
+         .replace(/\t/g, '\\t')
+         + "');}return __p.join('');";
+    var func = new Function('obj', tmpl);
+    return data ? func(data) : func;
+  };
+
+  // The OOP Wrapper
+  // ---------------
+
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+  var wrapper = function(obj) { this._wrapped = obj; };
+
+  // Expose `wrapper.prototype` as `_.prototype`
+  _.prototype = wrapper.prototype;
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj, chain) {
+    return chain ? _(obj).chain() : obj;
+  };
+
+  // A method to easily add functions to the OOP wrapper.
+  var addToWrapper = function(name, func) {
+    wrapper.prototype[name] = function() {
+      var args = slice.call(arguments);
+      unshift.call(args, this._wrapped);
+      return result(func.apply(_, args), this._chain);
+    };
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    wrapper.prototype[name] = function() {
+      method.apply(this._wrapped, arguments);
+      return result(this._wrapped, this._chain);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    wrapper.prototype[name] = function() {
+      return result(method.apply(this._wrapped, arguments), this._chain);
+    };
+  });
+
+  // Start chaining a wrapped Underscore object.
+  wrapper.prototype.chain = function() {
+    this._chain = true;
+    return this;
+  };
+
+  // Extracts the result from a wrapped and chained object.
+  wrapper.prototype.value = function() {
+    return this._wrapped;
+  };
+
+})();

public/stylesheets/mixins/css3.styl

+/* https://gist.github.com/895668 */
+vendor(prop, args)
+  -webkit-{prop} args
+  -moz-{prop} args
+  -o-{prop} args
+  {prop} args
+
+animation()
+  vendor('animation', arguments)
+
+border-radius()
+  vendor('border-radius', arguments)
+
+border-top-radius() 
+  vendor('border-top-left-radius', arguments)
+  vendor('border-top-right-radius', arguments)
+
+border-bottom-radius() 
+  vendor('border-bottom-left-radius', arguments)
+  vendor('border-bottom-right-radius', arguments)
+
+linear-gradient(start_color, end_color, start = left top, end = left bottom)
+  mozstart = top if start = left top
+  background start_color
+  background -moz-linear-gradient(mozstart, start_color 0%, end_color 100%)
+  background -webkit-gradient(linear, start, end, color-stop(0%, start_color), color-stop(100%, end_color))
+  -webkit-background-origin padding-box
+
+background-clip()
+  vendor('background-clip', arguments)
+
+box-shadow() 
+  -moz-box-shadow arguments
+  -webkit-box-shadow arguments
+  box-shadow arguments
+
+box(orient, pack, align) 
+  display -webkit-box
+  display -moz-box
+  display box
+  vendor('box-orient', orient)
+  vendor('box-pack', pack)
+  vendor('box-align', align)
+  vendor('box-lines', multiple)
+
+box_flex()
+  vendor('box-flex', arguments)
+
+text-fill-color()
+  vendor('text-fill-color', arguments)
+
+transition()
+  vendor('transition', arguments)
+
+transform()
+  vendor('transform', arguments)
+

public/stylesheets/partials/typography.styl

+h1
+  color #666
+  font-size 4em
+  line-height 0.9em
+  margin-bottom 0.9em
+
+h2 
+  font-size: 1.8em
+  line-height 1em
+  margin-bottom 1em
+
+h3 
+  font-size 1.6em
+  line-height 1.13em
+  margin-bottom 1.13em
+
+h4 
+  font-size 1.4em
+  line-height 1.29em
+  margin-bottom 1.29em
+
+h5 
+  font-size 1.3em
+  line-height 1.38em
+  margin-bottom 1.38em
+
+h6 
+  font-size 1.2em
+  line-height 1.5em
+  margin-bottom 1.5em 
+
+p, q, cite, address, ul, ol, dl
+  font-size 1.2em
+  line-height 1.5em
+  margin-bottom 1.5em 
+
+li, dd, dt
+  font-size 1em 
+
+a
+  text-decoration underline
+  color #3333cc
+  outline none
+
+a:hover
+  text-decoration none
+
+code, samp, dfn, kbd, var, acronym, ins, del
+
+abbr 
+  speak spell-out
+
+acronym 
+  speak normal
+

public/stylesheets/style.css

+/* Reset
+Based on http://meyerweb.com/eric/thoughts/2008/01/15/resetting-again/
+Removed elements not supported in HTML5
+-----------------------------------------------------------------------------*/
+html,
+body,
+div,
+span,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+address,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+var,
+b,
+i,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-size: 100%;
+  vertical-align: baseline;
+}
+/* 
+Trigger the new block level elements in to the correct content flow
+-----------------------------------------------------------------------------*/
+article,
+aside,
+dialog,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+  display: block;
+}
+/* Set up document
+-----------------------------------------------------------------------------*/
 body {
-  padding: 50px;
-  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+  font: 62.5%/1.5 Helvetica, Arial, "Lucida Grande", "Lucida Sans", Tahoma, Verdana, sans-serif;
+  text-align: center;
+  background: #000;
+}
+#wrapper {
+  width: 920px;
+  text-align: left;
+  margin-left: auto;
+  margin-right: auto;
+  background: #fff;
+  padding: 20px;
+  -webkit-border-bottom-left-radius: 15px;
+  -moz-border-bottom-left-radius: 15px;
+  -o-border-bottom-left-radius: 15px;
+  border-bottom-left-radius: 15px;
+  -webkit-border-bottom-right-radius: 15px;
+  -moz-border-bottom-right-radius: 15px;
+  -o-border-bottom-right-radius: 15px;
+  border-bottom-right-radius: 15px;
+}
+/* Typography
+-----------------------------------------------------------------------------*/
+h1 {
+  color: #666;
+  font-size: 4em;
+  line-height: 0.9em;
+  margin-bottom: 0.9em;
+}
+h2 {
+  font-size: 1.8em;
+  line-height: 1em;
+  margin-bottom: 1em;
+}
+h3 {
+  font-size: 1.6em;
+  line-height: 1.13em;
+  margin-bottom: 1.13em;
+}
+h4 {
+  font-size: 1.4em;
+  line-height: 1.29em;
+  margin-bottom: 1.29em;
+}
+h5 {
+  font-size: 1.3em;
+  line-height: 1.38em;
+  margin-bottom: 1.38em;
+}
+h6 {
+  font-size: 1.2em;
+  line-height: 1.5em;
+  margin-bottom: 1.5em;
+}
+p,
+q,
+cite,
+address,
+ul,
+ol,
+dl {
+  font-size: 1.2em;
+  line-height: 1.5em;
+  margin-bottom: 1.5em;
+}
+li,
+dd,
+dt {
+  font-size: 1em;
 }
-
 a {
-  color: #00B7FF;
-}
+  text-decoration: underline;
+  color: #33c;
+  outline: none;
+}
+a:hover {
+  text-decoration: none;
+}
+code,
+samp,
+dfn,
+kbd,
+var,
+acronym,
+ins,
+del,
+abbr {
+  speak: spell-out;
+}
+acronym {
+  speak: normal;
+}
+/* CSS Tables
+-----------------------------------------------------------------------------*/
+.css-table {
+  width: 100%;
+  display: table;
+}
+.css-table .two-column {
+  display: table-row;
+}
+.css-table .two-column .cell {
+  display: table-cell;
+  vertical-align: top;
+  width: 50%;
+}
+.css-table .two-column .cell * {
+  width: 90%;
+}
+.css-table .three-column {
+  display: table-row;
+}
+.css-table .three-column .cell {
+  display: table-cell;
+  width: 33.33333%;
+}
+.css-table .three-column .cell * {
+  width: 90%;
+}
+.css-table .four-column {
+  display: table-row;
+}
+.css-table .four-column .cell {
+  display: table-cell;
+  width: 25%;
+}
+/* Header
+-----------------------------------------------------------------------------*/
+header {
+  background: #f00060;
+}
+header nav {
+  width: 960px;
+  text-align: right;
+  margin-left: auto;
+  margin-right: auto;
+}
+header nav ul {
+  list-style: none;
+  padding: 10px;
+  margin-bottom: 0;
+}
+header nav ul li {
+  display: inline;
+  margin-left: 1%;
+}
+header nav ul li a {
+  background: #fc9200;
+  -webkit-border-radius: 15px;
+  -moz-border-radius: 15px;
+  -o-border-radius: 15px;
+  border-radius: 15px;
+  display: inline-block;
+  padding: 5px 15px 6px;
+  color: #fff;
+  text-decoration: none;
+  position: relative;
+  cursor: pointer;
+}
+header nav ul li a:hover {
+  background-color: #db0d5f;
+}
+/* Footer
+-----------------------------------------------------------------------------*/
+footer {
+  color: #666;
+  border-top: 1px solid #ccc;
+  padding-top: 1.5em;
+}
+footer ul {
+  list-style: none;
+}

public/stylesheets/style.styl

+@import "mixins/css3"
+
+/* Reset
+Based on http://meyerweb.com/eric/thoughts/2008/01/15/resetting-again/
+Removed elements not supported in HTML5
+-----------------------------------------------------------------------------*/
+html, body, div, span, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, address, cite, code,
+del, dfn, em, img, ins, kbd, q, samp,
+small, strike, strong, sub, sup, var,
+b, i,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td 
+  margin 0
+  padding 0
+  border 0
+  outline 0
+  font-size 100%
+  vertical-align baseline
+  
+/* 
+Trigger the new block level elements in to the correct content flow
+-----------------------------------------------------------------------------*/
+article, aside, dialog, figure, 
+footer, header, hgroup, menu, 
+nav, section  
+  display block 
+      
+
+/* Set up document
+-----------------------------------------------------------------------------*/
+body
+  font 62.5%/1.5  Helvetica, Arial, "Lucida Grande", "Lucida Sans", Tahoma, Verdana, sans-serif
+  text-align center
+  background #000
+
+#wrapper
+  width 920px
+  text-align left 
+  margin-left auto 
+  margin-right auto
+  background #fff
+  padding 20px
+  border-bottom-radius(15px)
+
+/* Typography
+-----------------------------------------------------------------------------*/
+@import "partials/typography"
+
+/* CSS Tables
+-----------------------------------------------------------------------------*/
+.css-table
+  width 100%
+  display table
+  .two-column
+    display table-row
+    .cell
+      display table-cell
+      vertical-align top
+      width 50%
+      *
+        width 90%
+  .three-column
+    display table-row
+    .cell
+      display table-cell
+      width 33.33333%
+      *
+        width 90%
+  .four-column
+    display table-row
+    .cell
+      display table-cell
+      width 25%
+
+/* Header
+-----------------------------------------------------------------------------*/
+header
+  background #F00060
+  nav 
+    width 960px
+    text-align right
+    margin-left auto 
+    margin-right auto
+    ul 
+      list-style none
+      padding 10px
+      margin-bottom 0
+      li 
+        display inline
+        margin-left 1%
+        a 
+          background #fc9200 
+          border-radius(15px)
+          display inline-block
+          padding 5px 15px 6px 
+          color #fff
+          text-decoration none
+          position relative
+          cursor pointer
+          &:hover
+            background-color #db0d5f 
+
+/* Footer
+-----------------------------------------------------------------------------*/
+footer
+  color #666
+  border-top 1px solid #ccc
+  padding-top 1.5em
+  ul 
+    list-style none
+
+
+
       res.render('index', { title: 'Express' })
     });
     return app;
-}
+}

sensor_stream.coffee

+request = require 'request'
+readability = require 'readability'
+events = require 'events'
+cradle = require 'cradle'
+_ = require 'underscore'
+
+class SensorStream extends events.EventEmitter
+  constructor: ->
+    @db = new(cradle.Connection)('http://192.168.1.5', 5984, { cache: false , auth: { username: 'kalkov', password: 'parola' } }).database 'sensors'
+    #@db = couchDBMiddleware()	
+  watch: ->
+    self = this
+    ids = []
+    @db.changes({limit:1}).on 'response', (res) ->
+      res.on 'data', (change) ->
+        console.log('Change : '+change)
+        self.db.get change.id, (err, doc) ->