dont know what happened

This commit is contained in:
Eric Hulburd
2016-02-22 14:36:07 -06:00
parent 0ddae601bd
commit b8d0a9434b
69 changed files with 3711 additions and 0 deletions

19
client/api/energy_data.js Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
api.js

9
client/config/db.js Normal file
View 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
View File

@@ -0,0 +1 @@
store.js

48
client/d3/bar/base.js vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View 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>

View 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);
};

View File

View 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;

View 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>

View 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);
};

View File

@@ -0,0 +1,9 @@
#layout {
h1 {
color: red;
}
}
// needless comment
#yada {
div { padding: 200px; }
}

View 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;

View 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>

View 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
View 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
View File

@@ -0,0 +1,9 @@
import Loki from 'lokijs';
class Model {
}

View 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
View 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;

View 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
View 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
View 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;
}