dont know what happened
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
server/bin/
|
||||||
|
client/build/
|
||||||
|
shared/data/
|
||||||
19
client/api/energy_data.js
Normal file
19
client/api/energy_data.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
const ENDPOINT = '/data/v1/energy';
|
||||||
|
|
||||||
|
class EnergyDataApi {
|
||||||
|
|
||||||
|
static index(params){
|
||||||
|
return jQuery.ajax({
|
||||||
|
url: ENDPOINT + '?' + jQuery.param(params),
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json'
|
||||||
|
}).then((res)=>{
|
||||||
|
return res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnergyDataApi;
|
||||||
19
client/api/houses.js
Normal file
19
client/api/houses.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
const ENDPOINT = '/data/v1/houses';
|
||||||
|
|
||||||
|
class HousesApi {
|
||||||
|
|
||||||
|
static index(params){
|
||||||
|
return jQuery.ajax({
|
||||||
|
url: ENDPOINT + '?' + jQuery.param(params),
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json'
|
||||||
|
}).then((res)=>{
|
||||||
|
return res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HousesApi;
|
||||||
20
client/api/power_data.js
Normal file
20
client/api/power_data.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const ENDPOINT = '/data/v1/power';
|
||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
// send all date parameters as unix timestamps;
|
||||||
|
class PowerDataApi {
|
||||||
|
|
||||||
|
static index(params){
|
||||||
|
return jQuery.ajax({
|
||||||
|
url: ENDPOINT + '?' + jQuery.param(params),
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json'
|
||||||
|
}).then((res)=>{
|
||||||
|
return res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PowerDataApi;
|
||||||
|
|
||||||
11
client/app.js
Normal file
11
client/app.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'babel-polyfill';
|
||||||
|
import 'bootstrap/dist/js/bootstrap.min';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Layout from './dashboard/layout/layout';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(Layout),
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
1
client/config/api.js
Normal file
1
client/config/api.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
api.js
|
||||||
9
client/config/db.js
Normal file
9
client/config/db.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Loki from 'lokijs';
|
||||||
|
|
||||||
|
var db = new Loki('spike');
|
||||||
|
|
||||||
|
db.addCollection('PowerData');
|
||||||
|
db.addCollection('EnergyData');
|
||||||
|
db.addCollection('Houses');
|
||||||
|
|
||||||
|
export default
|
||||||
1
client/config/store.js
Normal file
1
client/config/store.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
store.js
|
||||||
48
client/d3/bar/base.js
vendored
Normal file
48
client/d3/bar/base.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
// This class is inspired by // http://bl.ocks.org/mbostock/3885304.
|
||||||
|
class BarChart {
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return {
|
||||||
|
series_opacity_gradient: true,
|
||||||
|
margin: {top: 0, left: 70, bottom: 50, right: 20}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeData(data){
|
||||||
|
var bar_chart = this,
|
||||||
|
serialized_data = {
|
||||||
|
max: undefined,
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
|
||||||
|
data.forEach(function(data_set){
|
||||||
|
var series = extend({
|
||||||
|
css_class: bar_chart.toClass ? bar_chart.toClass(data_set) : "",
|
||||||
|
title: bar_chart.titleize ? bar_chart.titleize(data_set) : ""
|
||||||
|
}, data_set);
|
||||||
|
series.total = 0;
|
||||||
|
series.values = [];
|
||||||
|
data_set.values.forEach(function(datum, j){
|
||||||
|
var series_datum = extend({
|
||||||
|
name: datum.name,
|
||||||
|
value: datum.value,
|
||||||
|
cummulative: series.total,
|
||||||
|
css_class: bar_chart.toClass ? bar_chart.toClass(data_set, datum) : "",
|
||||||
|
title: bar_chart.titleize ? bar_chart.titleize(data_set, datum) : "",
|
||||||
|
opacity: 1.0 - 0.5 * (j / data_set.values.length)
|
||||||
|
}, datum);
|
||||||
|
series_datum.series = series;
|
||||||
|
series.total += datum.value;
|
||||||
|
series.values.push(series_datum);
|
||||||
|
});
|
||||||
|
serialized_data.series.push(series);
|
||||||
|
serialized_data.max = serialized_data.max === undefined ?
|
||||||
|
series.total :
|
||||||
|
Math.max(serialized_data.max, Math.abs(series.total));
|
||||||
|
});
|
||||||
|
return serialized_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
135
client/d3/bar/composite.js
vendored
Normal file
135
client/d3/bar/composite.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import Chart from './../base';
|
||||||
|
|
||||||
|
class BarLineChart extends Chart {
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return Object.assign(Chart.DEFAULTS, {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAxes(){
|
||||||
|
var chart = this;
|
||||||
|
line_chart.fnLine = d3.svg.line()
|
||||||
|
.interpolate(line_chart.chart_options.interpolation)
|
||||||
|
.x(function(d){ return line_chart.x_scale(d[line_chart.domain_attr]); })
|
||||||
|
.y(function(d){ return line_chart.y_scale_right(d[line_chart.line_attr]); });
|
||||||
|
}
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var chart = this;
|
||||||
|
|
||||||
|
// Axes Left
|
||||||
|
chart.y_scale_left = d3.scale.linear()
|
||||||
|
.range([chart.height, 0]);
|
||||||
|
chart.y_axis_left = d3.svg.axis()
|
||||||
|
.scale(chart.y_scale_left)
|
||||||
|
.orient("left")
|
||||||
|
.outerTickSize(0);
|
||||||
|
|
||||||
|
// Axes Right
|
||||||
|
chart.y_scale_right = d3.scale.linear()
|
||||||
|
.range([chart.height, 0]);
|
||||||
|
chart.y_axis_left = d3.svg.axis()
|
||||||
|
.scale(chart.y_scale_right)
|
||||||
|
.orient("right")
|
||||||
|
.outerTickSize(0);
|
||||||
|
|
||||||
|
chart.x_scale = d3.scale.ordinal()
|
||||||
|
.rangeRoundBands([chart.height, 0], 0.1);
|
||||||
|
|
||||||
|
chart.x_axis = d3.svg.axis()
|
||||||
|
.scale(chart.x_scale)
|
||||||
|
.orient("bottom")
|
||||||
|
.outerTickSize(0)
|
||||||
|
//chart.x_axis.tickFormat(d3.time.format('%b %d at %H'))
|
||||||
|
//chart.x_axis.ticks(d3.time.hour, 12);
|
||||||
|
|
||||||
|
// append axis groups.
|
||||||
|
chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-range-left d3-chart-axis");
|
||||||
|
chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-range-right d3-chart-axis");
|
||||||
|
chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-domain d3-chart-axis")
|
||||||
|
.attr("transform", "translate(0, " + (chart.height) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
defineDomain(domain){
|
||||||
|
var chart = this;
|
||||||
|
|
||||||
|
chart.domain = domain;
|
||||||
|
chart.x_scale.domain(domain);
|
||||||
|
chart.svg.select(".d3-chart-domain")
|
||||||
|
.call(chart.x_axis);
|
||||||
|
.selectAll("text")
|
||||||
|
.attr("transform", function(){
|
||||||
|
var elem = this,
|
||||||
|
bbox = elem.getBBox(),
|
||||||
|
middleX = bbox.x + (bbox.width / 2),
|
||||||
|
middleY = bbox.y + (bbox.height / 2);
|
||||||
|
return "rotate(-30," + middleX + "," + middleY + ")";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBarData(data){
|
||||||
|
var chart = this;
|
||||||
|
data = chart.serializeBarData(data);
|
||||||
|
|
||||||
|
chart.y_scale_left.domain(data.range_extent);
|
||||||
|
chart.svg.select(".d3-chart-range").call(chart.y_axis_left);
|
||||||
|
|
||||||
|
data.series.forEach(function(series){
|
||||||
|
var filtered_values = series.values.filter((value){ return chart.domain.indexOf(value[chart.domain_attr]) < 0; })
|
||||||
|
bars = chart.svg.selectAll(".d3-chart-bar")
|
||||||
|
.data(series.values);
|
||||||
|
chart.applyData(series, bars.enter().append("rect"));
|
||||||
|
chart.applyData(series, bars.transition());
|
||||||
|
bars.exit().remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper method for drawData
|
||||||
|
applyBarData(series, elements){
|
||||||
|
var chart = this,
|
||||||
|
series_class = "d3-chart-bar " + series.css_class;
|
||||||
|
elements
|
||||||
|
.attr("class", function(d){ return series_class + " " + d.css_class; })
|
||||||
|
.attr("title", function(d){ return d.title; })
|
||||||
|
.attr("width", chart.x_scale.rangeBand())
|
||||||
|
.attr("x", chart.x_scale(series.title))
|
||||||
|
.attr("height", return chart.y_scale(d[chart.bar_attr]))
|
||||||
|
.attr("y", function(d) { return chart.y_scale(d.cummulative); })
|
||||||
|
.attr('fill', function(d){ return chart.fnColor(d.title); });
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLineData(data){
|
||||||
|
var chart = this,
|
||||||
|
nested_extent = chart.nestedExtent(data.series, 'values', chart.domain_attr, chart.line_attr);
|
||||||
|
|
||||||
|
// calibrate axes
|
||||||
|
bar_chart.y_scale_right.domain([Math.min(0, nested_extent.range_min), nested_extent.range_max]);
|
||||||
|
bar_chart.svg.select(".d3-chart-range-right")
|
||||||
|
.call(bar_chart.y_axis_right);
|
||||||
|
|
||||||
|
// draw lines
|
||||||
|
var line = g.selectAll(".d3-chart-line")
|
||||||
|
.data(data.series);
|
||||||
|
|
||||||
|
[line.enter().append('g'), line.transition()].forEach((groups)=>{
|
||||||
|
line_chart.applyLineData(groups, data.series);
|
||||||
|
});
|
||||||
|
line.exit().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLineData(groups){
|
||||||
|
var chart = this;
|
||||||
|
groups
|
||||||
|
.attr('class', function(series){ return "d3-chart-line " + chart.cssClass(series); })
|
||||||
|
.attr("title", function(series){ return series.title; })
|
||||||
|
.append("path")
|
||||||
|
.attr("d", function(series){ return chart.fnLine(series.values.filter((value)=>{ return chart.domain.indexOf(value[chart.domain_attr]) < 0; })); })
|
||||||
|
.style("stroke", function(series){ return line_chart.fnColor(series.title); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
client/d3/bar/horizontal.js
vendored
Normal file
76
client/d3/bar/horizontal.js
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import BarChart from './bar.base';
|
||||||
|
|
||||||
|
class HorizontalBarChart extends BarChart {
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var bar_chart = this;
|
||||||
|
bar_chart.y_scale = d3.scale.ordinal()
|
||||||
|
.rangeRoundBands([bar_chart.height, 0], 0.1);
|
||||||
|
|
||||||
|
bar_chart.y_axis = d3.svg.axis()
|
||||||
|
.scale(bar_chart.y_scale)
|
||||||
|
.orient("left");
|
||||||
|
|
||||||
|
bar_chart.x_scale = d3.scale.linear()
|
||||||
|
.range([0, bar_chart.width]);
|
||||||
|
|
||||||
|
bar_chart.x_axis = d3.svg.axis()
|
||||||
|
.scale(bar_chart.x_scale)
|
||||||
|
.orient("bottom")
|
||||||
|
.ticks(bar_chart.range_ticks)
|
||||||
|
.outerTickSize(0);
|
||||||
|
|
||||||
|
// append axes
|
||||||
|
bar_chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-domain d3-chart-axis");
|
||||||
|
bar_chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-axis")
|
||||||
|
.attr("transform", "translate(0, " + (bar_chart.height - bar_chart.margin.top) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var bar_chart = this;
|
||||||
|
data = bar_chart.serializeData(data);
|
||||||
|
|
||||||
|
// calibrate axes
|
||||||
|
bar_chart.y_scale.domain(data.series.reverse().map(function(d) { return d.name; }));
|
||||||
|
bar_chart.svg.select(".d3-chart-domain.d3-chart-axis")
|
||||||
|
.call(bar_chart.y_axis)
|
||||||
|
.selectAll("text")
|
||||||
|
.attr("transform", function(){
|
||||||
|
var elem = this,
|
||||||
|
bbox = elem.getBBox(),
|
||||||
|
middleX = bbox.x + (bbox.width / 2),
|
||||||
|
middleY = bbox.y + (bbox.height / 2);
|
||||||
|
return "rotate(-30,"+middleX + "," + middleY+")";
|
||||||
|
});
|
||||||
|
|
||||||
|
bar_chart.x_scale.domain([0, data.max]);
|
||||||
|
bar_chart.svg.select(".d3-chart-range.d3-chart-axis").call(bar_chart.x_axis);
|
||||||
|
|
||||||
|
data.series.forEach(function(series){
|
||||||
|
var bars = bar_chart.svg.selectAll("d3-chart-rect.d3-chart-bar." + series.css_class)
|
||||||
|
.data(series.values);
|
||||||
|
bar_chart.applyData(series, bars.enter().append("rect"));
|
||||||
|
bar_chart.applyData(series, bars.transition());
|
||||||
|
bars.exit().remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper method for drawData.
|
||||||
|
applyData(series, elements){
|
||||||
|
var bar_chart = this,
|
||||||
|
series_class = "d3-chart-bar " + series.css_class;
|
||||||
|
elements
|
||||||
|
.attr("class", function(d){ return series_class + " " + d.css_class; })
|
||||||
|
.attr("title", function(d){ return d.title; })
|
||||||
|
.attr("y", function(d) { return bar_chart.y_scale(series.name); })
|
||||||
|
.attr("height", bar_chart.y_scale.rangeBand())
|
||||||
|
.attr("x", function(d) { return bar_chart.x_scale(d.cummulative); })
|
||||||
|
.attr("width", function(d) { return bar_chart.x_scale(d.value); })
|
||||||
|
.attr("opacity", function(d) { return d.opacity; });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HorizontalBarChart;
|
||||||
76
client/d3/bar/vertical.js
vendored
Normal file
76
client/d3/bar/vertical.js
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import BarChart from './bar.base';
|
||||||
|
|
||||||
|
class VerticalBarChart extends BarChart {
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var bar_chart = this;
|
||||||
|
bar_chart.y_scale = d3.scale.ordinal()
|
||||||
|
.rangeRoundBands([bar_chart.height, 0], 0.1);
|
||||||
|
|
||||||
|
bar_chart.y_axis = d3.svg.axis()
|
||||||
|
.scale(bar_chart.y_scale)
|
||||||
|
.ticks(bar_chart.range_ticks)
|
||||||
|
.orient("left")
|
||||||
|
.outerTickSize(0);
|
||||||
|
|
||||||
|
bar_chart.x_scale = d3.scale.linear()
|
||||||
|
.range([0, bar_chart.width]);
|
||||||
|
|
||||||
|
bar_chart.x_axis = d3.svg.axis()
|
||||||
|
.scale(bar_chart.x_scale)
|
||||||
|
.orient("bottom");
|
||||||
|
|
||||||
|
// append axes
|
||||||
|
bar_chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-axis");
|
||||||
|
bar_chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-domain d3-chart-axis")
|
||||||
|
.attr("transform", "translate(0, " + (bar_chart.height - bar_chart.margin.top) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var bar_chart = this;
|
||||||
|
data = bar_chart.serializeData(data);
|
||||||
|
|
||||||
|
// calibrate axes
|
||||||
|
bar_chart.x_scale.domain(data.series.reverse().map((d)=>{ return d.name; }));
|
||||||
|
bar_chart.svg.select(".d3-chart-domain.d3-chart-axis")
|
||||||
|
.call(bar_chart.x_axis)
|
||||||
|
.selectAll("text")
|
||||||
|
.attr("transform", function(){
|
||||||
|
var elem = this,
|
||||||
|
bbox = elem.getBBox(),
|
||||||
|
middleX = bbox.x + (bbox.width / 2),
|
||||||
|
middleY = bbox.y + (bbox.height / 2);
|
||||||
|
return "rotate(-30,"+middleX + "," + middleY+")";
|
||||||
|
});
|
||||||
|
|
||||||
|
bar_chart.y_scale.domain([data.min, data.max]);
|
||||||
|
bar_chart.svg.select(".d3-chart-range.d3-chart-axis").call(bar_chart.y_axis);
|
||||||
|
|
||||||
|
data.series.forEach(function(series){
|
||||||
|
var bars = bar_chart.svg.selectAll(".d3-chart-rect.d3-chart-bar." + series.css_class)
|
||||||
|
.data(series.values);
|
||||||
|
bar_chart.applyData(series, bars.enter().append("rect"));
|
||||||
|
bar_chart.applyData(series, bars.transition());
|
||||||
|
bars.exit().remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper method for drawData
|
||||||
|
applyData(series, elements){
|
||||||
|
var bar_chart = this,
|
||||||
|
series_class = "d3-chart-bar " + series.css_class;
|
||||||
|
elements
|
||||||
|
.attr("class", function(d){ return series_class + " " + d.css_class; })
|
||||||
|
.attr("title", function(d){ return d.title; })
|
||||||
|
.attr("width", function(d) { return bar_chart.x_scale.rangeBand(); })
|
||||||
|
.attr("x", function(d) { return bar_chart.x_scale(series.name); })
|
||||||
|
.attr("height", return bar_chart.y_scale(d.value))
|
||||||
|
.attr("y", function(d) { return bar_chart.y_scale(d.cummulative); })
|
||||||
|
.attr("opacity", function(d) { return d.opacity; });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VerticalBarChart;
|
||||||
75
client/d3/base.js
vendored
Normal file
75
client/d3/base.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
outer_width: 500,
|
||||||
|
outer_height: 300,
|
||||||
|
margin: {top: 30, left: 70, bottom: 50, right: 20},
|
||||||
|
domain_ticks: 10,
|
||||||
|
range_ticks: 8,
|
||||||
|
container: "container",
|
||||||
|
time_series: false,
|
||||||
|
range_label: undefined,
|
||||||
|
domain_attr: undefined,
|
||||||
|
range_attr: undefined,
|
||||||
|
toCssClass: function(series){
|
||||||
|
return series ? series.title.toLowerCase().replace(/\s+/g, '-') : "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class Chart {
|
||||||
|
|
||||||
|
constructor(options){
|
||||||
|
var chart = this;
|
||||||
|
chart = extend(chart, chart.chart_options, options);
|
||||||
|
|
||||||
|
chart.height = chart.outer_height - chart.margin.top - chart.margin.bottom;
|
||||||
|
chart.width = chart.outer_width - chart.margin.left - chart.margin.right;
|
||||||
|
|
||||||
|
chart.svg = d3.select(chart.container).append("svg")
|
||||||
|
.attr("width", chart.outer_width)
|
||||||
|
.attr("height", chart.outer_height)
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")");
|
||||||
|
chart.defineAxes();
|
||||||
|
if (chart.afterAxes) chart.afterAxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
cssClass(series){
|
||||||
|
var chart = this;
|
||||||
|
if (!chart.toCssClass) return '';
|
||||||
|
return chart.toCssClass(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedExtent(a, series_values, domain_attr, range_attr){
|
||||||
|
var extent = {
|
||||||
|
min_domain: Infinity,
|
||||||
|
max_domain: -Infinity,
|
||||||
|
min_range: Infinity,
|
||||||
|
max_range: -Infinity
|
||||||
|
};
|
||||||
|
a.forEach((series)=>{
|
||||||
|
series[series_values].forEach((value)=>{
|
||||||
|
extent.min_domain = Math.min(min_domain, value[domain_attr]);
|
||||||
|
extent.max_domain = Math.max(max_domain, value[domain_attr]);
|
||||||
|
extent.min_range = Math.min(min_range, value[range_attr]);
|
||||||
|
extent.max_range = Math.max(max_range, value[range_attr]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
titleize(s){
|
||||||
|
var words = s.split(' '),
|
||||||
|
array = [];
|
||||||
|
for (var i=0; i<words.length; ++i) {
|
||||||
|
array.push(words[i].charAt(0).toUpperCase() + words[i].toLowerCase().slice(1));
|
||||||
|
}
|
||||||
|
return array.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Chart.DEFAULTS = DEFAULTS;
|
||||||
|
|
||||||
|
export default Chart;
|
||||||
5
client/d3/chart.scss
Normal file
5
client/d3/chart.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.d3-chart-axis path {
|
||||||
|
stroke-width: 2;
|
||||||
|
fill: none;
|
||||||
|
stroke:#000000;
|
||||||
|
}
|
||||||
153
client/d3/grid/calendar_grid.js
vendored
Normal file
153
client/d3/grid/calendar_grid.js
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import Chart from './../base';
|
||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
// inspired by https://gist.github.com/mbostock/4b66c0d9be9a0d56484e
|
||||||
|
class CalendarGridChart extends Chart{
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
var chart = this;
|
||||||
|
return extend(Object.assign({}, Chart.DEFAULTS), {
|
||||||
|
margin: {top: 30, left: 150, bottom: 0, right: 0},
|
||||||
|
grid_padding: 0.05,
|
||||||
|
parse_date_format: '%Y-%m-%d',
|
||||||
|
display_date_format: '%B %Y',
|
||||||
|
date_attr: 'date',
|
||||||
|
range_attr: undefined,
|
||||||
|
min_range_zero: false,
|
||||||
|
color: '#FFF',
|
||||||
|
extent: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var grid_chart = this;
|
||||||
|
|
||||||
|
// y scale is dependent on number of months.
|
||||||
|
grid_chart.y_axis = d3.svg.axis().orient("left").outerTickSize(0);
|
||||||
|
grid_chart.y_scale = d3.scale.ordinal()
|
||||||
|
grid_chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-axis");
|
||||||
|
|
||||||
|
grid_chart.x_scale = d3.scale.ordinal()
|
||||||
|
.domain(d3.range(31).map((n)=>{ return n+1; }))
|
||||||
|
.rangeRoundBands([0, grid_chart.width], grid_chart.grid_padding, 0);
|
||||||
|
|
||||||
|
grid_chart.x_axis = d3.svg.axis()
|
||||||
|
.scale(grid_chart.x_scale)
|
||||||
|
.orient("top")
|
||||||
|
.outerTickSize(0);
|
||||||
|
|
||||||
|
// append x axis
|
||||||
|
grid_chart.svg.append("g").attr("class", "d3-chart-domain d3-chart-axis");
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAxes(){
|
||||||
|
var grid_chart = this;
|
||||||
|
grid_chart.grid_unit_size = grid_chart.width / 31 - grid_chart.grid_padding * grid_chart.width / 30;
|
||||||
|
|
||||||
|
if (grid_chart.display_date_format) grid_chart.displayDate = d3.time.format(grid_chart.display_date_format);
|
||||||
|
|
||||||
|
if (!grid_chart.toDate && grid_chart.parse_date_format){
|
||||||
|
grid_chart.parseDate = d3.time.format(grid_chart.parse_date_format);
|
||||||
|
grid_chart.toDate = (datum)=>{
|
||||||
|
grid_chart.parseDate(datum[grid_chart.date_attr]);
|
||||||
|
}
|
||||||
|
} else if (!grid_chart.toDate){
|
||||||
|
grid_chart.toDate = (datum)=>{ return datum[grid_chart.date_attr] };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
grid_chart.monthFormat = d3.time.format('%B %Y');
|
||||||
|
grid_chart.toMonthString = (datum)=>{
|
||||||
|
return grid_chart.monthFormat(grid_chart.toDate(datum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeData(data){
|
||||||
|
var grid_chart = this;
|
||||||
|
data.css_class = data.css_class || grid_chart.toClass ? grid_chart.toClass(data) : "";
|
||||||
|
|
||||||
|
grid_chart.rangeValue = grid_chart.range_attr ? function(d){ return d[grid_chart.range_attr]; } : grid_chart.rangeValue;
|
||||||
|
|
||||||
|
data.months = [];
|
||||||
|
if (data.min_range !== undefined && data.max_range !== undefined){
|
||||||
|
data.range = {min: data.min_range, max: data.max_range};
|
||||||
|
data.values.forEach((value)=>{
|
||||||
|
var date = grid_chart.toDate(value),
|
||||||
|
date_s = grid_chart.monthFormat(date);
|
||||||
|
if (data.months.indexOf(date_s) < 0) data.months.push(date_s);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var min_range = Infinity,
|
||||||
|
max_range = -Infinity;
|
||||||
|
data.values.forEach((value)=>{
|
||||||
|
var date = grid_chart.toDate(value),
|
||||||
|
date_s = grid_chart.monthFormat(date),
|
||||||
|
range_value =grid_chart.rangeValue(value);
|
||||||
|
min_range = Math.min(min_range, range_value);
|
||||||
|
max_range = Math.max(max_range, range_value);
|
||||||
|
if (data.months.indexOf(date_s) < 0) data.months.push(date_s);
|
||||||
|
});
|
||||||
|
if (grid_chart.min_range_zero) min_range = Math.min(min_range, 0);
|
||||||
|
data.range = { min: min_range, max: max_range };
|
||||||
|
}
|
||||||
|
data.range.diff = data.range.max - data.range.min;
|
||||||
|
|
||||||
|
data.months = data.months.sort((date_s1, date_s2)=>{
|
||||||
|
var date1 = grid_chart.monthFormat.parse(date_s1),
|
||||||
|
date2 = grid_chart.monthFormat.parse(date_s2);
|
||||||
|
return date1.getTime() - date2.getTime();
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var grid_chart = this;
|
||||||
|
grid_chart.i = grid_chart.i || 1;
|
||||||
|
data = grid_chart.serializeData(data);
|
||||||
|
|
||||||
|
// calibrate axes
|
||||||
|
var y_axis_height = grid_chart.grid_unit_size * (1 + grid_chart.grid_padding) * data.months.length;
|
||||||
|
grid_chart.y_scale.rangeRoundBands([0, y_axis_height], grid_chart.grid_padding, 0);
|
||||||
|
grid_chart.y_scale.domain(data.months);
|
||||||
|
grid_chart.y_axis.scale(grid_chart.y_scale);
|
||||||
|
|
||||||
|
grid_chart.svg.select(".d3-chart-range")
|
||||||
|
.call(grid_chart.y_axis);
|
||||||
|
|
||||||
|
grid_chart.svg.select(".d3-chart-domain").call(grid_chart.x_axis);
|
||||||
|
|
||||||
|
var grid_units = grid_chart.svg.selectAll(".d3-chart-grid-unit")
|
||||||
|
.data(data.values);
|
||||||
|
grid_units.exit().remove();
|
||||||
|
grid_chart.applyData(data, grid_units.enter().append("rect"));
|
||||||
|
grid_chart.applyData(data, grid_units);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper method for drawData.
|
||||||
|
applyData(data, elements){
|
||||||
|
var grid_chart = this,
|
||||||
|
series_class = "d3-chart-grid-unit " + data.css_class;
|
||||||
|
elements
|
||||||
|
.attr("class", series_class)
|
||||||
|
.attr("y", function(d) {
|
||||||
|
var bottom = grid_chart.y_scale(grid_chart.toMonthString(d)),
|
||||||
|
middle = grid_chart.y_scale.rangeBand() / 2 - grid_chart.grid_unit_size / 2;
|
||||||
|
return bottom + middle;
|
||||||
|
})
|
||||||
|
.attr("height", grid_chart.grid_unit_size)
|
||||||
|
.attr("x", function(d) {
|
||||||
|
return grid_chart.x_scale(grid_chart.toDate(d).getDate());
|
||||||
|
})
|
||||||
|
.attr("width", function(d) { return grid_chart.grid_unit_size; })
|
||||||
|
.attr('fill', grid_chart.color)
|
||||||
|
.attr("opacity", function(d) { return grid_chart.calculateOpacity(75, data.range); });
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateOpacity(value, range){
|
||||||
|
return Math.max(0, Math.min(1, 1 - (range.max - (value - range.min)) / range.diff));
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarGridChart;
|
||||||
122
client/d3/line/line.js
vendored
Normal file
122
client/d3/line/line.js
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
import Chart from './../base';
|
||||||
|
|
||||||
|
|
||||||
|
// inspired by https://gist.github.com/mbostock/4b66c0d9be9a0d56484e
|
||||||
|
class LineChart extends Chart {
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return {
|
||||||
|
interpolation: 'basis'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var chart = this;
|
||||||
|
|
||||||
|
chart.y_scale = d3.scale.linear()
|
||||||
|
.range([chart.height, 0]);
|
||||||
|
chart.y_axis = d3.svg.axis()
|
||||||
|
.scale(chart.y_scale)
|
||||||
|
.orient("left")
|
||||||
|
.outerTickSize(1);
|
||||||
|
|
||||||
|
if (chart.time_series){
|
||||||
|
chart.x_scale = d3.time.scale()
|
||||||
|
.range([0, chart.width]);
|
||||||
|
} else {
|
||||||
|
chart.x_scale = d3.scale.linear()
|
||||||
|
.range([0, chart.width]);
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.x_axis = d3.svg.axis()
|
||||||
|
.scale(chart.x_scale)
|
||||||
|
.orient("bottom")
|
||||||
|
.outerTickSize(0)
|
||||||
|
//chart.x_axis.tickFormat(d3.time.format('%b %d at %H'))
|
||||||
|
//chart.x_axis.ticks(d3.time.hour, 12);
|
||||||
|
|
||||||
|
// append axes
|
||||||
|
chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-range d3-chart-axis");
|
||||||
|
chart.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-domain d3-chart-axis")
|
||||||
|
.attr("transform", "translate(0, " + (chart.height) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAxes(){
|
||||||
|
var line_chart = this;
|
||||||
|
// function that draws the lines.
|
||||||
|
line_chart.line = d3.svg.line()
|
||||||
|
.interpolate(line_chart.chart_options.interpolation)
|
||||||
|
.x(function(d){ return line_chart.x_scale(d[line_chart.domain_attr]); })
|
||||||
|
.y(function(d){ return line_chart.y_scale(d[line_chart.range_attr]); });
|
||||||
|
|
||||||
|
// function that returns unique color based on series_title.
|
||||||
|
line_chart.color = d3.scale.category20();
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeData(data){
|
||||||
|
var line_chart = this,
|
||||||
|
serialized_data = {
|
||||||
|
series: [],
|
||||||
|
range_min: Infinity,
|
||||||
|
range_max: -Infinity,
|
||||||
|
domain_min: Infinity,
|
||||||
|
domain_max: -Infinity,
|
||||||
|
};
|
||||||
|
|
||||||
|
data.forEach(function(data_set){
|
||||||
|
var series = extend({
|
||||||
|
css_class: line_chart.toClass ? line_chart.toClass(data_set) : "",
|
||||||
|
title: line_chart.titleize ? line_chart.titleize(data_set) : "",
|
||||||
|
color: ''
|
||||||
|
}, data_set);
|
||||||
|
|
||||||
|
series.values.forEach((value)=>{
|
||||||
|
series_data.range_min = Math.min(series_data.range_min, value[line_chart.range_attr]);
|
||||||
|
series_data.range_max = Math.max(series_data.range_max, value[line_chart.range_attr]);
|
||||||
|
series_data.domain_min = Math.min(series_data.domain_min, value[line_chart.domain_attr]);
|
||||||
|
series_data.domain_max = Math.max(series_data.domain_max, value[line_chart.domain_attr]);
|
||||||
|
});
|
||||||
|
serialized_data.series.push(series);
|
||||||
|
});
|
||||||
|
return serialized_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var line_chart = this;
|
||||||
|
data = line_chart.serialize_data;
|
||||||
|
|
||||||
|
// calibrate axes
|
||||||
|
bar_chart.y_scale.domain([Math.min(0, data.range_min), data.range_max]);
|
||||||
|
bar_chart.svg.select(".d3-chart-range.d3-chart-axis")
|
||||||
|
.call(bar_chart.y_axis);
|
||||||
|
|
||||||
|
bar_chart.x_scale.domain([data.domain_max, Math.min(data.domain_min)]);
|
||||||
|
bar_chart.svg.select(".d3-chart-domain.d3-chart-axis").call(bar_chart.x_axis);
|
||||||
|
|
||||||
|
// draw lines
|
||||||
|
var line = g.selectAll(".d3-chart-series")
|
||||||
|
.data(data.series);
|
||||||
|
|
||||||
|
[line.enter().append('g'), line.transition()].forEach((groups)=>{
|
||||||
|
line_chart.applyData(groups);
|
||||||
|
});
|
||||||
|
line.exit().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyData(groups){
|
||||||
|
var line_chart = this;
|
||||||
|
groups
|
||||||
|
.attr('class', function(series){ return "d3-chart-line " + series.css_class; })
|
||||||
|
.attr("title", function(series){ return series.title; })
|
||||||
|
.append("path")
|
||||||
|
.attr("d", function(series){ return line_chart.line(series.values); })
|
||||||
|
.style("stroke", function(series){ return line_chart.color(series.title); });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LineChart;
|
||||||
13
client/d3/line/spline.js
vendored
Normal file
13
client/d3/line/spline.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import LineChart from './line';
|
||||||
|
|
||||||
|
const INTEPOLATION = 'cardinal';
|
||||||
|
|
||||||
|
class SplineChart extends LineChart {
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return {
|
||||||
|
interpolation: INTEPOLATION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
108
client/d3/line/spline_stack.js
vendored
Normal file
108
client/d3/line/spline_stack.js
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import LineChart from './line';
|
||||||
|
|
||||||
|
const INTERPOLATION = 'cardinal';
|
||||||
|
|
||||||
|
// inspired by https://bl.ocks.org/mbostock/3885211
|
||||||
|
class SplineStackChart extends LineChart {
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return Object.assign(Object.assign({}, LineChart.DEFAULTS), {
|
||||||
|
interpolation: INTERPOLATION
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAxes(){
|
||||||
|
var spline_stack = this;
|
||||||
|
spline_stack.fnArea = d3.svg.area()
|
||||||
|
.x(function(d, i) { return spline_stack.x_scale(d.x); })
|
||||||
|
.y0(function(d) { return spline_stack.y_scale(d.y0); })
|
||||||
|
.y1(function(d) { return spline_stack.y_scale(d.y0 + d.y); })
|
||||||
|
.interpolate(spline_stack.interpolation);
|
||||||
|
|
||||||
|
spline_stack.fnStack = d3.layout.stack()
|
||||||
|
.values(function(d) { return d.values; });
|
||||||
|
|
||||||
|
// function that returns unique color based on series_title.
|
||||||
|
spline_stack.fnColor = d3.scale.category20();
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeData(data){
|
||||||
|
var spline_stack = this,
|
||||||
|
serialized_data = {
|
||||||
|
series: [] };
|
||||||
|
data.series.forEach(function(series, i){
|
||||||
|
series.css_class = series.css_class || spline_stack.toClass ? spline_stack.toClass(series) : "series-" + i;
|
||||||
|
if (spline_stack.domain_attr !== 'x' && spline_stack.range_attr !== 'y'){
|
||||||
|
series.values = series.values.map((value)=>{
|
||||||
|
return {x: value[spline_stack.domain_attr], y: value[spline_stack.range_attr], series: series};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
serialized_data.series.push(series);
|
||||||
|
});
|
||||||
|
|
||||||
|
serialized_data.series = spline_stack.fnStack(serialized_data.series);
|
||||||
|
// assume all series have same domain, use first series to establish extent.
|
||||||
|
serialized_data.domain_extent = d3.extent(serialized_data.series[0].values.map((value)=>{ return value.x; }));
|
||||||
|
// final series will have the highest y values.
|
||||||
|
serialized_data.range_max = d3.max(serialized_data.series[serialized_data.series.length - 1].values.map((value)=>{ return value.y0 + value.y; }))
|
||||||
|
|
||||||
|
return serialized_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var spline_stack = this;
|
||||||
|
data = spline_stack.serializeData(data);
|
||||||
|
|
||||||
|
// calibrate axes.
|
||||||
|
spline_stack.y_scale.domain([0, data.range_max]);
|
||||||
|
spline_stack.svg.select(".d3-chart-range.d3-chart-axis")
|
||||||
|
.call(spline_stack.y_axis);
|
||||||
|
|
||||||
|
spline_stack.x_scale.domain(data.domain_extent);
|
||||||
|
spline_stack.svg.select(".d3-chart-domain.d3-chart-axis").call(spline_stack.x_axis);
|
||||||
|
|
||||||
|
var stack = spline_stack.svg.selectAll(".d3-chart-spline-stack")
|
||||||
|
.data(data.series);
|
||||||
|
[stack.enter().append("path"), stack].forEach((paths)=>{
|
||||||
|
spline_stack.applyData(paths);
|
||||||
|
});
|
||||||
|
stack.exit().remove();
|
||||||
|
|
||||||
|
if (spline_stack.include_dots){
|
||||||
|
data.series.forEach((series)=>{
|
||||||
|
var dots = spline_stack.svg.selectAll(".d3-chart-spline-dot." + series.css_class)
|
||||||
|
.data(series.values);
|
||||||
|
[dots.enter().append("circle"), dots].forEach((circles)=>{
|
||||||
|
spline_stack.applyDots(series, circles);
|
||||||
|
});
|
||||||
|
dots.exit().remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyData(paths){
|
||||||
|
var spline_stack = this;
|
||||||
|
paths
|
||||||
|
.attr("class", function(series){ return "d3-chart-spline-stack " + series.css_class;})
|
||||||
|
.attr("d", function(series){ return spline_stack.fnArea(series.values); })
|
||||||
|
.style("fill", function(series){ return spline_stack.fnColor(series.title); })
|
||||||
|
.attr('opacity', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDots(series, circles){
|
||||||
|
var spline_stack = this;
|
||||||
|
circles
|
||||||
|
.attr('class', 'd3-chart-spline-dot ' + series.css_class)
|
||||||
|
.attr("r", 2)
|
||||||
|
.attr("cx", function(d, i){ return spline_stack.x_scale(d.x); })
|
||||||
|
.attr("cy", function(d, i){ return spline_stack.y_scale(d.y + d.y0); })
|
||||||
|
.attr("title", function(d, i){ return spline_stack.titleizeDatum(series, d); })
|
||||||
|
.style("fill", spline_stack.fnColor(series.title))
|
||||||
|
.attr('opacity', 1)
|
||||||
|
.attr('stroke-width', 0)
|
||||||
|
.attr('stroke', '#fff');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SplineStackChart;
|
||||||
133
client/d3/sliders/date_range.js
vendored
Normal file
133
client/d3/sliders/date_range.js
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import Chart from './../base';
|
||||||
|
|
||||||
|
class DateRange extends Chart {
|
||||||
|
|
||||||
|
|
||||||
|
get chart_options(){
|
||||||
|
return Object.assign(Object.assign({}, Chart.DEFAULTS), {
|
||||||
|
outer_width: 600,
|
||||||
|
outer_height: 250,
|
||||||
|
margin: {top: 20, left: 10, bottom: 20, right: 10},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineAxes(){
|
||||||
|
var date_range = this;
|
||||||
|
|
||||||
|
date_range.x_scale = d3.time.scale()
|
||||||
|
.range([0, date_range.width])
|
||||||
|
.clamp(true);
|
||||||
|
|
||||||
|
date_range.x_axis = d3.svg.axis()
|
||||||
|
.scale(date_range.x_scale)
|
||||||
|
.orient("bottom")
|
||||||
|
.ticks(d3.time.weeks, 1)
|
||||||
|
//.tickFormat(function(d) { return d + "°"; })
|
||||||
|
.tickSize(1)
|
||||||
|
.outerTickSize(1)
|
||||||
|
.tickPadding(12)
|
||||||
|
|
||||||
|
date_range.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-domain")
|
||||||
|
.attr("transform", "translate(0," + date_range.height / 2 + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAxes(){
|
||||||
|
var date_range = this;
|
||||||
|
|
||||||
|
date_range.slider = date_range.svg.append("g")
|
||||||
|
.attr("class", "d3-chart-slider");
|
||||||
|
|
||||||
|
date_range.min_handle = date_range.slider.append("circle")
|
||||||
|
.attr("class", "d3-chart-min-handle")
|
||||||
|
.attr("transform", "translate(0," + date_range.height / 2 + ")")
|
||||||
|
.attr("r", 9);
|
||||||
|
|
||||||
|
date_range.max_handle = date_range.slider.append("circle")
|
||||||
|
.attr("class", "d3-chart-max-handle")
|
||||||
|
.attr("transform", "translate(0," + date_range.height / 2 + ")")
|
||||||
|
.attr("r", 9);
|
||||||
|
|
||||||
|
date_range.brush = d3.svg.brush()
|
||||||
|
.x(date_range.x_scale);
|
||||||
|
|
||||||
|
date_range.slider
|
||||||
|
.call(date_range.brush)
|
||||||
|
//.select(".background")
|
||||||
|
// .attr("height", date_range.height);
|
||||||
|
|
||||||
|
date_range.slider.call(date_range.brush)
|
||||||
|
.selectAll(".extent,.resize")
|
||||||
|
.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawData(data){
|
||||||
|
var date_range = this;
|
||||||
|
date_range.x_scale.domain([data.abs_min, data.abs_max]);
|
||||||
|
|
||||||
|
date_range.svg.select(".d3-chart-domain")
|
||||||
|
.call(date_range.x_axis);
|
||||||
|
|
||||||
|
date_range.min_handle.attr('cx', date_range.x_scale(data.current_min));
|
||||||
|
date_range.max_handle.attr('cx', date_range.x_scale(data.current_max));
|
||||||
|
|
||||||
|
date_range.brush.extent([data.current_min, data.current_min])
|
||||||
|
.on("brush", ()=>{
|
||||||
|
DateRange.handleBrush(date_range, eval('this'));
|
||||||
|
});
|
||||||
|
|
||||||
|
date_range.slider
|
||||||
|
.call(date_range.brush.event)
|
||||||
|
.transition() // gratuitous intro!
|
||||||
|
.duration(750)
|
||||||
|
.call(date_range.brush.extent([data.current_min, data.current_min]))
|
||||||
|
.call(date_range.brush.event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static handleBrush(date_range, elem){
|
||||||
|
var date = date_range.brush.extent()[0],
|
||||||
|
current_min = parseInt(date_range.min_handle.attr('cx')),
|
||||||
|
current_max = parseInt(date_range.max_handle.attr('cx'));
|
||||||
|
|
||||||
|
if (!current_min && !current_max) return false
|
||||||
|
if (d3.event.sourceEvent) { // not a programmatic event
|
||||||
|
date = date_range.x_scale.invert(d3.mouse(elem)[0]);
|
||||||
|
date_range.brush.extent([date, date]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = date_range.x_scale(date);
|
||||||
|
|
||||||
|
if (value < current_max && value > current_min){
|
||||||
|
if (Math.abs(value - current_min) < Math.abs(value - current_max)){
|
||||||
|
date_range.min_handle.attr('cx', value);
|
||||||
|
current_min = value;
|
||||||
|
} else {
|
||||||
|
date_range.max_handle.attr('cx', value);
|
||||||
|
current_max = value;
|
||||||
|
}
|
||||||
|
} else if (value >= current_max){
|
||||||
|
date_range.max_handle.attr('cx', value);
|
||||||
|
current_max = value;
|
||||||
|
if (d3.event.sourceEvent && date_range.maxDelta){
|
||||||
|
var new_current_min = date_range.maxDelta(date, date_range.x_scale.invert(current_min));
|
||||||
|
if (new_current_min) date_range.min_handle.attr('cx', date_range.x_scale(new_current_min));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
date_range.min_handle.attr('cx', value);
|
||||||
|
current_min = value;
|
||||||
|
if (d3.event.sourceEvent && date_range.maxDelta){
|
||||||
|
var new_current_max = date_range.maxDelta(date, date_range.x_scale.invert(current_max));
|
||||||
|
if (new_current_max) date_range.max_handle.attr('cx', date_range.x_scale(new_current_max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (d3.event.sourceEvent && date_range.onRangeUpdated) {
|
||||||
|
date_range.onRangeUpdated(date_range.x_scale.invert(current_min), date_range.x_scale.invert(current_max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateRange;
|
||||||
134
client/dashboard/energy/energy.js
Normal file
134
client/dashboard/energy/energy.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import energyRt from './energy.rt.js';
|
||||||
|
import House from './../../models/house';
|
||||||
|
import CalendarGridChart from './../../d3/grid/calendar_grid';
|
||||||
|
|
||||||
|
var Energy = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function(){
|
||||||
|
var energy = this;
|
||||||
|
return {
|
||||||
|
graph_attr: 'production',
|
||||||
|
loading_data: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: function(e) {
|
||||||
|
this.setState({windowWidth: window.innerWidth});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// window.addEventListener('resize', this.handleResize);
|
||||||
|
var energy = this,
|
||||||
|
house = energy.props.house;
|
||||||
|
energy.graph_title = 'Daily Consumption';
|
||||||
|
house.setEnergyData().then(()=>{
|
||||||
|
energy.setState({loading_data: false});
|
||||||
|
if (energy.props.view === 'graph') energy.initGraph();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function(){
|
||||||
|
var energy = this;
|
||||||
|
energy.destroyGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(new_props){
|
||||||
|
var energy = this;
|
||||||
|
if (new_props.house !== energy.props.house){
|
||||||
|
energy.setState({loading_data: true});
|
||||||
|
new_props.house.setEnergyData().then(()=>{
|
||||||
|
energy.setState({loading_data: false});
|
||||||
|
if (energy.props.view === 'graph') energy.initGraph();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (new_props.view !== 'graph' && energy.props.view === 'graph') energy.destroyGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function(prev_props, _prev_state){
|
||||||
|
var energy = this,
|
||||||
|
house = energy.props.house;
|
||||||
|
if (prev_props.view !== 'graph' && energy.props.view === 'graph') energy.initGraph();
|
||||||
|
if (prev_props.year !== energy.props.year){
|
||||||
|
energy.updateCurrentMonth();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setGraphAttr: function(event){
|
||||||
|
var energy = this,
|
||||||
|
graph_attr = event.target.dataset.value;
|
||||||
|
if (graph_attr !== energy.state.graph_attr){
|
||||||
|
energy.graph_title = 'Daily ' + event.target.innerText;
|
||||||
|
energy.setState({
|
||||||
|
graph_attr: graph_attr
|
||||||
|
}, function(){
|
||||||
|
if (energy.props.view === 'graph') energy.updateGraph();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initGraph: function(){
|
||||||
|
var energy = this;
|
||||||
|
if (energy.graph === undefined){
|
||||||
|
energy.graph = new CalendarGridChart({
|
||||||
|
container: '#energy_graph',
|
||||||
|
outer_width: 800,
|
||||||
|
outer_height: 300,
|
||||||
|
date_attr: 'day',
|
||||||
|
color: '#0404B4',
|
||||||
|
toDate: (energy_datum)=>{ return energy_datum.day_to_date; }
|
||||||
|
});
|
||||||
|
jQuery('#energy_graph').tooltip({
|
||||||
|
selector: '.d3-chart-grid-unit',
|
||||||
|
container: 'body',
|
||||||
|
title: function(){
|
||||||
|
var energy_datum = this.__data__,
|
||||||
|
date_s = d3.time.format('%a %b %d, %Y')(energy_datum.day_to_date),
|
||||||
|
range_value = `${Math.round(energy_datum.data[energy.state.graph_attr])} kWh`;
|
||||||
|
return `${date_s}: ${range_value}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
energy.updateGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateGraph: function(){
|
||||||
|
var energy = this,
|
||||||
|
house = energy.props.house;
|
||||||
|
energy.graph.rangeValue = (datum)=>{ return datum.data[energy.state.graph_attr]; }
|
||||||
|
energy.graph.drawData({
|
||||||
|
title: energy.graph_title,
|
||||||
|
css_class: '',
|
||||||
|
min_range: 0,
|
||||||
|
max_range: 150,
|
||||||
|
values: house.energy_data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyGraph: function(){
|
||||||
|
var energy = this;
|
||||||
|
document.getElementById('energy_graph').innerHTML = '';
|
||||||
|
energy.graph = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCurrentMonth: function(){
|
||||||
|
var energy = this,
|
||||||
|
house = energy.props.house;
|
||||||
|
house.setEnergyData()
|
||||||
|
.then(()=>{
|
||||||
|
if (energy.props.view === 'graph'){
|
||||||
|
// no update necessary since year already updated in layout.rt.
|
||||||
|
energy.updateGraph();
|
||||||
|
} else {
|
||||||
|
// force update to render correct data in table.
|
||||||
|
energy.forceUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return energyRt.call(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Energy;
|
||||||
39
client/dashboard/energy/energy.rt
Normal file
39
client/dashboard/energy/energy.rt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<div id="energy_view">
|
||||||
|
<div class="alert alert-warning" rt-if="this.state.loading_data">
|
||||||
|
Retrieving energy data for the {this.props.house.name} household...
|
||||||
|
</div>
|
||||||
|
<div rt-if="this.props.view === 'graph'">
|
||||||
|
<h4>Select Data</h4>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button
|
||||||
|
data-value="consumption"
|
||||||
|
rt-class="{active: this.state.graph_attr === 'consumption'}"
|
||||||
|
onClick="{this.setGraphAttr}"
|
||||||
|
type="button" class="btn btn-primary">Consumption</button>
|
||||||
|
<button
|
||||||
|
data-value="production"
|
||||||
|
rt-class="{active: this.state.graph_attr === 'production'}"
|
||||||
|
onClick="{this.setGraphAttr}"
|
||||||
|
type="button" class="btn btn-primary">Production</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="table" rt-if="this.props.view === 'table'">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Day</th>
|
||||||
|
<th>Consumption (kWh)</th>
|
||||||
|
<th>Production (kWh)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr rt-repeat="energy_datum in this.props.house.energy_data" key="{energy_datum.scoped_id}">
|
||||||
|
<td></td>
|
||||||
|
<td>{energy_datum.day_to_s}</td>
|
||||||
|
<td>{energy_datum.consumption_to_s}</td>
|
||||||
|
<td>{energy_datum.production_to_s}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div rt-if="this.props.view === 'graph'" id="energy_graph"></div>
|
||||||
|
</div>
|
||||||
25
client/dashboard/energy/energy.rt.js
Normal file
25
client/dashboard/energy/energy.rt.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
function repeatEnergy_datum1(energy_datum, energy_datumIndex) {
|
||||||
|
return React.createElement('tr', { 'key': energy_datum.scoped_id }, React.createElement('td', {}), React.createElement('td', {}, energy_datum.day_to_s), React.createElement('td', {}, energy_datum.consumption_to_s), React.createElement('td', {}, energy_datum.production_to_s));
|
||||||
|
}
|
||||||
|
export default function () {
|
||||||
|
return React.createElement('div', { 'id': 'energy_view' }, this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving energy data for the ', this.props.house.name, ' household...\n ') : null, this.props.view === 'graph' ? React.createElement('div', {}, React.createElement('h4', {}, 'Select Data'), React.createElement('div', {
|
||||||
|
'className': 'btn-group',
|
||||||
|
'role': 'group'
|
||||||
|
}, React.createElement('button', {
|
||||||
|
'data-value': 'consumption',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.graph_attr === 'consumption' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setGraphAttr,
|
||||||
|
'type': 'button'
|
||||||
|
}, 'Consumption'), React.createElement('button', {
|
||||||
|
'data-value': 'production',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.graph_attr === 'production' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setGraphAttr,
|
||||||
|
'type': 'button'
|
||||||
|
}, 'Production'))) : null, this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Day'), React.createElement('th', {}, 'Consumption (kWh)'), React.createElement('th', {}, 'Production (kWh)'))), React.createElement.apply(this, [
|
||||||
|
'tbody',
|
||||||
|
{},
|
||||||
|
_.map(this.props.house.energy_data, repeatEnergy_datum1.bind(this))
|
||||||
|
])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'energy_graph' }) : null);
|
||||||
|
};
|
||||||
0
client/dashboard/energy/energy.scss
Normal file
0
client/dashboard/energy/energy.scss
Normal file
88
client/dashboard/layout/layout.js
Normal file
88
client/dashboard/layout/layout.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import layoutRt from './layout.rt.js';
|
||||||
|
import House from './../../models/house';
|
||||||
|
import PowerDatum from './../../models/power_datum';
|
||||||
|
|
||||||
|
var Layout = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function(){
|
||||||
|
var layout = this;
|
||||||
|
return {
|
||||||
|
houses: null,
|
||||||
|
house: null,
|
||||||
|
view: 'graph',
|
||||||
|
dataset: 'power',
|
||||||
|
requesting_data: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: function(e) {
|
||||||
|
this.setState({windowWidth: window.innerWidth});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
var layout = this;
|
||||||
|
// window.addEventListener('resize', this.handleResize);
|
||||||
|
House.ensureHouses().then((houses)=>{
|
||||||
|
layout.setState({
|
||||||
|
houses: houses,
|
||||||
|
house: houses[0],
|
||||||
|
requesting_data: false,
|
||||||
|
month: houses[0].current_month,
|
||||||
|
year: houses[0].current_year
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setHouse: function(event){
|
||||||
|
var layout = this,
|
||||||
|
house_id = event.target.value,
|
||||||
|
old_house = layout.state.house,
|
||||||
|
house = layout.state.houses.find((house)=>{ return house.data.id == house_id });
|
||||||
|
layout.setState({house: house}, ()=>{
|
||||||
|
old_house.closeDb();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setView: function(event){
|
||||||
|
var layout = this,
|
||||||
|
view = event.target.dataset.value;
|
||||||
|
layout.view_name = event.target.innerText;
|
||||||
|
layout.setState({view: view});
|
||||||
|
},
|
||||||
|
|
||||||
|
setDataset: function(event){
|
||||||
|
var layout = this,
|
||||||
|
dataset = event.target.dataset.value;
|
||||||
|
layout.setState({dataset: dataset});
|
||||||
|
},
|
||||||
|
|
||||||
|
setYear: function(event){
|
||||||
|
var layout = this,
|
||||||
|
year = event.target.dataset.value,
|
||||||
|
house = layout.state.house;
|
||||||
|
if (year != house.current_year){
|
||||||
|
house.setYear(year);
|
||||||
|
layout.setState({year: year});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData: function(){
|
||||||
|
var layout = this,
|
||||||
|
houses = layout.state.houses,
|
||||||
|
all = [];
|
||||||
|
houses.forEach((house)=>{
|
||||||
|
all.push(house.clearData());
|
||||||
|
});
|
||||||
|
Promise.all(all)
|
||||||
|
.then(()=>{
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return layoutRt.call(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
60
client/dashboard/layout/layout.rt
Normal file
60
client/dashboard/layout/layout.rt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<rt-require dependency="./../energy/energy" as="Energy"/>
|
||||||
|
<rt-require dependency="./../power/power" as="Power"/>
|
||||||
|
<div id="layout">
|
||||||
|
<div class="alert alert-warning" rt-if="this.state.requesting_data">Retrieving houses...</div>
|
||||||
|
|
||||||
|
<h4>Select household:</h4>
|
||||||
|
<select rt-if="this.state.houses" class="form-control" onChange="{this.setHouse}">
|
||||||
|
<option rt-repeat="house in this.state.houses" value="{house.data.id}" key="{house.scoped_id}">{house.data.name}</option>
|
||||||
|
</select>
|
||||||
|
<button rt-if="this.state.house" onClick="{this.refreshData}" class="btn btn-xs btn-default">Refresh House Data</button>
|
||||||
|
|
||||||
|
<h4>Select dataset:</h4>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button
|
||||||
|
data-value="energy"
|
||||||
|
rt-class="{active: this.state.dataset === 'energy'}"
|
||||||
|
onClick="{this.setDataset}"
|
||||||
|
type="button" class="btn btn-primary">Daily Energy Statistics</button>
|
||||||
|
<button
|
||||||
|
data-value="power"
|
||||||
|
rt-class="{active: this.state.dataset === 'power'}"
|
||||||
|
onClick="{this.setDataset}"
|
||||||
|
type="button" class="btn btn-primary">15-minute Power Statistics</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>View as:</h4>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button
|
||||||
|
data-value="graph"
|
||||||
|
rt-class="{active: this.state.view === 'graph'}"
|
||||||
|
onClick="{this.setView}"
|
||||||
|
type="button" class="btn btn-primary">Graph</button>
|
||||||
|
<button
|
||||||
|
data-value="table"
|
||||||
|
rt-class="{active: this.state.view === 'table'}"
|
||||||
|
onClick="{this.setView}"
|
||||||
|
type="button" class="btn btn-primary">Table</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Select dates:</h4>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
rt-if="this.state.house"
|
||||||
|
rt-repeat="year in this.state.house.years"
|
||||||
|
data-value="{year}"
|
||||||
|
key="data-year-{year}"
|
||||||
|
class="btn-info btn btn-sm"
|
||||||
|
rt-class="{active: year == this.state.house.current_year}"
|
||||||
|
onClick="{this.setYear}">{year}</button>
|
||||||
|
</div><br/>
|
||||||
|
|
||||||
|
<Energy rt-if="this.state.house && this.state.dataset === 'energy'"
|
||||||
|
house="{this.state.house}"
|
||||||
|
view="{this.state.view}"
|
||||||
|
year="{this.state.year}"></Energy>
|
||||||
|
<Power rt-if="this.state.house && this.state.dataset === 'power'"
|
||||||
|
house="{this.state.house}"
|
||||||
|
view="{this.state.view}"
|
||||||
|
year="{this.state.year}"></Power>
|
||||||
|
</div>
|
||||||
69
client/dashboard/layout/layout.rt.js
Normal file
69
client/dashboard/layout/layout.rt.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import Energy from './../energy/energy';
|
||||||
|
import Power from './../power/power';
|
||||||
|
function repeatHouse1(house, houseIndex) {
|
||||||
|
return React.createElement('option', {
|
||||||
|
'value': house.data.id,
|
||||||
|
'key': house.scoped_id
|
||||||
|
}, house.data.name);
|
||||||
|
}
|
||||||
|
function repeatYear2(year, yearIndex) {
|
||||||
|
return React.createElement('button', {
|
||||||
|
'data-value': year,
|
||||||
|
'key': 'data-year-' + year,
|
||||||
|
'className': 'btn-info btn btn-sm' + ' ' + _.keys(_.pick({ active: year == this.state.house.current_year }, _.identity)).join(' '),
|
||||||
|
'onClick': this.setYear
|
||||||
|
}, year);
|
||||||
|
}
|
||||||
|
export default function () {
|
||||||
|
return React.createElement('div', { 'id': 'layout' }, this.state.requesting_data ? React.createElement('div', { 'className': 'alert alert-warning' }, 'Retrieving houses...') : null, React.createElement('h4', {}, 'Select household:'), this.state.houses ? React.createElement.apply(this, [
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
'className': 'form-control',
|
||||||
|
'onChange': this.setHouse
|
||||||
|
},
|
||||||
|
_.map(this.state.houses, repeatHouse1.bind(this))
|
||||||
|
]) : null, this.state.house ? React.createElement('button', {
|
||||||
|
'onClick': this.refreshData,
|
||||||
|
'className': 'btn btn-xs btn-default'
|
||||||
|
}, 'Refresh House Data') : null, React.createElement('h4', {}, 'Select dataset:'), React.createElement('div', {
|
||||||
|
'className': 'btn-group',
|
||||||
|
'role': 'group'
|
||||||
|
}, React.createElement('button', {
|
||||||
|
'data-value': 'energy',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.dataset === 'energy' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setDataset,
|
||||||
|
'type': 'button'
|
||||||
|
}, 'Daily Energy Statistics'), React.createElement('button', {
|
||||||
|
'data-value': 'power',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.dataset === 'power' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setDataset,
|
||||||
|
'type': 'button'
|
||||||
|
}, '15-minute Power Statistics')), React.createElement('h4', {}, 'View as:'), React.createElement('div', {
|
||||||
|
'className': 'btn-group',
|
||||||
|
'role': 'group'
|
||||||
|
}, React.createElement('button', {
|
||||||
|
'data-value': 'graph',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.view === 'graph' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setView,
|
||||||
|
'type': 'button'
|
||||||
|
}, 'Graph'), React.createElement('button', {
|
||||||
|
'data-value': 'table',
|
||||||
|
'className': _.keys(_.pick({ active: this.state.view === 'table' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
|
||||||
|
'onClick': this.setView,
|
||||||
|
'type': 'button'
|
||||||
|
}, 'Table')), React.createElement('h4', {}, 'Select dates:'), React.createElement.apply(this, [
|
||||||
|
'div',
|
||||||
|
{ 'className': 'btn-group' },
|
||||||
|
this.state.house ? _.map(this.state.house.years, repeatYear2.bind(this)) : null
|
||||||
|
]), React.createElement('br', {}), this.state.house && this.state.dataset === 'energy' ? React.createElement(Energy, {
|
||||||
|
'house': this.state.house,
|
||||||
|
'view': this.state.view,
|
||||||
|
'year': this.state.year
|
||||||
|
}) : null, this.state.house && this.state.dataset === 'power' ? React.createElement(Power, {
|
||||||
|
'house': this.state.house,
|
||||||
|
'view': this.state.view,
|
||||||
|
'year': this.state.year
|
||||||
|
}) : null);
|
||||||
|
};
|
||||||
9
client/dashboard/layout/layout.scss
Normal file
9
client/dashboard/layout/layout.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#layout {
|
||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// needless comment
|
||||||
|
#yada {
|
||||||
|
div { padding: 200px; }
|
||||||
|
}
|
||||||
199
client/dashboard/power/power.js
Normal file
199
client/dashboard/power/power.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import powerRt from './power.rt.js';
|
||||||
|
import House from './../../models/house';
|
||||||
|
import SplineStackChart from './../../d3/line/spline_stack';
|
||||||
|
import DateRangeSlider from './../../d3/sliders/date_range';
|
||||||
|
|
||||||
|
var Power = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function(){
|
||||||
|
var power = this;
|
||||||
|
return {
|
||||||
|
loading_data: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: function(e) {
|
||||||
|
this.setState({windowWidth: window.innerWidth});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// window.addEventListener('resize', this.handleResize);
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house;
|
||||||
|
power.graph_title = '';
|
||||||
|
power.initDateRange();
|
||||||
|
house.setPowerData().then(()=>{
|
||||||
|
power.setState({loading_data: false});
|
||||||
|
if (power.props.view === 'graph'){
|
||||||
|
power.initGraph();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function(){
|
||||||
|
var power = this;
|
||||||
|
power.destroyGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(new_props){
|
||||||
|
var power = this;
|
||||||
|
if (new_props.house !== power.props.house){
|
||||||
|
// house will change.
|
||||||
|
power.setState({loading_data: true});
|
||||||
|
new_props.house.setPowerData().then(()=>{
|
||||||
|
power.setState({loading_data: false});
|
||||||
|
if (power.props.view === 'graph'){
|
||||||
|
power.initGraph();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// view will change from graph to table.
|
||||||
|
if (new_props.view !== 'graph' && power.props.view === 'graph') power.destroyGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function(prev_props, _prev_state){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house;
|
||||||
|
// view has changed from graph to table.
|
||||||
|
if (prev_props.view !== 'graph' && power.props.view === 'graph'){
|
||||||
|
power.initGraph();
|
||||||
|
}
|
||||||
|
if (prev_props.house !== house) power.initDateRange();
|
||||||
|
var need_update = false;
|
||||||
|
if (prev_props.year !== power.props.year){
|
||||||
|
power.updateCurrentMonth();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initGraph: function(){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house;
|
||||||
|
if (power.graph === undefined){
|
||||||
|
power.graph = new SplineStackChart({
|
||||||
|
container: '#power_graph',
|
||||||
|
outer_width: 800,
|
||||||
|
outer_height: 200,
|
||||||
|
color: '#0404B4',
|
||||||
|
time_series: true,
|
||||||
|
domain_attr: 'x',
|
||||||
|
range_attr: 'y',
|
||||||
|
include_dots: true,
|
||||||
|
titleizeDatum: (series, d)=>{
|
||||||
|
return series.title + '<br/>' + Math.round(d.y) + ' W<br/>' + house.formatDate(d.power_datum.data.time, 'MMM D [at] HH:mm');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jQuery('#power_graph').tooltip({
|
||||||
|
selector: 'circle',
|
||||||
|
container: 'body',
|
||||||
|
html: true,
|
||||||
|
title: function(){
|
||||||
|
return this.__data__.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
power.updateGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateGraph: function(){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house,
|
||||||
|
net_power = {
|
||||||
|
title: 'Net Power Consumption',
|
||||||
|
values: house.power_data.map((power_datum)=>{
|
||||||
|
return {
|
||||||
|
power_datum: power_datum,
|
||||||
|
x: power_datum.time_to_date,
|
||||||
|
y: Math.max(0, power_datum.data.consumption - power_datum.data.production) }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
savings = {
|
||||||
|
title: 'Power Production',
|
||||||
|
values: house.power_data.map((power_datum)=>{
|
||||||
|
return {
|
||||||
|
power_datum: power_datum,
|
||||||
|
x: power_datum.time_to_date,
|
||||||
|
y: power_datum.data.production }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
power.graph.drawData({
|
||||||
|
title: power.graph_title,
|
||||||
|
css_class: '',
|
||||||
|
series: [net_power, savings]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initDateRange: function(){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house;
|
||||||
|
if (power.date_range_slider === undefined){
|
||||||
|
power.date_range_slider = new DateRangeSlider({
|
||||||
|
container: '#power_date_setter',
|
||||||
|
outer_height: 100,
|
||||||
|
maxDelta: function(changed_date, other_date){
|
||||||
|
if (Math.abs(changed_date.getTime() - other_date.getTime()) > 3600 * 24 * 4 * 1000){
|
||||||
|
if (changed_date > other_date){
|
||||||
|
return new Date(changed_date.getTime() - 3600 * 24 * 4 * 1000);
|
||||||
|
} else {
|
||||||
|
return new Date(changed_date.getTime() + 3600 * 24 * 4 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
power.date_range_slider.onRangeUpdated = (min, max)=>{
|
||||||
|
if (power.date_range_update) clearTimeout(power.date_range_update)
|
||||||
|
power.date_range_update = setTimeout(()=>{
|
||||||
|
house.power_date_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)]
|
||||||
|
house.setPowerData()
|
||||||
|
.then(()=>{
|
||||||
|
if (power.props.view === 'graph') power.updateGraph();
|
||||||
|
else power.forceUpdate();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
power.date_range_slider.drawData({
|
||||||
|
abs_min: house.current_month_moment.toDate(),
|
||||||
|
abs_max: house.end_of_current_data_moment.toDate(),
|
||||||
|
current_min: house.toDate(house.power_date_range[0]),
|
||||||
|
current_max: house.toDate(house.power_date_range[1])
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyGraph: function(){
|
||||||
|
var power = this;
|
||||||
|
document.getElementById('power_graph').innerHTML = '';
|
||||||
|
power.graph = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
setMonth: function(event){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house,
|
||||||
|
month = event.target.dataset.value;
|
||||||
|
if (month !== house.current_month){
|
||||||
|
var need_update = house.setMonth(month);
|
||||||
|
if (need_update) power.updateCurrentMonth();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCurrentMonth: function(){
|
||||||
|
var power = this,
|
||||||
|
house = power.props.house;
|
||||||
|
power.initDateRange();
|
||||||
|
house.setPowerData()
|
||||||
|
.then(()=>{
|
||||||
|
power.forceUpdate();
|
||||||
|
if (power.props.view === 'graph') power.updateGraph();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return powerRt.call(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Power;
|
||||||
35
client/dashboard/power/power.rt
Normal file
35
client/dashboard/power/power.rt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<div id="power_view">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
rt-if="this.props.house"
|
||||||
|
rt-repeat="month in this.props.house.availableMonths()"
|
||||||
|
data-value="{month}"
|
||||||
|
key="data-month-{month}"
|
||||||
|
class="btn-warning btn btn-sm"
|
||||||
|
rt-class="{active: month === this.props.house.current_month}"
|
||||||
|
onClick="{this.setMonth}">{month}</button>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning" rt-if="this.state.loading_data">
|
||||||
|
Retrieving power data for the {this.props.house.name} household...
|
||||||
|
</div>
|
||||||
|
<div id="power_date_setter"></div>
|
||||||
|
<table rt-if="this.props.view === 'table'" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Consumption (W)</th>
|
||||||
|
<th>Production (W)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr rt-repeat="power_datum in this.props.house.power_data" key="{power_datum.scoped_id}">
|
||||||
|
<td>{power_datum.data.id}</td>
|
||||||
|
<td>{power_datum.time_to_s}</td>
|
||||||
|
<td>{power_datum.consumption_to_s}</td>
|
||||||
|
<td>{power_datum.production_to_s}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div rt-if="this.props.view === 'graph'" id="power_graph"></div>
|
||||||
|
</div>
|
||||||
24
client/dashboard/power/power.rt.js
Normal file
24
client/dashboard/power/power.rt.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
function repeatMonth1(month, monthIndex) {
|
||||||
|
return React.createElement('button', {
|
||||||
|
'data-value': month,
|
||||||
|
'key': 'data-month-' + month,
|
||||||
|
'className': 'btn-warning btn btn-sm' + ' ' + _.keys(_.pick({ active: month === this.props.house.current_month }, _.identity)).join(' '),
|
||||||
|
'onClick': this.setMonth
|
||||||
|
}, month);
|
||||||
|
}
|
||||||
|
function repeatPower_datum2(power_datum, power_datumIndex) {
|
||||||
|
return React.createElement('tr', { 'key': power_datum.scoped_id }, React.createElement('td', {}, power_datum.data.id), React.createElement('td', {}, power_datum.time_to_s), React.createElement('td', {}, power_datum.consumption_to_s), React.createElement('td', {}, power_datum.production_to_s));
|
||||||
|
}
|
||||||
|
export default function () {
|
||||||
|
return React.createElement('div', { 'id': 'power_view' }, React.createElement.apply(this, [
|
||||||
|
'div',
|
||||||
|
{ 'className': 'btn-group' },
|
||||||
|
this.props.house ? _.map(this.props.house.availableMonths(), repeatMonth1.bind(this)) : null
|
||||||
|
]), this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data for the ', this.props.house.name, ' household...\n ') : null, React.createElement('div', { 'id': 'power_date_setter' }), this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Time'), React.createElement('th', {}, 'Consumption (W)'), React.createElement('th', {}, 'Production (W)'))), React.createElement.apply(this, [
|
||||||
|
'tbody',
|
||||||
|
{},
|
||||||
|
_.map(this.props.house.power_data, repeatPower_datum2.bind(this))
|
||||||
|
])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'power_graph' }) : null);
|
||||||
|
};
|
||||||
70
client/lib/databasable.js
Normal file
70
client/lib/databasable.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Loki from 'lokijs/src/lokijs';
|
||||||
|
import LokiIndexedAdapter from 'lokijs/src/loki-indexed-adapter';
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
autosave: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var databasable = {
|
||||||
|
|
||||||
|
accessDb: function(db_name, opts){
|
||||||
|
var databasable = this;
|
||||||
|
opts = Object.assign(Object.assign({
|
||||||
|
adapter: new LokiIndexedAdapter(db_name)
|
||||||
|
}, DEFAULTS), opts || {});
|
||||||
|
return new Promise((fnResolve, fnReject)=>{
|
||||||
|
if (!databasable.db) {
|
||||||
|
databasable.db = new Loki(db_name, opts);
|
||||||
|
databasable.db.loadDatabase({}, ()=>{
|
||||||
|
fnResolve(databasable.db);
|
||||||
|
});
|
||||||
|
} else { fnResolve(databasable.db); }
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDb: function(){
|
||||||
|
var databasable = this;
|
||||||
|
if (databasable.db){
|
||||||
|
databasable.db.save();
|
||||||
|
databasable.db.close();
|
||||||
|
databasable.db = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
collection: function(db_name, collection_name, options){
|
||||||
|
var databasable = this;
|
||||||
|
return databasable.accessDb(db_name)
|
||||||
|
.then((db)=>{
|
||||||
|
var collection = db.getCollection(collection_name)
|
||||||
|
if (!collection){
|
||||||
|
collection = db.addCollection(collection_name, options);
|
||||||
|
if (options && options.unique_indices){
|
||||||
|
options.unique_indices.forEach((field)=>{
|
||||||
|
collection.ensureUniqueIndex(field);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
rangeToLokiParams: function(attr, range){
|
||||||
|
var date_params = {};
|
||||||
|
if (range[0] !== undefined && range[0] !== undefined){
|
||||||
|
var start_condition = {},
|
||||||
|
end_condition = {};
|
||||||
|
date_params['$and'] = [start_condition, end_condition];
|
||||||
|
start_condition[attr] = {'$gte': range[0]};
|
||||||
|
end_condition[attr] = {'$lte': range[1]};
|
||||||
|
} else if (range[0] !== undefined) {
|
||||||
|
date_params[attr] = {'$gte': range[0]}
|
||||||
|
} else if (range[1] !== undefined) {
|
||||||
|
date_params[attr] = {'$lte': range[1]}
|
||||||
|
}
|
||||||
|
return date_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default databasable;
|
||||||
9
client/lib/model.js
Normal file
9
client/lib/model.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Loki from 'lokijs';
|
||||||
|
|
||||||
|
|
||||||
|
class Model {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
47
client/models/energy_datum.js
Normal file
47
client/models/energy_datum.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
const NAME = 'EnergyDatum';
|
||||||
|
const COLLECTION_DEFAULTS = {
|
||||||
|
indices: ['day']
|
||||||
|
};
|
||||||
|
|
||||||
|
class EnergyDatum {
|
||||||
|
constructor(data, house){
|
||||||
|
var energy_datum = this;
|
||||||
|
energy_datum.house = house;
|
||||||
|
energy_datum.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get react_key(){
|
||||||
|
return `energy-datum-${this.data.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a datestamp that has the client timezone, but actually is house local time.
|
||||||
|
get day_to_date(){
|
||||||
|
var energy_datum = this,
|
||||||
|
house = energy_datum.house;
|
||||||
|
return house.toDate(energy_datum.data.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
get day_to_s(){
|
||||||
|
var energy_datum = this;
|
||||||
|
return moment.tz(energy_datum.data.day * 1000, energy_datum.house.data.timezone).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
get consumption_to_s(){
|
||||||
|
var energy_datum = this;
|
||||||
|
return Math.round(energy_datum.data.consumption);
|
||||||
|
}
|
||||||
|
|
||||||
|
get production_to_s(){
|
||||||
|
var energy_datum = this;
|
||||||
|
return Math.round(energy_datum.data.production);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyDatum.NAME = NAME;
|
||||||
|
EnergyDatum.COLLECTION_DEFAULTS = COLLECTION_DEFAULTS;
|
||||||
|
|
||||||
|
export default EnergyDatum;
|
||||||
261
client/models/house.js
Normal file
261
client/models/house.js
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
import Loki from 'lokijs/src/lokijs';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
import PowerDatum from './power_datum';
|
||||||
|
import EnergyDatum from './energy_datum';
|
||||||
|
import PowerDataApi from './../api/power_data';
|
||||||
|
import EnergyDataApi from './../api/energy_data';
|
||||||
|
import HousesApi from './../api/houses';
|
||||||
|
import ArrayUtil from './../../shared/utils/array';
|
||||||
|
import MathUtil from './../../shared/utils/math';
|
||||||
|
import DateRange from './../../shared/utils/date_range';
|
||||||
|
import Databasable from './../lib/databasable';
|
||||||
|
|
||||||
|
const NAME = 'House';
|
||||||
|
|
||||||
|
class House {
|
||||||
|
|
||||||
|
// must be initiated with a dataset already in Loki database (not directly JSON).
|
||||||
|
constructor(data){
|
||||||
|
var house = this;
|
||||||
|
house.data = data;
|
||||||
|
Object.assign(house, Databasable);
|
||||||
|
|
||||||
|
var n_years = house.data_until_moment.year() - house.data_from_moment.year() + 1;
|
||||||
|
house.years = [];
|
||||||
|
for (var year=house.data_from_moment.year(); year<=house.data_until_moment.year(); year+=1){
|
||||||
|
house.years.push(year);
|
||||||
|
}
|
||||||
|
house.current_month = house.data_until_moment.format('MMM');
|
||||||
|
house.current_year = house.data_until_moment.year();
|
||||||
|
house.setCurrentMonthMoment();
|
||||||
|
}
|
||||||
|
|
||||||
|
get data_from_moment(){
|
||||||
|
var house = this;
|
||||||
|
return moment.tz(house.data.data_from * 1000, house.data.timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
get data_until_moment(){
|
||||||
|
var house = this;
|
||||||
|
return moment.tz(house.data.data_until * 1000, house.data.timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
get end_of_current_data_moment(){
|
||||||
|
var house = this,
|
||||||
|
end_of_month = house.current_month_moment.clone().endOf('month');
|
||||||
|
return end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scoped_id(){
|
||||||
|
return `house-${this.data.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
availableMonths(){
|
||||||
|
var house = this,
|
||||||
|
all_months = moment.monthsShort(),
|
||||||
|
year = house.current_year.toString();
|
||||||
|
if ((year) === house.data_from_moment.format('YYYY')){
|
||||||
|
return all_months.slice(house.data_from_moment.month(), 12);
|
||||||
|
} else if (year === house.data_until_moment.format('YYYY')){
|
||||||
|
return all_months.slice(0, house.data_until_moment.month() + 1);
|
||||||
|
} else {
|
||||||
|
return all_months;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setYear(year){
|
||||||
|
var house = this;
|
||||||
|
house.current_year = year;
|
||||||
|
return house.setCurrentMonthMoment();
|
||||||
|
}
|
||||||
|
|
||||||
|
setMonth(month){
|
||||||
|
var house = this;
|
||||||
|
house.current_month = month;
|
||||||
|
return house.setCurrentMonthMoment();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentMonthMoment(){
|
||||||
|
var house = this,
|
||||||
|
month_i = moment.monthsShort().indexOf(house.current_month),
|
||||||
|
new_month_moment = moment.tz({year: house.current_year, month: month_i, day: 1}, house.data.timezone).startOf('month');
|
||||||
|
if (!house.current_month_moment || new_month_moment.unix() !== house.current_month_moment.unix()){
|
||||||
|
house.current_month_moment = new_month_moment;
|
||||||
|
house.power_date_range = [house.end_of_current_data_moment.clone().subtract(4, 'days').unix(), house.end_of_current_data_moment.unix()];
|
||||||
|
house.energy_date_range = [house.end_of_current_data_moment.clone().startOf('year').unix(), house.end_of_current_data_moment.clone().endOf('year').unix()]
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset_diff(unix){
|
||||||
|
var house = this,
|
||||||
|
tz = moment.tz.zone(house.data.timezone);
|
||||||
|
return (new Date().getTimezoneOffset() - tz.offset(unix * 1000)) * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDate(unix){
|
||||||
|
var house = this;
|
||||||
|
return new Date((unix + house.offset_diff(unix)) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(unix, format){
|
||||||
|
var house = this;
|
||||||
|
return moment.tz(unix * 1000, house.data.timezone).format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
save(){
|
||||||
|
var house = this;
|
||||||
|
return House.collection(House.NAME, House.NAME)
|
||||||
|
.then((house_collection)=>{
|
||||||
|
house_collection.update(house.data);
|
||||||
|
return House.db.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPowerData(){
|
||||||
|
var house = this;
|
||||||
|
return house.collection(house.scoped_id, PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS)
|
||||||
|
.then((power_collection)=>{
|
||||||
|
return house.ensurePowerData()
|
||||||
|
.then(()=>{
|
||||||
|
var params = house.rangeToLokiParams('time', house.power_date_range);
|
||||||
|
house.power_data = power_collection.find(params)
|
||||||
|
.sort((pd1, pd2)=>{
|
||||||
|
if (pd1.time === pd2.time) return 0;
|
||||||
|
if (pd1.time > pd2.time) return 1;
|
||||||
|
if (pd1.time < pd2.time) return -1;
|
||||||
|
})
|
||||||
|
.map((data)=>{ return new PowerDatum(data, house); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensurePowerData(){
|
||||||
|
var house = this,
|
||||||
|
query_ranges;
|
||||||
|
|
||||||
|
query_ranges = DateRange.addRange(house.power_date_range, house.data.power_datum_ranges || []);
|
||||||
|
if (query_ranges.gaps_filled.length > 0){
|
||||||
|
var params = {dates: query_ranges.gaps_filled};
|
||||||
|
return house.getPowerData(params)
|
||||||
|
.then(()=>{
|
||||||
|
house.data.power_datum_ranges = query_ranges.new_ranges;
|
||||||
|
house.save();
|
||||||
|
});
|
||||||
|
} else { return Promise.resolve(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
getPowerData(params){
|
||||||
|
var house = this;
|
||||||
|
params.house_id = house.data.id;
|
||||||
|
return house.collection(house.scoped_id, PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS)
|
||||||
|
.then((power_collection)=>{
|
||||||
|
return PowerDataApi.index(params)
|
||||||
|
.then((power_data)=>{
|
||||||
|
power_collection.insert(power_data);
|
||||||
|
house.db.save();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnergyData(){
|
||||||
|
var house = this;
|
||||||
|
return house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS)
|
||||||
|
.then((energy_collection)=>{
|
||||||
|
return house.ensureEnergyData()
|
||||||
|
.then(()=>{
|
||||||
|
var params = house.rangeToLokiParams('day', house.energy_date_range);
|
||||||
|
house.energy_data = energy_collection.find(params)
|
||||||
|
.sort((pd1, pd2)=>{
|
||||||
|
if (pd1.day === pd2.day) return 0;
|
||||||
|
if (pd1.day > pd2.day) return 1;
|
||||||
|
if (pd1.day < pd2.day) return -1;
|
||||||
|
})
|
||||||
|
.map((data)=>{ return new EnergyDatum(data, house); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureEnergyData(){
|
||||||
|
var house = this,
|
||||||
|
query_ranges = DateRange.addRange(house.energy_date_range, house.data.energy_datum_ranges || []);
|
||||||
|
if (query_ranges.gaps_filled.length > 0){
|
||||||
|
return house.getEnergyData({dates: query_ranges.gaps_filled})
|
||||||
|
.then(()=>{
|
||||||
|
house.data.energy_datum_ranges = query_ranges.new_ranges;
|
||||||
|
house.save();
|
||||||
|
});
|
||||||
|
} else { return Promise.resolve(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnergyData(params){
|
||||||
|
var house = this;
|
||||||
|
params.house_id = house.data.id;
|
||||||
|
return house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS)
|
||||||
|
.then((energy_collection)=>{
|
||||||
|
return EnergyDataApi.index(params)
|
||||||
|
.then((energy_data)=>{
|
||||||
|
energy_collection.insert(energy_data);
|
||||||
|
house.db.save();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes all energy and power data from LokiJs (memory and persisted) database.
|
||||||
|
clearData(){
|
||||||
|
var house = this,
|
||||||
|
all = [
|
||||||
|
new Promise((fnResolve, fnReject)=>{
|
||||||
|
house.collection(house.scoped_id, PowerDatum.NAME)
|
||||||
|
.then((power_collection)=>{
|
||||||
|
power_collection.removeWhere({});
|
||||||
|
house.db.save(fnResolve);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
new Promise((fnResolve, fnReject)=>{
|
||||||
|
house.collection(house.scoped_id, EnergyDatum.NAME)
|
||||||
|
.then((energy_collection)=>{
|
||||||
|
energy_collection.removeWhere({});
|
||||||
|
house.db.save(fnResolve);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
new Promise((fnResolve, fnReject)=>{
|
||||||
|
House.collection(House.NAME, House.NAME)
|
||||||
|
.then((house_collection)=>{
|
||||||
|
house_collection.remove(house.data);
|
||||||
|
House.db.save(fnResolve);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
]
|
||||||
|
return Promise.all(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
static ensureHouses(ids){
|
||||||
|
return House.collection(House.NAME, House.NAME)
|
||||||
|
.then((house_collection)=>{
|
||||||
|
var houses_data = ids ? house_collection.find({id: {$in: ids}}) : house_collection.find();
|
||||||
|
if (!ids && houses_data.length === 0 || ids && houses_data.length !== ids.length){
|
||||||
|
var required_ids = ids ? ArrayUtil.diff(ids, houses_data.map((data)=>{ return data.id; })) : undefined;
|
||||||
|
return HousesApi.index({id: ids})
|
||||||
|
.then((required_houses)=>{
|
||||||
|
required_houses.forEach((house_data)=>{
|
||||||
|
house_collection.insert(house_data);
|
||||||
|
});
|
||||||
|
House.db.save();
|
||||||
|
return houses_data.concat(required_houses);
|
||||||
|
});
|
||||||
|
} else { return Promise.resolve(houses_data); }
|
||||||
|
}).then((houses_data)=>{
|
||||||
|
return houses_data.map((house_data)=>{ return new House(house_data); })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
House.NAME = NAME;
|
||||||
|
|
||||||
|
Object.assign(House, Databasable);
|
||||||
|
export default House;
|
||||||
46
client/models/power_datum.js
Normal file
46
client/models/power_datum.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import extend from 'extend';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
const NAME = 'PowerDatum';
|
||||||
|
const COLLECTION_DEFAULTS = {
|
||||||
|
indices: ['time'],
|
||||||
|
unique_indices: ['time']
|
||||||
|
};
|
||||||
|
|
||||||
|
class PowerDatum {
|
||||||
|
constructor(data, house){
|
||||||
|
var power_datum = this;
|
||||||
|
power_datum.house = house;
|
||||||
|
power_datum.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get react_key(){
|
||||||
|
return `power-datum-${this.data.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get time_to_date(){
|
||||||
|
var power_datum = this,
|
||||||
|
house = power_datum.house;
|
||||||
|
return house.toDate(power_datum.data.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
get time_to_s(){
|
||||||
|
var power_datum = this,
|
||||||
|
moment_tz = moment.tz(power_datum.data.time * 1000, power_datum.house.data.timezone);
|
||||||
|
return moment_tz.format('YYYY-MM-DD HH:mm');
|
||||||
|
}
|
||||||
|
get consumption_to_s(){
|
||||||
|
var power_datum = this;
|
||||||
|
return Math.round(power_datum.data.consumption);
|
||||||
|
}
|
||||||
|
get production_to_s(){
|
||||||
|
var power_datum = this;
|
||||||
|
return Math.round(power_datum.data.production);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerDatum.NAME = NAME;
|
||||||
|
PowerDatum.COLLECTION_DEFAULTS = COLLECTION_DEFAULTS;
|
||||||
|
|
||||||
|
export default PowerDatum;
|
||||||
9
client/style.js
Normal file
9
client/style.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Vendor Stylesheets
|
||||||
|
require('bootstrap/dist/css/bootstrap.min.css');
|
||||||
|
require('font-awesome/css/font-awesome.min.css');
|
||||||
|
|
||||||
|
|
||||||
|
// Component Stylesheets
|
||||||
|
require(__dirname + '/style.scss');
|
||||||
|
require(__dirname + '/dashboard/layout/layout.scss');
|
||||||
|
require(__dirname + '/d3/chart.scss');
|
||||||
16
client/style.scss
Normal file
16
client/style.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
html, body {
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
#spike_container {
|
||||||
|
min-height: 100%;
|
||||||
|
position:relative;
|
||||||
|
padding-bottom:100px;
|
||||||
|
}
|
||||||
|
#spike_footer {
|
||||||
|
width:100%;
|
||||||
|
padding:15px;
|
||||||
|
position:absolute;
|
||||||
|
bottom:0px;
|
||||||
|
border-top:2px solid darkgrey;
|
||||||
|
background-color:#F8F8F8;
|
||||||
|
}
|
||||||
68
gulpfile.babel.js
Normal file
68
gulpfile.babel.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import gulp from 'gulp';
|
||||||
|
import yargs from 'yargs';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import gutil from 'gulp-util';
|
||||||
|
|
||||||
|
import DB from './server/config/database';
|
||||||
|
import {PowerDataSeed, HouseSeed} from './server/lib/tasks/seed_data';
|
||||||
|
import rtCompile from './server/lib/tasks/react_template_compile';
|
||||||
|
import rt_config from './server/config/react_templates';
|
||||||
|
|
||||||
|
gulp.task('generate_power_csv', function(done){
|
||||||
|
DB.sync().then(()=>{
|
||||||
|
PowerDataSeed.generateCsv(yargs.argv, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('save_power_csv', function(done){
|
||||||
|
DB.sync().then(()=>{
|
||||||
|
PowerDataSeed.saveCsv(yargs.argv, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('save_house_csv', function(done){
|
||||||
|
DB.sync().then(()=>{
|
||||||
|
HouseSeed.saveCsv(yargs.argv, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('compile_react_templates', function() {
|
||||||
|
gulp.src('./client/dashboard/**/*.rt')
|
||||||
|
.pipe(rtCompile(rt_config))
|
||||||
|
.pipe(gulp.dest('./client/dashboard'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
gulp.task('build', function(done) {
|
||||||
|
var config, env;
|
||||||
|
|
||||||
|
if (yargs.argv.production){
|
||||||
|
env = 'production';
|
||||||
|
} else if (yargs.argv.design){
|
||||||
|
env = 'design';
|
||||||
|
} else if (yargs.argv.test){
|
||||||
|
env = 'test';
|
||||||
|
} else {
|
||||||
|
throw new gutil.PluginError("webpack", "Must include '--production' or '--design' option.");
|
||||||
|
}
|
||||||
|
config = require(`${__dirname}/server/config/webpack/${env}`);
|
||||||
|
// run webpack
|
||||||
|
webpack(config, function(err, stats) {
|
||||||
|
if(err) throw new gutil.PluginError("webpack", err);
|
||||||
|
gutil.log("[webpack]", stats.toString({
|
||||||
|
// output options
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('test', function(done) {
|
||||||
|
var Jasmine = require('jasmine');
|
||||||
|
var jasmine = new Jasmine();
|
||||||
|
|
||||||
|
jasmine.loadConfigFile('test/jasmine.json');
|
||||||
|
jasmine.configureDefaultReporter({
|
||||||
|
showColors: true
|
||||||
|
});
|
||||||
|
jasmine.execute();
|
||||||
|
});
|
||||||
58
package.json
Normal file
58
package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "spike_proto",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "babel-node ./server/app.express.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "~1.12.0",
|
||||||
|
"debug": "~2.1.1",
|
||||||
|
"jade": "~1.9.2",
|
||||||
|
"morgan": "~1.5.1",
|
||||||
|
"serve-favicon": "~2.2.0",
|
||||||
|
"node-forge": "~0.6.26",
|
||||||
|
"sequelize": "~3.15.1",
|
||||||
|
"pg": "~4.4.3",
|
||||||
|
"pg-hstore": "~2.3.2",
|
||||||
|
"fast-csv": "0.6.0",
|
||||||
|
"babel-polyfill": "6.3.14",
|
||||||
|
"babel-preset-es2015": "6.3.13",
|
||||||
|
"babel-preset-react": "6.3.13",
|
||||||
|
"babel-preset-stage-0": "6.3.13",
|
||||||
|
"babel-core": "6.3.21",
|
||||||
|
"babel-loader": "6.2.0",
|
||||||
|
"express": "4.13.3",
|
||||||
|
"react": "0.14.3",
|
||||||
|
"react-dom": "0.14.3",
|
||||||
|
"webpack": "1.12.9",
|
||||||
|
"webpack-dev-server": "1.14.0",
|
||||||
|
"extract-text-webpack-plugin": "1.0.1",
|
||||||
|
"jquery": "2.2.0",
|
||||||
|
"bootstrap": "3.3.6",
|
||||||
|
"d3": "3.5.12",
|
||||||
|
"font-awesome": "4.5.0",
|
||||||
|
"raw-loader": "0.5.1",
|
||||||
|
"sass-loader": "3.1.2",
|
||||||
|
"style-loader": "^0.12.3",
|
||||||
|
"json-loader": "0.5.4",
|
||||||
|
"node-sass": "3.4.2",
|
||||||
|
"moment-timezone": "0.5.0",
|
||||||
|
"yargs": "3.32.0",
|
||||||
|
"extend": "3.0.0",
|
||||||
|
"through2": "2.0.1",
|
||||||
|
"lokijs": "1.3.11",
|
||||||
|
"babel-cli": "^6.3.17",
|
||||||
|
"babel-standalone": "6.4.4",
|
||||||
|
"gulp": "^3.9.0",
|
||||||
|
"gulp-util": "3.0.7",
|
||||||
|
"jasmine-core": "2.4.1",
|
||||||
|
"karma": "^0.13.19",
|
||||||
|
"karma-babel-preprocessor": "^6.0.1",
|
||||||
|
"karma-chrome-launcher": "^0.2.2",
|
||||||
|
"karma-jasmine": "^0.3.6",
|
||||||
|
"react-templates": "0.4.1",
|
||||||
|
"requirejs": "~2.1",
|
||||||
|
"jasmine-es6": "0.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
127
server/app.express.js
Normal file
127
server/app.express.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Serve GraphQL Backend
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import WebpackDevServer from 'webpack-dev-server';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
|
||||||
|
|
||||||
|
import DB from './config/database';
|
||||||
|
import routes from './routes';
|
||||||
|
|
||||||
|
const API_PORT = 8080;
|
||||||
|
const APP_PORT = 3000;
|
||||||
|
|
||||||
|
var api = express();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Serve API App
|
||||||
|
*/
|
||||||
|
|
||||||
|
DB.sync().then(()=>{
|
||||||
|
|
||||||
|
routes(api);
|
||||||
|
|
||||||
|
api.use(bodyParser.json());
|
||||||
|
api.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
|
||||||
|
api.listen(API_PORT, () => {
|
||||||
|
console.log(`API is now running on http://localhost:${API_PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Development Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
var config = require('./config/webpack/development'),
|
||||||
|
dev_server = new WebpackDevServer(webpack(config), {
|
||||||
|
contentBase: __dirname + '/../client/build/development',
|
||||||
|
publicPath: "/assets/",
|
||||||
|
proxy: {
|
||||||
|
'/data*': `http://localhost:${API_PORT}`,
|
||||||
|
},
|
||||||
|
stats: {colors: true}
|
||||||
|
}),
|
||||||
|
app = dev_server.app;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Serve Vendor Scripts, CSS, and Templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import favicon from 'serve-favicon';
|
||||||
|
import logger from 'morgan';
|
||||||
|
|
||||||
|
// uncomment after placing your favicon in /public
|
||||||
|
app.use(favicon(__dirname + '/public/favicon.ico'));
|
||||||
|
app.use(logger('dev'));
|
||||||
|
|
||||||
|
// serve fonts in /assets/fonts
|
||||||
|
import assets from "connect-assets";
|
||||||
|
|
||||||
|
// TODO: These routes need to match references in the bootstrap and font awesome files.
|
||||||
|
app.use("/assets/fonts", express.static("bootstrap/dist/fonts"));
|
||||||
|
app.use("/assets/fonts", express.static("font-awesome/fonts"));
|
||||||
|
// serve compiled vendor assets and application.css.
|
||||||
|
app.use(assets({
|
||||||
|
paths: ["./../node_modules"],
|
||||||
|
build: true,
|
||||||
|
buildDir: false,
|
||||||
|
//compile: false,
|
||||||
|
compress: true
|
||||||
|
}));
|
||||||
|
// serve public static files.
|
||||||
|
dev_server.app.use('/', express.static(path.resolve(__dirname, 'public')));
|
||||||
|
|
||||||
|
// view engine set up
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
app.set('view engine', 'jade');
|
||||||
|
app.get("/", (req, res, next)=>{
|
||||||
|
res.render("index");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handle Errors
|
||||||
|
*/
|
||||||
|
|
||||||
|
// catch 404 and forward to error handler
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
var err = new Error('Not Found');
|
||||||
|
err.status = 404;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// development error handler
|
||||||
|
// will print stacktrace
|
||||||
|
if (app.get('env') === 'development') {
|
||||||
|
app.use(function(err, req, res, next) {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.render('error', {
|
||||||
|
message: err.message,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// production error handler
|
||||||
|
// no stacktraces leaked to user
|
||||||
|
app.use(function(err, req, res, next) {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.render('error', {
|
||||||
|
message: err.message,
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dev_server.listen(APP_PORT, () => {
|
||||||
|
console.log(`App is now running on http://localhost:${APP_PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
17
server/config/controllers.js
Normal file
17
server/config/controllers.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const CONTROLLER_DIR = __dirname + '/../controllers';
|
||||||
|
|
||||||
|
class Controllers {
|
||||||
|
|
||||||
|
static sync(){
|
||||||
|
fs.readdirSync(CONTROLLER_DIR).forEach(function(file) {
|
||||||
|
var controller = require(CONTROLLER_DIR + '/' + file);
|
||||||
|
Controllers[controller.NAME] = controller;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controllers;
|
||||||
37
server/config/database.js
Normal file
37
server/config/database.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import Sequelize from 'sequelize';
|
||||||
|
|
||||||
|
var sequelize = new Sequelize("postgres://spikeuser:123456@localhost:5432/spike2", {
|
||||||
|
pool: {
|
||||||
|
max: 5,
|
||||||
|
min: 0,
|
||||||
|
idle: 10000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const MODEL_DIR = __dirname + '/../models'
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
|
||||||
|
static sync(){
|
||||||
|
console.log("syncing db")
|
||||||
|
fs.readdirSync(MODEL_DIR).forEach(function(file) {
|
||||||
|
var model = require(MODEL_DIR + '/' + file);
|
||||||
|
Database[model.NAME] = model;
|
||||||
|
Database.models.push(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add associations
|
||||||
|
for (var model of Database.models){
|
||||||
|
model.set();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sequelize.sync().then(()=>{ console.log("done syncing db") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Database.sequelize = sequelize;
|
||||||
|
Database.Sequelize = Sequelize;
|
||||||
|
Database.models = [];
|
||||||
|
|
||||||
|
export default Database;
|
||||||
7
server/config/react_templates.js
Normal file
7
server/config/react_templates.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
var config = {
|
||||||
|
modules: 'es6',
|
||||||
|
targetVersion: '0.14.0',
|
||||||
|
suffix: '.rt'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
server/config/webpack/design.js
Normal file
1
server/config/webpack/design.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
design.js
|
||||||
48
server/config/webpack/development.js
Normal file
48
server/config/webpack/development.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import webpack from 'webpack';
|
||||||
|
|
||||||
|
const ROOT = __dirname + '/../../../';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: ROOT + 'client/app',
|
||||||
|
style: ROOT + 'client/style'
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: ROOT + 'client/build/development'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
loaders: ['style', 'raw', 'sass']
|
||||||
|
}, {
|
||||||
|
test: /\.css$/,
|
||||||
|
loaders: ['style', 'raw']
|
||||||
|
}, {
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel'
|
||||||
|
}, {
|
||||||
|
test: /\.json$/,
|
||||||
|
loader: 'json-loader'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sassLoader: {
|
||||||
|
includePaths: [ROOT + 'client', ROOT + 'node_modules']
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
$: "jquery",
|
||||||
|
jQuery: "jquery",
|
||||||
|
"window.jQuery": "jquery"
|
||||||
|
}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
d3: "d3",
|
||||||
|
"window.d3": "d3"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
node: {
|
||||||
|
fs: "empty"
|
||||||
|
}
|
||||||
|
}
|
||||||
58
server/config/webpack/production.js
Normal file
58
server/config/webpack/production.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import ExtractTextPlugin from 'extract-text-webpack-plugin';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
|
||||||
|
const ROOT = __dirname + '/../../../';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: ROOT + 'client/app',
|
||||||
|
style: ROOT + 'client/style'
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
output: {
|
||||||
|
filename: '[name].min.js',
|
||||||
|
path: ROOT + 'client/build/production'
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
jquery: "$",
|
||||||
|
d3: "d3"
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
loader: ExtractTextPlugin.extract("style-loader", "raw-loader!sass-loader")
|
||||||
|
}, {
|
||||||
|
test: /\.css$/,
|
||||||
|
loader: ExtractTextPlugin.extract("style-loader", "raw-loader")
|
||||||
|
}, {
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel'
|
||||||
|
}, {
|
||||||
|
test: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
|
loader: "url-loader?limit=10000&minetype=application/font-woff"
|
||||||
|
}, {
|
||||||
|
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
|
loader: "file-loader"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sassLoader: {
|
||||||
|
includePaths: [ROOT + 'client', ROOT + 'node_modules']
|
||||||
|
},
|
||||||
|
// Use the plugin to specify the resulting filename (and add needed behavior to the compiler)
|
||||||
|
plugins: [
|
||||||
|
new ExtractTextPlugin("style.min.css", {
|
||||||
|
allChunks: true
|
||||||
|
}),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({minimize: true}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
$: "jquery",
|
||||||
|
jQuery: "jquery",
|
||||||
|
"window.jQuery": "jquery"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
node: {
|
||||||
|
fs: "empty"
|
||||||
|
}
|
||||||
|
};
|
||||||
16
server/controllers/energy_controller.js
Normal file
16
server/controllers/energy_controller.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import DB from './../config/database.js';
|
||||||
|
|
||||||
|
const NAME = 'EnergyController';
|
||||||
|
|
||||||
|
class EnergyController{
|
||||||
|
|
||||||
|
static index(req, res){
|
||||||
|
DB.EnergyDatum.exposeForHouseAtDates(req.query.house_id, req.query.dates).then((energy_data)=>{
|
||||||
|
res.json({data: energy_data});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyController.NAME = NAME;
|
||||||
|
module.exports = EnergyController;
|
||||||
18
server/controllers/houses_controller.js
Normal file
18
server/controllers/houses_controller.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import DB from './../config/database.js';
|
||||||
|
|
||||||
|
const NAME = 'HousesController';
|
||||||
|
|
||||||
|
class HousesController {
|
||||||
|
|
||||||
|
static index(req, res){
|
||||||
|
var params = {};
|
||||||
|
if (req.query.ids) query.id = ids;
|
||||||
|
DB.House.findAll({where: params}).then((houses)=>{
|
||||||
|
res.json({data: houses.map((house)=>{ return house.dataValues; })});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
HousesController.NAME = NAME;
|
||||||
|
module.exports = HousesController;
|
||||||
16
server/controllers/power_controller.js
Normal file
16
server/controllers/power_controller.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import DB from './../config/database.js';
|
||||||
|
|
||||||
|
const NAME = 'PowerController';
|
||||||
|
|
||||||
|
class PowerController{
|
||||||
|
|
||||||
|
static index(req, res){
|
||||||
|
DB.PowerDatum.exposeForHouseAtDates(req.query.house_id, req.query.dates).then((power_data)=>{
|
||||||
|
res.json({data: power_data});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerController.NAME = NAME;
|
||||||
|
module.exports = PowerController;
|
||||||
29
server/helpers/api_helper.js
Normal file
29
server/helpers/api_helper.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
class ApiHelper {
|
||||||
|
|
||||||
|
// assume all dates from api coming as UNIX timestamps.
|
||||||
|
static datesParamToSequelize(dates, field_name){
|
||||||
|
if (!dates) return {};
|
||||||
|
var params = {};
|
||||||
|
|
||||||
|
if (dates.length > 1){
|
||||||
|
params['$or'] = [];
|
||||||
|
dates.forEach((min_max)=>{
|
||||||
|
var condition_n = {};
|
||||||
|
condition_n[field_name] = {};
|
||||||
|
if (min_max[0]) condition_n[field_name]['$gte'] = min_max[0];
|
||||||
|
if (min_max[1]) condition_n[field_name]['$lte'] = min_max[1];
|
||||||
|
if (Object.keys(condition_n).length) params['$or'].push(condition_n);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var min_max = dates[0],
|
||||||
|
condition = {}
|
||||||
|
if (min_max[0]) condition['$gte'] = min_max[0];
|
||||||
|
if (min_max[1]) condition['$lte'] = min_max[1];
|
||||||
|
if (Object.keys(condition).length) params[field_name] = condition;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiHelper;
|
||||||
57
server/lib/tasks/react_template_compile.js
Normal file
57
server/lib/tasks/react_template_compile.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
'use strict';
|
||||||
|
// through2 is a thin wrapper around node transform streams
|
||||||
|
import through from 'through2';
|
||||||
|
import gutil from 'gulp-util';
|
||||||
|
import rt from 'react-templates';
|
||||||
|
import path from 'path';
|
||||||
|
import extend from 'extend';
|
||||||
|
|
||||||
|
// Consts
|
||||||
|
const PLUGIN_NAME = 'gulp-react-templates';
|
||||||
|
var PluginError = gutil.PluginError;
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return name.replace(/-/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (opt) {
|
||||||
|
function replaceExtension(filePath) {
|
||||||
|
return filePath + '.js';
|
||||||
|
}
|
||||||
|
|
||||||
|
function transform(file, enc, cb) {
|
||||||
|
if (file.isNull()) {
|
||||||
|
return cb(null, file);
|
||||||
|
}
|
||||||
|
if (file.isStream()) {
|
||||||
|
return cb(new PluginError(PLUGIN_NAME, 'Streaming not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath = file.path,
|
||||||
|
str = file.contents.toString('utf8'),
|
||||||
|
data;
|
||||||
|
|
||||||
|
var options = extend({
|
||||||
|
filename: file.path,
|
||||||
|
sourceFiles: [file.relative],
|
||||||
|
generatedFile: replaceExtension(file.relative)
|
||||||
|
}, opt);
|
||||||
|
|
||||||
|
if (options.suffix && !options.name) {
|
||||||
|
options.name = normalizeName(path.basename(filePath, path.extname(filePath))) + options.suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = rt.convertTemplateToReact(str, options);
|
||||||
|
} catch (err) {
|
||||||
|
return cb(new PluginError(PLUGIN_NAME, err));
|
||||||
|
}
|
||||||
|
|
||||||
|
file.contents = new Buffer(data);
|
||||||
|
|
||||||
|
file.path = replaceExtension(file.path);
|
||||||
|
cb(null, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return through.obj(transform);
|
||||||
|
};
|
||||||
103
server/lib/tasks/seed_data.js
Normal file
103
server/lib/tasks/seed_data.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import extend from "extend";
|
||||||
|
import moment from "moment";
|
||||||
|
import csv from "fast-csv";
|
||||||
|
import fs from 'fs';
|
||||||
|
import MathUtils from "./../../../shared/utils/math"
|
||||||
|
import DB from './../../config/database';
|
||||||
|
|
||||||
|
const DATA_PATH = __dirname + '/../../../shared/data/'
|
||||||
|
|
||||||
|
export class PowerDataSeed {
|
||||||
|
|
||||||
|
static saveCsv(opts, done){
|
||||||
|
opts = extend({
|
||||||
|
path: DATA_PATH + "power_data.csv"
|
||||||
|
}, opts || {});
|
||||||
|
var stream = fs.createReadStream(opts.path),
|
||||||
|
csvStream = csv.fromStream(stream, {headers: ['house_id', 'time', 'consumption', 'production']}),
|
||||||
|
rows = [];
|
||||||
|
|
||||||
|
csvStream.on("data", function(data){
|
||||||
|
data.time = data.time;
|
||||||
|
rows.push(data);
|
||||||
|
if (rows.length % 100 === 0){
|
||||||
|
DB.PowerDatum.bulkCreate(rows, {validate: true}).catch((error)=>{
|
||||||
|
console.error(JSON.stringify(error));
|
||||||
|
console.error(JSON.stringify(rows));
|
||||||
|
});
|
||||||
|
rows = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
csvStream.on("end", function(){
|
||||||
|
console.log("all rows parsed")
|
||||||
|
DB.PowerDatum.bulkCreate(rows, {validate: true}).then(()=>{
|
||||||
|
return DB.House.findAll().then((houses)=>{
|
||||||
|
var promises = [];
|
||||||
|
for (var house of houses){
|
||||||
|
var p = house.aggregatePowerToEnergyData();
|
||||||
|
promises.push(p);
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}).then(()=>{
|
||||||
|
console.log("DONE!")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateCsv(opts, done){
|
||||||
|
opts = extend({
|
||||||
|
start_date: moment().subtract(120, "months").unix(),
|
||||||
|
end_date: moment().unix(),
|
||||||
|
interval: 900, // every 15 minutes (in s)
|
||||||
|
average: 1400, // Wh
|
||||||
|
path: DATA_PATH + "power_data.csv"
|
||||||
|
}, opts || {});
|
||||||
|
console.log(opts.start_date, opts.end_date)
|
||||||
|
var row_date = opts.start_date,
|
||||||
|
csvStream = csv.format({headers: true}),
|
||||||
|
writableStream = fs.createWriteStream(opts.path),
|
||||||
|
house_ids = opts.house_ids.split(",")
|
||||||
|
|
||||||
|
DB.House.findAll({where: {id: house_ids}}).then((houses)=>{
|
||||||
|
|
||||||
|
csvStream.pipe(writableStream);
|
||||||
|
writableStream.on("finish", ()=>{
|
||||||
|
console.log("DONE!")
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
while (row_date <= opts.end_date){
|
||||||
|
for (var house of houses){
|
||||||
|
var consumption = MathUtils.normal(opts.average),
|
||||||
|
production = MathUtils.normal(opts.average) * house.productionMultiplier(row_date * 1000);
|
||||||
|
csvStream.write([house.id, row_date, consumption, production]);
|
||||||
|
}
|
||||||
|
row_date += opts.interval;
|
||||||
|
}
|
||||||
|
csvStream.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HouseSeed {
|
||||||
|
static saveCsv(opts, done){
|
||||||
|
opts = extend({
|
||||||
|
path: DATA_PATH + "houses.csv"
|
||||||
|
}, opts || {});
|
||||||
|
var stream = fs.createReadStream(opts.path),
|
||||||
|
csvStream = csv.fromStream(stream, {headers: ['id', 'name', 'timezone']}),
|
||||||
|
rows = [];
|
||||||
|
|
||||||
|
csvStream.on("data", function(data){
|
||||||
|
rows.push(data);
|
||||||
|
});
|
||||||
|
csvStream.on("end", function(){
|
||||||
|
console.log(rows);
|
||||||
|
DB.House.bulkCreate(rows, {validate: true}).then(()=>{
|
||||||
|
console.log("DONE!")
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
52
server/models/energy_datum.js
Normal file
52
server/models/energy_datum.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import DB from "./../config/database";
|
||||||
|
import extend from 'extend';
|
||||||
|
import ApiHelper from './../helpers/api_helper';
|
||||||
|
|
||||||
|
const NAME = 'EnergyDatum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define your own types here
|
||||||
|
*/
|
||||||
|
|
||||||
|
var EnergyDatum = DB.sequelize.define(NAME, {
|
||||||
|
id: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||||
|
},
|
||||||
|
day: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
},
|
||||||
|
production: DB.Sequelize.FLOAT,
|
||||||
|
consumption: DB.Sequelize.FLOAT
|
||||||
|
}, {
|
||||||
|
paranoid: true,
|
||||||
|
underscored: true,
|
||||||
|
tableName: "energy_data",
|
||||||
|
instanceMethods: {},
|
||||||
|
classMethods: {
|
||||||
|
set: ()=>{
|
||||||
|
EnergyDatum.associate();
|
||||||
|
},
|
||||||
|
associate: ()=>{
|
||||||
|
EnergyDatum.belongsTo(DB.House);
|
||||||
|
},
|
||||||
|
exposeForHouseAtDates: (house_id, dates)=>{
|
||||||
|
var params = {house_id: house_id};
|
||||||
|
extend(params, ApiHelper.datesParamToSequelize(dates, 'day'));
|
||||||
|
console.log('EnergyDatum#exposeForHouseAtDates')
|
||||||
|
console.log(params, dates)
|
||||||
|
return EnergyDatum.findAll({
|
||||||
|
where: params,
|
||||||
|
attributes: ['id', 'production', 'consumption', 'day']
|
||||||
|
}).then((energy_data)=>{
|
||||||
|
return energy_data.map((energy_datum)=>{
|
||||||
|
return energy_datum.dataValues;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EnergyDatum.NAME = NAME;
|
||||||
|
module.exports = EnergyDatum;
|
||||||
93
server/models/house.js
Normal file
93
server/models/house.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
|
import DB from "./../config/database";
|
||||||
|
|
||||||
|
const NAME = 'House';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequelize Definition
|
||||||
|
*/
|
||||||
|
|
||||||
|
var House = DB.sequelize.define(NAME, {
|
||||||
|
id: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||||
|
},
|
||||||
|
timezone: DB.Sequelize.STRING,
|
||||||
|
name: DB.Sequelize.STRING,
|
||||||
|
data_until: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
},
|
||||||
|
data_from: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
paranoid: true,
|
||||||
|
underscored: true,
|
||||||
|
tableName: "houses",
|
||||||
|
instanceMethods: {
|
||||||
|
productionMultiplier: function(timestamp){
|
||||||
|
var house = this,
|
||||||
|
minute = moment.tz(timestamp, house.timezone).hour() * 60 + moment.tz(timestamp, house.timezone).minute(),
|
||||||
|
multiplier = 0;
|
||||||
|
if (minute > 420 && minute < 1140){
|
||||||
|
multiplier = 1 - Math.abs(780 - minute) / 360;
|
||||||
|
}
|
||||||
|
return multiplier;
|
||||||
|
},
|
||||||
|
unixToLocalDay: function(unix){
|
||||||
|
var house = this;
|
||||||
|
return moment.tz(unix * 1000, house.timezone).startOf('day').unix();
|
||||||
|
},
|
||||||
|
aggregatePowerToEnergyData: function(){
|
||||||
|
var house = this;
|
||||||
|
return DB.EnergyDatum.destroy({where: {house_id: house.id}})
|
||||||
|
.then(()=>{
|
||||||
|
return DB.PowerDatum.count({where: {house_id: house.id}})
|
||||||
|
})
|
||||||
|
.then((count)=>{
|
||||||
|
var limit = 0,
|
||||||
|
energy_data = new Map(),
|
||||||
|
promises = [];
|
||||||
|
while (limit < count){
|
||||||
|
let complete = DB.PowerDatum.findAll({where: {house_id: house.id}, limit: 1000, offset: limit, order: 'id ASC'})
|
||||||
|
.then((power_data)=>{
|
||||||
|
power_data.forEach((power_datum)=>{
|
||||||
|
var day = house.unixToLocalDay(power_datum.time),
|
||||||
|
energy_datum = energy_data.get(day) || {production: 0, consumption: 0, day: day, house_id: house.id};
|
||||||
|
energy_datum.production += power_datum.production / 1000; // convert Wh to kWh
|
||||||
|
energy_datum.consumption += power_datum.consumption / 1000; // convert Wh to kWh
|
||||||
|
energy_data.set(day, energy_datum);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
promises.push(complete);
|
||||||
|
limit += 1000;
|
||||||
|
}
|
||||||
|
return Promise.all(promises).then(()=>{
|
||||||
|
return DB.EnergyDatum.bulkCreate(Array.from(energy_data.values()), {validate: true});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(()=>{
|
||||||
|
return house.getPowerData({order: 'time DESC', limit: 1});
|
||||||
|
})
|
||||||
|
.then((max_data)=>{
|
||||||
|
house.data_until = max_data[0].time;
|
||||||
|
return house.getPowerData({order: 'time ASC', limit: 1});
|
||||||
|
}).then((min_data)=>{
|
||||||
|
house.data_from = min_data[0].time;
|
||||||
|
return house.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
classMethods: {
|
||||||
|
set: ()=>{
|
||||||
|
House.associate();
|
||||||
|
},
|
||||||
|
associate: ()=>{
|
||||||
|
House.hasMany(DB.PowerDatum, {as: 'PowerData'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
House.NAME = NAME;
|
||||||
|
module.exports = House;
|
||||||
59
server/models/power_datum.js
Normal file
59
server/models/power_datum.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import DB from "./../config/database";
|
||||||
|
import extend from 'extend';
|
||||||
|
import ApiHelper from './../helpers/api_helper';
|
||||||
|
|
||||||
|
const NAME = 'PowerDatum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define your own types here
|
||||||
|
*/
|
||||||
|
|
||||||
|
var PowerDatum = DB.sequelize.define(NAME, {
|
||||||
|
id: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
type: DB.Sequelize.INTEGER,
|
||||||
|
},
|
||||||
|
consumption: DB.Sequelize.FLOAT,
|
||||||
|
production: DB.Sequelize.FLOAT
|
||||||
|
}, {
|
||||||
|
paranoid: true,
|
||||||
|
underscored: true,
|
||||||
|
tableName: "power_data",
|
||||||
|
instanceMethods: {
|
||||||
|
exposeToApi: function(){
|
||||||
|
var power_datum = this,
|
||||||
|
data = power_datum.dataValues;
|
||||||
|
data.consumption = data.consumption * 4; // convert Wh / 15 minutes, to W
|
||||||
|
data.production = data.production * 4; // convert Wh / 15 minutes, to W
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
classMethods: {
|
||||||
|
exposeForHouseAtDates: (house_id, dates)=>{
|
||||||
|
var params = {house_id: house_id};
|
||||||
|
params = extend(params, ApiHelper.datesParamToSequelize(dates, 'time'));
|
||||||
|
console.log(params);
|
||||||
|
return PowerDatum.findAll({
|
||||||
|
where: params,
|
||||||
|
attributes: ['id', 'production', 'consumption', 'time']
|
||||||
|
}).then((power_data)=>{
|
||||||
|
return power_data.map((power_datum)=>{
|
||||||
|
return power_datum.exposeToApi();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set: ()=>{
|
||||||
|
PowerDatum.associate();
|
||||||
|
},
|
||||||
|
associate: ()=>{
|
||||||
|
PowerDatum.belongsTo(DB.House);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
PowerDatum.NAME = NAME;
|
||||||
|
module.exports = PowerDatum;
|
||||||
BIN
server/public/favicon.ico
Normal file
BIN
server/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
41
server/public/index2.html
Normal file
41
server/public/index2.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="/assets/css/vendor.css" rel="stylesheet" type="text/css">
|
||||||
|
<link href="/assets/css/application.css" rel="stylesheet" type="text/css">
|
||||||
|
<title>Spike Prototype</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="spike_container">
|
||||||
|
<div id="spike_content">
|
||||||
|
<nav style="margin-bottom:0px;" class="navbar navbar-default">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button><a href="/" class="navbar-brand">Spike</a>
|
||||||
|
</div>
|
||||||
|
<div id="navbar" class="collapse navbar-collapse">
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<li><a href="/">Spike</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div id="root"></div>
|
||||||
|
</div>
|
||||||
|
<div id="spike_footer">
|
||||||
|
<div class="container">Footer</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Force `fetch` polyfill to workaround Chrome not displaying request body
|
||||||
|
// in developer tools for the native `fetch`.
|
||||||
|
self.fetch = null;
|
||||||
|
</script>
|
||||||
|
<script src="http://localhost:3000/webpack-dev-server.js"></script>
|
||||||
|
<script src="/assets/js/vendor.js"></script>
|
||||||
|
<script src="/assets/js/application.js"></script>
|
||||||
|
</html>
|
||||||
11
server/routes.js
Normal file
11
server/routes.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Controllers from './config/controllers'
|
||||||
|
|
||||||
|
export default function(app){
|
||||||
|
|
||||||
|
Controllers.sync();
|
||||||
|
|
||||||
|
app.use('/data/v1/power', Controllers.PowerController.index);
|
||||||
|
app.use('/data/v1/energy', Controllers.EnergyController.index);
|
||||||
|
app.use('/data/v1/houses', Controllers.HousesController.index);
|
||||||
|
|
||||||
|
};
|
||||||
6
server/views/error.jade
Normal file
6
server/views/error.jade
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
extends layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1= message
|
||||||
|
h2= error.status
|
||||||
|
pre #{error.stack}
|
||||||
3
server/views/index.jade
Normal file
3
server/views/index.jade
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
extends layout
|
||||||
|
block content
|
||||||
|
div(id="root")
|
||||||
34
server/views/layout.jade
Normal file
34
server/views/layout.jade
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
meta(charset='utf-8')
|
||||||
|
meta(http-equiv='content-type', content='text/html; charset=UTF-8')
|
||||||
|
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||||
|
title Spike Prototype
|
||||||
|
body
|
||||||
|
#spike_container
|
||||||
|
#spike_content
|
||||||
|
nav.navbar.navbar-default(style='margin-bottom:0px;')
|
||||||
|
.container
|
||||||
|
.navbar-header
|
||||||
|
button.navbar-toggle.collapsed(type='button', data-toggle='collapse', data-target='#navbar', aria-expanded='false', aria-controls='navbar')
|
||||||
|
span.sr-only Toggle navigation
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
a.navbar-brand(href='/') Spike
|
||||||
|
#navbar.collapse.navbar-collapse
|
||||||
|
ul.nav.navbar-nav.navbar-right
|
||||||
|
li
|
||||||
|
a(href='/') Spike
|
||||||
|
.container
|
||||||
|
block content
|
||||||
|
#spike_footer
|
||||||
|
.container Footer
|
||||||
|
script(type='text/javascript').
|
||||||
|
// Force `fetch` polyfill to workaround Chrome not displaying request body
|
||||||
|
// in developer tools for the native `fetch`.
|
||||||
|
self.fetch = null;
|
||||||
|
script(src='http://localhost:3000/webpack-dev-server.js')
|
||||||
|
script(src='/assets/style.js')
|
||||||
|
script(src='/assets/app.js')
|
||||||
10
shared/models/house.js
Normal file
10
shared/models/house.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
class House {
|
||||||
|
|
||||||
|
timeToDateString(timestamp){
|
||||||
|
var house = this;
|
||||||
|
return moment.tz(timestamp, house.timezone).format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
shared/utils/array.js
Normal file
28
shared/utils/array.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
class ArrayUtil {
|
||||||
|
|
||||||
|
static diff(a1, a2){
|
||||||
|
return a1.filter((a1n)=>{ return a2.indexOf(a1n) < 0; });
|
||||||
|
}
|
||||||
|
|
||||||
|
static selectMap(a, fnSelect, fnMap){
|
||||||
|
var map = [];
|
||||||
|
for (var elem of a){
|
||||||
|
if (fnSelect(elem)) map.push(fnMap(elem));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
static all(a, fnCondition){
|
||||||
|
var all = true;
|
||||||
|
for (var elem of a){
|
||||||
|
if (!fnCondition(elem)){
|
||||||
|
all = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArrayUtil;
|
||||||
68
shared/utils/date_range.js
Normal file
68
shared/utils/date_range.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
class DateRange {
|
||||||
|
|
||||||
|
static addRange(new_range, ranges){
|
||||||
|
var gaps_filled = [], new_ranges = [],
|
||||||
|
start = new_range[0], end = new_range[1];
|
||||||
|
if (start === undefined && end === undefined && ranges.length === 0){
|
||||||
|
gaps_filled = [undefined, undefined];
|
||||||
|
new_ranges = [[undefined, undefined]];
|
||||||
|
} else {
|
||||||
|
var covered = false,
|
||||||
|
last_start = start,
|
||||||
|
last_end = start;
|
||||||
|
|
||||||
|
ranges.forEach((range, i)=>{
|
||||||
|
if (covered){ new_ranges.push(range); return true; }
|
||||||
|
if (DateRange.lte(start, range[0])){
|
||||||
|
if (end && !DateRange.eq(end, range[0]) && DateRange.lte(end, range[0])){
|
||||||
|
new_ranges.push([last_start, end]);
|
||||||
|
new_ranges.push(range);
|
||||||
|
gaps_filled.push([last_end, end]);
|
||||||
|
covered = true;
|
||||||
|
} else if (end && !DateRange.gte(end, range[1])) {
|
||||||
|
new_ranges.push([last_start, range[1]]);
|
||||||
|
if (range[0] && !DateRange.eq(last_end, range[0])){ gaps_filled.push([last_end, range[0]]); }
|
||||||
|
covered = true
|
||||||
|
} else {
|
||||||
|
if (range[0] && !DateRange.eq(last_end, range[0])) gaps_filled.push([last_end, range[0]]);
|
||||||
|
last_end = range[1]
|
||||||
|
}
|
||||||
|
} else if (start && DateRange.gte(range[1], start)){
|
||||||
|
if (!DateRange.eq(end, range[1]) && DateRange.gte(end, range[1])){
|
||||||
|
last_start = range[0];
|
||||||
|
last_end = range[1];
|
||||||
|
} else {
|
||||||
|
new_ranges.push(range);
|
||||||
|
covered = true;
|
||||||
|
}
|
||||||
|
} else { new_ranges.push(range); }
|
||||||
|
});
|
||||||
|
if (!covered) {
|
||||||
|
new_ranges.push([last_start, end]);
|
||||||
|
if (!DateRange.eq(last_end, end)) gaps_filled.push([last_end, end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { gaps_filled: gaps_filled, new_ranges: new_ranges }
|
||||||
|
}
|
||||||
|
|
||||||
|
static gte(date1, date2){
|
||||||
|
return (date1 === undefined || (date2 !== undefined && date1 >= date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static lte(date1, date2){
|
||||||
|
return (date1 === undefined || (date2 !== undefined && date1 <= date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static eq(date1, date2){
|
||||||
|
if (date1 && date1.constructor === Date) date1 = date1.getTime();
|
||||||
|
if (date2 && date2.constructor === Date) date2 = date2.getTime();
|
||||||
|
return (date1 !== undefined && date2 !== undefined && date1 === date2) || date1 === undefined && date2 === undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
static add(date, s){
|
||||||
|
return new Date(date.getTime() + s);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export default DateRange;
|
||||||
38
shared/utils/math.js
Normal file
38
shared/utils/math.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export default class {
|
||||||
|
|
||||||
|
static normal(average){
|
||||||
|
return average + this.n6() * average;
|
||||||
|
}
|
||||||
|
|
||||||
|
static n6(){
|
||||||
|
return ((Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random()) - 3) / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// min_max1 and min_max2 arrays of length two representing mins and maxes of their ranges;
|
||||||
|
// returns array of array length two, representing mins and maxes not within min_max2.
|
||||||
|
static minusRange(min_max1, min_max2){
|
||||||
|
var minus = [];
|
||||||
|
|
||||||
|
// return undefined if min_max1 not provided
|
||||||
|
if (!min_max1 || (!min_max1[0] && !min_max2[1])) return undefined;
|
||||||
|
|
||||||
|
if (min_max1[0] >= min_max2[0]){
|
||||||
|
if (min_max1[1] > min_max2[1]) minus.push([min_max2[1], min_max1[1]]);
|
||||||
|
} else if (min_max1[1] <= min_max2[1]){
|
||||||
|
if (min_max1[0] < min_max2[0]) minus.push([min_max1[0], min_max2[0]]);
|
||||||
|
} else if (min_max1[0] < min_max2[0] && min_max1[1] > min_max2[1]){
|
||||||
|
minus.push([min_max1[0], min_max2[0]]);
|
||||||
|
minus.push([min_max2[1], min_max1[1]]);
|
||||||
|
} else {
|
||||||
|
minus.push([min_max1[0], min_max1[1]]);
|
||||||
|
}
|
||||||
|
return minus;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inRange(n, min_max){
|
||||||
|
var min = min_max[0],
|
||||||
|
max = min_max[1];
|
||||||
|
return ((n >= min) && (n <= max));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
424
spec/shared/utils/date_range.test.js
Normal file
424
spec/shared/utils/date_range.test.js
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import DateRange from './../../../shared/utils/date_range.js';
|
||||||
|
|
||||||
|
describe('DateRange.gte', ()=>{
|
||||||
|
|
||||||
|
it('considers undefined as a large date', ()=>{
|
||||||
|
var date1 = new Date(),
|
||||||
|
date2 = new Date(date1.getTime() + 1000);
|
||||||
|
expect(DateRange.gte(undefined, date1)).toEqual(true);
|
||||||
|
expect(DateRange.gte(undefined, undefined)).toEqual(true);
|
||||||
|
expect(DateRange.gte(date1, undefined)).toEqual(false);
|
||||||
|
expect(DateRange.gte(date1, date2)).toEqual(false);
|
||||||
|
expect(DateRange.gte(date2, date1)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('DateRange.lte', ()=>{
|
||||||
|
it('considers undefined as a small date', ()=>{
|
||||||
|
var date1 = new Date(),
|
||||||
|
date2 = new Date(date1.getTime() + 1000);
|
||||||
|
expect(DateRange.lte(undefined, date1)).toEqual(true);
|
||||||
|
expect(DateRange.lte(undefined, undefined)).toEqual(true);
|
||||||
|
expect(DateRange.lte(date1, undefined)).toEqual(false);
|
||||||
|
expect(DateRange.lte(date1, date2)).toEqual(true);
|
||||||
|
expect(DateRange.lte(date2, date1)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DateRange.addRange', ()=>{
|
||||||
|
var date1 = new Date(),
|
||||||
|
date01 = DateRange.add(date1, -1000),
|
||||||
|
date11 = DateRange.add(date1, 1000),
|
||||||
|
date2 = DateRange.add(date1, 2000),
|
||||||
|
date21 = DateRange.add(date2, 1000),
|
||||||
|
date3 = DateRange.add(date2, 2000),
|
||||||
|
date31 = DateRange.add(date3, 1000),
|
||||||
|
date4 = DateRange.add(date3, 2000),
|
||||||
|
date41 = DateRange.add(date4, 1000),
|
||||||
|
date5 = DateRange.add(date4, 2000),
|
||||||
|
date51 = DateRange.add(date5, 1000),
|
||||||
|
date6 = DateRange.add(date5, 2000),
|
||||||
|
date61 = DateRange.add(date6, 1000),
|
||||||
|
date7 = DateRange.add(date6, 2000),
|
||||||
|
date71 = DateRange.add(date7, 1000);
|
||||||
|
|
||||||
|
describe('no ranges exist', ()=>{
|
||||||
|
it('returns the new ranges', ()=>{
|
||||||
|
var result = DateRange.addRange([date1, date2], []);
|
||||||
|
expect(result.gaps_filled).toEqual([[date1, date2]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('infinite range exists', ()=>{
|
||||||
|
it('returns the infinite range, no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange([date1, date2], [[undefined, undefined]]);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('-Infinity to definite date exists', ()=>{
|
||||||
|
describe('with gaps', ()=>{
|
||||||
|
var ranges = [[undefined, date1], [date2, date3], [date4, date5]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([])
|
||||||
|
expect(result.new_ranges).toEqual(ranges);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date31];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date1, date2], [date3, date31]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date31], [date4, date5]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date1, date2], [date3, date4], [date5, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date11, date41];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date11, date2], [date3, date4]])
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1], [date11, date5]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date11, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date11, date2], [date3, date4], [date5, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1], [date11, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date5, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date5, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1], [date2, date3], [date4, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no gaps', ()=>{
|
||||||
|
var ranges = [[undefined, date1]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([])
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date1];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date1, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date01, date1];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([])
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date1, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date1, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date5, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date5, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date1], [date5, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('definite to Infinity range exists', ()=>{
|
||||||
|
describe('with gaps', ()=>{
|
||||||
|
var ranges = [[date1, date2], [date3, date4], [date5, undefined]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date01]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date01], [date1, date2], [date3, date4], [date5, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date3];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1], [date2, date3]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date4], [date5, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1], [date2, date3], [date4, date5]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date1, date41];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date2, date3], [date4, date41]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date41], [date5, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date31, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date4, date5]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2], [date3, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date5, date61];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2], [date3, date4], [date5, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no gaps', ()=>{
|
||||||
|
var ranges = [[date1, undefined]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date01]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date01], [date1, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date1];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date1, date1];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date1, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date5, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('definite range exists', ()=>{
|
||||||
|
describe('with gaps', ()=>{
|
||||||
|
var ranges = [[date1, date2], [date3, date4], [date5, date6]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date01]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date01], [date1, date2], [date3, date4], [date5, date6]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date41];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1], [date2, date3], [date4, date41]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date41], [date5, date6]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1], [date2, date3], [date4, date5], [date6, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date11, date41];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date2, date3], [date4, date41]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date41], [date5, date6]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date31, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date4, date5], [date6, undefined]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2], [date3, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date61, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date61, undefined]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2], [date3, date4], [date5, date6], [date61, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no gaps', ()=>{
|
||||||
|
var ranges = [[date1, date2]];
|
||||||
|
|
||||||
|
describe('new range low low', ()=>{
|
||||||
|
var new_range = [undefined, date01];
|
||||||
|
it('no gaps filled', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date01]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date01], [date1, date2]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low mid', ()=>{
|
||||||
|
var new_range = [undefined, date11];
|
||||||
|
it('fills mid gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date2]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range low high', ()=>{
|
||||||
|
var new_range = [undefined, date61];
|
||||||
|
it('fills mid and high gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[undefined, date1], [date2, date61]]);
|
||||||
|
expect(result.new_ranges).toEqual([[undefined, date61]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid mid', ()=>{
|
||||||
|
var new_range = [date11, date2];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range mid high', ()=>{
|
||||||
|
var new_range = [date11, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date2, undefined]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('new range high high', ()=>{
|
||||||
|
var new_range = [date61, undefined];
|
||||||
|
it('includes gaps', ()=>{
|
||||||
|
var result = DateRange.addRange(new_range, ranges);
|
||||||
|
expect(result.gaps_filled).toEqual([[date61, undefined]]);
|
||||||
|
expect(result.new_ranges).toEqual([[date1, date2], [date61, undefined]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
12
spec/support/jasmine.json
Normal file
12
spec/support/jasmine.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": [
|
||||||
|
"**/*.test.js"
|
||||||
|
],
|
||||||
|
"helpers": [
|
||||||
|
"../node_modules/babel-core/register.js",
|
||||||
|
"helpers/**/*.js"
|
||||||
|
],
|
||||||
|
"stopSpecOnExpectationFailure": false,
|
||||||
|
"random": false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user