d3-jetpackās d3.conventions can now create canvas and html elements. Here d3.conventions({layers: 'csd'})
makes an canvas ctx, svg and div with a shared coordinate system. Yellow shapes are drawn on canvas, cyan on svg and purple on html.
Layers are position absolutely on top of each other in the order listed in the layer string. To create an svg with two canvas elements on top:
var {layers: [svg, bg_ctx, fg_ctx]} = d3.conventions({layers: 'scc'})
Hurricane How-To describes using multiple renders for something more practical than bouncing circles.
console.clear()
var sel = d3.select('body').html('')
var c = d3.conventions({sel, layers: 'csd', margin: {left: 30, bottom: 40}})
var xMax = Math.floor(c.width/2)
c.x.domain([0, xMax])
c.y.domain([0, 100])
d3.drawAxis(c)
var [ctx, svg, div] = c.layers
svg.select('.y').append('text')
.text('ms per frame')
.at({textAnchor: 'start', x: -25, y: 15, fill: '#000'})
var points = d3.range(150)
.map(d => [Math.random()*c.width, Math.random()*c.height])
points.forEach(d => {d.vx = 0, d.vy = 0})
var r = 6
var svgCircles = svg.appendMany('circle', points)
.translate(d => d)
.at({r, fill: '#0ff', stroke: '#0ff', fillOpacity: .2})
var divCircles = div.appendMany('div', points)
.translate(d => d)
.st({
position: 'absolute',
width: r*2,
height: r*2,
left: -r,
top: -r,
borderRadius: r*2 + 'px',
background: 'rgba(255, 0, 255, .2)',
backgroundOpacity: .2,
border: '1px solid #f0f'
})
var i = 0
var prevT = 0
if (window.timer) timer.stop()
timer = d3.timer(t => {
i++
var dt = t - prevT
prevT = t
var color = ['#0ff', '#f0f', '#ff0'][i % 3]
var x = c.x(i)
var y = c.y(dt)
if (i > xMax){
i = i - xMax
c.sel.selectAll('.time-bar').remove()
}
points.forEach(d => {
d.vx = d3.clamp(-3, d.vx + Math.random()*.6 - .6*.5, 3)
d.vy = d3.clamp(-3, d.vy + Math.random()*.6 - .6*.5, 3)
d[0] += d.vx
d[1] += d.vy
if (d[0] < 0 || d[0] > c.width) d.vx = -d.vx
if (d[1] < 0 || d[1] > c.height) d.vy = -d.vy
})
if (i % 3 == 0){
svgCircles.translate(d => d)
svg.append('path.time-bar')
.at({d: `M ${x} ${c.height} V ${y}`, stroke: color})
} else if (i % 3 == 1){
divCircles.translate(d => d)
div.append('div.time-bar')
.st({left: x, top: y, height: c.height - y})
.st({width: 1, background: color, position: 'absolute'})
} else if (i % 3 == 2){
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
ctx.fillRect(-c.margin.left, -c.margin.top, c.totalWidth, c.totalHeight)
ctx.beginPath()
ctx.fillStyle = 'rgba(255, 255, 0, .2)'
ctx.strokeStyle = '#ff0'
points.forEach(([x, y]) => {
ctx.moveTo(x + r, y)
ctx.arc(x, y, r, 0, 2 * Math.PI)
})
ctx.stroke()
ctx.fill()
ctx.strokeStyle = color
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x, c.height)
ctx.stroke()
}
})
<!DOCTYPE html>
<meta charset='utf-8'>
<link rel="stylesheet" type="text/css" href="style.css">
<body>
<div class='graph'></div>
</body>
<script src='d3+_.js'></script>
<script src='_script.js'></script>
body{
font-family: monaco, Consolas, 'Lucida Console', monospace;
margin: 0px;
background: black;
overflow: hidden;
}
canvas{
position: absolute;
top: 0px;
left: 0px;
}
.tooltip {
top: -1000px;
position: fixed;
padding: 10px;
background: rgba(255, 255, 255, .90);
border: 1px solid lightgray;
pointer-events: none;
}
.tooltip-hidden{
opacity: 0;
transition: all .3s;
transition-delay: .1s;
}
@media (max-width: 590px){
div.tooltip{
bottom: -1px;
width: calc(100%);
left: -1px !important;
right: -1px !important;
top: auto !important;
width: auto !important;
}
}
svg{
overflow: visible;
}
.domain{
display: none;
}
text{
pointer-events: none;
fill: #fff;
/*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/
}
.time-bar{
/*shape-rendering: crispEdges;*/
}