Public
Edited
May 17, 2023
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
graphic = () => {
const svg = d3.create("svg")
.attr("width", w)
.attr("height", height);
const start = svg.append("circle")
.attr("class", "a")
.attr("r", r)
.attr("cx", mutable a[0])
.attr("cy", mutable a[1])
.style("fill", color(.02))
.call(drag);
const controlA = svg.append("circle")
.attr("class", "b")
.attr("r", r)
.attr("cx", mutable b[0])
.attr("cy", mutable b[1])
.style("fill", "white")
.style("fill-opacity", 0)
.style("stroke", "black")
.call(drag);
const controlB = svg.append("circle")
.attr("class", "c")
.attr("r", r)
.attr("cx", mutable c[0])
.attr("cy", mutable c[1])
.style("fill", "white")
.style("fill-opacity", 0)
.style("stroke", "black")
.call(drag);
const end = svg.append("circle")
.attr("class", "d")
.attr("r", r)
.attr("cx", mutable d[0])
.attr("cy", mutable d[1])
.style("fill", color(1))
.call(drag);

const line = svg.append("polyline")
.attr("fill", "none")
.attr("stroke", "black")
.lower();

updateCurve();
function updateCurve(){
mutable bezier = cubicBezier(a, b, c, d, precision);

line.attr("points", bezier);

if (showPoints) {
const curve = svg.selectAll(".bezier")
.data(bezier);
curve.exit().remove();
curve.enter().append("circle")
.attr("class", "bezier")
.style("pointer-events", "none")
.attr("r", r / 2)
.merge(curve)
.style("fill", (d, i) => color(i / (bezier.length - 1)))
.attr("cx", d => d[0])
.attr("cy", d => d[1]);
}
}

return svg.node();
}
Insert cell
mutable bezier = cubicBezier(a, b, c, d, precision)
Insert cell
mutable a = [w * 0.1, height * 0.9];
Insert cell
mutable b = [w * 0.2, height * 0.25];
Insert cell
mutable c = [w * 0.8, height * 0.75];
Insert cell
mutable d = [w * 0.9, height * 0.1];
Insert cell
w = Math.min(640, width)
Insert cell
height = 500
Insert cell
drag = {
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(event, d) {
const p = [event.sourceEvent.offsetX, event.sourceEvent.offsetY];
const C = Array.from(this.classList);
if (C.includes("a")) {
mutable a = p;
}
else if (C.includes("b")) {
mutable b = p;
}
else if (C.includes("c")) {
mutable c = p;
}
else {
mutable d = p;
}
d3.select(this)
.attr("cx", p[0])
.attr("cy", p[1]);
}

function dragended(event, d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function cubicBezier(a, b, c, d, precision = 0.1){
return sample(interpolateCubicBezier(a, b, c, d), precision);
}
Insert cell
class Node {
constructor(value, point, next = null) {
this.value = value; // t value
this.point = point; // corresponding interpolator(t)
this.next = next;
}
}
Insert cell
// Recursive subdivision using perpendicular distance threshold
function sample(interpolator, precision = 0.1, maxIters = 1e3){
const start = new Node(0, interpolator(0));
const mid = new Node(0.5, interpolator(0.5));
const end = new Node(1, interpolator(1));
start.next = mid;
mid.next = end;

let iters = 0;
while (iters < maxIters) {
let any = false;
let current = start;
while (current && current.next) {
const t = 0.5 * (current.value + current.next.value);
const p = interpolator(t);
if (length([ p, midpoint([current.point, current.next.point]) ]) > precision) {
// Insert the new point
any = true;
const n = new Node(t, p, current.next);
current.next = n;
current = n.next; // Skip the next pair since we've already checked it
} else {
current = current.next; // Move to the next pair
}
}

// If there are no more lengths exceeding the threshold, end the algorithm
if (!any) {
break;
} else {
iters++;
}
}

// Collect the points into an array
const points = [];
let current = start;
while (current) {
points.push(current.point);
current = current.next;
}
return points;
}
Insert cell
// See https://math.stackexchange.com/questions/26846/is-there-an-explicit-form-for-cubic-b%C3%A9zier-curves
function interpolateCubicBezier(a, b, c, d){
const interpolate = (i, t) => pow((1 - t), 3) * a[i] + 3 * t * pow((1 - t), 2) * b[i] + 3 * pow(t, 2) * (1 - t) * c[i] + pow(t, 3) * d[i];
return t => [interpolate(0, t), interpolate(1, t)]
}
Insert cell
function length(line) {
const [[x0, y0], [x1, y1]] = line;
return sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2));
}
Insert cell
function midpoint(line) {
const [[x0, y0], [x1, y1]] = line;
return [0.5 * (x0 + x1), 0.5 * (y0 + y1)];
}
Insert cell
pow = Math.pow
Insert cell
sqrt = Math.sqrt
Insert cell
color = d3.interpolateTurbo;
Insert cell
r = 8
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more