D3 and React working together

Michael Rovinsky
Apr 26, 2018 · 3 min read

How to build a D3 - based SVG component within a React.js application? Here is an example. 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!

Michael Rovinsky

Written by

Welldone Software

The leading full-stack consultancy. Creating amazing frontends and rock-solid backends using top notch technologies and practices. Visit us at https://welldone.software.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade