Public
Edited
Dec 2, 2022
Insert cell
Insert cell
Insert cell
chart()
Insert cell
chart = (radius = 10, padding = 2) => {
// set the width and height
let width = window.innerWidth;
let height = 0.5 * width;
// create an svg element
let svg = d3.create("svg").attr("width", width).attr("height", height).style("background", "whitesmoke");
let container = svg.append("g").attr("class", "board").attr("transform", `translate(${[width/2, height/2]})`);

// set the path generator function
let line = d3.linkHorizontal()
.x(d => d.x)
.y(d => d.y);

// set the start and end points
let lineData = { source: { x: -0.4 * width, y: -0.36 * height }, target: { x: 0.43 * width, y: 0.36 * height } };
let linePoints = [{x: -0.4 * width, y: -0.36 * height},
{x: 0.4 * width, y: -0.36 * height},
{x: 0.4 * width + 20, y: -0.36 * height - 20},
{x: 0.4 * width, y: -0.36 * height - 40},
{x: -0.4 * width, y: -0.36 * height - 40}
];

let lc = d3.line()
.curve(d3.curveMonotoneY)
.x(d => d.x)
.y(d => d.y);
// draw the path
let path = container
.append("path")
.attr("d", lc(linePoints))
.style("stroke", "#FF7F0F")
.style("fill", "none");

// draw the path
let path1 = container
.append("path")
.attr("d", line(lineData))
.style("stroke", "#FF7F0F")
.style("fill", "none");
// get the circle centre points along the curve
let pos = lineup(path, radius, padding);
const updateQueued = (data) => {
let circles = container.selectAll("circle.queued").data(data, d => d.id);
circles
.enter()
.append("circle")
.attr("class", "queued")
.attr("id", d => d.id)
.attr("r", 10)
.style("stroke", d3.rgb("steelblue").darker())
.style("fill", "steelblue")
.style("opacity", 0.1)
.attr("transform", `translate(${[-width/2, linePoints[linePoints.length - 1].y]})`)
.transition()
.duration(500)
.style("opacity", 1)
.attr("transform", `translate(${[linePoints[linePoints.length - 1].x, linePoints[linePoints.length - 1].y]})`)
.transition()
.duration(1500)
.ease(d3.easeSinInOut)
.attrTween("transform", (d,i) => {
const s = d3.interpolate(d.startPos * path.node().getTotalLength(), d.endPos * path.node().getTotalLength());
return function(t) {
let p = path.node().getPointAtLength(s(t));
//console.log(t, p.x, p.y)
return 'translate(' + p.x + ',' + p.y + ')';
};
});

circles
.transition()
.duration(1500)
.delay((d,i) => 50 * i)
.attrTween("transform", function(d,i) {
const s = d3.interpolate(d.startPos * path.node().getTotalLength(), d.endPos * path.node().getTotalLength());
return function(t) {
let p = path.node().getPointAtLength(s(t));
//console.log(t, p.x, p.y)
return 'translate(' + p.x + ',' + p.y + ')';
};
});

circles
.exit()
.transition()
.duration(1000)
.attrTween("transform", (d,i) => {
const s = d3.interpolate(0 * path1.node().getTotalLength(), 1 * path1.node().getTotalLength());
return function(t) {
let p = path1.node().getPointAtLength(s(t));
//console.log(t, p.x, p.y)
return 'translate(' + p.x + ',' + p.y + ')';
};
})
.transition()
.duration(500)
.style("fill", "white")
.transition()
.duration(500)
.style("opacity", 0)
.attr("transform", `translate(${[width/2,lineData.target.y]})`)
.remove();
};

let data = []; //[{id: "aa", pos: 1}, {id: "bb", pos: 1}];
let t = 0;
d3.select("#startButton").on("click", () => {

let ticker = d3.interval(elapsed => {
console.log(elapsed, t, data.length)
data.forEach((d,i) => {d.startPos = d.endPos;});
data.push({id: Math.random(), startPos: 1, endPos: pos[data.length].t});
if (t % 3 === 0) {
data.forEach((d,i) => {d.endPos = i > 0 ? data[i-1].startPos : 0});
data.shift();
//
}
updateQueued(data);
t += 1;
if (elapsed > 40000) ticker.stop();
}, 2600);
});
// return the node
return svg.node();
}
Insert cell
d3 = require("d3@7")
Insert cell
lineup = (path, radius, padding) => {
// convenience method that returns the length of the path between 0 and 1
let s = d3.interpolate(0, path.node().getTotalLength());

// the distance between two adjacent circle centres
let circleDistance = 2 * radius + padding;

// Euclidean distance
let distance = (pointA, pointB) => {
return Math.sqrt(
Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)
);
};

// The Bolzano algorith to return the solution point on the path
let bolzano = (t0) => {
let t,
point0 = path.node().getPointAtLength(s(t0)),
point,
s0 = t0,
s1 = 1.0,
diff = 1,
i = 0;

while (Math.abs(diff) > 1e-1 && i < 100) {
t = (s0 + s1) / 2;
point = path.node().getPointAtLength(s(t));
diff = distance(point0, point) - circleDistance;
if (diff > 0) {
s1 = t;
} else if (diff < 0) {
s0 = t;
}
i += 1;
}
return t;
};

// Start with the path origin point
// Start with the path origin p
let t = 0;
let point = path.node().getPointAtLength(s(t));
let endPoint = path.node().getPointAtLength(s(1));
let results = [{ t, x: point.x, y: point.y }];

// Repeat while the circles distance still fits between the current point and the endPoint
while (distance(point, endPoint) >= circleDistance) {
t = bolzano(t);
point = path.node().getPointAtLength(s(t));
results.push({ t: t, x: point.x, y: point.y });
}
return results;
};
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