From 299589b9c39e709330ad9bd5c21eb6b563863e77 Mon Sep 17 00:00:00 2001 From: Gregory Eremin Date: Mon, 16 Mar 2015 23:53:54 +0700 Subject: [PATCH] Intractable stacked area chart --- app/scripts/src/charts/stacked_area_chart.jsx | 142 ++++++++++-------- app/styles/charts.css | 32 ++-- 2 files changed, 100 insertions(+), 74 deletions(-) diff --git a/app/scripts/src/charts/stacked_area_chart.jsx b/app/scripts/src/charts/stacked_area_chart.jsx index 31b9240..5600e53 100644 --- a/app/scripts/src/charts/stacked_area_chart.jsx +++ b/app/scripts/src/charts/stacked_area_chart.jsx @@ -75,20 +75,14 @@ var StackedAreaChart = React.createClass({ node.className = 'sachart-container focused item-'+ i; }, - handleFocusOut: function(i) { + handleFocusOut: function() { var node = this.refs.container.getDOMNode(); node.className = 'sachart-container'; }, handleNewData: function() { // Group commits by items - var weeksList = _.chain(this.state.rawData) - .pluck('week') - .uniq() - .sort() - .reverse() - .take(this.maxWeeks) - .value(); + var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value(); var counts = _.reduce(this.state.rawData, function(res, el) { if (weeksList.indexOf(el.week) === -1) { @@ -103,7 +97,7 @@ var StackedAreaChart = React.createClass({ }, {}); // Extract top items from - var top = _.chain(_.pairs(counts)) // Take [item, count] pairs from counts object + var top = _(_.pairs(counts)) // Take [item, count] pairs from counts object .sortBy(1).reverse() // sort them by count (descending) .take(this.numElements) // take first N pairs .pluck(0) // keep only items, omit the counts @@ -125,9 +119,7 @@ var StackedAreaChart = React.createClass({ return res; }, {}); - var max = _.max(_.map(weeksList, function(week) { - return _.sum(_.values(weeks[week])); - })); + var max = _.max(_.map(weeksList, function(week){ return _.sum(_.values(weeks[week])); })); this.setState({ top: top, @@ -141,11 +133,12 @@ var StackedAreaChart = React.createClass({ var maxWidth = this.state.canvasWidth, maxHeight = this.height; - var d = _.map(this.buildDots(points), function(dot) { - return 'L'+ dot[0] +','+ dot[1]; - }); - d.unshift('M0,'+ maxHeight); - d.push('L'+ maxWidth +','+ maxHeight +'Z'); + var dots = this.buildDots(points); + var first = dots.shift(); + var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; }); + d.unshift('M'+ first.x +','+ first.y); + d.push('L'+ maxWidth +','+ maxHeight); + d.push('L0,'+ maxHeight +' Z'); return d.join(' '); }, @@ -157,7 +150,21 @@ var StackedAreaChart = React.createClass({ len = points.length; return _.map(points, function(point, i) { - return [Math.floor(i/(len-1)*maxWidth), Math.floor(maxHeight - point)]; + point.x = i/(len-1)*maxWidth; + point.y = maxHeight - point.point; + + if (point.x < 10) { // Radius + point.x = 10 + } else if (point.x > maxWidth - 10) { + point.x = maxWidth - 10; + } + if (point.y < 10) { + point.y = 10; + } else if (point.y > maxHeight - 10) { + point.y = maxHeight - 10; + } + + return point; }); }, @@ -167,7 +174,8 @@ var StackedAreaChart = React.createClass({ top = this.state.top, max = this.state.max; - var points = _.chain(this.state.weeks) + // [week, [{val, point}, ...]] + var points = _(this.state.weeks) .map(function(items, week) { var values = _.map(top, function(item) { return items[item] || 0; @@ -175,8 +183,11 @@ var StackedAreaChart = React.createClass({ var sum = 0; var points = _.map(values, function(val) { - sum += Math.floor(val/max*maxHeight*0.96); - return sum; + sum += val/max*maxHeight*0.96; + return { + val: val, + point: sum + }; }); return [week, points]; @@ -187,6 +198,7 @@ var StackedAreaChart = React.createClass({ .reverse() .value(); + // [item, [{val, point}, ...]] var paths = _.map(top, function(item, i) { var itemPoints = _.map(points, function(pair) { return pair[1][i]; @@ -194,62 +206,63 @@ var StackedAreaChart = React.createClass({ return[item, itemPoints]; }); - var colors = {}; var areas = _.map(paths, function(pair, i) { - var item = pair[0], path = pair[1]; - if (item !== null) { - colors[item] = Colors[i]; - } + var item = pair[0], + path = pair[1]; + return ( + onMouseOver={this.handleFocusIn.bind(this, i)} /> ); }.bind(this)); - var dots = _.map(paths, function(pair, i) { - var item = pair[0], path = pair[1]; - var dots = this.buildDots(path); - var lastY = 0; - var renderDot = function(dot, j) { - if (lastY === dot[1]) { - return null; - } - lastY = dot[1]; - return ( - - ); - }; - - return dots.map(renderDot); - }.bind(this)); - var words = this.words, - who = this.getParams().repo || this.getParams().team || this.getParams().user || this.getParams().org; + who = this.getParams().repo || + this.getParams().team || + this.getParams().user || + this.getParams().org; var params = Object.keys(this.getParams()); params.splice(params.indexOf('org'), 1); var subject = params[0]; - var renderLegend = function(pair, i){ + var renderDot = function(item, i, dot, j) { + if (dot.val === 0) { + return null; + } return ( -
  • + ); + }.bind(this); + + var renderedDots = _.map(paths, function(pair, i) { + var item = pair[0], path = pair[1]; + var dots = this.buildDots(path); + return dots.map(renderDot.bind(this, item, i)); + }.bind(this)); + + var renderLegend = function(item, i){ + return ( +
  • -
    - {pair[0]} +
    + {item}
  • ); }.bind(this); + var legend = _(paths).pluck(0).filter(function(el){ return el !== null; }).value(); + return (
    @@ -267,13 +280,14 @@ var StackedAreaChart = React.createClass({ onChange={this.handleFilter.bind(this, 'item')} />
    {areas.reverse()} - {dots} + {renderedDots}
      - {_.pairs(colors).map(renderLegend)} + {legend.map(renderLegend)}
    ); @@ -305,7 +319,6 @@ var StackedArea = React.createClass({ d={this.state.lastd || this.props.d} fill={this.props.color} onMouseOver={this.props.onMouseOver} - onMouseOut={this.props.onMouseOut} shapeRendering="optimizeQuality" /> ); } @@ -314,7 +327,7 @@ var StackedArea = React.createClass({ var Dot = React.createClass({ mixins: [ChartAnimationMixin], - radius: 12, + radius: 10, getInitialState: function() { return {}; @@ -333,15 +346,18 @@ var Dot = React.createClass({ render: function() { return ( - + + fill="#fff" + stroke="#f0f0f0" + strokeWidth="1" /> + x={this.props.x} + y={this.props.y+4} + textAnchor="middle"> {this.props.value} diff --git a/app/styles/charts.css b/app/styles/charts.css index 30ed2be..ba916e2 100644 --- a/app/styles/charts.css +++ b/app/styles/charts.css @@ -62,23 +62,23 @@ } .sachart-container.focused .path { - opacity: 0.03; + fill: #f0f0f0; } .sachart-container.focused .label { opacity: 0.3; } -.sachart-container.item-0 .path-0, .sachart-container.item-0 .label-0 { opacity: 1; } -.sachart-container.item-1 .path-1, .sachart-container.item-1 .label-1 { opacity: 1; } -.sachart-container.item-2 .path-2, .sachart-container.item-2 .label-2 { opacity: 1; } -.sachart-container.item-3 .path-3, .sachart-container.item-3 .label-3 { opacity: 1; } -.sachart-container.item-4 .path-4, .sachart-container.item-4 .label-4 { opacity: 1; } -.sachart-container.item-5 .path-5, .sachart-container.item-5 .label-5 { opacity: 1; } -.sachart-container.item-6 .path-6, .sachart-container.item-6 .label-6 { opacity: 1; } -.sachart-container.item-7 .path-7, .sachart-container.item-7 .label-7 { opacity: 1; } -.sachart-container.item-8 .path-8, .sachart-container.item-8 .label-8 { opacity: 1; } -.sachart-container.item-9 .path-9, .sachart-container.item-9 .label-9 { opacity: 1; } +.sachart-container.item-0 .path-0 { fill: #EA8369; } +.sachart-container.item-1 .path-1 { fill: #FCCD67; } +.sachart-container.item-2 .path-2 { fill: #B5A9F2; } +.sachart-container.item-3 .path-3 { fill: #9DD7F2; } +.sachart-container.item-4 .path-4 { fill: #FFA3A4; } +.sachart-container.item-5 .path-5 { fill: #A8EAA8; } +.sachart-container.item-6 .path-6 { fill: #F9D08B; } +.sachart-container.item-7 .path-7 { fill: #6CCECB; } +.sachart-container.item-8 .path-8 { fill: #F9E559; } +.sachart-container.item-9 .path-9 { fill: #FFBAD2; } .sachart-container.item-0 .dot-0 { display: inline; } .sachart-container.item-1 .dot-1 { display: inline; } .sachart-container.item-2 .dot-2 { display: inline; } @@ -89,6 +89,16 @@ .sachart-container.item-7 .dot-7 { display: inline; } .sachart-container.item-8 .dot-8 { display: inline; } .sachart-container.item-9 .dot-9 { display: inline; } +.sachart-container.item-0 .label-0 { opacity: 1; } +.sachart-container.item-1 .label-1 { opacity: 1; } +.sachart-container.item-2 .label-2 { opacity: 1; } +.sachart-container.item-3 .label-3 { opacity: 1; } +.sachart-container.item-4 .label-4 { opacity: 1; } +.sachart-container.item-5 .label-5 { opacity: 1; } +.sachart-container.item-6 .label-6 { opacity: 1; } +.sachart-container.item-7 .label-7 { opacity: 1; } +.sachart-container.item-8 .label-8 { opacity: 1; } +.sachart-container.item-9 .label-9 { opacity: 1; } .selector {