|
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); |