diff --git a/js/components/ReportsTab.js b/js/components/ReportsTab.js index 9924db7..a96c143 100644 --- a/js/components/ReportsTab.js +++ b/js/components/ReportsTab.js @@ -1,24 +1,6 @@ var React = require('react'); -var ReactDOM = require('react-dom'); -var ReactBootstrap = require('react-bootstrap'); -var Grid = ReactBootstrap.Grid; -var Row = ReactBootstrap.Row; -var Col = ReactBootstrap.Col; -var Form = ReactBootstrap.Form; -var FormGroup = ReactBootstrap.FormGroup; -var FormControl = ReactBootstrap.FormControl; -var ControlLabel = ReactBootstrap.ControlLabel; -var Button = ReactBootstrap.Button; -var ButtonGroup = ReactBootstrap.ButtonGroup; -var ButtonToolbar = ReactBootstrap.ButtonToolbar; -var Glyphicon = ReactBootstrap.Glyphicon; -var ListGroup = ReactBootstrap.ListGroup; -var ListGroupItem = ReactBootstrap.ListGroupItem; -var Modal = ReactBootstrap.Modal; -var Panel = ReactBootstrap.Panel; - -var Combobox = require('react-widgets').Combobox; +var StackedBarChart = require('../components/StackedBarChart'); module.exports = React.createClass({ displayName: "ReportsTab", @@ -29,9 +11,13 @@ module.exports = React.createClass({ this.props.onFetchReport("monthly_expenses"); }, render: function() { - console.log(this.props.reports); + report = []; + if (this.props.reports['monthly_expenses']) + report = (); return ( -
hello
+
+ {report} +
); } }); diff --git a/js/components/StackedBarChart.js b/js/components/StackedBarChart.js new file mode 100644 index 0000000..56b3783 --- /dev/null +++ b/js/components/StackedBarChart.js @@ -0,0 +1,162 @@ +var d3 = require('d3'); +var React = require('react'); + +var Panel = require('react-bootstrap').Panel; + +module.exports = React.createClass({ + displayName: "StackedBarChart", + calcMinMax: function(data) { + var children = []; + for (var child in data) { + if (data.hasOwnProperty(child)) + children.push(data[child]); + } + + var positiveValues = [0]; + var negativeValues = [0]; + if (children.length > 0 && children[0].length > 0) { + for (var j = 0; j < children[0].length; j++) { + positiveValues.push(children.reduce(function(accum, curr, i, arr) { + if (arr[i][j] > 0) + return accum + arr[i][j]; + return accum; + }, 0)); + negativeValues.push(children.reduce(function(accum, curr, i, arr) { + if (arr[i][j] < 0) + return accum + arr[i][j]; + return accum; + }, 0)); + } + } + + return [Math.min.apply(Math, negativeValues), Math.max.apply(Math, positiveValues)]; + }, + calcAxisMarkSeparation: function(minMax, height, ticksPerHeight) { + var targetTicks = height / ticksPerHeight; + var range = minMax[1]-minMax[0]; + var rangePerTick = range/targetTicks; + var roundOrder = Math.floor(Math.log(rangePerTick) / Math.LN10); + var roundTo = Math.pow(10, roundOrder); + 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; + xMargin = 70; + yMargin = 70; + height -= yMargin*2; + width -= xMargin*2; + + var minMax = this.calcMinMax(data); + var y = d3.scaleLinear() + .range([0, height]) + .domain(minMax); + + var xAxisMarksEvery = this.calcAxisMarkSeparation(minMax, height, 40); + + var x = d3.scaleLinear() + .range([0, width]) + .domain([0, this.props.data.Labels.length + 0.5]); + + var bars = []; + var labels = []; + + var barWidth = x(0.75); + var barStart = x(0.25) + (x(1) - barWidth)/2; + var childId=0; + + // Add Y axis marks and labels, and initialize positive- and + // negativeSum arrays + var positiveSum = []; + var negativeSum = []; + for (var i=0; i < this.props.data.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]} + )); + labels.push(( + + )); + } + + // Make X axis marks and labels + var makeXLabel = function(value) { + labels.push(( + + )); + labels.push(( + {value} + )); + } + for (var i=0; i < minMax[1]; i+= xAxisMarksEvery) + makeXLabel(i); + 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]; + if (value == 0) + continue; + legendMap[child] = childId; + if (value > 0) { + rectHeight = y(value) - y(0); + positiveSum[i] += rectHeight; + rectY = height - y(0) - positiveSum[i]; + } else { + rectHeight = y(0) - y(value); + rectY = height - y(0) + negativeSum[i]; + negativeSum[i] += rectHeight; + } + + bars.push(( + + )); + } + } + } + + var legend = []; + for (var series in legendMap) { + var legendClasses = "chart-color" + (legendMap[series] % 12); + var legendY = (legendMap[series] - 1)*15; + legend.push(( + + )); + legend.push(( + {series} + )); + } + + return ( + + + + {bars} + + + {labels} + + + {legend} + + + + ); + } +}); diff --git a/js/models.js b/js/models.js index 3a78419..bf505bc 100644 --- a/js/models.js +++ b/js/models.js @@ -425,6 +425,39 @@ Series.prototype.fromJSONobj = function(json_obj) { } } +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); + } + return children; +} + +Series.prototype.mapReduce = function(mapFn, reduceFn) { + var childValues = []; + if (mapFn) + childValues.push(this.Values.map(mapFn)); + 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)); + } + + var reducedValues = []; + if (reduceFn && childValues.length > 0 && childValues[0].length > 0) { + for (var j = 0; j < childValues[0].length; j++) { + reducedValues.push(childValues.reduce(function(accum, curr, i, arr) { + return reduceFn(accum, arr[i][j]); + }, childValues[0][j])); + } + } + + return reducedValues; +} + function Report() { this.ReportId = ""; this.Title = ""; @@ -475,6 +508,19 @@ Report.prototype.fromJSON = function(json_input) { } } +Report.prototype.mapReduceChildren = function(mapFn, reduceFn) { + var series = {} + for (var child in this.Series) { + if (this.Series.hasOwnProperty(child)) + series[child] = this.Series[child].mapReduce(mapFn, reduceFn); + } + return series; +} + +Report.prototype.mapReduceSeries = function(mapFn, reduceFn) { + return this.mapReduceChildren(mapFn, reduceFn); +} + module.exports = models = { // Classes diff --git a/package.json b/package.json index 7cb61ea..88a4d49 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "big.js": "^3.1.3", "browserify": "^13.1.0", "cldr-data": "^29.0.2", + "d3": "^4.5.0", "globalize": "^1.1.1", "keymirror": "^0.1.1", "react": "^15.3.2", diff --git a/static/css/reports.css b/static/css/reports.css new file mode 100644 index 0000000..f522620 --- /dev/null +++ b/static/css/reports.css @@ -0,0 +1,67 @@ +.chart-color1 { + fill: #a6cee3; + stroke: #86aec3; +} +.chart-color2 { + fill: #1f78b4; + stroke: #005894; +} +.chart-color3 { + fill: #b2df8a; + stroke: #92bf6a; +} +.chart-color4 { + fill: #33a02c; + stroke: #13800c; +} +.chart-color5 { + fill: #fb9a99; + stroke: #db7a79; +} +.chart-color6 { + fill: #e31a1c; + stroke: #c30000; +} +.chart-color7 { + fill: #fdbf6f; + stroke: #dd9f4f; +} +.chart-color8 { + fill: #ff7f00; + fill: #df5f00; +} +.chart-color9 { + fill: #cab2d6; + stroke: #aa92b6; +} +.chart-color10 { + fill: #6a3d9a; + stroke: #4a1d7a; +} +.chart-color11 { + fill: #ffff99; + stroke: #dfdf79; +} +.chart-color12 { + fill: #b15928; + stroke: #913908; +} + +.chart-element { + stroke-width: 0; +} +.chart-element:hover { + stroke-width: 2; +} +.chart-legend rect { + stroke-width: 2; +} + +.axis { + stroke: #000; + stroke-width: 2; +} +.axis-tick { + stroke: #000; + stroke-width: 1; +} diff --git a/static/stylesheet.css b/static/css/stylesheet.css similarity index 98% rename from static/stylesheet.css rename to static/css/stylesheet.css index cdc733a..9da7490 100644 --- a/static/stylesheet.css +++ b/static/css/stylesheet.css @@ -1,3 +1,5 @@ +@import url("reports.css"); + html, body { height: 100%; } diff --git a/static/index.html b/static/index.html index 9e4e78b..34b4c9c 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@ - +