From 7a7392705b462893a9d97c528d0f93af01c13d0f Mon Sep 17 00:00:00 2001 From: Raphael Benitte Date: Wed, 13 Apr 2016 16:59:49 +0900 Subject: [PATCH] feat(charts): improve pie and gauge components --- package.json | 2 +- src/browser/components/charts/Gauge.jsx | 58 +++++--- src/browser/components/charts/Pie.js | 161 +++++++++++++++------ src/browser/components/charts/Pie.jsx | 13 +- src/browser/components/charts/PieChart.jsx | 4 +- src/styl/__vars.styl | 6 + src/styl/_animations.styl | 7 + src/styl/_main.styl | 4 + src/styl/components/list.styl | 46 +++--- src/styl/components/pie.styl | 36 +++-- src/styl/mozaik.styl | 1 + src/themes/bordeau/_vars.styl | 8 +- src/themes/night-blue/_vars.styl | 4 + src/themes/snow/_vars.styl | 6 +- src/themes/yellow/_vars.styl | 4 + 15 files changed, 254 insertions(+), 106 deletions(-) create mode 100644 src/styl/_animations.styl diff --git a/package.json b/package.json index 9a2c90ec..e9a6d98d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/browser/components/charts/Gauge.jsx b/src/browser/components/charts/Gauge.jsx index 593d8388..e68c70e1 100644 --- a/src/browser/components/charts/Gauge.jsx +++ b/src/browser/components/charts/Gauge.jsx @@ -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; @@ -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; diff --git a/src/browser/components/charts/Pie.js b/src/browser/components/charts/Pie.js index c710152b..8bc98a22 100644 --- a/src/browser/components/charts/Pie.js +++ b/src/browser/components/charts/Pie.js @@ -4,13 +4,12 @@ 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, @@ -18,15 +17,22 @@ class Pie { 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]); } @@ -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); @@ -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([ @@ -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) @@ -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. @@ -156,6 +164,73 @@ 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; } @@ -163,6 +238,10 @@ class Pie { 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; diff --git a/src/browser/components/charts/Pie.jsx b/src/browser/components/charts/Pie.jsx index 254e702f..070c589f 100644 --- a/src/browser/components/charts/Pie.jsx +++ b/src/browser/components/charts/Pie.jsx @@ -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({ @@ -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; diff --git a/src/browser/components/charts/PieChart.jsx b/src/browser/components/charts/PieChart.jsx index 259dd4ce..1bb47a41 100644 --- a/src/browser/components/charts/PieChart.jsx +++ b/src/browser/components/charts/PieChart.jsx @@ -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({ @@ -49,7 +49,7 @@ PieChart.propTypes = { }; PieChart.defaultProps = { - spacing: 0.1, + spacing: {}, innerRadius: 0, transitionDuration: 600, data: [] diff --git a/src/styl/__vars.styl b/src/styl/__vars.styl index dde5b325..7717a74c 100644 --- a/src/styl/__vars.styl +++ b/src/styl/__vars.styl @@ -94,6 +94,12 @@ $chart-axis-txt-color = default('$chart-axis-txt-color', $chart-ele $chart-tick-txt-size = default('$chart-tick-txt-size', 1.2vmin) $chart-axis-tick-color = default('$chart-axis-tick-color', $chart-elements-color) $chart-grid-line-color = default('$chart-grid-line-color', $chart-elements-color) +$pie-chart-outline-fill = default('$pie-chart-outline-fill', none) +$pie-chart-outline-stroke = default('$pie-chart-outline-stroke', none) +$pie-chart-outline-stroke-width = default('$pie-chart-outline-stroke-width', 0) +$pie-gauge-needle-color = default('$pie-gauge-needle-color', #000) +$pie-svg-legend-bg-color = default('$pie-svg-legend-bg-color', none) +$pie-svg-legend-txt-color = default('$pie-svg-legend-txt-color', $main-txt-color) // PROPS $prop-key-txt-color = default('$prop-key-txt-color', $main-txt-color) diff --git a/src/styl/_animations.styl b/src/styl/_animations.styl new file mode 100644 index 00000000..c96c8222 --- /dev/null +++ b/src/styl/_animations.styl @@ -0,0 +1,7 @@ +@keyframes pulse-opacity + 0% + opacity 1 + 50% + opacity 0.1 + 100% + opacity 1 \ No newline at end of file diff --git a/src/styl/_main.styl b/src/styl/_main.styl index 2ae490d2..1fff49b1 100644 --- a/src/styl/_main.styl +++ b/src/styl/_main.styl @@ -10,6 +10,10 @@ html, body *, *:before, *:after box-sizing border-box +a + color inherit + text-decoration none + .histogram &__axis &__legend diff --git a/src/styl/components/list.styl b/src/styl/components/list.styl index 5eaf581b..a93b8176 100644 --- a/src/styl/components/list.styl +++ b/src/styl/components/list.styl @@ -11,8 +11,6 @@ &__item padding $list_item_padding - //line-height 24px - position relative &--with-status @@ -20,7 +18,7 @@ padding $list_item_with_status_padding &:before - content " " + content ' ' display block width $list_item_status_icon_size @@ -35,21 +33,33 @@ background-color #000 - &--aborted:before - background-color $unknown-color - - &--ok:before, - &--passed:before, - &--success:before - background-color $success-color - - &--warning:before - background-color $warning-color - - &--critical:before, - &--failed:before, - &--failure:before - background-color $failure-color + &--aborted, + &--pending + &:before + background-color $unknown-color + + &--ok, + &--passed, + &--success + &:before + background-color $success-color + + &--warning, + &--running + &:before + background-color $warning-color + + &--running + &:before + animation-name pulse-opacity + animation-duration 1000ms + animation-iteration-count infinite + + &--critical, + &--failed, + &--failure + &:before + background-color $failure-color &__time font-size 85% diff --git a/src/styl/components/pie.styl b/src/styl/components/pie.styl index e6db8c33..61405a19 100644 --- a/src/styl/components/pie.styl +++ b/src/styl/components/pie.styl @@ -8,22 +8,24 @@ float left width 60% height 100% + &_wrapper width 100% height 100% - &_shadow - path - fill #000000 - opacity 0.15 - mix-blend-mode multiply + &_outline + fill $pie-chart-outline-fill + stroke $pie-chart-outline-stroke + stroke-width $pie-chart-outline-stroke-width + + &_svg_legend + rect, path + fill $pie-svg-legend-bg-color + stroke none - &_light - display none - path - fill #fff - opacity 0.25 - mix-blend-mode screen + text + font-size 1.6vmin + fill $pie-svg-legend-txt-color &_legends float left @@ -79,8 +81,10 @@ display block margin-top 1vmin - &_hand - &_anchor - fill #000 - &_line - fill #000 + &_gauge + &_needle + &_base + fill $pie-gauge-needle-color + + &_arrow + fill $pie-gauge-needle-color diff --git a/src/styl/mozaik.styl b/src/styl/mozaik.styl index 8c92b3ab..36315557 100644 --- a/src/styl/mozaik.styl +++ b/src/styl/mozaik.styl @@ -4,6 +4,7 @@ @require '__vars' +@require '_animations' @require '_main' @require 'components/dashboard' @require 'components/widget' diff --git a/src/themes/bordeau/_vars.styl b/src/themes/bordeau/_vars.styl index 6a372408..d0ef3a84 100644 --- a/src/themes/bordeau/_vars.styl +++ b/src/themes/bordeau/_vars.styl @@ -47,8 +47,12 @@ $success-color = #50a3b2; $failure-color = #a31c12; // CHARTS -$histogram-bar-bg-color = lighten($card-bg-color, 4) -$chart-axis-txt-color = $main-txt-color +$histogram-bar-bg-color = lighten($card-bg-color, 4) +$chart-axis-txt-color = $main-txt-color +$pie-chart-outline-stroke = darken($card-bg-color, 7) +$pie-chart-outline-stroke-width = 6px +$pie-gauge-needle-color = darken($main-bg-color, 7) +$pie-svg-legend-bg-color = lighten($card-bg-color, 5) // PROPS $prop-key-txt-color = $main-txt-color diff --git a/src/themes/night-blue/_vars.styl b/src/themes/night-blue/_vars.styl index 146d518b..68563028 100644 --- a/src/themes/night-blue/_vars.styl +++ b/src/themes/night-blue/_vars.styl @@ -68,6 +68,10 @@ $failure-color = #de5029 // CHART $chart-elements-color = lighten($widget-bg-color, 40) $histogram-bar-bg-color = lighten($widget-bg-color, 7) +$pie-chart-outline-stroke = #27313e +$pie-chart-outline-stroke-width = 6px +$pie-gauge-needle-color = #151b26 +$pie-svg-legend-bg-color = $widget-header-bg-color // PROPS $prop-key-txt-color = $main-txt-color diff --git a/src/themes/snow/_vars.styl b/src/themes/snow/_vars.styl index 538dd368..44e70779 100644 --- a/src/themes/snow/_vars.styl +++ b/src/themes/snow/_vars.styl @@ -55,7 +55,11 @@ $failure-color = #e37856 // CHART $histogram-bar-bg-color = #fafafa -$chart-axis-txt-color = #999 +$chart-axis-txt-color = #777 +$chart-grid-line-color = #ccc +$pie-chart-outline-stroke = #ddd +$pie-chart-outline-stroke-width = 6px +$pie-svg-legend-bg-color = #ededed // PROPS $prop-key-txt-color = $main-txt-color diff --git a/src/themes/yellow/_vars.styl b/src/themes/yellow/_vars.styl index 92c59cf5..61195d6b 100644 --- a/src/themes/yellow/_vars.styl +++ b/src/themes/yellow/_vars.styl @@ -55,6 +55,10 @@ $label-border-radius = 2px // CHARTS $histogram-bar-bg-color = #dcd1b5 $chart-axis-txt-color = #806b3f +$pie-chart-outline-stroke = #e5dabe +$pie-chart-outline-stroke-width = 8px +$pie-gauge-needle-color = #735e39 +$pie-svg-legend-bg-color = #e6d280 // PROPS $prop-key-txt-color = $main-txt-color