Final Project – Interactive Pandemic Visualizer

sketch
/*
 * Eric Zhao
 * ezhao2
 * 15-104D
 * Final Project: Pandemic Simulation
 */

var p;
var noiseStep = 0.005;
var noiseParam = 0;
var particles = []; //all particles
var numParticles = 50;
var infectedList = []; //all the particles that are infected
var generation = 0; //what "round" of infection the dots are on
var colorList = []; //list of colors to mark the generation of infection
var paused = false; //is the program paused?
var numCols = 8; //number of dots per row (color picker)
var menuMode = 0; //shows controls or visualizations
var masksOn = false; //are the particles wearing masks
var filterCol = 0; //color 
var filterState = false; //is the program filtering dots?
var masksOn = false; //are the dots wearing masks?
var sketchState = 0; //affects if title screen is displayed


function setup() {
    //makes array of particles and infects the first one
    createCanvas(400, 600);
    background(100);
    colorMode(HSB);
    for(var i = 0; i < numParticles; i++){
        p = makeParticle(random(0, width), random(0, height), 3, 3,
            random(0, 100), i);
        particles.push(p);
    }
    var startColor = color(0, 100, 100);
    colorList.push(startColor);

    particles[0].infectedBy = 0;
    particles[0].size = 25;
    particles[0].infectColor = colorList[0];
    infectedList.push(particles[0]);

}

function draw(){
    stroke(100);
    background(50, 20, 20);
    fill(100);
    for(var i = 0; i < numParticles; i++){
        if(particles[i].infectedBy != -1){
            drawConnections(particles[i]);
        } 
    }
    for(var i = 0; i < numParticles; i++){
        particles[i].draw();
        if(paused == false){
            particles[i].step();
        }
    }
    if(paused == false){
        noiseParam+=noiseStep;
    }
    drawUI();
}


function makeParticle(px, py, dpx, dpy, noiseOffset, index){
    p = {x: px, y: py, dx: dpx, dy: dpy,
         infectedBy: -1, //if infected, which dot infected it?
         ID: index, //the dot's position in the original array
         infectColor: color(0, 0, 100), //color of the dot
         size: 10, //size of the dot
         offset: noiseOffset,
         step: stepParticle,
         draw: drawParticle};
    return p;
}

function stepParticle(){
    //moves particle based on Perlin noise

    this.x = width * map(noise(noiseParam + this.offset), 0, 1, -0.4, 1.4);
    this.y = width * map(noise(noiseParam + this.offset + 5), 0, 1, -0.4, 1.4);
}

function drawParticle(){
    fill(255);
    push();
    noStroke();
    if(this.infectedBy > -1){
            fill(this.infectColor); 
    }
    //show dot only if not filtering or if it matches filter color
    if(filterState == true){
        var tempCLR = getRGB(this.infectColor);
        if(compareColors(tempCLR, filterCol) == false){
            noFill();
        }
    }
    circle(this.x, this.y, this.size);
    pop();
}

function drawConnections(infectedP){
    //draws a line between a dot and its infector
    push();
    var ind = infectedP.infectedBy;
    if(infectedP.infectedBy > -1){
        var tempCLR = particles[ind].infectColor;
        strokeWeight(1);
        stroke(tempCLR);
    }
    //show line only if it matches the filter color
    if(filterState == true){
        var infectedRGB = getRGB(tempCLR);
        if(compareColors(tempCLR, filterCol) == false){
            noStroke();
        }
    }
    line(infectedP.x, infectedP.y, particles[ind].x, particles[ind].y);   
    pop();
}

function infection(population, infected){
    //make empty array, push newly infected particles here

    //if particles were infected, generate a new fill color
    //for them

    //adjust size of infected particles
    var base = infected.length;
    var newCases = [];
    print("Previous number: "+infected.length);

    //the basic logic: for all infected particles, check
    //if any non-infected particles are nearby. If so,
    //infect the particle and push it to an empty array.
    //add this array to the whole list of infected particles
    //at the end.
    for(var i = 0; i < infected.length; i++){
        for(var j = 0; j < numParticles; j++){
            if(dist(population[j].x, population[j].y,
            infected[i].x, infected[i].y) < 25
            & population[j].infectedBy == -1){
                if(masksOn == true){
                    if(random(0,1) > 0.66){
                        population[j].infectedBy = infected[i].ID;
                        newCases.push(population[j]);
                    }
                } else {
                    population[j].infectedBy = infected[i].ID;
                    newCases.push(population[j]);
                }
            }
        }
    }

    if(newCases.length > 0) {
        for(var i = 0; i < newCases.length; i++){
            infected.push(newCases[i]);
        }
        generation++;
        var newC = color(random(360), random(25, 100), 100);
        colorList.push(newC);
        print(infected.length);
        print(colorList.length);
        for(i = 0; i < (infected.length-base); i++){
            //makes particle size 90 percent of the one it was infected by
            infected[base+i].size =
                0.9*(particles[infected[base+i].infectedBy].size);
            //changes color to a new random fill (per generation)
            infected[base+i].infectColor = colorList[generation]; //
        }
    }
    print("Number infected: " + infected.length);
    print("Generation: "+ generation);
    print(colorList.length);
    print("__________h_______");
}

function mousePressed(){
    if(sketchState == 0){
        sketchState = 1;
    } else {
        if(mouseY < width){
            infection(particles, infectedList);
        } else if(mouseY >= width & mouseY <= height){
            filterStatus(get(mouseX, mouseY));
        }
    }
}

function keyTyped(){
    if(key === 'p'){
        //pause function that freezes movement but allows infection
        pauseMove();
    }
    if(key === 'r'){
    //Resets dot states but not entire sketch
        resetSketch();
    }
    if(key === 'h'){
    //switches menu between guide and data visualization
        if(menuMode == 0){
        menuMode = 1;
        print('data');
        } else {
            menuMode = 0;
            print('help');
        }
    }
    if(key === 'm'){
    //toggles masks on/off
        if (masksOn){
            masksOn = false;
        } else{
            masksOn = true;
        }
    }
}

function titleScreen(){
    push();
        translate(25, 75);
        stroke(100);
        fill(50, 5, 100, 0.9);
        textSize(14);
        var rectW = width - 50;
        var rectH = 0.48*height;
        var backgroundTxt = "This is a simple simulation of a viral outbreak \
such as COVID-19 modeled with a number of colored dots.";
        var inst_1 = "1) Each time you click, all infected (colored) dots \
        will infect those nearby."
        var inst_2 = "2) Connections between the infector and newly infected \
            dot are shown."
        var inst_3 = "3) Make the dots wear masks by pressing [ W ]."
        var inst_4 = "4) Filter infected dots by pressing [ H ]."
        var inst_5 = "5) Reset all dots with [ R ]."
        var inst_6 = "(click anywhere to hide)"

        rect(0, 0, rectW, rectH, 10);
        fill(0);
        textSize(18);
        textStyle(BOLD);
        text("Visualizing a Pandemic", 20, 30);
        textSize(14);
        text("How to Use:", 20, 105);
        textSize(12);
        textStyle(NORMAL);
        text(backgroundTxt, 20, 50, 300, 150);
        text(inst_1, 20, 120, 300, 150);
        text(inst_2, 20, 160, 300, 150);
        text(inst_3, 20, 200, 300, 150);
        text(inst_4, 20, 225, 300, 150);
        text(inst_5, 20, 260);
        text(inst_6, 215, 28);
        stroke(0);
        line(20, 40, rectW-20, 40);
    pop();
}

function drawUI(){
    push();
        if(sketchState == 0)
            titleScreen();
        translate(25, width);
        fill(50, 20, 25, 0.75);
        textSize(14);
        var rectW = width - 50;
        var rectH = (height-width)-25;
        rect(0, 0, rectW, rectH, 10);
        noStroke();
        fill(100);
        textSize(14);
        textStyle(BOLD);

        if(masksOn){
            fill(120, 72, 90);
            text("Masks ON", 275, -10);
        } else{
            fill(0, 72, 90);
            text("Masks OFF", 275, -10);
        }

        fill(100);
        if(menuMode == 0){
            displayHelp();
        } else{
            displayData();
        }
    pop();
    push();
    if(paused){
        rect(width-50, 25, 8, 25);
        rect(width-34, 25, 8, 25);
    } else {
        triangle(width-50, 25, width-50, 50, width-25, 37.5);
    }
}

function displayHelp(){
        textSize(14);
        textStyle(BOLD);
        text("Guide", 20, 30);
        textStyle(NORMAL);
        textSize(12);
        text("( Press [ H ] to switch to filters )", 65, 30);
        text("[ P ] : Freeze / Unfreeze dots", 20, 100);
        text("[ R ] : Disinfect all dots (except Patient Zero)", 20, 80);
        text("[ CLICK ] to infect nearby dots", 20, 60);
        text("[ M ] : Toggle masks ON / OFF", 20, 120);
        text("[ H ] : Show / Hide controls", 20, 140);
        stroke(100);
}

function displayData(){
        noStroke();
        fill(100);
        textSize(14);
        textStyle(BOLD);
        text("Filter Data", 20, 30);
        textStyle(NORMAL);
        textSize(12);
        if(filterState){
            text("( CLICK away from squares to show all )", 90, 30);
        } else{
            text("( CLICK to isolate )", 90, 30);
        }
        drawPalette();
}

function resetSketch(){
    //code modified from setup();
    for(var i = 0; i < numParticles; i++){
            particles[i].infectedBy = -1;
            particles[i].infectColor = color(0, 0, 100);
            particles[i].size = 10;
    }
    //wipes all modifier arrays
    filterState = false;
    generation = 0;  
    infectedList = [];
    colorList = [];
    var startColor = color(0, 100, 100);
    colorList.push(startColor);

    //reinfects first dot
    particles[0].infectedBy = 0;
    particles[0].size = 25;
    particles[0].infectColor = colorList[0];
    infectedList.push(particles[0]);
}

function pauseMove(){
    if(paused === true){
        paused = false;
        print('resumed');
    } else {
        paused = true;
        print('paused');
    }
}

function drawPalette() {
    //on visualization UI, draw color palette to filter dots
    translate(20, 50);
    var count = 0;
    numRows = ceil(colorList.length / numCols);
    for(var i = 0; i < numRows; i++) {
        for(var j = 0; j < numCols; j++) {
            if(count < colorList.length) {
                fill(colorList[(i * 6) + j]);
                drawColor(40 * (j), 40*i, 30);
                count++;
            }
        }
    }
}

function drawColor(x, y, rSize) {
    rect(x, y, rSize, rSize, 0.125*rSize);
}

function filterStatus(currentColor){
    //reads color of cursor location, enables filtering
    //if the mouse is over a colored square
    if(menuMode != 0){
        for(var i = 0; i < colorList.length; i++){
            print("Infected Color "+i);
            var infectRGB = getRGB(colorList[i]);
            var cursorRGB = getRGB(currentColor);
            print("infected color: " + infectRGB);
            print("cursor color: " + cursorRGB);

            if(compareColors(infectRGB, cursorRGB) == true){
                filterCol = cursorRGB;
                filterState = true;
                return;
            }
        }
    }
    filterState = false;
}

function getRGB(inputColor){
    //converts a color into RGB
    push();
    sampledColor = inputColor;
    colorMode(RGB, 255, 255, 255, 1);
    var r = red(sampledColor);
    var g = green(sampledColor);
    var b = blue(sampledColor);
    sampledColor = color(r, g, b);
    pop();
    return sampledColor;
}

function compareColors(color1, color2){
    //approximates two colors and checks if they are identical
    var r1 = round(red(color1));
    var g1 = round(green(color1));
    var b1 = round(blue(color1));
    var r2 = round(red(color2));
    var g2 = round(green(color2));
    var b2 = round(blue(color2));

    if(r1 == r2 & g1 == g2 && b1 == b2){
        return true;
    }

    return false;

}

When quarantines first started being imposed, I found Youtube channels dedicated to data visualization and education answered all my questions about the COVID-19 pandemic and engaged me to learn further.

Inspired by channels like Primer and 3Blue1Brown, I made an interactive simulation / visualization of a pandemic. Since I had been curious about contact tracing for a while but never understood the concept, I decided to use this opportunity to learn more about it and make it a core feature in my project.

In my visualizer, a population of dots are wandering around the screen. Clicking will cause all infected dots to infect nearby ones. Colored, larger dots have been infected. Additionally, you can see which dot infected which other dot through connecting lines. Since this rough form of contact tracing still gets messy, you can also filter the dots by color (or “generation” of infection) to find “super-spreader” dots.

A good chunk of my time was spent making sure all the interactions worked together without any errors or slowdowns. With more time, I would try mimicking real-life contact tracing more closely through recursion and add animations to the dots.

Below is a short demo of my project demonstrating the difference that having masks on makes on the speed of infection.

Leave a Reply