WIP: Stacked bar chart

This commit is contained in:
Aaron Lindsay 2017-02-15 20:56:41 -05:00
parent d3d79fb613
commit 4d642d1772
7 changed files with 286 additions and 22 deletions

View File

@ -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>
);
}
});

View 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>
);
}
});

View File

@ -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

View File

@ -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
View 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;
}

View File

@ -1,3 +1,5 @@
@import url("reports.css");
html, body {
height: 100%;
}

View File

@ -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>