mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-12-26 15:42:27 -05:00
reports: Allow drilling down
This commit is contained in:
parent
4d642d1772
commit
b443963375
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetch: fetch
|
||||
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,
|
||||
select: select
|
||||
};
|
||||
|
@ -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 = (<StackedBarChart data={this.props.reports['monthly_expenses']} />);
|
||||
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((
|
||||
<Button bsStyle="link"
|
||||
onClick={navOnClick}>
|
||||
{name}
|
||||
</Button>
|
||||
));
|
||||
titleTracks.push((<span>/</span>));
|
||||
seriesTraversal.push(this.props.selectedReport.seriesTraversal[i]);
|
||||
}
|
||||
if (titleTracks.length == 0)
|
||||
titleTracks.push((
|
||||
<Button bsStyle="link">
|
||||
{this.props.selectedReport.report.Title}
|
||||
</Button>
|
||||
));
|
||||
else
|
||||
titleTracks.push((
|
||||
<Button bsStyle="link">
|
||||
{this.props.selectedReport.seriesTraversal[this.props.selectedReport.seriesTraversal.length-1]}
|
||||
</Button>
|
||||
));
|
||||
|
||||
report = (<Panel header={titleTracks}>
|
||||
<StackedBarChart
|
||||
report={this.props.selectedReport.report}
|
||||
onSelectSeries={this.onSelectSeries}
|
||||
seriesTraversal={this.props.selectedReport.seriesTraversal} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{report}
|
||||
|
@ -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((
|
||||
<text x={labelX} y={labelY} transform={"rotate(45 "+labelX+" "+labelY+")"}>{this.props.data.Labels[i]}</text>
|
||||
<text x={labelX} y={labelY} transform={"rotate(45 "+labelX+" "+labelY+")"}>{this.props.report.Labels[i]}</text>
|
||||
));
|
||||
labels.push((
|
||||
<line className="axis-tick" x1={labelX} y1={height-3} x2={labelX} y2={height+3} />
|
||||
@ -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) {
|
||||
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);
|
||||
if (data.hasOwnProperty(child)) {
|
||||
for (var i=0; i < data[child].length; i++) {
|
||||
var value = data[child][i];
|
||||
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((
|
||||
<rect className={rectClasses} x={x(i) + barStart} y={rectY} width={barWidth} height={rectHeight} rx={1} ry={1}/>
|
||||
seriesBars.push((
|
||||
<rect onClick={rectOnClick} className={rectClasses} x={x(i) + barStart} y={rectY} width={barWidth} height={rectHeight} rx={1} ry={1}/>
|
||||
));
|
||||
}
|
||||
bars.push((
|
||||
<g className="chart-series">
|
||||
{seriesBars}
|
||||
</g>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +151,6 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel header={this.props.data.Title}>
|
||||
<svg height={height + 2*yMargin} width={width + 2*xMargin + legendWidth}>
|
||||
<g className="stacked-bar-chart" transform={"translate("+xMargin+" "+yMargin+")"}>
|
||||
{bars}
|
||||
@ -156,7 +162,6 @@ module.exports = React.createClass({
|
||||
{legend}
|
||||
</g>
|
||||
</svg>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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))}
|
||||
}
|
||||
}
|
||||
|
||||
|
35
js/models.js
35
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;
|
||||
|
@ -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
|
||||
});
|
||||
|
23
js/reducers/SelectedReportReducer.js
Normal file
23
js/reducers/SelectedReportReducer.js
Normal file
@ -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;
|
||||
}
|
||||
};
|
@ -26,7 +26,7 @@ const luaTimeoutSeconds time.Duration = 5 // maximum time a lua request can run
|
||||
|
||||
type Series struct {
|
||||
Values []float64
|
||||
Children map[string]*Series
|
||||
Series map[string]*Series
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
|
@ -97,7 +97,7 @@ func luaReportSeries(L *lua.LState) int {
|
||||
ud.Value = s
|
||||
} else {
|
||||
report.Series[name] = &Series{
|
||||
Children: make(map[string]*Series),
|
||||
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),
|
||||
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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user