mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-10-31 16:00:05 -04:00
WIP: Stacked bar chart
This commit is contained in:
parent
d3d79fb613
commit
4d642d1772
@ -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 = (<StackedBarChart data={this.props.reports['monthly_expenses']} />);
|
||||
return (
|
||||
<div>hello</div>
|
||||
<div>
|
||||
{report}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
162
js/components/StackedBarChart.js
Normal file
162
js/components/StackedBarChart.js
Normal file
@ -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((
|
||||
<text x={labelX} y={labelY} transform={"rotate(45 "+labelX+" "+labelY+")"}>{this.props.data.Labels[i]}</text>
|
||||
));
|
||||
labels.push((
|
||||
<line className="axis-tick" x1={labelX} y1={height-3} x2={labelX} y2={height+3} />
|
||||
));
|
||||
}
|
||||
|
||||
// Make X axis marks and labels
|
||||
var makeXLabel = function(value) {
|
||||
labels.push((
|
||||
<line className="axis-tick" x1={-3} y1={height - y(value)} x2={3} y2={height - y(value)} />
|
||||
));
|
||||
labels.push((
|
||||
<text is x={-10} y={height - y(value) + 6} text-anchor={"end"}>{value}</text>
|
||||
));
|
||||
}
|
||||
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((
|
||||
<rect className={rectClasses} x={x(i) + barStart} y={rectY} width={barWidth} height={rectHeight} rx={1} ry={1}/>
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var legend = [];
|
||||
for (var series in legendMap) {
|
||||
var legendClasses = "chart-color" + (legendMap[series] % 12);
|
||||
var legendY = (legendMap[series] - 1)*15;
|
||||
legend.push((
|
||||
<rect className={legendClasses} x={0} y={legendY} width={10} height={10}/>
|
||||
));
|
||||
legend.push((
|
||||
<text x={0 + 15} y={legendY + 10}>{series}</text>
|
||||
));
|
||||
}
|
||||
|
||||
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}
|
||||
<line className="axis x-axis" x1={0} y1={height} x2={width} y2={height} />
|
||||
<line className="axis y-axis" x1={0} y1={0} x2={0} y2={height} />
|
||||
{labels}
|
||||
</g>
|
||||
<g className="chart-legend" transform={"translate("+(width + 2*xMargin)+" "+yMargin+")"}>
|
||||
{legend}
|
||||
</g>
|
||||
</svg>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
});
|
46
js/models.js
46
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
|
||||
|
@ -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",
|
||||
|
67
static/css/reports.css
Normal file
67
static/css/reports.css
Normal file
@ -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;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
@import url("reports.css");
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">
|
||||
<link rel="stylesheet" href="static/react-widgets/css/react-widgets.css">
|
||||
<link rel="stylesheet" href="static/stylesheet.css">
|
||||
<link rel="stylesheet" href="static/css/stylesheet.css">
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user