mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-12-26 07:33:21 -05:00
WIP: Stacked bar chart
This commit is contained in:
parent
d3d79fb613
commit
4d642d1772
@ -1,24 +1,6 @@
|
|||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
|
||||||
|
|
||||||
var ReactBootstrap = require('react-bootstrap');
|
var StackedBarChart = require('../components/StackedBarChart');
|
||||||
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;
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "ReportsTab",
|
displayName: "ReportsTab",
|
||||||
@ -29,9 +11,13 @@ module.exports = React.createClass({
|
|||||||
this.props.onFetchReport("monthly_expenses");
|
this.props.onFetchReport("monthly_expenses");
|
||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
console.log(this.props.reports);
|
report = [];
|
||||||
|
if (this.props.reports['monthly_expenses'])
|
||||||
|
report = (<StackedBarChart data={this.props.reports['monthly_expenses']} />);
|
||||||
return (
|
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() {
|
function Report() {
|
||||||
this.ReportId = "";
|
this.ReportId = "";
|
||||||
this.Title = "";
|
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 = {
|
module.exports = models = {
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"big.js": "^3.1.3",
|
"big.js": "^3.1.3",
|
||||||
"browserify": "^13.1.0",
|
"browserify": "^13.1.0",
|
||||||
"cldr-data": "^29.0.2",
|
"cldr-data": "^29.0.2",
|
||||||
|
"d3": "^4.5.0",
|
||||||
"globalize": "^1.1.1",
|
"globalize": "^1.1.1",
|
||||||
"keymirror": "^0.1.1",
|
"keymirror": "^0.1.1",
|
||||||
"react": "^15.3.2",
|
"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 {
|
html, body {
|
||||||
height: 100%;
|
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.min.css">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.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/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>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user