Public
Edited
Aug 23, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
plotCanvas("myCanvas1", 900)
Insert cell
pVelocity = plotVelocity(300)
Insert cell
plotLeadtimeVsTime(300)
Insert cell
plotLeadtime(300)
Insert cell
plotPacmenVelocityDistribution(300)
Insert cell
async function* plotCanvas(id="myCanvas1",w=900, r=20) {
let averageLeadTime = 0
let averageVelocity = 0
const textLine1 = () => "pacmen: "+n
const textLine2 = () => "pacmen velocity: "+pacmenVelocity(particles)
const textLine3 = () => "lead time: "+statistics.at(-1).leadTime.toFixed(1)+" s"
const textLine4 = () => "parcel velocity: "+averageVelocity+" p/min"
const radius = r
const height = w/2
const minDistance= 40
const maxDistance = 60
const simulationStart = new Date()
const timeSpent = () => new Date()-simulationStart
const minDistance2 = minDistance * minDistance;
const width = w
const maxDistance2 = maxDistance * maxDistance;
const radius2 = radius * radius;
//const context = DOM.context2d(width, height);
const context = document.getElementById(id).getContext('2d');
const yOnPath = (x) => {
const x1 = width*0.25
const x2 = width*0.75
const slope = (height-radius*2)/(x2-x1)
if (x<x1) return radius
if (x>x2) return height - radius
return radius+(x-x1)*slope
}
const gaussianRand = () => {
var rand = 0;
for (var i = 0; i < 6; i += 1) rand += Math.random()
return rand / 6;
}
const gaussianRandom = (start, end) => Math.floor(start + gaussianRand() * (end - start + 1))
const particles = new Array(n);
for (let i = 0; i < n; i++) {
const rnd = Math.random()
const startVelocity = -3 + gaussianRandom(-3,3)
particles[i] = {
i: i,
x: (i+0.5)/n * width,
y: yOnPath((i+0.5)/n) * height,
velocity: startVelocity,
originalVelocity: startVelocity,
color: "rgb(42,42,73)",
hasParcel: false,
rotation: 0, // 0°..180°
turning: false,
hasParcel: false,
pickupTime: 0,
jammed: 0
};
pacmen.push({"x": particles[i].x, "velocity": -particles[i].originalVelocity, "color": particles[i].color})
}
const pacmenVelocity = (pa) => (pa.reduce((acc, p) => acc+=Math.abs(p.originalVelocity),0)/pa.length).toFixed(2)
const goingUp = p => p.originalVelocity < 0
const goingDown = p => p.originalVelocity > 0
const upStream = (pi,pj) => pi.x < pj.x ? pi : pj
const downStream = (pi,pj) => pi.x < pj.x ? pj : pi
const showValInId = (val = "empty", id) => {if ( id = document.getElementById(id)) id.innerText = val}
const maxVelocity = particles.reduce((acc,p) => acc = Math.abs(p.velocity) > acc ? Math.abs(p.velocity) : acc, 0)
const sameDirection = (pi, pj) => (pi.originalVelocity*pj.originalVelocity > 0)
const upfrontIsAtStandStill = (pi,pj) => {
// if opposite direction return false
if (!sameDirection(pi,pj)) return false
// going up, return true is upstream(pi,pj).velocity ==0
if (goingUp(pi)) return upStream(pi,pj).velocity == 0
// opposite
if (goingDown(pi)) return downStream(pi,pj).velocity == 0
// catchall
return false
}

const handoverOrSlowdownDistance = 4*radius2 //+ maxVelocity*2
const d2 = (pi, pj) => (pi.x - pj.x)**2 + (pi.y - pj.y)**2
const touching = (pi,pj) => d2(pi,pj) <= handoverOrSlowdownDistance
const p_downStream = p => (p.i < particles.length-1) ? particles[p.i+1] : particles[0]
const p_upStream = p => (p.i > 0) ? particles[p.i-1] : particles[particles.length-1]
const noOneAhead = p => goingDown(p) ? !touching(p_downStream(p),p) : !touching(p_upStream(p),p)
const oppositeDirection = (pi, pj) => (pi.velocity*pj.velocity < 0)
const oneAtStandstill = (pi,pj) => pi.velocity * pj.velocity == 0
const slowDown = (pi, pj) => {
let slowVelocity = Math.abs(pi.velocity) < Math.abs(pj.velocity) ? pi.velocity : pj.velocity
pi.velocity = slowVelocity
pj.velocity = slowVelocity
}
const swapParcels = (pi, pj) => {
const swap = pi.hasParcel
pi.hasParcel = pj.hasParcel
pj.hasParcel = swap
const swapColor = pi.color
pi.color = pj.color
pj.color = swapColor
pj.pickupTime = pi.pickupTime
pj.jammed = pi.jammed
if (trafficJamDownstream(pj, particles)) pj.jammed++
}
const pickParcel = p => {
p.hasParcel = true
p.color = d3.interpolate("red", "blue")(d3.randomUniform()())
p.pickupTime = new Date()
}
const leadTime = p => (new Date() - p.pickupTime)/1000
const updateStatistics = (p) => {
mutable statistics.push({
timestamp : timeSpent()/1000,
transportedParcels: transportedParcels,
parcelsPerMinute: parcelSpeed(),
parcelsEnRoute: particles.reduce((acc,p) => acc += p.hasParcel ? 1:0 ,0),
jammed: p.jammed / n,
particlesMoving: particles.reduce((acc,p) => acc += (p.velocity != 0) ? 1:0 ,0),
leadTime: leadTime(p)
})
averageLeadTime = (statistics.reduce((sum,cv) => sum += cv.leadTime, 0) / statistics.length).toFixed(2)
averageVelocity = parcelSpeed()
// keep array at max 200 entries
if (statistics.length > 200) statistics.shift()
}
const dropParcel = p => {p.hasParcel = false ; updateStatistics(p)}
const atTop = (p) => p.x <= radius
const atBottom = p => p.x >= width-radius
const changeDirection = p => {p.velocity *= -1; p.originalVelocity *= -1}
const ABS = x => Math.abs(x)
const goToBottom = p => {p.velocity = ABS(p.velocity); p.originalVelocity = ABS(p.originalVelocity)}
const goToTop = p => {p.velocity = -Math.abs(p.velocity); p.originalVelocity = -Math.abs(p.originalVelocity)}
const restoreOriginalVelocity = p => p.velocity = p.originalVelocity
const goForABit = p => { p.x += p.velocity; p.y = yOnPath(p.x) }
const trafficJam = () => particles.reduce((acc,p) => acc || !p.turning && (p.velocity != p.originalVelocity), false)
const particlesDownstream = (oPart, particles) => particles.filter (p => p.x >= oPart.x)
const trafficJamDownstream = (p, partticles) => particlesDownstream(p, particles).reduce((acc,p) => acc || !p.turning && (p.velocity != p.originalVelocity), false)
let transportedParcels = 0
const parcelSpeed = () => Math.round(transportedParcels / timeSpent() * 600000)/10
const drawCircularShape = (context, p) => {
context.beginPath()
context.arc(p.x, p.y, radius, (-0.6+p.rotation/180)*Math.PI, (0.4+p.rotation/180)* Math.PI);
context.fillStyle = p.hasParcel ? p.color : "white";
context.fill()
context.closePath()
context.beginPath()
context.arc(p.x, p.y, radius, p.rotation/180*Math.PI, (1+p.rotation/180)* Math.PI);
context.fillStyle = p.hasParcel ? p.color : "white";
context.fill()
context.closePath()
}
const upStreamHasParcel = (pi,pj) => upStream(pi,pj).hasPacel
const oneOfThemIsTurning = (pi,pj) => pi.turning || pj.turning
const rotate = p => {
p.turning = true
p.velocity = 0
p.rotation += (p.originalVelocity < 0 ) ? 10 : -10
if (goingUp(p) && p.rotation >= 120) {
p.turning = false
goToBottom(p)
restoreOriginalVelocity(p)
}
if (goingDown(p) && p.rotation <= 0) {
p.turning = false
goToTop(p)
restoreOriginalVelocity(p)
}
}


// ------------------
while (true) {
await Promises.delay(10);
context.save();
context.clearRect(0, 0, width, height);
showValInId(maxVelocity, "_maxVelocity")
const ptext = particles.reduce((acc,p, i) => {
acc += "\n"+i+": px: "+Math.round(p.x)+ "\t py: "+Math.round(p.y)
acc += "\t p.velocity: "+p.velocity+" oV: "+ p.originalVelocity
acc += " rotation: "+p.rotation
acc += " turning: "+p.turning
acc += " goingUp: "+(p.originalVelocity < 0)
return acc
}
,"")
showValInId(ptext, "_particles")
mutable trafficJam = trafficJam()
showValInId(trafficJam(), "_trafficJam")
showValInId(transportedParcels, "_transportedParcels")
showValInId(timeSpent(), "_timeSpent")
showValInId(parcelSpeed(), "_parcelSpeed")
// ----------
for (let i = 0; i < n; ++i) {
const p = particles[i];
// if at zero, take a parcel
if (atTop(p)) {
if (!p.hasParcel) pickParcel(p)
rotate(p)
}
// if at width, drop a parcel
if (atBottom(p)) {
if (p.hasParcel) {
dropParcel(p)
transportedParcels++
}
rotate(p)
}
// go a bit if not turning
if (!p.turning) goForABit(p)
// draw circular shape
drawCircularShape(context, p)
}
let pc = ""
for (let i = 0; i < n; ++i) {
for (let j = i + 1; j < n; ++j) {
const pi = particles[i];
const pj = particles[j];
const dx = pi.x - pj.x
const dy = pi.y - pj.y
const d2 = dx*dx + dy*dy

// what happens if you come close to another --> draw a connecting line
if (d2 < maxDistance2) {
context.globalAlpha = d2 > minDistance2 ? (maxDistance2 - d2) / (maxDistance2 - minDistance2) : 1;
context.beginPath();
context.moveTo(pi.x, pi.y);
context.lineTo(pj.x, pj.y);
//context.strokeStyle = pi.color;
context.stroke();
}
// if neither edge nor other particle is near by, do not do anything
pc += "\n"+i + ":"+j+" handOverD: "+handoverOrSlowdownDistance+" d2: "+d2
if ( touching(pi,pj) ){ // d2 <= handoverOrSlowdownDistance) {
if (sameDirection (pi, pj) || upfrontIsAtStandStill(pi,pj)) {
// traffic jam
// if the other dot has the same direction, slow down to the slower speed
slowDown(pi, pj)
}
else if (upStream(pi,pj).hasParcel && !downStream(pi,pj).hasParcel) {
rotate(pi)
rotate(pj)
if (!pj.turning) swapParcels(pi, pj)
}
}
}
showValInId(pc, "_particleCombinations")
}
context.restore();
context.font="24px Courier";
context.fillStyle = "white";
context.textAlign = "left";
context.fillText(textLine1(), width/2, 20);
context.fillText(textLine2(), width/2, 50);
context.fillText(textLine3(), width/2, 80);
context.fillText(textLine4(), width/2, 110);
//context.canvas.style.background = 'rgb(245,245,255)';
yield context.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable pacmen = []
Insert cell
async function* plotPacmenVelocityDistribution(w=400) {
while(true) {
yield Plot.plot({
width: w,
className: "custom-plot",
title: "pacmen velocity distribution",
subtitle: "",
margin: 0.065*w,
grid: true,
height: w/2 ,
color: {legend: true},
marks: [
Plot.ruleY([0]),
Plot.dot(pacmen, {x: "x", y: "velocity", stroke: "blue"})
]
})
await Promises.delay(1000);
}
}

Insert cell
mutable trafficJam= false
Insert cell
Insert cell
<style type="text/css">
p,li,ul,h1 { max-width: 90% }

svcontext.canvas;g.custom-plot {
background-color: rgb(210,210,255);
background-image: url("https://ahojsenn.github.io/assets/img/potsdamer-platz.png");
color: blue;
padding: 16px;
}

.custom-plot {
background-color: rgb(210,210,255);
color: blue;
padding: 16px;
}
</style>
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