dynamic react router

This commit is contained in:
Eric Hulburd
2016-02-29 18:20:00 -06:00
parent 51eaa19a92
commit c890132f2b
44 changed files with 1163 additions and 733 deletions

View File

@@ -1,13 +1,14 @@
import 'babel-polyfill';
import 'bootstrap/dist/js/bootstrap.min';
import React from 'react';
import ReactDOM from 'react-dom';
import Layout from './dashboard/layout/layout';
import {Router} from 'react-router';
export default function(){
import {ROUTES} from './dashboard/routes';
export default function(history){
ReactDOM.render(
React.createElement(Layout),
React.createElement(Router, {routes: ROUTES, history: history}),
document.getElementById('root')
);
};

View File

@@ -1,5 +1,6 @@
import Styles from 'config/styles';
import Templates from 'config/templates';
import {hashHistory} from 'react-router';
import app from './../../app';
Promise.all([
@@ -7,5 +8,5 @@ Promise.all([
Styles.sync()
]).then(()=>{
jQuery('#compiling_layouts').remove();
app();
app(hashHistory);
});

View File

@@ -11,11 +11,6 @@ const TEMPLATE_ROUTES = Object.freeze({
power: 'dashboard/power/power.rt'
});
const COMPONENTS = {
Power: Power,
Energy: Energy
};
var TEMPLATES = {};
class Templates {
@@ -40,16 +35,14 @@ class Templates {
url: TEMPLATE_ROUTES[view]
}).done((template)=>{
var code = rt.convertTemplateToReact(template, {modules: 'none', name: view}),
context = {};
code = code.replace('var '+view+' = ', 'context.'+view+' = ');
eval_context = {};
code = code.replace('var ' + view + ' = ', 'eval_context.' + view + ' = ');
new Function('with(this){ ' + code + ' } ').call({
Energy: Energy,
Power: Power,
context: context,
eval_context: eval_context,
'_': _,
'React': React
});
TEMPLATES[view] = context[view];
TEMPLATES[view] = eval_context[view];
fnResolve();
});
}

View File

@@ -1,3 +1,4 @@
import {browserHistory} from 'react-router';
import app from './../../app';
app();
app(browserHistory);

View File

@@ -1,15 +1,29 @@
// All react templates should be pre-compiled for development.
// run 'gulp compile_react_templates'
import fs from 'fs';
import aboutRt from './../../dashboard/about/about.rt.js';
import houseRt from './../../dashboard/house/house.rt.js';
import layoutRt from './../../dashboard/layout/layout.rt.js';
import energyRt from './../../dashboard/energy/energy.rt.js';
import energyGraphRt from './../../dashboard/energy/graph/graph.rt.js';
import energyTableRt from './../../dashboard/energy/table/table.rt.js';
import powerRt from './../../dashboard/power/power.rt.js';
import powerGraphRt from './../../dashboard/power/graph/graph.rt.js';
import powerTableRt from './../../dashboard/power/table/table.rt.js';
const TEMPLATES = Object.freeze({
const TEMPLATES = {
about: aboutRt,
house: houseRt,
layout: layoutRt,
energy: energyRt,
energy_graph: energyGraphRt,
energy_table: energyTableRt,
power: powerRt,
});
power_graph: powerGraphRt,
power_table: powerTableRt
};
class Templates {

View File

@@ -0,0 +1,13 @@
import React from 'react';
import Templates from 'config/templates';
class AboutComponent extends React.Component {
render() {
var aboutRt = Templates.forComponent('about');
return aboutRt.call(this);
}
}
export default AboutComponent;

View File

@@ -0,0 +1,18 @@
<div id="about">
<div class="panel panel-default">
<div class="panel-heading">About</div>
<div class="panel-body">
<p>This is a Spike bundle prototype using the following lirbaries:</p>
<ul>
<li>React</li>
<li>React Templates</li>
<li>React Router</li>
<li>LokiJs - persisting API calls to indexedDb</li>
<li>Webpack - hot mode developing and app bundling</li>
<li>Babel - ES6 transpiler</li>
</ul>
<p>The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.</p>
<p>Select a house below to get started.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import _ from 'lodash';
export default function () {
return React.createElement('div', { 'id': 'about' }, React.createElement('div', { 'className': 'panel panel-default' }, React.createElement('div', { 'className': 'panel-heading' }, 'About'), React.createElement('div', { 'className': 'panel-body' }, React.createElement('p', {}, 'This is a Spike bundle prototype using the following lirbaries:'), React.createElement('ul', {}, React.createElement('li', {}, 'React'), React.createElement('li', {}, 'React Templates'), React.createElement('li', {}, 'React Router'), React.createElement('li', {}, 'LokiJs - persisting API calls to indexedDb'), React.createElement('li', {}, 'Webpack - hot mode developing and app bundling'), React.createElement('li', {}, 'Babel - ES6 transpiler')), React.createElement('p', {}, 'The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.'), React.createElement('p', {}, 'Select a house below to get started.'))));
};

View File

@@ -0,0 +1,77 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../models/house';
import {RouteHelper} from './../routes';
class EnergyComponent extends React.Component {
constructor(props){
super(props);
var energy = this;
energy.state = {
loading_energy_data: true
};
}
componentDidMount(){
var energy = this,
house = energy.context.house;
if (!house || energy.context.loading_energy_data) return false;
house.setEnergyData()
.then(()=>{
energy.setState({loading_energy_data: false});
});
}
componentDidUpdate(prev_props, prev_state, prev_context){
var energy = this,
house = energy.context.house;
if (!house) return false;
if (!prev_context.house ||
prev_context.house.data.id != energy.context.house.data.id ||
!house.matchesYearState(prev_props.params)) {
energy.setState({loading_energy_data: true});
house.setEnergyData()
.then(()=>{
energy.setState({loading_energy_data: false}); // will update graph or table.
});
}
}
setParam(event){
var energy = this,
param = event.target.dataset.param,
value = event.target.dataset.value,
update = {}, route_helper;
override[param] = value;
route_helper = new RouteHelper(energy.context.house, energy.props);
if (route_helper.routeUpdated()){
route_helper.updateHouseState();
energy.context.router.push(makeRoute(house, energy.props, override));
}
}
getChildContext(){
var layout = this;
return {
loading_energy_data: layout.state.loading_energy_data
};
}
render() {
var energyRt = Templates.forComponent('energy');
return energyRt.call(this);
}
}
EnergyComponent.childContextTypes = {
loading_energy_data: React.PropTypes.bool.isRequired
};
EnergyComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
router: React.PropTypes.object.isRequired
};
export default EnergyComponent;

View File

@@ -1,130 +0,0 @@
import React from 'react';
import Templates from 'config/templates';
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();
});
},
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() {
var energyRt = Templates.forComponent('energy');
return energyRt.call(this);
}
});
export default Energy;

View File

@@ -1,39 +1,23 @@
<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 class="alert alert-warning" rt-if="this.state.loading_energy_data">
Retrieving energy data...
</div>
<div rt-if="this.props.view === 'graph'">
<h4>Select Data</h4>
<div class="btn-group" role="group">
<button
data-param="graph_attr"
data-value="consumption"
rt-class="{active: this.state.graph_attr === 'consumption'}"
onClick="{this.setGraphAttr}"
onClick="{this.setAttr}"
type="button" class="btn btn-primary">Consumption</button>
<button
data-param="graph_attr"
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>
{this.props.children}
</div>

View File

@@ -1,25 +1,20 @@
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', {
return React.createElement('div', { 'id': 'energy_view' }, this.state.loading_energy_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving energy data...\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-param': 'graph_attr',
'data-value': 'consumption',
'className': _.keys(_.pick({ active: this.state.graph_attr === 'consumption' }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
'onClick': this.setGraphAttr,
'onClick': this.setAttr,
'type': 'button'
}, 'Consumption'), React.createElement('button', {
'data-param': 'graph_attr',
'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);
}, 'Production'))) : null, '\n ', this.props.children, '\n');
};

View File

@@ -0,0 +1,76 @@
import React from 'react';
import Templates from 'config/templates';
import CalendarGridChart from './../../../d3/grid/calendar_grid';
import House from './../../../models/house';
class GraphComponent extends React.Component {
componentDidMount(){
var energy_graph = this,
house = energy_graph.context.house;
if (!energy_graph.context.loading_energy_data) energy_graph.updateGraph();
}
componentDidUpdate(prev_props, prev_state, prev_context){
var energy_graph = this,
house = energy_graph.context.house;
if (energy_graph.context.loading_energy_data) {return false;}
if (!prev_context.house ||
prev_context.loading_energy_data ||
prev_context.house.id != energy_graph.context.house.id) {
energy_graph.updateGraph();
}
}
updateGraph(){
var energy_graph = this,
house = energy_graph.context.house,
graph_attr = energy_graph.props.params.graph_attr;
if (energy_graph.graph === undefined){
energy_graph.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[graph_attr])} kWh`;
return `${date_s}: ${range_value}`;
}
});
}
energy_graph.graph.rangeValue = (datum)=>{ return datum.data[graph_attr]; }
energy_graph.graph.drawData({
title: energy_graph.graph_title,
css_class: '',
min_range: 0,
max_range: 150,
values: house.energy_data
});
}
render(){
var energyGraphRt = Templates.forComponent('energy_graph');
return energyGraphRt.call(this);
}
}
GraphComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
loading_energy_data: React.PropTypes.bool.isRequired,
router: React.PropTypes.object.isRequired
};
export default GraphComponent;

View File

@@ -0,0 +1 @@
<div id="energy_graph"></div>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import _ from 'lodash';
export default function () {
return React.createElement('div', { 'id': 'energy_graph' });
};

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../../models/house';
class TableComponent extends React.Component {
render() {
var tableRt = Templates.forComponent('energy_table');
return tableRt.call(this);
}
}
TableComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
router: React.PropTypes.object.isRequired
};
export default TableComponent;

View File

@@ -0,0 +1,18 @@
<table rt-if="this.context.house" class="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.context.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>

View File

@@ -0,0 +1,12 @@
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 this.context.house ? 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.context.house.energy_data, repeatEnergy_datum1.bind(this))
])) : null;
};

View File

@@ -0,0 +1,64 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../models/house';
import {RouteHelper} from './../routes';
import EnergyComponent from './../energy/energy.component';
import PowerComponent from './../power/power.component';
class HouseComponent extends React.Component {
constructor(props){
super(props);
this.renders = 0;
}
setParam(event){
var house_component = this,
house = house_component.context.house,
param = event.target.dataset.param,
value = event.target.dataset.value,
update = {}, route_helper;
update[param] = value;
route_helper = new RouteHelper(house, house_component.props, update);
if (route_helper.routeUpdated()){
route_helper.updateHouseState();
if (house_component.renders < 10){
house_component.context.router.push(route_helper.newRoute());
house_component.renders += 1;
}
}
}
graphSelected(){
var house_component = this;
return RouteHelper.graphSelected(house_component.props.routes);
}
tableSelected(){
var house_component = this;
return RouteHelper.tableSelected(house_component.props.routes);
}
energySelected(){
var house_component = this;
return RouteHelper.energySelected(house_component.props.routes);
}
powerSelected(){
var house_component = this;
return RouteHelper.powerSelected(house_component.props.routes);
}
render() {
var houseRt = Templates.forComponent('house');
return houseRt.call(this);
}
};
HouseComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
router: React.PropTypes.object.isRequired
};
export default HouseComponent;

View File

@@ -0,0 +1,49 @@
<div id="house">
<h4>Select dataset:</h4>
<div class="btn-group" role="group">
<button
data-param="dataset"
data-value="energy"
rt-class="{active: this.energySelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Daily Energy Statistics</button>
<button
data-param="dataset"
data-value="power"
rt-class="{active: this.powerSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">15-minute Power Statistics</button>
</div>
<h4>View as:</h4>
<div class="btn-group" role="group">
<button
data-param="view"
data-value="graph"
rt-class="{active: this.graphSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Graph</button>
<button
data-param="view"
data-value="table"
rt-class="{active: this.tableSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Table</button>
</div>
<div rt-if="this.context.house">
<h4>Select dates:</h4>
<div class="btn-group">
<button
rt-repeat="year in this.context.house.years"
data-param="year"
data-value="{year}"
key="data-year-{year}"
class="btn-info btn btn-sm"
rt-class="{active: year == this.context.house.state.year}"
onClick="{this.setParam.bind(this)}">{year}</button>
</div>
</div><br/>
{this.props.children}
</div>

View File

@@ -0,0 +1,48 @@
import React from 'react';
import _ from 'lodash';
function repeatYear1(year, yearIndex) {
return React.createElement('button', {
'data-param': 'year',
'data-value': year,
'key': 'data-year-' + year,
'className': 'btn-info btn btn-sm' + ' ' + _.keys(_.pick({ active: year == this.context.house.state.year }, _.identity)).join(' '),
'onClick': this.setParam.bind(this)
}, year);
}
export default function () {
return React.createElement('div', { 'id': 'house' }, React.createElement('h4', {}, 'Select dataset:'), React.createElement('div', {
'className': 'btn-group',
'role': 'group'
}, React.createElement('button', {
'data-param': 'dataset',
'data-value': 'energy',
'className': _.keys(_.pick({ active: this.energySelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
'onClick': this.setParam.bind(this),
'type': 'button'
}, 'Daily Energy Statistics'), React.createElement('button', {
'data-param': 'dataset',
'data-value': 'power',
'className': _.keys(_.pick({ active: this.powerSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
'onClick': this.setParam.bind(this),
'type': 'button'
}, '15-minute Power Statistics')), React.createElement('h4', {}, 'View as:'), React.createElement('div', {
'className': 'btn-group',
'role': 'group'
}, React.createElement('button', {
'data-param': 'view',
'data-value': 'graph',
'className': _.keys(_.pick({ active: this.graphSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
'onClick': this.setParam.bind(this),
'type': 'button'
}, 'Graph'), React.createElement('button', {
'data-param': 'view',
'data-value': 'table',
'className': _.keys(_.pick({ active: this.tableSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary',
'onClick': this.setParam.bind(this),
'type': 'button'
}, 'Table')), this.context.house ? React.createElement('div', {}, React.createElement('h4', {}, 'Select dates:'), React.createElement.apply(this, [
'div',
{ 'className': 'btn-group' },
_.map(this.context.house.years, repeatYear1.bind(this))
])) : null, React.createElement('br', {}), '\n\n ', this.props.children, '\n');
};

View File

@@ -0,0 +1,84 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../models/house';
import PowerDatum from './../../models/power_datum';
import {RouteHelper} from './../routes';
class LayoutComponent extends React.Component {
constructor(props, context){
super(props, context);
this.renders = 0;
this.state = {
houses: null,
house: null,
requesting_data: true
};
}
componentDidMount() {
var layout = this;
House.ensureHouses().then((houses)=>{
var house = null;
if (layout.props.params.house_id != undefined){
house = houses.find((h)=>{ return h.data.id == layout.props.params.house_id; });
var route_helper = new RouteHelper(house, layout.props);
if (route_helper.paramsHaveDateState()) route_helper.updateHouseToParams();
}
layout.setState({
houses: houses,
requesting_data: false,
house: house });
});
}
setHouse(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 });
if (!old_house || old_house.id != house_id){
var route_helper = new RouteHelper(house, layout.props);
route_helper.updateHouseToParams();
layout.setState({house: house}, ()=>{
if (layout.renders < 10){
layout.context.router.push(route_helper.newRoute());
layout.renders += 1
}
if (old_house) old_house.closeDb();
});
}
}
refreshData(){
var layout = this,
houses = layout.state.houses,
all = [];
houses.forEach((house)=>{
all.push(house.clearData());
});
Promise.all(all)
.then(()=>{
window.location.reload();
});
}
getChildContext(){
var layout = this;
return {house: layout.state.house};
}
render() {
var layoutRt = Templates.forComponent('layout');
return layoutRt.call(this);
}
}
LayoutComponent.contextTypes = {
router: React.PropTypes.object.isRequired
};
LayoutComponent.childContextTypes = {
house: React.PropTypes.instanceOf(House)
};
export default LayoutComponent;

View File

@@ -1,89 +0,0 @@
import React from 'react';
import Templates from 'config/templates';
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() {
var layoutRt = Templates.forComponent('layout');
return layoutRt.call(this);
}
});
export default Layout;

View File

@@ -1,60 +1,11 @@
<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}">
<select id="houses_select" rt-if="this.state.houses" class="form-control" onChange="{this.setHouse.bind(this)}" value="{this.props.params.house_id}">
<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>
<button rt-if="this.state.house" onClick="{this.refreshData.bind(this)}" 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>
{this.props.children}
</div>

View File

@@ -1,69 +1,23 @@
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',
{
'id': 'houses_select',
'className': 'form-control',
'onChange': this.setHouse
'onChange': this.setHouse.bind(this),
'value': this.props.params.house_id
},
_.map(this.state.houses, repeatHouse1.bind(this))
]) : null, this.state.house ? React.createElement('button', {
'onClick': this.refreshData,
'onClick': this.refreshData.bind(this),
'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);
}, 'Refresh House Data') : null, '\n\n ', this.props.children, '\n');
};

View File

@@ -0,0 +1,91 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../../models/house';
import SplineStackChart from './../../../d3/line/spline_stack';
class GraphComponent extends React.Component {
componentDidMount(){
var power_graph = this,
house = power_graph.context.house;
power_graph.graph_title = ' ';
if (!power_graph.context.loading_power_data) power_graph.updateGraph();
}
componentDidUpdate(prev_props, prev_state, prev_context){
var power_graph = this,
house = power_graph.context.house;
if (power_graph.context.loading_power_data) {return false;}
if (!prev_context.house ||
prev_context.loading_power_data ||
prev_context.house.id != power_graph.context.house.id) {
power_graph.updateGraph();
}
}
updateGraph(){
var power_graph = this,
house = power_graph.context.house;
if (power_graph.graph === undefined){
power_graph.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_graph_datum.data.time, 'MMM D [at] HH:mm');
}
});
jQuery('#power_graph').tooltip({
selector: 'circle',
container: 'body',
html: true,
title: function(){
return this.__data__.title;
}
});
}
var net_power_graph = {
title: 'Net Power Consumption',
values: house.power_data.map((power_graph_datum)=>{
return {
power_graph_datum: power_graph_datum,
x: power_graph_datum.time_to_date,
y: Math.max(0, power_graph_datum.data.consumption - power_graph_datum.data.production) }
})
},
savings = {
title: 'Power Production',
values: house.power_data.map((power_graph_datum)=>{
return {
power_graph_datum: power_graph_datum,
x: power_graph_datum.time_to_date,
y: power_graph_datum.data.production }
})
};
power_graph.graph.drawData({
title: power_graph.graph_title,
css_class: '',
series: [net_power_graph, savings]
});
}
render() {
var powerGraphRt = Templates.forComponent('power_graph');
return powerGraphRt.call(this);
}
}
GraphComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
loading_power_data: React.PropTypes.bool.isRequired,
router: React.PropTypes.object.isRequired
};
export default GraphComponent;

View File

@@ -0,0 +1 @@
<div id="power_graph"></div>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import _ from 'lodash';
export default function () {
return React.createElement('div', { 'id': 'power_graph' });
};

View File

@@ -0,0 +1,120 @@
import React from 'react';
import moment from 'moment-timezone';
import _ from 'lodash';
import Templates from 'config/templates';
import House from './../../models/house';
import DateRangeSlider from './../../d3/sliders/date_range';
import {RouteHelper} from './../routes';
class PowerComponent extends React.Component {
constructor(props){
super(props);
var power = this;
power.state = {
loading_power_data: true };
power.updates = 0;
}
componentDidMount(){
var power = this,
house = power.context.house;
power.renders = 0;
if (!house) return false;
power.initDateRange();
house.setPowerData()
.then(()=>{
power.setState({loading_power_data: false }); });
}
componentDidUpdate(prev_props, prev_state, prev_context){
var power = this,
house = power.context.house;
if (!house) return false;
var route_helper = new RouteHelper(house, power.props);
if ((!prev_context.house || prev_context.house.data.id != power.context.house.data.id) ||
!house.matchesPowerRange(prev_props.location.query['dates[]'] || [])) {
power.setState({loading_power_data: true});
house.setPowerData()
.then(()=>{
power.initDateRange();
power.setState({loading_power_data: false}); // will update graph or table.
});
} else if (!house.matchesMonthState(prev_props.params)) power.initDateRange();
}
initDateRange(){
var power = this,
house = power.context.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(()=>{
var power_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)],
route_helper = new RouteHelper(house, power.props, {power_range: power_range});
route_helper.updateHouseState();
power.context.router.push(route_helper.newRoute());
}, 500);
};
power.date_range_slider.drawData({
abs_min: house.state.current_month_moment.toDate(),
abs_max: house.state.end_of_current_data_moment.toDate(),
current_min: house.toDate(house.state.power_range[0]),
current_max: house.toDate(house.state.power_range[1])
});
}
setParam(event){
var power = this,
house = power.context.house,
param = event.target.dataset.param,
value = event.target.dataset.value,
update = {}, route_helper;
update[param] = value;
route_helper = new RouteHelper(house, power.props, update);
if (route_helper.routeUpdated()){
route_helper.updateHouseState();
power.context.router.push(route_helper.newRoute());
}
}
getChildContext(){
var layout = this;
return {
loading_power_data: layout.state.loading_power_data
};
}
render() {
var powerRt = Templates.forComponent('power');
return powerRt.call(this);
}
}
PowerComponent.childContextTypes = {
loading_power_data: React.PropTypes.bool.isRequired
};
PowerComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
router: React.PropTypes.object.isRequired
};
export default PowerComponent;

View File

@@ -1,195 +0,0 @@
import React from 'react';
import moment from 'moment-timezone';
import _ from 'lodash';
import Templates from 'config/templates';
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();
}
});
},
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() {
var powerRt = Templates.forComponent('power');
return powerRt.call(this);
}
});
export default Power;

View File

@@ -1,35 +1,18 @@
<div id="power_view">
<div class="btn-group">
<button
rt-if="this.props.house"
rt-repeat="month in this.props.house.availableMonths()"
rt-if="this.context.house"
rt-repeat="month in this.context.house.availableMonths()"
data-param="month"
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>
rt-class="{active: month === this.context.house.state.month}"
onClick="{this.setParam.bind(this)}">{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 class="alert alert-warning" rt-if="this.state.loading_power_data">
Retrieving power data...
</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></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>
{this.props.children}
</div>

View File

@@ -2,26 +2,17 @@ import React from 'react';
import _ from 'lodash';
function repeatMonth1(month, monthIndex) {
return React.createElement('button', {
'data-param': 'month',
'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
'className': 'btn-warning btn btn-sm' + ' ' + _.keys(_.pick({ active: month === this.context.house.state.month }, _.identity)).join(' '),
'onClick': this.setParam.bind(this)
}, month);
}
function repeatPower_datum2(power_datum, power_datumIndex) {
return React.createElement('tr', {
'className': 'fuck-you',
'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);
this.context.house ? _.map(this.context.house.availableMonths(), repeatMonth1.bind(this)) : null
]), this.state.loading_power_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data...\n ') : null, React.createElement('div', { 'id': 'power_date_setter' }), '\n ', this.props.children, '\n');
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import Templates from 'config/templates';
import House from './../../../models/house';
class TableComponent extends React.Component {
render() {
var powerTableRt = Templates.forComponent('power_table');
return powerTableRt.call(this);
}
}
TableComponent.contextTypes = {
house: React.PropTypes.instanceOf(House),
router: React.PropTypes.object.isRequired
};
export default TableComponent;

View File

@@ -0,0 +1,18 @@
<table class="table" rt-if="this.context.house">
<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.context.house.power_data" key="{power_datum.scoped_id}">
<td></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>

View File

@@ -0,0 +1,12 @@
import React from 'react';
import _ from 'lodash';
function repeatPower_datum1(power_datum, power_datumIndex) {
return React.createElement('tr', { 'key': power_datum.scoped_id }, React.createElement('td', {}), 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 this.context.house ? 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.context.house.power_data, repeatPower_datum1.bind(this))
])) : null;
};

166
client/dashboard/routes.js Normal file
View File

@@ -0,0 +1,166 @@
import House from './house/house.component'
import Power from './power/power.component';
import PowerGraph from './power/graph/graph.component';
import PowerTable from './power/table/table.component';
import Energy from './energy/energy.component';
import EnergyGraph from './energy/graph/graph.component';
import EnergyTable from './energy/table/table.component';
import About from './about/about.component';
import Layout from './layout/layout.component';
import ArrayUtil from './../../shared/utils/array';
const POWER_ROUTES = {
path: 'power',
component: Power,
childRoutes: [
{path: ':month/:year', component: PowerGraph},
{path: ':month/:year/graph', component: PowerGraph},
{path: ':month/:year/table', component: PowerTable}
]
};
const ENERGY_ROUTES = {
path: 'energy',
component: Energy,
childRoutes: [
{path: ':year/:graph_attr', component: EnergyGraph},
{path: ':year/:graph_attr/graph', component: EnergyGraph},
{path: ':year/:graph_attr/table', component: EnergyTable}
]
};
export const ROUTES = [{
path: '/',
component: Layout,
indexRoute: { component: About },
childRoutes: [{
path: 'houses/:house_id',
component: House,
childRoutes: [ENERGY_ROUTES, POWER_ROUTES]
}]
}];
export class RouteHelper {
constructor(house, props, update){
var route_helper = this;
route_helper.house = house;
route_helper.props = props;
route_helper.update = update || {};
}
get view(){
var route_helper = this;
return route_helper.update.view || (route_helper.tableSelected() ? 'table' : 'graph');
}
get graph_attr(){
var route_helper = this;
return route_helper.update.graph_attr || route_helper.props.params.graph_attr || 'consumption';
}
get dataset(){
var route_helper = this;
return route_helper.update.dataset || (route_helper.energySelected() ? 'energy' : 'power');
}
get power_range(){
var route_helper = this;
return route_helper.update.power_range || route_helper.props.location.query['dates[]'];
}
get date_params(){
var route_helper = this;
return {
month: route_helper.update.month || route_helper.house.month,
year: route_helper.update.year || route_helper.house.year };
}
routeUpdated(){
var route_helper = this,
house = route_helper.house;
return (route_helper.energySelected() && !house.matchesYearState(route_helper.date_params)) ||
(route_helper.powerSelected() && !house.matchesMonthState(route_helper.date_params) || !house.matchesPowerRange(route_helper.power_range));
}
// This will update the house state acccording to passed update parameters.
updateHouseState(){
var route_helper = this,
house = route_helper.house;
house.setMonthState(route_helper.date_params, route_helper.update.power_range);
}
paramsHaveDateState(){
var route_helper = this;
return !!route_helper.props.params.year;
}
// This will update the house according to URL parameters.
updateHouseToParams(){
var route_helper = this,
house = route_helper.house,
power_range;
if (route_helper.props.location.query['dates[]']){
power_range = [];
power_range[0] = +route_helper.props.location.query['dates[]'][0];
power_range[1] = +route_helper.props.location.query['dates[]'][1];
}
house.setMonthState(route_helper.props.params, power_range);
}
// should be run AFTER updateHouseState is called.
newRoute(){
var route_helper = this,
house = route_helper.house;
if (route_helper.dataset === 'energy'){
return `/houses/${house.data.id}/energy/${house.state.year}/${route_helper.graph_attr}/${route_helper.view}`;
} else {
return `/houses/${house.data.id}/power/${house.state.month}/${house.state.year}/${route_helper.view}?${jQuery.param({dates: house.state.power_range})}`;
}
}
graphSelected(){
return RouteHelper.graphSelected(this.props.routes);
}
static graphSelected(routes){
if (RouteHelper.energySelected(routes)){
return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyGraph; });
} else if (RouteHelper.powerSelected(routes)){
return ArrayUtil.any(routes, (route)=>{ return route.component === PowerGraph; });
}
return false;
}
tableSelected(){
return RouteHelper.tableSelected(this.props.routes);
}
static tableSelected(routes){
if (RouteHelper.energySelected(routes)){
return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyTable; });
} else if (RouteHelper.powerSelected(routes)){
return ArrayUtil.any(routes, (route)=>{ return route.component === PowerTable; });
}
return false;
}
energySelected(){
return RouteHelper.energySelected(this.props.routes);
}
static energySelected(routes){
return ArrayUtil.any(routes, (route)=>{ return route.component === Energy; });
}
powerSelected(){
return RouteHelper.powerSelected(this.props.routes);
}
static powerSelected(routes){
return ArrayUtil.any(routes, (route)=>{ return route.component === Power; });
}
}

View File

@@ -20,6 +20,7 @@ class House {
constructor(data){
var house = this;
house.data = data;
house.state = {};
Object.assign(house, Databasable);
var n_years = house.data_until_moment.year() - house.data_from_moment.year() + 1;
@@ -27,9 +28,10 @@ class House {
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();
house.setMonthState({
month: house.data_until_moment.format('MMM'),
year: house.data_until_moment.year()
});
}
get data_from_moment(){
@@ -42,20 +44,19 @@ class House {
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}`;
}
get select_props(){
if (this.selected) return {selected: true};
else return {};
}
availableMonths(){
var house = this,
all_months = moment.monthsShort(),
year = house.current_year.toString();
year = house.state.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')){
@@ -65,29 +66,76 @@ class House {
}
}
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(){
setMonthState(params, power_ranges){
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;
all_months = moment.monthsShort();
if (house.state.month !== params.month || house.state.year != params.year){
var new_year = +params.year;
if (new_year >= house.data_from_moment.year() && new_year <= house.data_until_moment.year()){
house.state.year = params.year;
} else if (!house.state.year){
house.state.year = house.years[house.years.length - 1];
}
var available_months = house.availableMonths();
if (available_months.indexOf(params.month) >= 0){
house.state.month = params.month;
} else if (!house.state.month || available_months.indexOf(house.state.month) < 0){
house.state.month = available_months[available_months.length - 1];
}
}
return false;
var month_i = all_months.indexOf(house.state.month),
new_month_moment = moment.tz({year: house.state.year, month: month_i, day: 1}, house.data.timezone).startOf('month');
if (!house.state.current_month_moment || new_month_moment.unix() !== house.state.current_month_moment.unix()){
house.state.current_month_moment = new_month_moment;
}
house.setDataRanges(power_ranges);
}
setDataRanges(power_ranges){
var house = this,
end_of_month = house.state.current_month_moment.clone().endOf('month'),
end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month,
energy_max = Math.min(end_of_current_data_moment.clone().endOf('year').unix(), house.data.data_until);
house.state.energy_range = [end_of_current_data_moment.clone().startOf('year').unix(), energy_max];
house.state.power_range = house.state.power_range || [];
house.state.end_of_current_data_moment = end_of_current_data_moment;
var current_data_range = [house.state.current_month_moment.unix(), end_of_current_data_moment.unix()],
power_min = house.state.power_range[0],
power_max = house.state.power_range[1];
if (power_ranges){
if (DateRange.inRange(power_ranges[1], current_data_range)){
power_max = power_ranges[1];
}
if (DateRange.inRange(power_ranges[0], current_data_range) && power_ranges[0] < power_max){
power_min = power_ranges[0];
}
}
if (!power_max || !DateRange.inRange(power_max, current_data_range)){
power_max = end_of_current_data_moment.unix();
}
if (!power_min || !DateRange.inRange(power_min, current_data_range) ||
power_max - power_min > 3600 * 24 * 4){
power_min = power_max - 3600 * 24 * 4;
}
house.state.power_range = [power_min, power_max];
}
matchesYearState(params){
var house = this;
return params.year == house.state.year;
}
matchesMonthState(params){
var house = this;
return params.month == house.state.month && params.year == house.state.year;
}
matchesPowerRange(dates){
var house = this;
return house.state.power_range[0] == dates[0] && house.state.power_range[1] == dates[1];
}
offset_diff(unix){
@@ -121,7 +169,7 @@ class House {
.then((power_collection)=>{
return house.ensurePowerData()
.then(()=>{
var params = house.rangeToLokiParams('time', house.power_date_range);
var params = house.rangeToLokiParams('time', house.state.power_range);
house.power_data = power_collection.find(params)
.sort((pd1, pd2)=>{
if (pd1.time === pd2.time) return 0;
@@ -137,7 +185,7 @@ class House {
var house = this,
query_ranges;
query_ranges = DateRange.addRange(house.power_date_range, house.data.power_datum_ranges || []);
query_ranges = DateRange.addRange(house.state.power_range, house.data.power_datum_ranges || []);
if (query_ranges.gaps_filled.length > 0){
var params = {dates: query_ranges.gaps_filled};
return house.getPowerData(params)
@@ -167,7 +215,7 @@ class House {
.then((energy_collection)=>{
return house.ensureEnergyData()
.then(()=>{
var params = house.rangeToLokiParams('day', house.energy_date_range);
var params = house.rangeToLokiParams('day', house.state.energy_range);
house.energy_data = energy_collection.find(params)
.sort((pd1, pd2)=>{
if (pd1.day === pd2.day) return 0;
@@ -181,7 +229,7 @@ class House {
ensureEnergyData(){
var house = this,
query_ranges = DateRange.addRange(house.energy_date_range, house.data.energy_datum_ranges || []);
query_ranges = DateRange.addRange(house.state.energy_range, house.data.energy_datum_ranges || []);
if (query_ranges.gaps_filled.length > 0){
return house.getEnergyData({dates: query_ranges.gaps_filled})
.then(()=>{

View File

@@ -1,45 +0,0 @@
0 info it worked if it ends with ok
1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'start' ]
2 info using npm@3.5.3
3 info using node@v5.4.1
4 verbose run-script [ 'prestart', 'start', 'poststart' ]
5 info lifecycle spike_proto@0.0.0~prestart: spike_proto@0.0.0
6 silly lifecycle spike_proto@0.0.0~prestart: no script for prestart, continuing
7 info lifecycle spike_proto@0.0.0~start: spike_proto@0.0.0
8 verbose lifecycle spike_proto@0.0.0~start: unsafe-perm in lifecycle true
9 verbose lifecycle spike_proto@0.0.0~start: PATH: /usr/local/lib/node_modules/npm/bin/node-gyp-bin:/home/eric/Code/spike2/node_modules/.bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@oroeco_dev/bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@global/bin:/home/eric/.rvm/rubies/ruby-1.9.3-p484/bin:/home/eric/.rvm/bin:/home/eric/bin:/usr/local/heroku/bin:/home/eric/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/opt/lampp/bin
10 verbose lifecycle spike_proto@0.0.0~start: CWD: /home/eric/Code/spike2
11 silly lifecycle spike_proto@0.0.0~start: Args: [ '-c', 'babel-node ./server/app.express.js' ]
12 silly lifecycle spike_proto@0.0.0~start: Returned: code: 1 signal: null
13 info lifecycle spike_proto@0.0.0~start: Failed to exec start script
14 verbose stack Error: spike_proto@0.0.0 start: `babel-node ./server/app.express.js`
14 verbose stack Exit status 1
14 verbose stack at EventEmitter.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/lifecycle.js:232:16)
14 verbose stack at emitTwo (events.js:87:13)
14 verbose stack at EventEmitter.emit (events.js:172:7)
14 verbose stack at ChildProcess.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:24:14)
14 verbose stack at emitTwo (events.js:87:13)
14 verbose stack at ChildProcess.emit (events.js:172:7)
14 verbose stack at maybeClose (internal/child_process.js:821:16)
14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:211:5)
15 verbose pkgid spike_proto@0.0.0
16 verbose cwd /home/eric/Code/spike2
17 error Linux 3.19.0-49-generic
18 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "start"
19 error node v5.4.1
20 error npm v3.5.3
21 error code ELIFECYCLE
22 error spike_proto@0.0.0 start: `babel-node ./server/app.express.js`
22 error Exit status 1
23 error Failed at the spike_proto@0.0.0 start script 'babel-node ./server/app.express.js'.
23 error Make sure you have the latest version of node.js and npm installed.
23 error If you do, this is most likely a problem with the spike_proto package,
23 error not with npm itself.
23 error Tell the author that this fails on your system:
23 error babel-node ./server/app.express.js
23 error You can get information on how to open an issue for this project with:
23 error npm bugs spike_proto
23 error Or if that isn't available, you can get their info via:
23 error npm owner ls spike_proto
23 error There is likely additional logging output above.
24 verbose exit [ 1, true ]

View File

@@ -26,6 +26,7 @@
"express": "4.13.3",
"react": "0.14.3",
"react-dom": "0.14.3",
"react-router": "2.0.0",
"webpack": "1.12.9",
"webpack-dev-server": "1.14.0",
"extract-text-webpack-plugin": "1.0.1",

View File

@@ -55,7 +55,7 @@ npm start
When developing, it can be really useful to clear all of the data saved to local client storage. A better alternative to clearing all data in your browser cache, hit the 'Refresh Data' button on the main page below the houses dropdown.
## Building the designer pack
## Building the design pack
To build a design pack, you first need to install [sass.js](https://github.com/medialize/sass.js/) in the design build directory so the design build can compile the sass in the browser.
@@ -64,7 +64,7 @@ cd client/build/design
git clone https://github.com/medialize/sass.js.git
```
Then to build the app, you'll need to build the app with webpack:
Then build the app with webpack:
```sh
gulp build --design
@@ -80,7 +80,7 @@ python -m SimpleHTTPServer 8000
python3 -m http.server
```
Access the app at localhost:8000. The app will run slow because json responses are not as finely paginated and both React templates and sass are compiled in the browser before the app runs.
Access the app at localhost:8000. The app will run slowly because json responses are not as finely paginated and both React templates and sass are compiled in the browser before the app runs.
The designer can change React templates and sass files in `/dashboard`. Refresh the page to see the changes reflected. They can then share this directory with us so we can update the files in this repository.

View File

@@ -29,7 +29,6 @@ DB.sync().then(()=>{
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}`);
});
@@ -74,7 +73,7 @@ app.use(assets({
paths: ["./../node_modules"],
build: true,
buildDir: false,
//compile: false,
// compile: false,
compress: true
}));
// serve public static files.
@@ -83,44 +82,10 @@ 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)=>{
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}`);
});

View File

@@ -64,5 +64,11 @@ class DateRange {
return new Date(date.getTime() + s);
}
static inRange(n, min_max){
var min = min_max[0],
max = min_max[1];
return ((n >= min) && (n <= max));
}
}
export default DateRange;

View File

@@ -29,10 +29,4 @@ export default class {
return minus;
}
static inRange(n, min_max){
var min = min_max[0],
max = min_max[1];
return ((n >= min) && (n <= max));
}
}

View File

@@ -0,0 +1,85 @@
"use strict";
import moment from 'moment-timezone';
import House from './../../../../client/models/house.js';
describe('house#setMonthState', ()=>{
var data_until = 1456589922, // Sat, 27 Feb 2016 16:18:42 +0000
house = new House({
id: 1,
name: 'Johnson',
data_from: data_until - 3600 * 24 * 365 * 3,
data_until: data_until,
timezone: 'America/New_York'
});
it('is updated properly on init', ()=>{
var current_month_moment = moment.tz({year: 2016, month: 1, day: 1}, 'America/New_York'),
energy_min = moment.tz({year: 2016, month: 0, day: 1}, 'America/New_York').unix(),
energy_max = data_until,
power_min = data_until - 3600 * 24 * 4,
power_max = data_until;
expect(house.state.month).toEqual('Feb');
expect(house.state.year).toEqual(2016);
expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix());
expect(house.state.energy_range).toEqual([energy_min, energy_max]);
expect(house.state.power_range).toEqual([power_min, power_max]);
});
it('is not updated when passed no params', ()=>{
var current_month_moment = moment.tz({year: 2016, month: 1, day: 1}, 'America/New_York'),
energy_min = moment.tz({year: 2016, month: 0, day: 1}, 'America/New_York').unix(),
energy_max = data_until,
power_min = data_until - 3600 * 24 * 4,
power_max = data_until;
house.setMonthState({});
expect(house.state.month).toEqual('Feb');
expect(house.state.year).toEqual(2016);
expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix());
expect(house.state.energy_range).toEqual([energy_min, energy_max]);
expect(house.state.power_range).toEqual([power_min, power_max]);
});
it('is updated properly when passed power params', ()=>{
var current_month_moment = moment.tz({year: 2015, month: 2, day: 1}, 'America/New_York'),
energy_min = moment.tz({year: 2015, month: 0, day: 1}, 'America/New_York').unix(),
energy_max = moment.tz({year: 2015, month: 0, day: 1}, 'America/New_York').endOf('year').unix(),
power_max = current_month_moment.clone().endOf('month').subtract(3, 'days').unix(),
power_min = current_month_moment.clone().endOf('month').subtract(6, 'days').unix()
house.setMonthState({
month: 'Mar',
year: 2015
}, [ power_min, power_max ]);
expect(house.state.month).toEqual('Mar');
expect(house.state.year).toEqual(2015);
expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix());
expect(house.state.energy_range).toEqual([energy_min, energy_max]);
expect(house.state.power_range).toEqual([power_min, power_max]);
});
it('is updated properly when passed energy params', ()=>{
var current_month_moment = moment.tz({year: 2014, month: 9, day: 1}, 'America/New_York'),
energy_min = moment.tz({year: 2014, month: 0, day: 1}, 'America/New_York').unix(),
energy_max = moment.tz({year: 2014, month: 0, day: 1}, 'America/New_York').endOf('year').unix(),
power_max = moment.tz({year: 2014, month: 9, day: 1}, 'America/New_York').endOf('month').unix(),
power_min = power_max - 3600 * 24 * 4;
house.setMonthState({
month: 'Oct',
year: 2014
});
expect(house.state.month).toEqual('Oct');
expect(house.state.year).toEqual(2014);
expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix());
expect(house.state.energy_range).toEqual([energy_min, energy_max]);
expect(house.state.power_range).toEqual([power_min, power_max]);
});
});