Skip to content

Instantly share code, notes, and snippets.

@larsvers
Last active November 25, 2021 12:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save larsvers/6049de0bcfa50f95d3dcbf1e3e44ad48 to your computer and use it in GitHub Desktop.
Save larsvers/6049de0bcfa50f95d3dcbf1e3e44ad48 to your computer and use it in GitHub Desktop.
d3 canvas tutorial - bind, draw, pick
license: mit

Part 2 of a little d3 canvas tutorial - how to bind data, draw it to canvas and use picking for interativity

Built with blockbuilder.org

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>d3 and canvas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="http://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<style type="text/css">
body {
font-family: 'Open Sans', sans-serif;
}
canvas {
border: 1px dotted #ccc;
}
#text-explain {
display: inline-block;
font-size: 0.75em;
margin-bottom: 1em;
}
.alert {
color: tomato;
}
/* new (in comparison to code w/o interactivty at:) ---- */
/* (http://blockbuilder.org/larsvers/d187337850d58a444082841c739985ca) */
div#tooltip {
position: absolute;
display: inline-block;
padding: 10px;
font-family: 'Open Sans' sans-serif;
color: #000;
background-color: #fff;
border: 1px solid #999;
border-radius: 2px;
pointer-events: none;
opacity: 0;
z-index: 1;
}
/* new ------------------------------------------------ */
</style>
</head>
<body>
<h3>Coloured grids</h3>
<input type="text" id="text-input" value="5000">
<div id="text-explain">...takes numbers between 1 and 10k</div>
<div id="container"></div>
<div id="tooltip">hello</div> <!-- new -->
<script>
var log = console.log.bind(console);
var dir = console.dir.bind(console);
var replace = function(string) { return string.replace(/[^a-z0-9]/gi,""); };
// === Set up canvas === //
var width = 750,
height = 400;
var data = [];
var value = 5000;
var colorScale;
var mainCanvas = d3.select('#container')
.append('canvas')
.classed('mainCanvas', true)
.attr('width', width)
.attr('height', height);
// new -----------------------------------------------------
var hiddenCanvas = d3.select('#container')
.append('canvas')
.classed('hiddenCanvas', true)
.attr('width', width)
.attr('height', height);
var colourToNode = {}; // map to track the colour of nodes
// function to create new colours for the picking
var nextCol = 1;
function genColor(){
var ret = [];
// via http://stackoverflow.com/a/15804183
if(nextCol < 16777215){
ret.push(nextCol & 0xff); // R
ret.push((nextCol & 0xff00) >> 8); // G
ret.push((nextCol & 0xff0000) >> 16); // B
nextCol += 1;
}
var col = "rgb(" + ret.join(',') + ")";
return col;
}
// new -----------------------------------------------------
// === Load and prepare the data === //
d3.range(value).forEach(function(el) {
data.push({ value: el });
});
// === Bind data to custom elements === //
var customBase = document.createElement('custom');
var custom = d3.select(customBase); // this is our svg replacement
// settings for a grid with 40 cells in a row and 2x5 cells in a group
var groupSpacing = 4;
var cellSpacing = 2;
var cellSize = Math.floor((width - 11 * groupSpacing) / 100) - cellSpacing;
// === First call === //
databind(data); // ...then update the databind function
var t = d3.timer(function(elapsed) {
draw(mainCanvas, false); // <--- new insert arguments
if (elapsed > 300) t.stop();
}); // start a timer that runs the draw function for 300 ms (this needs to be higher than the transition in the databind function)
// === Bind and draw functions === //
function databind(data) {
colorScale = d3.scaleSequential(d3.interpolateSpectral).domain(d3.extent(data, function(d) { return d.value; }));
var join = custom.selectAll('custom.rect')
.data(data);
var enterSel = join.enter()
.append('custom')
.attr('class', 'rect')
.attr('x', function(d, i) {
var x0 = Math.floor(i / 100) % 10, x1 = Math.floor(i % 10);
return groupSpacing * x0 + (cellSpacing + cellSize) * (x1 + x0 * 10);
})
.attr('y', function(d, i) {
var y0 = Math.floor(i / 1000), y1 = Math.floor(i % 100 / 10);
return groupSpacing * y0 + (cellSpacing + cellSize) * (y1 + y0 * 10);
})
.attr('width', 0)
.attr('height', 0);
join
.merge(enterSel)
.transition()
.attr('width', cellSize)
.attr('height', cellSize)
.attr('fillStyle', function(d) { return colorScale(d.value); })
// new -----------------------------------------------------
.attr('fillStyleHidden', function(d) {
if (!d.hiddenCol) {
d.hiddenCol = genColor();
colourToNode[d.hiddenCol] = d;
} // here we (1) add a unique colour as property to each element and (2) map the colour to the node in the colourToNode-dictionary
return d.hiddenCol;
});
// new -----------------------------------------------------
var exitSel = join.exit()
.transition()
.attr('width', 0)
.attr('height', 0)
.remove();
} // databind()
// === Draw canvas === //
function draw(canvas, hidden) { // <---- new arguments
// build context
var context = canvas.node().getContext('2d');
// clear canvas
context.clearRect(0, 0, width, height);
// draw each individual custom element with their properties
var elements = custom.selectAll('custom.rect') // this is the same as the join variable, but used here to draw
elements.each(function(d,i) { // for each virtual/custom element...
var node = d3.select(this);
context.fillStyle = hidden ? node.attr('fillStyleHidden') : node.attr('fillStyle'); // <--- new: node colour depends on the canvas we draw
context.fillRect(node.attr('x'), node.attr('y'), node.attr('width'), node.attr('height'))
});
} // draw()
// === Listeners/handlers === //
d3.select('#text-input').on('keydown', function() {
if (d3.event.keyCode === 13) {
d3.select('#alert').html('');
if (+this.value < 1 || +this.value > 10000) {
d3.select('#text-explain').classed('alert', true);
return;
} else {
d3.select('#text-explain').classed('alert', false);
data = [];
d3.range(+this.value).forEach(function(el) {
data.push({ value: el });
});
databind(data);
var t = d3.timer(function(elapsed) {
draw(mainCanvas, false); // <--- new insert arguments
if (elapsed > 300) t.stop();
}); // start a timer that runs the draw function for 300 ms (this needs to be higher than the transition in the databind function)
} // value test
} // keyCode 13 === return
}); // text input listener/handler
// new -----------------------------------------------------
d3.select('.mainCanvas').on('mousemove', function() {
// draw the hiddenCanvas
draw(hiddenCanvas, true);
// get mousePositions from the main canvas
var mouseX = d3.event.layerX || d3.event.offsetX;
var mouseY = d3.event.layerY || d3.event.offsetY;
// get the toolbox for the hidden canvas
var hiddenCtx = hiddenCanvas.node().getContext('2d');
// Now to pick the colours from where our mouse is then stringify it in a way our map-object can read it
var col = hiddenCtx.getImageData(mouseX, mouseY, 1, 1).data;
var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
// get the data from our map !
var nodeData = colourToNode[colKey];
log(nodeData);
if (nodeData) {
// Show the tooltip only when there is nodeData found by the mouse
d3.select('#tooltip')
.style('opacity', 0.8)
.style('top', d3.event.pageY + 5 + 'px')
.style('left', d3.event.pageX + 5 + 'px')
.html(nodeData.value);
} else {
// Hide the tooltip when there our mouse doesn't find nodeData
d3.select('#tooltip')
.style('opacity', 0);
}
}); // canvas listener/handler
// new -----------------------------------------------------
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment