react-gear-chart
Version:

215 lines (194 loc) • 6.28 kB
JavaScript
// @flow
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { sumBy, clone } from 'lodash-es'
import AnnulusSector from './AnnulusSector'
import GLC from './GearListChart'
import classnames from 'classnames'
import { RadialText } from '../utils/draw'
type Strip = {
color: string,
weight: number,
}
type ToothProps = {
startAngle: number,
endAngle: number,
offsetAngle: number,
cx: number,
cy: number,
outerRadius: number,
innerRadius: number,
mode: 'layer' | 'spokerib' | 'bar',
strips: Strip | Array<Strip>,
label: string,
labelMargin: number,
labelDirection: 'radial' | 'span' | 'tangent',
}
export default class Tooth extends PureComponent<void, ToothProps, void> {
state = {
focused: false,
}
_renderLabel() {
switch (this.props.labelDirection) {
case 'span':
return this._renderSpanLabel()
case 'tangent':
return this._renderTangentLabel()
default: // 'radial'
return this._renderRadialLabel()
}
}
_renderRadialLabel() {
let { startAngle, innerRadius, label, cx, cy, offsetAngle, labelMargin, style } = this.props
let centerlineAngle = startAngle + offsetAngle + this.toothAngle() / 2
let { x, y, textAnchor, rotate } = RadialText(innerRadius + labelMargin , centerlineAngle, cx, cy)
return (<text
style={style}
className="tooth-label"
ref="label"
pointerEvents="none"
x={x} y={y}
textAnchor={textAnchor}
transform={`rotate(${rotate.join(',')})`}>{label}</text>)
}
_renderSpanLabel() {
}
_renderTangentLabel() {
}
_renderSpokerib() {
let { startAngle, endAngle, innerRadius, outerRadius } = this.props
let strips = this.strips()
let divisor = sumBy(strips, 'weight')
let toothAngle = endAngle - startAngle
let _startAngle = startAngle
return (<g className={classnames("tooth-annulus-sector")} ref="sector">
{ strips.map((_, i) => {
let stripAngle = (_.weight / divisor) * toothAngle
let _id = _.id || i
return this._commonSector(i, _id, _startAngle, _startAngle += stripAngle, outerRadius, innerRadius, _.color)
}) }
</g>)
}
_renderLayer() {
let { startAngle, endAngle, innerRadius } = this.props
let strips = this.strips()
let toothHeight = this.toothHeight()
let _step = 0
return (<g className={classnames("tooth-annulus-sector")} ref="sector">
{ strips.map((_, i) => {
let _innerR = innerRadius + _step
let _height = _.weight * toothHeight
let _outerR = _innerR + _height
_step += _height
let _id = _.id || i
return this._commonSector(i, _id, startAngle, endAngle, _outerR, _innerR, _.color)
}) }
</g>)
}
_renderBar() {
let { startAngle, innerRadius } = this.props
let strips = this.strips()
let toothHeight = this.toothHeight()
let _perItemAngle = this.toothAngle() / strips.length
return (
<g className={classnames("tooth-annulus-sector")} ref="sector">
{ strips.map((_, i) => {
let _start = startAngle + i * _perItemAngle
let _end = _start+ _perItemAngle
let _height = _.weight * toothHeight
let _id = _.id || i
let _outerR = innerRadius + _height
return this._commonSector(i, _id, _start, _end, _outerR, innerRadius, _.color)
})}
</g>
)
}
_commonSector(key, id, startAngle, endAngle, outerR, innerR, color) {
let { cx, cy, offsetAngle, style,
onMouseMove, onMouseEnter, onMouseLeave, onMouseOver, onClick } = this.props
return (
<AnnulusSector
key={key}
id={id}
className="tooth-annulus-sector-path"
style={this.strips()[key].type !== 'placeholder' && style}
startAngle={startAngle} endAngle={endAngle}
outerRadius={outerR} innerRadius={innerR}
offsetAngle={offsetAngle}
cx={cx} cy={cy}
fill={color}
onMouseMove={onMouseMove && this.mouseEventProxy.bind(this)}
onMouseOver={onMouseOver && this.mouseEventProxy.bind(this)}
onMouseEnter={onMouseEnter && this.mouseEventProxy.bind(this)}
onMouseLeave={onMouseLeave && this.mouseEventProxy.bind(this)}
onClick={onClick && this.mouseEventProxy.bind(this)}
/>
)
}
mouseEventProxy = (evt) => {
evt.stripData = this.strips()[evt.sectorId] //TODO: expose a func 'GetStripById' ?
evt.stripData.id = evt.sectorId
evt.strips = this.strips()
let name = GLC.getRegistrationName(evt)
this.props[name](evt)
if (name === 'onClick') {
let { tooth } = this.refs
if (evt.target === tooth || tooth.contains(evt.target)) {
if (tooth.classList.contains('focused')) {
tooth.classList.remove('focused')
} else {
tooth.classList.add('focused')
}
}
}
}
strips() {
let strips = this.props.strips
return Array.isArray(strips) ? strips : [ strips ]
}
toothAngle() {
return this.props.endAngle - this.props.startAngle
}
toothHeight() {
return this.props.outerRadius -this.props.innerRadius
}
render() {
let { label, mode, style, extra, offsetAngle, clockwiseAnimate } = this.props
let props = clone(this.props)
let factor = clockwiseAnimate ? -1 : 1
props.startAngle += offsetAngle * factor
props.endAngle += offsetAngle * factor
let tooth
switch (mode) {
case 'layer':
tooth = this._renderLayer()
break
case 'bar':
tooth = this._renderBar()
break
default: // 'spokerib'
tooth = this._renderSpokerib()
}
return (
<g className="tooth" ref="tooth" style={style}>
{ tooth }
{ label && this._renderLabel() }
{ extra && extra(props) }
</g>
)
}
}
Tooth.defaultProps = {
cx: 0,
cy: 0,
labelDirection: 'radial',
labelMargin: 10,
offsetAngle: 0
}
Tooth.propTypes = {
innerRadius: PropTypes.number.isRequired,
outerRadius: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
strips: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired
}