Skip to content

Commit

Permalink
feat(charts): improve pie and gauge components
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Benitte committed Apr 13, 2016
1 parent dc2b8b1 commit 7a73927
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 106 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"del": "^1.1.1",
"dotenv": "^0.5.1",
"express": "^4.10.6",
"font-awesome": "^4.2.0",
"font-awesome": "4.5.0",
"glob": "^4.3.2",
"gulp": "^3.8.10",
"gulp-flatten": "0.0.4",
Expand Down
58 changes: 37 additions & 21 deletions src/browser/components/charts/Gauge.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,45 @@ import Pie from './Pie';

class Gauge extends Component {
componentDidMount() {
let { spacing, donutRatio, handAnchorRatio, handLengthRatio, transitionDuration } = this.props;
const { spacing, donutRatio, handAnchorRatio, handLengthRatio, transitionDuration } = this.props;

this.pie = new Pie(React.findDOMNode(this.refs.svg), {
spacing: spacing,
donutRatio: donutRatio,
handAnchorRatio: handAnchorRatio,
handLengthRatio: handLengthRatio,
transitionDuration: transitionDuration,
gauge: true,
startAngle: -120,
endAngle: 120
spacing,
donutRatio,
handAnchorRatio,
handLengthRatio,
transitionDuration,
gauge: true,
startAngle: -120,
endAngle: 120
});
}

shouldComponentUpdate(data) {
let { ranges, value } = data;
const { ranges, value } = data;
const { enableLegends } = this.props;

let wrapper = React.findDOMNode(this);
const wrapper = React.findDOMNode(this);
let legends = [];
if (enableLegends) {
legends = ranges.map((range, id) => ({
id,
label: range.upperBound,
count: id === 0 ? range.upperBound : (range.upperBound - ranges[id -1].upperBound)
}));
}

this.pie
.size(wrapper.offsetWidth, wrapper.offsetHeight)
.draw(ranges.map((range, i) => {
return {
id: i,
.draw(
ranges.map((range, id) => ({
id,
color: range.color,
count: i === 0 ? range.upperBound : (range.upperBound - ranges[i -1].upperBound)
};
}), value)
count: id === 0 ? range.upperBound : (range.upperBound - ranges[id -1].upperBound)
})),
value,
legends
)
;

return false;
Expand All @@ -47,24 +58,29 @@ class Gauge extends Component {
}

Gauge.propTypes = {
spacing: PropTypes.number.isRequired,
spacing: PropTypes.object.isRequired,
donutRatio: PropTypes.number.isRequired,
handAnchorRatio: PropTypes.number.isRequired,
handLengthRatio: PropTypes.number.isRequired,
transitionDuration: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
enableLegends: PropTypes.bool.isRequired,
ranges: PropTypes.arrayOf(PropTypes.shape({
upperBound: PropTypes.number.isRequired,
color: PropTypes.string.isRequired
})).isRequired
};

Gauge.displayName = 'Gauge';

Gauge.defaultProps = {
spacing: 0.1,
spacing: {},
donutRatio: 0.7,
handAnchorRatio: 0.05,
handLengthRatio: 0.85,
transitionDuration: 600
transitionDuration: 600,
enableLegends: true
};

export { Gauge as default };

export default Gauge;
161 changes: 120 additions & 41 deletions src/browser/components/charts/Pie.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@ import _ from 'lodash';

class Pie {
constructor(element, options) {
this.svg = d3.select(element);
this.arcsContainer = this.svg.append('g').attr('class', 'arcs');
this.paths = this.arcsContainer.selectAll('path');
this.shadowContainer = this.svg.append('g').attr('class', 'pie_shadow');
this.shadowPath = this.shadowContainer.append('path');
this.lightContainer = this.svg.append('g').attr('class', 'pie_light');
this.lightPath = this.lightContainer.append('path');
this.svg = d3.select(element);
this.arcsContainer = this.svg.append('g').attr('class', 'arcs');
this.paths = this.arcsContainer.selectAll('.pie_slice');
this.arcsOutline = this.arcsContainer.append('path').attr('class', 'pie_outline');
this.legendsContainer = this.svg.append('g').attr('class', 'pie_svg_legends');
this.legends = this.legendsContainer.selectAll('.pie_svg_legend');

this.options = _.merge({
sort: null,
gauge: false,
handAnchorRatio: 0.03,
handLengthRatio: 0.7,
startAngle: 0,
endAngle: 360
endAngle: 360,
spacing: _.merge({
top: 10,
right: 10,
bottom: 10,
left: 10
}, options.spacing || {})
}, options);

let { sort, gauge, padAngle, startAngle, endAngle } = this.options;
const { sort, gauge, padAngle, startAngle, endAngle } = this.options;

if (gauge === true) {
this.hand = this.svg.append('g').attr('class', 'pie_hand');
this.handAnchor = this.hand.append('circle').attr('class', 'pie_hand_anchor');
this.handLine = this.hand.append('path').attr('class', 'pie_hand_line');
this.hand = this.svg.append('g').attr('class', 'pie_gauge_needle');
this.handAnchor = this.hand.append('circle').attr('class', 'pie_gauge_anchor');
this.handBase = this.hand.append('circle').attr('class', 'pie_gauge_needle_base');
this.handLine = this.hand.append('path').attr('class', 'pie_gauge_needle_arrow');
this.angleScale = d3.scale.linear().range([startAngle, endAngle]);
}

Expand All @@ -52,7 +58,7 @@ class Pie {
return this;
}

draw(data, gaugeVal) {
draw(data, gaugeVal, legends = []) {
let prevData = this.paths.data();
let newData = this.pie(data);

Expand All @@ -61,32 +67,44 @@ class Pie {
height: this.height
});

let { donutRatio, spacing, transitionDuration, gauge } = this.options;
let { spacing, donutRatio, transitionDuration, gauge } = this.options;

let centerX = this.width / 2;
let centerY = this.height / 2;
let minSize = Math.min(this.width, this.height);
let radius = minSize / 2 - minSize * spacing;
const utilWidth = this.width - spacing.left - spacing.right;
const utilHeight = this.height - spacing.top - spacing.bottom;

if (utilWidth < 1 || utilHeight < 1) {
return;
}

let centerX = utilWidth / 2 + spacing.left;
let centerY = utilHeight / 2 + spacing.top;
let minSize = Math.min(utilWidth, utilHeight);
let radius = minSize / 2;
let innerRadius = radius * donutRatio;

const line = d3.svg.line()
.x(d => d.x)
.y(d => d.y)
;

if (gauge === true) {
if (gaugeVal === undefined) {
throw 'Pie: gauge value is undefined and gauge option is set to true';
}

let { handLengthRatio, handAnchorRatio } = this.options;
const { handLengthRatio, handAnchorRatio } = this.options;

let totalCount = d3.sum(data, d => d.count);
const totalCount = d3.sum(data, d => d.count);
this.angleScale.domain([0, totalCount]);

this.hand
.attr('transform', `translate(${ centerX },${ centerY }) rotate(${ this.angleScale(Math.min(gaugeVal, totalCount)) } 0 0)`)
.attr('transform', `translate(${ centerX },${ centerY })`)
;
this.handAnchor.attr('r', radius * handAnchorRatio);
this.handBase.attr('r', radius * handAnchorRatio);

var line = d3.svg.line()
.x(d => d.x)
.y(d => d.y)
this.handLine.transition()
.duration(transitionDuration)
.attr('transform', `rotate(${ this.angleScale(Math.min(gaugeVal, totalCount)) } 0 0)`)
;

this.handLine.attr('d', line([
Expand All @@ -97,22 +115,6 @@ class Pie {
]));
}

let shadowArc = d3.svg.arc()
.outerRadius(radius * (donutRatio + (1 - donutRatio) / 8))
.innerRadius(innerRadius)
;

this.shadowContainer.attr('transform', `translate(${ centerX },${ centerY })`);
this.shadowPath.attr('d', shadowArc({ startAngle: this.pie.startAngle(), endAngle: this.pie.endAngle() }));

let lightArc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius * (donutRatio + (1 - donutRatio) / 8 * 7))
;

this.lightContainer.attr('transform', `translate(${ centerX },${ centerY })`);
this.lightPath.attr('d', lightArc({ startAngle: this.pie.startAngle(), endAngle: this.pie.endAngle() }));

let arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(innerRadius)
Expand All @@ -123,12 +125,18 @@ class Pie {
this.paths = this.paths.data(newData, Pie.dataKey);

this.paths.enter().append('path')
.attr('class', 'pie_slice')
.each(function (d, i) {
this._current = Pie.findNeighborArc(i, prevData, newData) || d;
})
.attr('fill', d => d.data.color)
;

this.arcsOutline.attr('d', arc({
startAngle: Pie.degreesToRadians(this.options.startAngle),
endAngle: Pie.degreesToRadians(this.options.endAngle)
}));

// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
Expand Down Expand Up @@ -156,13 +164,84 @@ class Pie {
.attr('fill', d => d.data.color)
;

// —————————————————————————————————————————————————————————————————————————————————————————————————————————————
// legends
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————
let legendsArc = d3.svg.arc()
.innerRadius(radius + 24)
.outerRadius(radius + 24)
;

this.legendsContainer.attr('transform', `translate(${ centerX },${ centerY })`);

this.legends = this.legends.data(this.pie(legends));

this.legends.enter().append('g')
.attr('class', 'pie_svg_legend')
.each(function (d) {
const elem = d3.select(this);
elem.append('path')
.attr('d', line([
{ x: -9, y: 0 }, // eslint-disable-line key-spacing
{ x: 9, y: 0 }, // eslint-disable-line key-spacing
{ x: 0, y: 18 } // eslint-disable-line key-spacing
]));
;
elem.append('rect')
.attr('rx', 3)
.attr('ry', 3)
;
elem.append('text')
.attr('alignment-baseline', 'middle')
;
})
;

this.legends
.attr('transform', (d) => {
d.startAngle = d.endAngle;
const centroid = legendsArc.centroid(d);
d.x = centroid[0];
d.y = centroid[1];

return `translate(${d.x}, ${d.y})`;
})
.each(function (d) {
const elem = d3.select(this);
const legendText = elem.select('text')
.style('text-anchor', Math.abs(d.x) < 30 ? 'middle' : (d.x < 0 ? 'end' : 'start'))
.text(d.data.label)
;

const textBBox = legendText[0][0].getBBox();

const angle = Pie.radiansToDegrees(d.startAngle);
elem.select('path')
.attr('transform', `rotate(${angle} 0 0)`)
;

elem.select('rect')
.attr('x', textBBox.x - 8)
.attr('y', textBBox.y - 3)
.attr('width', textBBox.width + 16)
.attr('height', textBBox.height + 6)
;
})
;

this.legends.exit().remove();

return this;
}

static degreesToRadians(degrees) {
return degrees * Math.PI / 180;
}

static radiansToDegrees(radians) {
return 180 * radians / Math.PI;
}

// Return computed arc data key
static dataKey(d) {
return d.data.id;
Expand Down
13 changes: 9 additions & 4 deletions src/browser/components/charts/Pie.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Pie.propTypes = {
count: PropTypes.number,
countUnit: PropTypes.string,
countLabel: PropTypes.string,
spacing: PropTypes.number.isRequired,
spacing: PropTypes.object.isRequired,
innerRadius: PropTypes.number.isRequired,
transitionDuration: PropTypes.number.isRequired,
data: PropTypes.arrayOf(PropTypes.shape({
Expand All @@ -54,10 +54,15 @@ Pie.propTypes = {

Pie.defaultProps = {
innerRadius: 0,
spacing: 0.1,
transitionDuration: 600,
data: []
data: [],
spacing: {
top: 30,
right: 30,
bottom: 30,
left: 30
}
};


export {Pie as default};
export default Pie;
4 changes: 2 additions & 2 deletions src/browser/components/charts/PieChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class PieChart extends Component {
}

PieChart.propTypes = {
spacing: PropTypes.number.isRequired,
spacing: PropTypes.object.isRequired,
innerRadius: PropTypes.number.isRequired,
transitionDuration: PropTypes.number.isRequired,
data: PropTypes.arrayOf(PropTypes.shape({
Expand All @@ -49,7 +49,7 @@ PieChart.propTypes = {
};

PieChart.defaultProps = {
spacing: 0.1,
spacing: {},
innerRadius: 0,
transitionDuration: 600,
data: []
Expand Down
Loading

0 comments on commit 7a73927

Please sign in to comment.