Public
Edited
Dec 30, 2022
Insert cell
Insert cell
height = 500
Insert cell
FileAttachment("bunny@1.png").image()
Insert cell
{
const frame = 5;
const focus_agent_id = 44;
const data = sim;
const app = new PIXI.Application({
width: width,
height: height,
backgroundColor: 0x000000,
resolution: window.devicePixelRatio || 1,
});
invalidation.then(() => app.destroy(true, true));

const left_margin = 200;
const top_margin = 20;
const container = new PIXI.Container();

app.stage.addChild(container);

const x_pos = d3.scaleLinear()
.domain([0,49])
.range([left_margin,width-left_margin]);
const y_pos = d3.scaleLinear()
.domain([0,49])
.range([top_margin,height-top_margin]);

const texture = PIXI.Texture.from(await FileAttachment("bunny@1.png").image());
const agents = new Map();
// const next_step_index = new Map();
for (let i=0; i<data[0].length; i++) {
const agent = new PIXI.Sprite(texture);
agent.x = x_pos(data[0][i].x);
agent.y = y_pos(data[0][i].y);
agent.tint = get_tint(data[0][i].stage);
agent.scale.set(0.5);
if (data[0][i].id == focus_agent_id) agent.scale.set(1);
agent.anchor.set(0.5);
container.addChild(agent);
agents.set(data[0][i].id, agent);
// next_step_index.set(data[0][i].id, find_agent_index(data[0][i].id, 1));
}

function find_agent_index(id, step) {
return data[step].findIndex(d => d.id == id);
}

// Get the texture for rope.
const trailTexture = PIXI.Texture.from(await FileAttachment("trail.png").image());
// historySize determines how long the trail will be.
const historySize = 1000;
const historyX = new Array(historySize).fill(0);
const historyY = new Array(historySize).fill(0);
// ropeSize determines how smooth the trail will be.
const ropeSize = 500;
const points = [];
// Create rope points.
for (let i = 0; i < ropeSize; i++) {
points.push(new PIXI.Point(0, 0));
}


// Create the rope
const rope = new PIXI.SimpleRope(trailTexture, points);
rope.blendMode = PIXI.BLEND_MODES.ADD;

app.stage.addChild(rope);
let step = 0;
let t = 1;

app.ticker.add(() => {
for (let i=0; i<data[step].length; i++) {
if (agents.has(data[step][i].id)) {
const agent = agents.get(data[step][i].id);
if (t == 0) {
agent.x = x_pos(data[step][i].x);
agent.y = y_pos(data[step][i].y);
agent.tint = get_tint(data[step][i].stage);
agent.alpha = 1;
// next_step_index.set(data[step][i].id, find_agent_index(data[step][i].id, (step+1)%data.length));
}
else {
// let _i = next_step_index.get(data[step][i].id);
let _i = i;
if (Math.abs(data[(step+1)%data.length][_i].x - data[step][i].x) ==49) agent.alpha -= 1/frame;
else if (Math.abs(data[(step+1)%data.length][_i].y - data[step][i].y) ==49) agent.alpha -= 1/frame;
else {
agent.x = x_pos((data[(step+1)%data.length][_i].x - data[step][i].x)*t/frame + data[step][i].x);
agent.y = y_pos((data[(step+1)%data.length][_i].y - data[step][i].y)*t/frame + data[step][i].y);
}
}

if (data[step][i].id == focus_agent_id) {
historyX.pop();
historyX.unshift(agent.x);
historyY.pop();
historyY.unshift(agent.y);
// Update the points to correspond with history.
for (let i = 0; i < ropeSize; i++) {
const p = points[i];
// Smooth the curve with cubic interpolation to prevent sharp edges.
const ix = cubicInterpolation(historyX, i / ropeSize * historySize);
const iy = cubicInterpolation(historyY, i / ropeSize * historySize);
p.x = ix;
p.y = iy;
}
}
}
}

t = (t+1)%frame;
if (t == 0) step = (step+1)%data.length;
if (step == 0) {
const agent = agents.get(focus_agent_id);
for (let i = 0; i < ropeSize; i++) {
points[i].x = agent.x;
points[i].y = agent.y;
}
for (let i = 0; i < historySize; i++) {
historyX[i] = agent.x;
historyY[i] = agent.y;
}
}
});
return Object.assign(app.view, { style: `width: ${width}px` });
}
Insert cell
/**
* Cubic interpolation based on https://github.com/osuushi/Smooth.js
*/
function clipInput(k, arr) {
if (k < 0) k = 0;
if (k > arr.length - 1) k = arr.length - 1;
return arr[k];
}

Insert cell

function getTangent(k, factor, array) {
return factor * (clipInput(k + 1, array) - clipInput(k - 1, array)) / 2;
}

Insert cell

function cubicInterpolation(array, t, tangentFactor) {
if (tangentFactor == null) tangentFactor = 1;

const k = Math.floor(t);
const m = [getTangent(k, tangentFactor, array), getTangent(k + 1, tangentFactor, array)];
const p = [clipInput(k, array), clipInput(k + 1, array)];
t -= k;
const t2 = t * t;
const t3 = t * t2;
return (2 * t3 - 3 * t2 + 1) * p[0] + (t3 - 2 * t2 + t) * m[0] + (-2 * t3 + 3 * t2) * p[1] + (t3 - t2) * m[1];
}

Insert cell
Plot.plot({
y: {
grid: true,
type: "log"
},
color: {
legend: true,
fillOpacity: 0.5,
},
marks: [
Plot.areaY(unrolled_sim, Plot.binX({y: "count"}, {x: "step", fill: "stage"})),
Plot.ruleY([0])
]
})
Insert cell
viewof step = Inputs.range([0, sim.length-1], {value: 0, step: 1, label: "Step"})
Insert cell
Insert cell
{
const data = sim[step].filter(d => d.stage != "Stage.SUSCEPTIBLE");
// const data = sim[step].filter(d => d.stage == "Stage.SEVERE");
return Plot.plot({
inset: 20,
color: {
legend: true
},
marks: [
Plot.density(data, {x: "x", y: "y", stroke: "steelblue", strokeWidth: 0.15, bandwidth: 20}),
Plot.density(data, {x: "x", y: "y", stroke: "steelblue", thresholds: 3, bandwidth: 20}),
Plot.dot(data, {x: "x", y: "y", fill: "stage", r: 3})
]
});
}
Insert cell
unrolled_sim = sim.map((d,i) => d.map(e => {return {...e, step: i}})).flat();
Insert cell
function get_tint(stage) {
if (stage == "Stage.vaccinated") return 0xC5979D;
if (stage == "Stage.SUSCEPTIBLE") return 0x7cd4fc;
if (stage == "Stage.EXPOSED") return 0xf54242;
if (stage == "Stage.ASYMPTOMATIC") return 0xebaa28;
if (stage == "Stage.SYMPDETECTED") return 0xf3f595;
if (stage == "Stage.ASYMPDETECTED") return "cyan";
if (stage == "Stage.SEVERE") return 0xf5126b;
if (stage == "Stage.RECOVERED") return 0x42f5bc;
return 0x5c9fbd;
}

Insert cell
sim = (await FileAttachment("sim_200.json").json()).map(step => step.sort((a,b) => a.id - b.id));
Insert cell
PIXI = require("https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.0.5/pixi.min.js").catch(
() => window.PIXI
)
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