D3 and React Working Together

Michael Rovinsky
Welldone Software
3 min readApr 26, 2018

--

How to build a D3 — based SVG component within a React.js application? Here is an example (click on the Result tab):

Let’s create a simple React component displaying a classic clock with a given time:

<ClockControl size='200' time={Date.now()}/>

The only React methods we are going to define are:

  1. render()
  2. componentDidMount()
  3. componentDidUpdate()

Render is used to create an <svg> element with the given size and pass its handle to a D3 wrapper:

render() {
return (
<svg width={this.props.size}
height={this.props.size}
ref={element => (this.svg = d3.select(element))}>
</svg>
)
}

Now we need to create all the visual elements (frame, digits, marks, hands) and move the clock hands each time the ‘time’ property is updated. The elements are added to SVG when the component is mounted:

componentDidMount() {
this.drawFrame()
this.drawMarks()
this.drawHands()
this.drawDigits()
this.updateTime()
}

… then on update:

componentDidUpdate() {
this.updateTime()
}

The clock’s frame and marks are simply <circle> and <line> elements:

const COLOR = '#fff'
const PADDING = 10
drawFrame() {
const center = this.props.size / 2
const addCircle = (radius, border, color) =>
this.svg.append('circle')
.attr('cx', center)
.attr('cy', center)
.attr('r', radius)
.style('stroke', border)
.style('fill', color)
const clockRadius = center - PADDING
addCircle(clockRadius + 2, 'none', COLOR)
addCircle(clockRadius, 'none', COLOR)
addCircle(clockRadius * 0.075, 'none', COLOR)
}
drawMarks() {
const center = this.props.size / 2
const radius = center - PADDING
const y1 = center - Math.floor(radius * 0.97)
const y2 = center - Math.floor(radius * 0.90)

for(let mark = 0; mark < 60; mark++) {
const transform = `rotate(${mark * 6},${center},${center})`
const isHourMark = (mark % 5) === 0
this.svg.append('line')
.attr('x1', center).attr('y1', y1)
.attr('x2', center).attr('y2', y2)
.attr('transform', transform)
.style('stroke', COLOR)
.style('stroke-width', isHourMark ? 3 : 1)
}
}

To build the digits, we are going to stretch and rotate <text> elements:

drawDigits() {
const hours = ['I', 'II', 'III', 'IV', 'V', 'VI',
'VII', 'VIII', 'IX', 'X', 'XI', 'XII']
const center = this.props.size / 2
const radius = center - PADDING
const fontSize = `${Math.floor(radius / 8)}px`

const drawHourDigit = (v, i) => {
const transformG = `rotate(${(i+1) * 30},${center},${center})`
const transformT = `scale(1,3)
translate(${center}, ${center - radius * 0.95})`
const g = this.svg.append('g')
.attr('transform', transformG)
g.append('text').text(v)
.attr('text-anchor', 'middle')
.attr('transform', transformT)
.style('fill', COLOR)
.style('font-size', fontSize)
}
hours.forEach(drawHourDigit)
}

The clock hands are 3 lines of different width and length. Once created, we just change their rotation angle when the clock’s time is updated:

drawHands() {
const center = this.props.size / 2
const drawHand = (type, width, length) =>
this.svg.append('line')
.attr('hand-type', type)
.attr('x1', center)
.attr('y1', center)
.attr('x2', center)
.attr('y2', center - length)
.style('stroke', COLOR)
.style('stroke-width', width)

const radius = center - PADDING
drawHand('H', 5, radius * 0.5)
drawHand('M', 3, radius * 0.7)
drawHand('S', 2, radius * 0.9)
}

moveHand(type, angle) {
const center = this.props.size / 2
const transform = `rotate(${angle},${center},${center})`
const hand = this.svg.select(`line[hand-type='${type}']`)
hand.attr('transform', transform)
}

updateTime() {
const dt = new Date(this.props.time)
const hourAngle = dt.getHours() * 30 +
Math.floor(dt.getMinutes() / 12) * 6
this.moveHand('H', hourAngle)
this.moveHand('M', dt.getMinutes() * 6)
this.moveHand('S', dt.getSeconds() * 6)
}

That’s it, our clock component is ready!

To demonstrate a number of components with different properties, I’ve built a simple demo:

Here is a React code making it work:

class ClockApp extends React.Component {
constructor(props) {
super(props)
this.tzOffset = (new Date()).getTimezoneOffset()
this.state = {time: this.getUTC()}
}

getUTC() {
return Date.now() + this.tzOffset * 60 * 1000
}

componentDidMount() {
setInterval(() => {this.setState({time: this.getUTC()})}, 1000)
}

render() {
const places = {
'JERUSALEM': 180, 'ROME': 120, 'LONDON': 60,
'NEW YORK': -240, 'SAN FRANCISCO': -420, 'TOKYO': 540
};
const localTime = tzDelta => this.state.time +
tzDelta * 60 * 1000
return (
<div>
{
Object.keys(places).map((k,i) => (
<div className='container'
style={{left: 40 + (i%3) * 220,
top: Math.floor(i / 3) * 200 + 5}}>
<ClockControl size='170' time={localTime(places[k])}/>
<span>{k}</span>
</div>))
}
</div>
)
}
}

The whole demo is available in a fiddle. Have a nice day!

--

--