Public
Edited
May 18, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("overflow", "visible");

svg.append("polyline")
.attr("fill", "none")
.attr("points", P)
.attr("stroke", "black");
svg.selectAll(".point")
.data([a, b])
.join("circle")
.attr("class", "point")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("fill", (d, i) => color(i))
.attr("r", 4);
svg.selectAll(".interp")
.data(P)
.join("circle")
.attr("class", "interp")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("fill", (d, i, e) => color(i / (e.length - 1)))
.attr("r", showPoints ? 4 : 0);
return svg.node()
}
Insert cell
P = arc(a, b, offset)
Insert cell
a = [width * 0.4, height * 0.5]
Insert cell
b = [width * 0.6, height * 0.5]
Insert cell
height = 500
Insert cell
color = d3.interpolateTurbo;
Insert cell
function arc(a, b, offset = 1){
const r = lineLength([a, b]) / 2;
const i = interpolateArc(
a,
pointTranslate(
lineMidpoint([a, b]),
lineAngle([a, b]) - 90,
scale([-1, 1], [-r, r], offset)
),
b
);
return sample(i, precision);
}
Insert cell
// See https://observablehq.com/@jrus/circle-arc-interpolation
function interpolateArc(a, m, b){
// Calculate two vectors: b_m and m_a,
// which are the vectors from the midpoint to the end point
// and from the start point to the midpoint, respectively
const b_m = csub(b, m);
const m_a = csub(m, a);

// Calculate two more values: ab_m and bm_a.
// The first is the complex multiplication of the start point and b_m,
// and the second is the complex multiplication of the end point and m_a
const ab_m = cmul(a, b_m);
const bm_a = cmul(b, m_a);

// Perform a linear interpolation between ab_m and bm_a and between b_m and m_a,
// and divide the first result by the second.
// This results in a point on the circular arc between a and b
// that corresponds to the input parameter t.
return t => cdiv(
clerp(ab_m, bm_a, t),
clerp(b_m, m_a, t)
);
}
Insert cell
Insert cell
// Divides two complex numbers
function cdiv(z0, z1) {
return cmul(z0, cinv(z1));
}
Insert cell
// Calculates the multiplicative inverse (or reciprocal) of a complex number
function cinv([x, y]) {
const s = 1 / (x * x + y * y);
return [s * x, -s * y];
}
Insert cell
// Linearly interpolates between two complex numbers
function clerp([x0, y0], [x1, y1], t) {
return [x0 * (1 - t) + x1 * t, y0 * (1 - t) + y1 * t];
}
Insert cell
// Multiplies two complex numbers
function cmul([x0, y0], [x1, y1]) {
return [x0 * x1 - y0 * y1, x0 * y1 + y0 * x1];
}
Insert cell
// Subtracts one complex number from another
function csub([x0, y0], [x1, y1]) {
return [x0 - x1, y0 - y1];
}
Insert cell
Insert cell
// A linear scale
function scale([d0, d1], [r0, r1], value){
const dx = d1 - d0;
const rx = r1 - r0;
return rx * ((value - d0) / dx) + r0;
}
Insert cell
Insert cell
// Recursive subdivision using perpendicular distance threshold:
// An adaptive sampling strategy to discretize a path interpolator,
// ensuring that the sampled points are always within the given precision from the actual path.
// It does this by adding more points in the regions of the path where the curvature is higher
// and fewer points where it's lower, thus achieving an efficient and accurate sampling.
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 (lineLength([ p, lineMidpoint([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
class Node {
constructor(value, point, next = null) {
this.value = value; // t value
this.point = point; // corresponding interpolator(t)
this.next = next;
}
}
Insert cell
Insert cell
lineAngle = geometric.lineAngle
Insert cell
lineLength = geometric.lineLength
Insert cell
lineMidpoint = geometric.lineMidpoint
Insert cell
pointTranslate = geometric.pointTranslate
Insert cell
Insert cell
pow = Math.pow
Insert cell
sqrt = Math.sqrt
Insert cell
Insert cell
geometric = require("geometric@2")
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