mirror of
				https://github.com/aclindsa/moneygo.git
				synced 2025-11-03 18:13:27 -05:00 
			
		
		
		
	WIP: Stacked bar chart
This commit is contained in:
		@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user