Skip to content

Instantly share code, notes, and snippets.

@plmrry
Last active October 18, 2018 01:25
Show Gist options
  • Save plmrry/5c41b49ac8b3a4925dd23a2c9c00bd98 to your computer and use it in GitHub Desktop.
Save plmrry/5c41b49ac8b3a4925dd23a2c9c00bd98 to your computer and use it in GitHub Desktop.
Line Hovering With Point Sampling

Voronoi diagrams are often used to highlight nearby points based on mouse position.

However, the Voronoi will not always work as intended when working with paths.

Try hovering over the middle of the red path — although the mouse is closer to the red path, the blue line is highlighted.

We can fix this problem by sampling points along the path (similar to the technique used here), and adding these sampled points to the Voronoi mesh.

<style>
.hover {
opacity: 1 !important;
}
</style>
<div id="app"></div>
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3-delaunay"></script>
<script type="module" src="./script.js"></script>
const d3 = window.d3;
const appDiv = document.getElementById("app");
const parentStyle = `
display: flex;
width: 900px;
height: 500px;
justify-content: center;
align-items: center;
font-family: sans-serif;
font-weight: 100;
`;
const width = 400;
const height = 400;
const childStyle = `
width: ${width}px;
height: ${height}px;
`;
appDiv.innerHTML = `
<div style="${parentStyle}">
<div>Before<div style="${childStyle}" id="before"></div></div>
<div>After<div style="${childStyle}" id="after"></div></div>
</div>
`;
const points = [
{
group: "a",
x: 3,
y: 2
},
{
group: "a",
x: 6,
y: 8
},
{
group: "a",
x: 8,
y: 1
},
{
group: "b",
x: 1,
y: 5
},
{
group: "b",
x: 9,
y: 6
}
];
const xExtent = [0, 10];
const xRange = [0, width];
const xScale = d3
.scaleLinear()
.domain(xExtent)
.range(xRange);
const yExtent = [0, 10];
const yRange = [height, 0];
const yScale = d3
.scaleLinear()
.domain(yExtent)
.range(yRange);
const line = d3
.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
const color = group => (group === "a" ? "blue" : "red");
const opacity = 0.3;
const nested = d3
.nest()
.key(d => d.group)
.entries(points)
.map(point => {
return {
...point,
pathString: line(point.values)
};
});
const paths = nested.map(point => {
const { key } = point;
const pathStyle = `
fill: none;
stroke: ${color(point.key)};
opacity: ${opacity};
`;
const html = `<path d=${
point.pathString
} style="${pathStyle}" class="data group-${key}" />`;
return {
...point,
html
};
});
const circles = points.map(point => {
const { x, y, group } = point;
const transform = `translate(${xScale(x)}, ${yScale(y)})`;
const style = `fill: ${color(group)}; opacity: ${opacity}`;
return `
<circle r="5" transform="${transform}" style="${style}" class="data group-${group}" />
`;
});
// Based on: https://gist.github.com/mbostock/4163057
// Sample the SVG path string "d" uniformly with the specified precision.
function getPoints(d, step) {
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
var length = path.getTotalLength();
return d3.range(0, length, step).map(function(t) {
var point = path.getPointAtLength(t);
return { x: point.x, y: point.y, t: t / length };
});
}
const chart = (div, withExtraPoints) => {
let allPoints = points.map(point => {
const arr = [xScale(point.x), yScale(point.y)];
arr.group = point.group;
return arr;
});
if (withExtraPoints) {
const linePoints = paths
.map(path => {
const { pathString, key } = path;
const extraPoints = getPoints(pathString, 20)
.filter(p => p.x && p.y)
.map(point => {
const arr = [point.x, point.y];
arr.group = key;
return arr;
});
return extraPoints;
})
.flat();
allPoints = allPoints.concat(linePoints);
}
const delaunay = d3.Delaunay.from(allPoints);
const voronoi = delaunay.voronoi();
const cells = allPoints.map((point, i) => {
const cellStyle = `
fill: none;
stroke: black;
pointer-events: all;
opacity: 0.2;
`;
const { group } = point;
const d = voronoi.renderCell(i);
if (!d) {
return "";
}
return `<path class="cell" d="${d}" style="${cellStyle}" data-group="${group}" />`;
});
const childNode = div;
childNode.node().innerHTML = `
<svg width="${width}px" height="${height}px">
${paths.map(p => p.html).join("")}
${circles.join("")}
${cells.join("")}
</svg>
`;
div
.selectAll(".cell")
.on("mouseover", function() {
const { group } = this.dataset;
childNode.selectAll(`.data`).classed("hover", false);
childNode.selectAll(`.group-${group}`).classed("hover", true);
})
.on("mouseout", function() {
childNode.selectAll(`.data`).classed("hover", false);
});
};
const before = d3.select("#before");
const after = d3.select("#after");
chart(before);
chart(after, true);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment