diff --git a/accounts_lua.go b/accounts_lua.go
index 4ab1c07..016a47a 100644
--- a/accounts_lua.go
+++ b/accounts_lua.go
@@ -55,7 +55,6 @@ func luaGetAccounts(L *lua.LState) int {
return 1
}
-// Registers my account type to given L.
func luaRegisterAccounts(L *lua.LState) {
mt := L.NewTypeMetatable(luaAccountTypeName)
L.SetGlobal("account", mt)
diff --git a/balance_lua.go b/balance_lua.go
index 220edb0..8e9ae8b 100644
--- a/balance_lua.go
+++ b/balance_lua.go
@@ -12,7 +12,6 @@ type Balance struct {
const luaBalanceTypeName = "balance"
-// Registers my balance type to given L.
func luaRegisterBalances(L *lua.LState) {
mt := L.NewTypeMetatable(luaBalanceTypeName)
L.SetGlobal("balance", mt)
diff --git a/date_lua.go b/date_lua.go
index 1f7d2a6..437ae7b 100644
--- a/date_lua.go
+++ b/date_lua.go
@@ -8,7 +8,6 @@ import (
const luaDateTypeName = "date"
const timeFormat = "2006-01-02"
-// Registers my date type to given L.
func luaRegisterDates(L *lua.LState) {
mt := L.NewTypeMetatable(luaDateTypeName)
L.SetGlobal("date", mt)
diff --git a/js/actions/ErrorActions.js b/js/actions/ErrorActions.js
index 84532a9..289a14c 100644
--- a/js/actions/ErrorActions.js
+++ b/js/actions/ErrorActions.js
@@ -21,6 +21,17 @@ function ajaxError(error) {
};
}
+function clientError(error) {
+ var e = new Error();
+ e.ErrorId = 999;
+ e.ErrorString = "Client Error: " + error;
+
+ return {
+ type: ErrorConstants.ERROR_CLIENT,
+ error: e
+ };
+}
+
function clearError() {
return {
type: ErrorConstants.CLEAR_ERROR,
@@ -30,5 +41,6 @@ function clearError() {
module.exports = {
serverError: serverError,
ajaxError: ajaxError,
+ clientError: clientError,
clearError: clearError
};
diff --git a/js/actions/ReportActions.js b/js/actions/ReportActions.js
index d774a6b..725e21b 100644
--- a/js/actions/ReportActions.js
+++ b/js/actions/ReportActions.js
@@ -6,9 +6,10 @@ var models = require('../models.js');
var Report = models.Report;
var Error = models.Error;
-function fetchReport() {
+function fetchReport(reportName) {
return {
- type: ReportConstants.FETCH_REPORT
+ type: ReportConstants.FETCH_REPORT,
+ reportName: reportName
}
}
@@ -19,9 +20,25 @@ function reportFetched(report) {
}
}
+function selectReport(report, seriesTraversal) {
+ return {
+ type: ReportConstants.SELECT_REPORT,
+ report: report,
+ seriesTraversal: seriesTraversal
+ }
+}
+
+function reportSelected(flattenedReport, seriesTraversal) {
+ return {
+ type: ReportConstants.REPORT_SELECTED,
+ report: flattenedReport,
+ seriesTraversal: seriesTraversal
+ }
+}
+
function fetch(report) {
return function (dispatch) {
- dispatch(fetchReport());
+ dispatch(fetchReport(report));
$.ajax({
type: "GET",
@@ -45,6 +62,48 @@ function fetch(report) {
};
}
+function select(report, seriesTraversal) {
+ return function (dispatch) {
+ if (!seriesTraversal)
+ seriesTraversal = [];
+ dispatch(selectReport(report, seriesTraversal));
+
+ // Descend the tree to the right series to flatten
+ var series = report;
+ for (var i=0; i < seriesTraversal.length; i++) {
+ if (!series.Series.hasOwnProperty(seriesTraversal[i])) {
+ dispatch(ErrorActions.clientError("Invalid series"));
+ return;
+ }
+ series = series.Series[seriesTraversal[i]];
+ }
+
+ // Actually flatten the data
+ var flattenedSeries = series.mapReduceChildren(null,
+ function(accumulator, currentValue, currentIndex, array) {
+ return accumulator + currentValue;
+ }
+ );
+
+ // Add back in any values from the current level
+ if (series.hasOwnProperty('Values'))
+ flattenedSeries[report.topLevelAccountName] = series.Values;
+
+ var flattenedReport = new Report();
+
+ flattenedReport.ReportId = report.ReportId;
+ flattenedReport.Title = report.Title;
+ flattenedReport.Subtitle = report.Subtitle;
+ flattenedReport.XAxisLabel = report.XAxisLabel;
+ flattenedReport.YAxisLabel = report.YAxisLabel;
+ flattenedReport.Labels = report.Labels.slice();
+ flattenedReport.FlattenedSeries = flattenedSeries;
+
+ dispatch(reportSelected(flattenedReport, seriesTraversal));
+ };
+}
+
module.exports = {
- fetch: fetch
+ fetch: fetch,
+ select: select
};
diff --git a/js/components/ReportsTab.js b/js/components/ReportsTab.js
index a96c143..4a99f72 100644
--- a/js/components/ReportsTab.js
+++ b/js/components/ReportsTab.js
@@ -1,5 +1,10 @@
var React = require('react');
+var ReactBootstrap = require('react-bootstrap');
+
+var Button = ReactBootstrap.Button;
+var Panel = ReactBootstrap.Panel;
+
var StackedBarChart = require('../components/StackedBarChart');
module.exports = React.createClass({
@@ -10,10 +15,69 @@ module.exports = React.createClass({
componentWillMount: function() {
this.props.onFetchReport("monthly_expenses");
},
+ componentWillReceiveProps: function(nextProps) {
+ if (nextProps.reports['monthly_expenses'] && !nextProps.selectedReport.report) {
+ this.props.onSelectReport(nextProps.reports['monthly_expenses'], []);
+ }
+ },
+ onSelectSeries: function(series) {
+ if (series == this.props.selectedReport.report.topLevelAccountName)
+ return;
+ var seriesTraversal = this.props.selectedReport.seriesTraversal.slice();
+ seriesTraversal.push(series);
+ this.props.onSelectReport(this.props.reports[this.props.selectedReport.report.ReportId], seriesTraversal);
+ },
render: function() {
- report = [];
- if (this.props.reports['monthly_expenses'])
- report = ();
+ var report = [];
+ if (this.props.selectedReport.report) {
+ var titleTracks = [];
+ var seriesTraversal = [];
+
+ for (var i = 0; i < this.props.selectedReport.seriesTraversal.length; i++) {
+ var name = this.props.selectedReport.report.Title;
+ if (i > 0)
+ name = this.props.selectedReport.seriesTraversal[i-1];
+
+ // Make a closure for going up the food chain
+ var self = this;
+ var navOnClick = function() {
+ var onSelectReport = self.props.onSelectReport;
+ var report = self.props.reports[self.props.selectedReport.report.ReportId];
+ var mySeriesTraversal = seriesTraversal.slice();
+ return function() {
+ onSelectReport(report, mySeriesTraversal);
+ };
+ }();
+ titleTracks.push((
+
+ ));
+ titleTracks.push((/));
+ seriesTraversal.push(this.props.selectedReport.seriesTraversal[i]);
+ }
+ if (titleTracks.length == 0)
+ titleTracks.push((
+
+ ));
+ else
+ titleTracks.push((
+
+ ));
+
+ report = (
+
+
+ );
+ }
return (
{report}
diff --git a/js/components/StackedBarChart.js b/js/components/StackedBarChart.js
index 56b3783..ee3764c 100644
--- a/js/components/StackedBarChart.js
+++ b/js/components/StackedBarChart.js
@@ -1,15 +1,13 @@
var d3 = require('d3');
var React = require('react');
-var Panel = require('react-bootstrap').Panel;
-
module.exports = React.createClass({
displayName: "StackedBarChart",
- calcMinMax: function(data) {
+ calcMinMax: function(series) {
var children = [];
- for (var child in data) {
- if (data.hasOwnProperty(child))
- children.push(data[child]);
+ for (var child in series) {
+ if (series.hasOwnProperty(child))
+ children.push(series[child]);
}
var positiveValues = [0];
@@ -40,12 +38,6 @@ module.exports = React.createClass({
return Math.ceil(rangePerTick/roundTo)*roundTo;
},
render: function() {
- var data = this.props.data.mapReduceChildren(null,
- function(accumulator, currentValue, currentIndex, array) {
- return accumulator + currentValue;
- }
- );
-
height = 400;
width = 600;
legendWidth = 100;
@@ -54,7 +46,7 @@ module.exports = React.createClass({
height -= yMargin*2;
width -= xMargin*2;
- var minMax = this.calcMinMax(data);
+ var minMax = this.calcMinMax(this.props.report.FlattenedSeries);
var y = d3.scaleLinear()
.range([0, height])
.domain(minMax);
@@ -63,7 +55,7 @@ module.exports = React.createClass({
var x = d3.scaleLinear()
.range([0, width])
- .domain([0, this.props.data.Labels.length + 0.5]);
+ .domain([0, this.props.report.Labels.length + 0.5]);
var bars = [];
var labels = [];
@@ -76,13 +68,13 @@ module.exports = React.createClass({
// negativeSum arrays
var positiveSum = [];
var negativeSum = [];
- for (var i=0; i < this.props.data.Labels.length; i++) {
+ for (var i=0; i < this.props.report.Labels.length; i++) {
positiveSum.push(0);
negativeSum.push(0);
var labelX = x(i) + barStart + barWidth/2;
var labelY = height + 15;
labels.push((
-
{this.props.data.Labels[i]}
+
{this.props.report.Labels[i]}
));
labels.push((
@@ -103,14 +95,24 @@ module.exports = React.createClass({
for (var i=0-xAxisMarksEvery; i > minMax[0]; i -= xAxisMarksEvery)
makeXLabel(i);
- //TODO handle Values from current series?
var legendMap = {};
- for (var child in data) {
- childId++;
- var rectClasses = "chart-element chart-color" + (childId % 12);
- if (data.hasOwnProperty(child)) {
- for (var i=0; i < data[child].length; i++) {
- var value = data[child][i];
+ for (var child in this.props.report.FlattenedSeries) {
+ if (this.props.report.FlattenedSeries.hasOwnProperty(child)) {
+ childId++;
+ var childData = this.props.report.FlattenedSeries[child];
+ var rectClasses = "chart-element chart-color" + (childId % 12);
+ var self = this;
+ var rectOnClick = function() {
+ var childName = child;
+ var onSelectSeries = self.props.onSelectSeries;
+ return function() {
+ onSelectSeries(childName);
+ };
+ }();
+
+ var seriesBars = [];
+ for (var i=0; i < childData.length; i++) {
+ var value = childData[i];
if (value == 0)
continue;
legendMap[child] = childId;
@@ -124,10 +126,15 @@ module.exports = React.createClass({
negativeSum[i] += rectHeight;
}
- bars.push((
-
+ seriesBars.push((
+
));
}
+ bars.push((
+
+ {seriesBars}
+
+ ));
}
}
@@ -144,19 +151,17 @@ module.exports = React.createClass({
}
return (
-
-
-
+
);
}
});
diff --git a/js/constants/ReportConstants.js b/js/constants/ReportConstants.js
index 7e8a913..da955c3 100644
--- a/js/constants/ReportConstants.js
+++ b/js/constants/ReportConstants.js
@@ -2,5 +2,7 @@ var keyMirror = require('keymirror');
module.exports = keyMirror({
FETCH_REPORT: null,
- REPORT_FETCHED: null
+ REPORT_FETCHED: null,
+ SELECT_REPORT: null,
+ REPORT_SELECTED: null
});
diff --git a/js/containers/ReportsTabContainer.js b/js/containers/ReportsTabContainer.js
index 9a917e2..d9bb3e6 100644
--- a/js/containers/ReportsTabContainer.js
+++ b/js/containers/ReportsTabContainer.js
@@ -5,13 +5,15 @@ var ReportsTab = require('../components/ReportsTab');
function mapStateToProps(state) {
return {
- reports: state.reports
+ reports: state.reports,
+ selectedReport: state.selectedReport
}
}
function mapDispatchToProps(dispatch) {
return {
- onFetchReport: function(reportname) {dispatch(ReportActions.fetch(reportname))}
+ onFetchReport: function(reportname) {dispatch(ReportActions.fetch(reportname))},
+ onSelectReport: function(report, seriesTraversal) {dispatch(ReportActions.select(report, seriesTraversal))}
}
}
diff --git a/js/models.js b/js/models.js
index bf505bc..3efda47 100644
--- a/js/models.js
+++ b/js/models.js
@@ -399,16 +399,16 @@ Error.prototype.isError = function() {
function Series() {
this.Values = [];
- this.Children = {};
+ this.Series = {};
}
Series.prototype.toJSONobj = function() {
var json_obj = {};
json_obj.Values = this.Values;
- json_obj.Children = {};
- for (var child in this.Children) {
- if (this.Children.hasOwnProperty(child))
- json_obj.Children[child] = this.Children[child].toJSONobj();
+ json_obj.Series = {};
+ for (var child in this.Series) {
+ if (this.Series.hasOwnProperty(child))
+ json_obj.Series[child] = this.Series[child].toJSONobj();
}
return json_obj;
}
@@ -416,20 +416,20 @@ Series.prototype.toJSONobj = function() {
Series.prototype.fromJSONobj = function(json_obj) {
if (json_obj.hasOwnProperty("Values"))
this.Values = json_obj.Values;
- if (json_obj.hasOwnProperty("Children")) {
- for (var child in json_obj.Children) {
- if (json_obj.Children.hasOwnProperty(child))
- this.Children[child] = new Series();
- this.Children[child].fromJSONobj(json_obj.Children[child]);
+ if (json_obj.hasOwnProperty("Series")) {
+ for (var child in json_obj.Series) {
+ if (json_obj.Series.hasOwnProperty(child))
+ this.Series[child] = new Series();
+ this.Series[child].fromJSONobj(json_obj.Series[child]);
}
}
}
Series.prototype.mapReduceChildren = function(mapFn, reduceFn) {
var children = {}
- for (var child in this.Children) {
- if (this.Children.hasOwnProperty(child))
- children[child] = this.Children[child].mapReduce(mapFn, reduceFn);
+ for (var child in this.Series) {
+ if (this.Series.hasOwnProperty(child))
+ children[child] = this.Series[child].mapReduce(mapFn, reduceFn);
}
return children;
}
@@ -441,9 +441,9 @@ Series.prototype.mapReduce = function(mapFn, reduceFn) {
else
childValues.push(this.Values.slice());
- for (var child in this.Children) {
- if (this.Children.hasOwnProperty(child))
- childValues.push(this.Children[child].mapReduce(mapFn, reduceFn));
+ for (var child in this.Series) {
+ if (this.Series.hasOwnProperty(child))
+ childValues.push(this.Series[child].mapReduce(mapFn, reduceFn));
}
var reducedValues = [];
@@ -466,8 +466,11 @@ function Report() {
this.YAxisLabel = "";
this.Labels = [];
this.Series = {};
+ this.FlattenedSeries = {};
}
+Report.prototype.topLevelAccountName = "(top level)";
+
Report.prototype.toJSON = function() {
var json_obj = {};
json_obj.ReportId = this.ReportId;
diff --git a/js/reducers/MoneyGoReducer.js b/js/reducers/MoneyGoReducer.js
index bd534bc..8546436 100644
--- a/js/reducers/MoneyGoReducer.js
+++ b/js/reducers/MoneyGoReducer.js
@@ -8,6 +8,7 @@ var SecurityTemplateReducer = require('./SecurityTemplateReducer');
var SelectedAccountReducer = require('./SelectedAccountReducer');
var SelectedSecurityReducer = require('./SelectedSecurityReducer');
var ReportReducer = require('./ReportReducer');
+var SelectedReportReducer = require('./SelectedReportReducer');
var ErrorReducer = require('./ErrorReducer');
module.exports = Redux.combineReducers({
@@ -19,5 +20,6 @@ module.exports = Redux.combineReducers({
selectedAccount: SelectedAccountReducer,
selectedSecurity: SelectedSecurityReducer,
reports: ReportReducer,
+ selectedReport: SelectedReportReducer,
error: ErrorReducer
});
diff --git a/js/reducers/SelectedReportReducer.js b/js/reducers/SelectedReportReducer.js
new file mode 100644
index 0000000..87ae68c
--- /dev/null
+++ b/js/reducers/SelectedReportReducer.js
@@ -0,0 +1,23 @@
+var assign = require('object-assign');
+
+var ReportConstants = require('../constants/ReportConstants');
+var UserConstants = require('../constants/UserConstants');
+
+const initialState = {
+ report: null,
+ seriesTraversal: []
+};
+
+module.exports = function(state = initialState, action) {
+ switch (action.type) {
+ case ReportConstants.REPORT_SELECTED:
+ return {
+ report: action.report,
+ seriesTraversal: action.seriesTraversal
+ };
+ case UserConstants.USER_LOGGEDOUT:
+ return initialState;
+ default:
+ return state;
+ }
+};
diff --git a/reports.go b/reports.go
index d4067d3..7c15826 100644
--- a/reports.go
+++ b/reports.go
@@ -25,8 +25,8 @@ const (
const luaTimeoutSeconds time.Duration = 5 // maximum time a lua request can run for
type Series struct {
- Values []float64
- Children map[string]*Series
+ Values []float64
+ Series map[string]*Series
}
type Report struct {
diff --git a/reports_lua.go b/reports_lua.go
index efb0e0f..9da36fe 100644
--- a/reports_lua.go
+++ b/reports_lua.go
@@ -97,8 +97,8 @@ func luaReportSeries(L *lua.LState) int {
ud.Value = s
} else {
report.Series[name] = &Series{
- Children: make(map[string]*Series),
- Values: make([]float64, cap(report.Labels)),
+ Series: make(map[string]*Series),
+ Values: make([]float64, cap(report.Labels)),
}
ud.Value = report.Series[name]
}
@@ -157,8 +157,8 @@ func luaSeries__index(L *lua.LState) int {
switch field {
case "Value", "value":
L.Push(L.NewFunction(luaSeriesValue))
- case "Series", "series", "Child", "child":
- L.Push(L.NewFunction(luaSeriesChildren))
+ case "Series", "series":
+ L.Push(L.NewFunction(luaSeriesSeries))
default:
L.ArgError(2, "unexpected series attribute: "+field)
}
@@ -179,20 +179,20 @@ func luaSeriesValue(L *lua.LState) int {
return 0
}
-func luaSeriesChildren(L *lua.LState) int {
+func luaSeriesSeries(L *lua.LState) int {
parent := luaCheckSeries(L, 1)
name := L.CheckString(2)
ud := L.NewUserData()
- s, ok := parent.Children[name]
+ s, ok := parent.Series[name]
if ok {
ud.Value = s
} else {
- parent.Children[name] = &Series{
- Children: make(map[string]*Series),
- Values: make([]float64, cap(parent.Values)),
+ parent.Series[name] = &Series{
+ Series: make(map[string]*Series),
+ Values: make([]float64, cap(parent.Values)),
}
- ud.Value = parent.Children[name]
+ ud.Value = parent.Series[name]
}
L.SetMetatable(ud, L.GetTypeMetatable(luaSeriesTypeName))
L.Push(ud)
diff --git a/securities_lua.go b/securities_lua.go
index 77feb5b..6a9d347 100644
--- a/securities_lua.go
+++ b/securities_lua.go
@@ -53,7 +53,6 @@ func luaGetSecurities(L *lua.LState) int {
return 1
}
-// Registers my security type to given L.
func luaRegisterSecurities(L *lua.LState) {
mt := L.NewTypeMetatable(luaSecurityTypeName)
L.SetGlobal("security", mt)
diff --git a/static/css/reports.css b/static/css/reports.css
index f522620..45bba8a 100644
--- a/static/css/reports.css
+++ b/static/css/reports.css
@@ -28,7 +28,7 @@
}
.chart-color8 {
fill: #ff7f00;
- fill: #df5f00;
+ stroke: #df5f00;
}
.chart-color9 {
fill: #cab2d6;
@@ -50,7 +50,7 @@
.chart-element {
stroke-width: 0;
}
-.chart-element:hover {
+g.chart-series:hover .chart-element {
stroke-width: 2;
}
.chart-legend rect {