energy spline stack
This commit is contained in:
4
client/d3/base.js
vendored
4
client/d3/base.js
vendored
@@ -3,7 +3,7 @@ import extend from 'extend';
|
||||
const DEFAULTS = {
|
||||
outer_width: 500,
|
||||
outer_height: 300,
|
||||
margin: {top: 0, left: 70, bottom: 50, right: 20},
|
||||
margin: {top: 30, left: 70, bottom: 50, right: 20},
|
||||
domain_ticks: 10,
|
||||
range_ticks: 8,
|
||||
container: "container",
|
||||
@@ -12,7 +12,7 @@ const DEFAULTS = {
|
||||
domain_attr: undefined,
|
||||
range_attr: undefined,
|
||||
titleize: function(series, datum){
|
||||
var s = datum ? datum.name : series.title;
|
||||
var s = datum ? datum.title : series.title;
|
||||
if (!s) return '';
|
||||
var words = s.split(' '),
|
||||
array = [];
|
||||
|
||||
95
client/d3/line/line.js
vendored
95
client/d3/line/line.js
vendored
@@ -1,42 +1,10 @@
|
||||
import extend from 'extend';
|
||||
|
||||
const DEFAULTS = {
|
||||
outer_width: 500,
|
||||
outer_height: 300,
|
||||
margin: {top: 0, left: 70, bottom: 50, right: 20},
|
||||
domain_ticks: 10,
|
||||
range_ticks: 8,
|
||||
container: "container",
|
||||
time_series: true,
|
||||
range_label: "range",
|
||||
domain_attr: null,
|
||||
range_attr: 'y',
|
||||
titleize: function(series, datum){
|
||||
var s = datum ? datum.name : series.name,
|
||||
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(' ');
|
||||
},
|
||||
toClass: function(series){
|
||||
return series ? series.title.toLowerCase().replace(/\s+/g, '-') : "";
|
||||
}
|
||||
}
|
||||
import Chart from './../base';
|
||||
|
||||
|
||||
// inspired by https://gist.github.com/mbostock/4b66c0d9be9a0d56484e
|
||||
class LineChart {
|
||||
|
||||
constructor(options){
|
||||
var line_chart = this;
|
||||
line_chart = extend(line_chart, DEFAULTS, options);
|
||||
|
||||
line_chart.height = line_chart.outer_height - line_chart.margin.top - line_chart.margin.bottom;
|
||||
line_chart.width = line_chart.outer_width - line_chart.margin.left - line_chart.margin.right;
|
||||
|
||||
line_chart.init();
|
||||
}
|
||||
class LineChart extends Chart {
|
||||
|
||||
get chart_options(){
|
||||
return {
|
||||
@@ -44,27 +12,6 @@ class LineChart {
|
||||
};
|
||||
}
|
||||
|
||||
init(){
|
||||
var line_chart = this;
|
||||
|
||||
line_chart.svg = d3.select(line_chart.container).append("svg")
|
||||
.attr("width", line_chart.outer_width)
|
||||
.attr("height", line_chart.outer_height)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + line_chart.margin.left + "," + line_chart.margin.top + ")");
|
||||
|
||||
// function that draws the lines.
|
||||
line_chart.line = d3.svg.line()
|
||||
.interpolate(line_chart.chart_options.interpolation)
|
||||
.x(function(d){ return x(d[line_chart.domain_attr]); })
|
||||
.y(function(d){ return y(d[line_chart.range_attr]); });
|
||||
|
||||
// function that returns unique color based on series_title.
|
||||
line_chart.color = d3.scale.category20();
|
||||
|
||||
line_chart.defineAxes();
|
||||
}
|
||||
|
||||
defineAxes(){
|
||||
var line_chart = this;
|
||||
|
||||
@@ -72,7 +19,8 @@ class LineChart {
|
||||
.range([line_chart.height, 0]);
|
||||
line_chart.y_axis = d3.svg.axis()
|
||||
.scale(line_chart.y_scale)
|
||||
.orient("left");
|
||||
.orient("left")
|
||||
.outerTickSize(1);
|
||||
|
||||
if (line_chart.time_series){
|
||||
line_chart.x_scale = d3.time.scale()
|
||||
@@ -84,24 +32,39 @@ class LineChart {
|
||||
|
||||
line_chart.x_axis = d3.svg.axis()
|
||||
.scale(line_chart.x_scale)
|
||||
.orient("bottom");
|
||||
.orient("bottom")
|
||||
.outerTickSize(0)
|
||||
//line_chart.x_axis.tickFormat(d3.time.format('%b %d at %H'))
|
||||
//line_chart.x_axis.ticks(d3.time.hour, 12);
|
||||
|
||||
// append axes
|
||||
line_chart.svg.append("g")
|
||||
.attr("class", "d3-chart-range d3-chart-axis")
|
||||
.attr("transform", "translate(0, " + (line_chart.height - line_chart.margin.top) + ")");
|
||||
.attr("class", "d3-chart-range d3-chart-axis");
|
||||
line_chart.svg.append("g")
|
||||
.attr("class", "d3-chart-domain d3-chart-axis");
|
||||
.attr("class", "d3-chart-domain d3-chart-axis")
|
||||
.attr("transform", "translate(0, " + (line_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,
|
||||
range_min: Infinity,
|
||||
range_max: -Infinity,
|
||||
domain_min: Infinity,
|
||||
domain_max: -Infinity,
|
||||
};
|
||||
|
||||
data.forEach(function(data_set){
|
||||
@@ -131,7 +94,7 @@ class LineChart {
|
||||
bar_chart.svg.select(".d3-chart-range.d3-chart-axis")
|
||||
.call(bar_chart.y_axis);
|
||||
|
||||
bar_chart.x_scale.domain([Math.min(data.domain_min), data.domain_max]);
|
||||
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
|
||||
|
||||
97
client/d3/line/spline_stack.js
vendored
97
client/d3/line/spline_stack.js
vendored
@@ -5,85 +5,78 @@ const INTERPOLATION = 'cardinal';
|
||||
// inspired by https://bl.ocks.org/mbostock/3885211
|
||||
class SplineStackChart extends LineChart {
|
||||
|
||||
init(){
|
||||
var spline_stack = this;
|
||||
|
||||
spline_stack.svg = d3.select(spline_stack.container).append("svg")
|
||||
.attr("width", spline_stack.outer_width)
|
||||
.attr("height", spline_stack.outer_height)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + spline_stack.margin.left + "," + spline_stack.margin.top + ")");
|
||||
|
||||
spline_stack.area = d3.svg.area.interpolate(INTERPOLATION)
|
||||
.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); });
|
||||
|
||||
spline_stack.stack = d3.layout.stack()
|
||||
.values(function(d) { return d.values; });
|
||||
|
||||
// function that returns unique color based on series_title.
|
||||
spline_stack.color = d3.scale.category20();
|
||||
spline_stack.defineAxes();
|
||||
get chart_options(){
|
||||
return Object.assign(LineChart.DEFAULTS, {
|
||||
interpolation: INTERPOLATION
|
||||
});
|
||||
}
|
||||
|
||||
get chart_options(){
|
||||
return {
|
||||
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 = {
|
||||
range_min: -Infinity,
|
||||
range_max: Infinity,
|
||||
domain_min: -Infinity,
|
||||
domain_max: Infinity };
|
||||
series: [] };
|
||||
|
||||
data.series.forEach(function(series, i){
|
||||
series.css_class = series.css_class || spline_stack.toClass ? spline_stack.toClass(series) : "";
|
||||
series.title = series.title || spline_stack.toClass ? spline_stack.titleize(series) : "";
|
||||
series.xy_values = series.values.map((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]);
|
||||
return {x: value[spline_stack.domain_attr], y: value[spline_stack.range_attr]};
|
||||
});
|
||||
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.serialize_data;
|
||||
data = spline_stack.serializeData(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);
|
||||
spline_stack.y_scale.domain([0, data.range_max]);
|
||||
spline_stack.svg.select(".d3-chart-range.d3-chart-axis")
|
||||
.call(spline_stack.y_axis);
|
||||
|
||||
bar_chart.x_scale.domain([Math.min(data.domain_min), data.domain_max]);
|
||||
bar_chart.svg.select(".d3-chart-domain.d3-chart-axis").call(bar_chart.x_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_data = spline_stack.stack(data.series),
|
||||
stack = svg.selectAll(".d3-chart-spline-stack")
|
||||
.data(stack_data);
|
||||
|
||||
[stack.enter().append("g"), stack.transition()].forEach((groups)=>{
|
||||
applyData(groups);
|
||||
var stack = spline_stack.svg.selectAll(".d3-chart-spline-stack")
|
||||
.data(data.series);
|
||||
[stack.enter().append("path"), stack.transition()].forEach((paths)=>{
|
||||
spline_stack.applyData(paths);
|
||||
});
|
||||
stack.exit().remove();
|
||||
}
|
||||
|
||||
applyData(groups){
|
||||
applyData(paths){
|
||||
var spline_stack = this;
|
||||
groups
|
||||
paths
|
||||
.attr("class", function(series){"d3-chart-spline-stack " + series.css_class;})
|
||||
.append("path")
|
||||
.attr("d", function(series){ return spline_stack.area(series.xy_values); })
|
||||
.style("fill", function(series){ return spline_stack.color(series.name); });
|
||||
.attr("d", function(series){ return spline_stack.fnArea(series.values); })
|
||||
.style("fill", function(series){ return spline_stack.fnColor(series.title); });
|
||||
}
|
||||
}
|
||||
|
||||
export default SplineStackChart;
|
||||
|
||||
@@ -10,7 +10,7 @@ var Layout = React.createClass({
|
||||
houses: null,
|
||||
house: null,
|
||||
view: 'graph',
|
||||
dataset: 'energy',
|
||||
dataset: 'power',
|
||||
requesting_data: true
|
||||
};
|
||||
},
|
||||
|
||||
@@ -36,10 +36,11 @@ var Power = React.createClass({
|
||||
if (power.props.view === 'graph') power.initGraph();
|
||||
});
|
||||
}
|
||||
if (new_props.view !== 'graph' && power.props.view === 'graph') power.destroyGraph();
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prev_props, _prev_state){
|
||||
var power_datum = this,
|
||||
var power = this,
|
||||
house = power.props.house;
|
||||
if (prev_props.view !== 'graph' && power.props.view === 'graph') power.initGraph();
|
||||
},
|
||||
@@ -47,23 +48,20 @@ var Power = React.createClass({
|
||||
initGraph: function(){
|
||||
var power = this;
|
||||
if (power.graph === undefined){
|
||||
document.getElementById('power_graph').innerHTML = '';
|
||||
power.graph = new SplineStackChart({
|
||||
container: '#power_graph',
|
||||
outer_width: 800,
|
||||
outer_height: 200,
|
||||
date_attr: 'day',
|
||||
color: '#0404B4',
|
||||
toDate: (power_datum)=>{ return power_datum.data.day.toDate(); }
|
||||
range_attr: 'y',
|
||||
domain_attr: 'x',
|
||||
time_series: true
|
||||
});
|
||||
jQuery('#power_graph').tooltip({
|
||||
selector: '.d3-chart-grid-unit',
|
||||
selector: 'path',
|
||||
container: 'body',
|
||||
title: function(){
|
||||
var power_datum = this.__data__,
|
||||
date_s = d3.time.format('%a %b %d, %Y')(power_datum.data.day.toDate()),
|
||||
range_value = `${Math.round(power_datum.data[power.state.graph_attr])} kWh`;
|
||||
return `${date_s}: ${range_value}`;
|
||||
return this.__data__.title;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -72,17 +70,28 @@ var Power = React.createClass({
|
||||
|
||||
updateGraph: function(){
|
||||
var power = this,
|
||||
house = power.props.house;
|
||||
power.graph.rangeValue = (datum)=>{ return datum.data[power.state.graph_attr]; }
|
||||
house = power.props.house,
|
||||
net_power = {
|
||||
title: 'Net Power Consumption',
|
||||
values: house.power_data.slice(0, 200).map((power_datum)=>{ return {y: Math.max(0, power_datum.data.consumption - power_datum.data.production), x: power_datum.data.time.toDate() } })
|
||||
},
|
||||
savings = {
|
||||
title: 'Power Production',
|
||||
values: house.power_data.slice(0, 200).map((power_datum)=>{ return {y: power_datum.data.production, x: power_datum.data.time.toDate() } })
|
||||
};
|
||||
power.graph.drawData({
|
||||
title: power.graph_title,
|
||||
css_class: '',
|
||||
min_range: 0,
|
||||
max_range: 150,
|
||||
values: house.power_data
|
||||
series: [net_power, savings]
|
||||
});
|
||||
},
|
||||
|
||||
destroyGraph: function(){
|
||||
var power = this;
|
||||
document.getElementById('power_graph').innerHTML = '';
|
||||
power.graph = undefined;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return powerRt.call(this);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="alert alert-warning" rt-if="this.state.loading_data">
|
||||
Retrieving power data for the {this.props.house.name} household...
|
||||
</div>
|
||||
<table>
|
||||
<table rt-if="this.props.view === 'table'" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -13,7 +13,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr rt-repeat="power_datum in this.state.house.power_data" key="{power_datum.react_key}">
|
||||
<tr rt-repeat="power_datum in this.props.house.power_data" key="{power_datum.react_key}">
|
||||
<td></td>
|
||||
<td>{power_datum.time_to_s}</td>
|
||||
<td>{power_datum.consumption_to_s}</td>
|
||||
@@ -21,5 +21,5 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="power_data"></div>
|
||||
<div rt-if="this.props.view === 'graph'" id="power_graph"></div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ function repeatPower_datum1(power_datum, power_datumIndex) {
|
||||
return React.createElement('tr', { 'key': power_datum.react_key }, 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 React.createElement('div', { 'id': 'power_view' }, React.createElement('h2', {}, 'Household 15-minute Power Statistics'), 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('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, [
|
||||
return React.createElement('div', { 'id': 'power_view' }, React.createElement('h2', {}, 'Household 15-minute Power Statistics'), this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data for the ', this.props.house.name, ' household...\n ') : null, 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.state.house.power_data, repeatPower_datum1.bind(this))
|
||||
])), React.createElement('div', { 'id': 'power_data' }));
|
||||
_.map(this.props.house.power_data, repeatPower_datum1.bind(this))
|
||||
])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'power_graph' }) : null);
|
||||
};
|
||||
Reference in New Issue
Block a user