Skip to content

Instantly share code, notes, and snippets.

@kkdd
Last active May 25, 2022 06:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kkdd/c13a7d51b2f2afe297dbd7712853ebbb to your computer and use it in GitHub Desktop.
Save kkdd/c13a7d51b2f2afe297dbd7712853ebbb to your computer and use it in GitHub Desktop.
d3.js: Zoomable scatterplot with handling window-resize
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<style>
html,body{
height: 100%;
margin: 0;
}
svg,
#canvas {
position: absolute;
border: 0;
margin: 0;
padding: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.axis path {
display: none;
}
.axis line {
stroke-opacity: 0.1;
shape-rendering: crispEdges;
}
#debug {
z-index: 10;
}
</style>
</head>
<body>
<div id="canvas"></div>
<div id='debug' style='padding:4px;background-color:#fffc;position:absolute;right:0;top:0;color:#0af;font-family:courier;font-size:12px;user-select:none'></div>
</body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const nCircles = 1000, circleSize = 3, sigmaRandom = 40;
const scaleInitial = 1, nTick = 10, tickPadding = {x: 12, y: 6};
const canvas = document.querySelector('#canvas');
const svg = d3.select("#canvas").append('svg');
const gX = svg.append("g").attr("class", "axis axis--x");
const gY = svg.append("g").attr("class", "axis axis--y");
let circles, debug = [];
let d3Transform, [widthFormer, heightFormer] = [0, 0];
/*============= main ===============*/
resetD3Transform(scaleInitial);
createRenderCircles(randomCircles(nCircles));
(new ResizeObserver(renderCircles)).observe(canvas, {box: 'content-box'});
document.getElementById("debug").onclick = resetScale;
/*============= functions ===============*/
function resetD3Transform(scale) {
d3Transform = d3.zoomIdentity.translate(0, 0, scale);
}
function resetScale() {
resetD3Transform(scaleInitial);
renderCircles();
}
function renderCircles() {
const [width, height] = widthHeightCanvas();
const xScale = d3.scaleLinear()
.domain([-width / 2, width / 2])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([-height / 2, height / 2])
.range([height, 0]);
const xAxis = d3.axisBottom(xScale)
.ticks(nTick)
.tickSize(height)
.tickPadding(-tickPadding.x);
const yAxis = d3.axisRight(yScale)
.ticks(nTick * height / width)
.tickSize(width)
.tickPadding(tickPadding.y - width);
circles
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.style("fill", d => d.color);
const zoomed = (event, _) => {
d3Transform = event.transform;
gX.call(xAxis.scale(d3Transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3Transform.rescaleY(yScale)));
svg.selectAll('circle').attr('transform', d3Transform);
circles.attr("transform", d3Transform).attr("r", circleSize/d3Transform.k);
}
const zoom = d3.zoom().on("zoom", zoomed);
svg.call(zoom).call(zoom.transform, d3Transform);
function widthHeightCanvas() {
const {width, height} = canvas.getBoundingClientRect();
setCenterD3Transform(width, height);
return [width, height];
}
function setCenterD3Transform(width, height) {
const v = (1/d3Transform.k-1)/2;
d3Transform = d3Transform.translate((width-widthFormer)*v, (height-heightFormer)*v);
[widthFormer, heightFormer] = [width, height];
}
};
function createRenderCircles(data) {
svg.selectAll("circle").remove();
circles = svg.append("g").selectAll("circle")
.data(data)
.enter()
.append("circle");
renderCircles();
debug = [`total = ${data.length.toLocaleString()}`];
document.getElementById("debug").innerText = debug.join("\n");
}
function randomCircles(n) {
const rng = d3.randomNormal(0, sigmaRandom);
const randomColor = () => "#" + Math.random().toString(16).slice(-4) + "00";
return [...Array(n)].map(()=>({x: rng(), y: rng(), color: randomColor()}));
};
/*============= read files when dropped ===============*/
d3.select('svg')
.on('dragover', function(event, _) {
event.stopPropagation();
event.preventDefault();
})
.on('drop', async function(event, _) {
event.stopPropagation();
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
const data = await Promise.all(files.map(f => f.text()));
createRenderCircles(data.map(d3.csvParse).flat());
});
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment