basic/graphs/Gauge.js
/**
* Created by kinneretzin on 14/02/2017.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
function deg2rad(deg) {
return (deg * Math.PI) / 180;
}
/**
* Gauge is a component to present value in graphical form
*
* ## Access
* `Stage.Basic.Graphs.Gauge`
*
* ## Usage
*
* ### Gauge with default angles
* ![Gauge 0](manual/asset/graphs/Gauge_0.png)
*
* ```
* <Gauge value={10} min={0} max={20} high={15} low={5} />
* ```
*
* ### Gauge with defined angles and value below low marker
* ![Gauge 1](manual/asset/graphs/Gauge_1.png)
*
* ```
* <Gauge minAngle={-90} maxAngle={0} value={3} min={0} max={20} high={15} low={5} />
* ```
*
* ### Gauge with defined angles and value above high marker
* ![Gauge 2](manual/asset/graphs/Gauge_2.png)
*
* ```
* <Gauge minAngle={-45} maxAngle={90} value={18} min={0} max={20} high={15} low={5} />
* ```
*
*/
export default class Gauge extends Component {
constructor(props) {
super(props);
this.svgRef = React.createRef();
}
/**
* propTypes
*
* @property {number} value actual value to be marked on the gauge
* @property {number} min minimal value to be presented
* @property {number} max maximum value to be presented
* @property {number} [high] value above which the colour of the gauge bar changes to green
* @property {number} [low] value below which the colour of the gauge bar changes to red
* @property {number} [minAngle=-90] minimum angle of the gauge chart, associated with minimum value
* @property {number} [maxAngle=90] maximum angle of the gauge chart, associated with maximum value
*/
static propTypes = {
value: PropTypes.number.isRequired,
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
high: PropTypes.number,
low: PropTypes.number,
minAngle: PropTypes.number,
maxAngle: PropTypes.number
};
static defaultProps = {
minAngle: -90,
maxAngle: 90
};
_buildProps(svgComponent) {
const width = svgComponent.clientWidth;
const height = svgComponent.clientHeight;
// Calc radius
let radius = width / 2;
if (height < radius) {
radius = height;
}
radius *= 0.93; // Leave room for the last tick that goes under the graph
const range = this.props.maxAngle - this.props.minAngle;
// Calculate ring size
const ringWidth = radius * 0.4;
const valueRange = this.props.max - this.props.min;
let valRatio = (this.props.value - this.props.min) / valueRange;
valRatio = valRatio < 0 ? 0 : valRatio > 1 ? 1 : valRatio;
const isHigh = this.props.high && this.props.value > this.props.high;
const isLow = this.props.low && this.props.value < this.props.low;
const textSize = (radius - ringWidth) * 0.8;
const tickTextSize = ringWidth * 0.15;
const arcPadding = tickTextSize;
return {
radius,
range,
ringWidth,
valueRange,
valRatio,
isHigh,
isLow,
arcPadding,
value: this.props.value,
minAngle: this.props.minAngle,
maxAngle: this.props.maxAngle,
ticksNumber: 7,
minValue: this.props.min,
maxValue: this.props.max,
textSize,
tickTextSize
};
}
componentDidMount() {
this._initGauge(this.svgRef.current);
$(window).resize(() => this._initGauge(this.svgRef.current));
}
componentWillUnmount() {
$(window).off('resize');
}
componentDidUpdate() {
setTimeout(() => {
this._initGauge(this.svgRef.current);
}, 100);
}
_initGauge(svgComponent) {
if (svgComponent === null) {
return;
}
const opts = this._buildProps(svgComponent);
const svg = d3.select(svgComponent);
// First clear everything
svg.selectAll('*').remove();
// Make sure the svg width contains the gauge and it only (so there wont be some widths mess up)
svg.attr('width', opts.radius * 2);
// Define the 2 arcs (one for the filled area and one for the non-filled area
const arcBackground = d3.svg
.arc()
.innerRadius(opts.radius - opts.ringWidth - opts.arcPadding)
.outerRadius(opts.radius - opts.arcPadding)
.startAngle(deg2rad(opts.minAngle + opts.valRatio * opts.range))
.endAngle(deg2rad(opts.maxAngle));
const arcValue = d3.svg
.arc()
.innerRadius(opts.radius - opts.ringWidth - opts.arcPadding)
.outerRadius(opts.radius - opts.arcPadding)
.startAngle(deg2rad(opts.minAngle))
.endAngle(deg2rad(opts.minAngle + opts.valRatio * opts.range));
// Create the arcs container
const arcs = svg
.append('g')
.attr('class', 'arc')
.attr('transform', `translate(${opts.radius},${opts.radius})`);
// Draw the arcs
arcs.append('path')
.attr('class', 'arcBackground backgroundColor')
.attr('d', arcBackground);
arcs.append('path')
.attr('class', 'arcValue')
.classed('highColor', opts.isHigh)
.classed('lowColor', opts.isLow)
.classed('okColor', !opts.isLow && !opts.isHigh)
.attr('d', arcValue);
// Draw the ticks
const scale = d3.scale
.linear()
.range([0, 1])
.domain([opts.minValue, opts.maxValue]);
const ticks = scale.ticks(opts.ticksNumber);
const ticksContainer = svg
.append('g')
.attr('class', 'ticks')
.attr('transform', `translate(${opts.radius},${opts.radius})`);
ticksContainer
.selectAll('text')
.data(ticks)
.enter()
.append('text')
.attr('transform', function(d) {
const ratio = scale(d);
const newAngle = opts.minAngle + ratio * opts.range;
return `rotate(${newAngle}) translate(0,${-opts.radius + opts.arcPadding - 3})`;
})
.attr('font-size', opts.tickTextSize)
.text(d => d);
// Draw the text
svg.append('text')
.attr('class', 'valueText')
.attr('transform', `translate(${opts.radius},${opts.radius})`)
.attr('text-anchor', 'middle')
.attr('font-size', opts.textSize)
.text(opts.value);
}
render() {
return (
<div className="gaugeContainer">
<svg className="gauge" ref={this.svgRef} />
</div>
);
}
}