Categories
Coding

A COVID-19 / Coronavirus Simulator Tutorial in JavaScript

I saw a terrific Coronavirus simulation about transmission called “Simulating an Epidemic”. If you haven’t seen it, you should watch it now: it communicates science well and answers a lot of my questions about social isolation, PPE and travel restrictions.

Coronavirus Simulation basic simple outputThis looked like something I would like to play with. I looked at the source code and quickly realized that it was hacked together to make a video. It was too messy to salvage. I decided to write my own version from scratch. I wound up with something that isn’t terribly accurate, but lets you play around with things like tipping point and measure total confinement time and maximum hospitalization. Unlike the other simulations, this one runs on your desktop or phone – all you need is a Web Browser.

Planning

Questions

Some questions I would like to answer are:

  • How does reinfection change the result?
  • We typically measure the effects of this in deaths, but what is the economic impact of How much misery is incurred in terms of total time that a population is sick?
  • Is there a strategy that results in less than “all” the population from getting infected?
  • Do the people who “break the rules” suffer more or the same as the population?
  • Can some regions “break the rules” without imperiling others?
  • What effect do imperfect tests have on the total sick time?

Requirements

The user should be able to:

  • Visualize the simulation and summarize the results,
  • Control the speed of the simulation,
  • Modify the simulation, and
  • Expect the results to be meaningful without being precise.

Technologies

I chose pure ES6-compliant JavaScript and HTML5 Canvas for this exercise for several reasons:

  1. JavaScript is popular and easy to understand.
  2. Anybody can run it: it is available everywhere and doesn’t depend on a back-end server.
  3. There are interactive scripting tools like JSFiddle where the layperson can mess with the code, like I learned to code in BASIC.
  4. I need to brush up on my ES6 JavaScript and animation skills.

I chose to use one file rather than spreading it across many class files in order to make the simulation more portable and easier to fiddle with.

Licensing

I want other people to be free to use this work, talk about it and build upon it. I’m going to use a Creative Commons license, specifically the Creative Commons Attribution-ShareAlike CC BY-SA license.

This license lets others remix, adapt, and build upon your work even for commercial purposes, as long as they credit you and license their new creations under the identical terms…. All new works based on yours will carry the same license, so any derivatives will also allow commercial use.

Design

The code is going to be broken into sections, and we also proceed somewhat linearly through the sections.

  1. Configuration – this is where we make our settings.
  2. Model – lay out the data structures for the critters and the infection, and include some processing that determines how they behave.
  3. View – these are classes that help us display data, charts and visualizations.
  4. Controller – this runs the simulation.
    1. A simulation loop that is driven by a timer. This advances with each cycle (the equivalent of a day in our simulation) and advances each critter by triggering its model’s functionality.
    2. A visualization loop that is also driven by a timer. This updates the displays. Separating these loops lets us animate the simulation independently from the speed of the simulation.
  5. Initialization – sets up the model and views, and activates the controller.
  6. Conclusion – produces a final summary.

Project Plan

  1. Set up a visualization and demonstrate that we can animate it.
  2. Build a simple 2D model for a community and demonstrate that we can animate it.
  3. Model a population with an object that will support our simulation, e.g. location in physical space, social distancing, infection rates, etc.
  4. Build a loop that with each cycle, iterates over the array of critters to update their properties.
  5. With each cycle, calculate statistics for the population.
  6. Provide a visualization to keep track of what’s going on.
  7. Validate the simulation by plugging in values and confirming that the results match the expectations.
  8. Program the generic simulator to answer specific questions.

Getting Started

Basic Toolbox

Let’s using JSFiddle to sketch out the solution. Go to jsfiddle.net and start a new project. Close the “framework” window – you’re using pure JavaScript! This is going to look like Pong.

I’m going to write this in a way that sets up its own HTML and CSS, so you don’t have to put anything in the HTML or CSS boxes.

Critter in a Box

To get to model people in a community, let’s start with one critter in a box. It’s less stressful to perform these kinds of experiments on critters.

To get confidence in modeling and drawing, I’ll start by defining a critter and plotting them in a box.

// https://bigbrainsr.us/coronavirus-simulator-tutorial-in-javascript/#Critter_in_a_Box
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/

// make a box
const h=300, w=300; // h is xmax, w is ymax

let theBox = document.createElement('canvas');
  theBox.id = "theBox";
  theBox.width=w.toString();
  theBox.height=h.toString();
  document.body.appendChild(theBox);

var context = theBox.getContext("2d");
  context.fillStyle = "white";
  context.strokeStyle = "green";
  context.lineWidth = "5";
  context.fillRect(0, 0, w, h);
  context.strokeRect(0, 0, w, h);

// a bit of utility
// this is a good random number generator that gives a uniform distribution over the range
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// define a simple critter
let critter = {x:getRandomInt(0,w), y:getRandomInt(0,h), w:5, h:5, c:"red", dx:1, dy:1};

// put it in the box
context.fillStyle = critter.c;
context.fillRect(critter.x, critter.y, critter.w, critter.h);

I’m using “let” instead of “var” when declaring my variables. I had to look this up. Var is function-scoped, but Let is block-scoped, which reduces the chances of leakage.

Make sure this works for you before you move on.

Let the Critter Move

Now, let’s move the critter around to see what animation looks like.

// https://bigbrainsr.us/coronavirus-simulator-tutorial-in-javascript/#Let_the_Critter_Move
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
let fps=15; // at 15 fps it should take 20 seconds to traverse 300 px
let duration=40; // 40 seconds at 15fps, it should come back to start
var updateIntervalID = window.setInterval(updateCritter, 1000/fps, critter);
var clearIntervalID = window.setTimeout(clearInterval, duration*1000, updateIntervalID);

function updateCritter(critter) {
  // clear
  context.fillStyle = "blue";
  context.fillRect(critter.x, critter.y, critter.w, critter.h);
  // move
  critter.x += critter.dx;
  if ((critter.x < 0)||(critter.x > w) {
    critter.x -= critter.dx; // undo
    critter.dx = -critter.dx; // reverse
    critter.x += critter.dx; // do
  }
  if ((critter.y < 0)||(critter.y > h) {
    critter.y -= critter.dy; // undo
    critter.dy = -critter.dy; // reverse
    critter.y += critter.dy; // do
  }
  // draw
  context.fillStyle = critter.c;
  context.fillRect(critter.x, critter.y, critter.w, critter.h);
}

You can see that there are some issues with the boundary on the right side and the bottom because of the way that we draw the critter (which is intended to be quick), so we are going to eventually need to fix this. Try changing some of the values like the frames per second to see how fast you can get the animation to go, and try setting a random dx and/or dy and/or color.

Drawing the objects isn’t the point of the simulation, but it sure helps understand what the visualization is doing.

That wraps up a proof-of-concept of our basic building blocks. The next step is to generalize and compartmentalize it so we can expand on it easily in our simulation.

Objectifying

Here is a version that does the same thing, but puts the code into a few JavaScript classes and enables some things like creating more than one critter. Based on the tests so far, I’m also going to separate the animation from the calculation.

If you’re following along in our design, this is where we are building out the model, views and controllers.

// https://bigbrainsr.us/coronavirus-simulator-tutorial-in-javascript/#Objectifying
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
const settings = {
  xmax: 1200,
  ymax: 300,
  cycles: 3000,
  fps: 15
}

class Utility {
  static getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min; 
  }
  static log(message) {
    console.log(message);
  }
}

// Models 

class Critter {
  constructor() {
    this.x = Utility.getRandomInt(0, settings.xmax);
    this.y = Utility.getRandomInt(0, settings.ymax);
    this.c = "red";
    this.dx = Utility.getRandomInt(-2, 2);
    this.dy = Utility.getRandomInt(-2, 2);
  }
 
  update() {
    this.x += this.dx;
    if ((this.x < 0) || (this.x > settings.xmax)) {
      this.dx = -this.dx; // reverse
      this.x += this.dx; // do
    }
    this.y += this.dy;
    if ((this.y < 0) || (this.y > settings.ymax)) {
      this.dy = -this.dy; // reverse
      this.y += this.dy; // do
    }
  }
}

// Views

class Visualization {
  constructor(divID) {
    this.elementID = divID;
    this.width = settings.width || 400;
    this.height = settings.height || 600;
    this.xmax = settings.xmax || settings.width;
    this.ymax = settings.ymax ||settings.height;
    this.fillColor = "white";
    this.borderColor = settings.borderColor || "green";
    this.borderSize = 5;
    this.pointWidth = 5; // lets you make the points big enough to see
    this.pointHeight = 5; // lets you make the points big enough to see

    this.canvas = document.getElementById(this.elementID);
    if (!this.canvas) {
      this.canvas = this.createCanvas();
    }

    this.context = this.canvas.getContext("2d");
    this.clearCanvas();
    return this;
  }
  createCanvas() {
    let theBox = document.createElement('canvas');
    theBox.id = this.elementID;
    theBox.width = this.width.toString();
    theBox.height = this.height.toString();
    document.body.appendChild(theBox);
    return theBox;
  }
  clearCanvas() {
    this.context.fillStyle = this.fillColor;
    this.context.fillRect(0, 0, this.width, this.height);

    this.context.strokeStyle = this.borderColor;
    this.context.lineWidth = this.borderSize;
    this.context.strokeRect(0, 0, this.width, this.height); // strokeRectangle
  }
  /* Quickly plots a point into the visualization.
   * Transforms the points so they fit in the boundries.
   * Assumes that all the elements that are ever drawn have the same w&amp;h
   */
  drawPoint(color, x, y) {
    let w=this.pointWidth, h=this.pointHeight;
    let cx = Math.floor(this.borderSize + ( (this.width - 2*this.borderSize - this.pointWidth) * x / this.xmax ));
    let cy = Math.floor(this.borderSize + ( (this.height - 2*this.borderSize - this.pointHeight) * y / this.ymax ));
    this.context.fillStyle = color;
    this.context.fillRect(cx, cy, this.pointWidth, this.pointHeight);
  }
  drawCritter(critter) {
    this.drawPoint(critter.c, critter.x, critter.y);
  }
  eraseCritter(critter) {
    this.drawPoint(this.fillColor, critter.x, critter.y);
  }
}

// Controllers 

// definitions
let visualization = new Visualization("visualization");

let critters = [];

function animateCritters(critters) {
  critters.forEach(function(critter) {
    visualization.eraseCritter(critter);
    critter.update();
    visualization.drawCritter(critter);
    graph.drawCritter(critter);
  });
}

// start
critters.push(new Critter());

animateCritters(critters);

// animate
var animationIntervalID = window.setInterval(animateCritters, 1000 / fps, critters);

// calculate
for (let cycle = 0; cycle &lt; settings.cycles; cycle++) {
  critters.forEach(function(critter) {
    critter.update();
  });
}

// stop animation
clearInterval(animationIntervalID);

// present final state
animateCritters(critters);

At this point, you should have a good idea of the framework that we’re going to be using going forward.

Assembly

Well, now that that’s working, it’s time to give it some personality.

Configuration

Some of the things that you can set in the global configuration section are the length and speed of the simulation, how often to update the display.

Infection!

The heart of the infection modeling component is a JavaScript object that, for each state you can be in, maps out the various states you can get to. So if a critter is infected, on every cycle it has a probability of becoming symptomatic, hospitalized, removed (dead), or recovered.

this.advancements = {
  infected: [
      { atLeast: 3, noMore: 14, chance: 25, next: "symptomatic" },
      { atLeast: 7, noMore: 14, chance: 75, next: "recovered" },
  ],
  symptomatic: [
      { atLeast: 7, noMore: 14, chance: 20, next: "hospitalized" },
      { atLeast: 7, noMore: 14, chance: 5, next: "dead" },
      { atLeast: 2, noMore: 14, chance: 75, next: "recovered" },
  ],
  hospitalized: [
      { atLeast: 7, noMore: 14, chance: 25, next: "dead" },
      { atLeast: 7, noMore: 14, chance: 75, next: "recovered" },
  ]
};

This matrix is implemented the update() method.

Object.keys(chances).forEach(next => {
   if (Utility.coinToss(chances[next])) { // congratulations, you advance a state
      this.status = next;
      return;
   }
}

The Code

Here’s our baseline code. After the code listing there’s a fiddle so you can play with it right here, and a final section that handles the conclusion.

// https://bigbrainsr.us/coronavirus-simulator-tutorial-in-javascript/#The_Code
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/

// Part I: Global Configuration
const settings = {
  xmax: 300,
  ymax: 300,
  critters: 2400, //2400,
  cycles: 100, //100
  separation: 100, // number of pixels that is considered a collision
  cps: 2, // cycles per second, use zero to run without interrupts
  fps: 4, // frames per second, use zero to turn off animation
  roammax: 50, // the 0<chance<100 that they'll roam on a particular cycle
  speedmax: 2, // how far they might move with each iteration if they roam
}

// Part II: Utilities

class Utility {
  static getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
  static coinToss(pTrue = 50) { // 0<pTrue<100 
    if (pTrue <= 0) return false; if (pTrue >= 100) return true;
    return ((Math.random() * 100) < pTrue); } static log(...theArgs) { console.log(Utility.withSpaces(theArgs)); } // this takes a number and formats it to the specified precision, then uses the label if provided to plural it static niceNumber(number, precision = 0, label = "", plural = "") { let text = ""; text += number.toFixed(precision); if (label !== "") text += (" " + Utility.plural(number, label, plural)); return text; } // uses the singular if it's 1, plural if not static plural(number, singular = "", plural = "") { if (plural === "") plural = singular + "s"; // standard English plural return (number == 1) ? singular : plural; } // this takes a series of arguments and separates them with spaces static withSpaces(...theArgs) { theArgs.forEach(arg => {
      return Array.isArray(arg) ? arg.join(" ") : arg;
    });
    return theArgs.join(" ");
  }
  static nicePercent(numerator, denominator, precision = 0) {
    return (denominator == 0) ? "" : "(" + Utility.asPercent(numerator, denominator, precision) + "%)";
  }
  static asPercent(numerator, denominator, precision = 0) {
    return (denominator == 0) ? "&infinity;" : (100 * numerator / denominator).toFixed(precision);
  }

}
// Part III: Models

class CritterInfection {
  constructor() {
    this.statusTextColors = [ // key, text, color
      ["uninfected", "Uninfected", "green"],
      ["infected", "Infected - no symptoms", "orange"],
      ["symptomatic", "Infected - symptomatic", "red"],
      ["hospitalized", "Infected - hospitalized", "pink"],
      ["recovered", "Recovered", "blue"],
      ["dead", "Removed", "black"],
    ];
    // State.
    // this defines several probabilty distributions starting after spending atLeast cycles in a state, 
    // you have a uniform chance/(noMore-atLeast) probabality of transitioning to the next state.
    // if you hit noMore in all  state(s) there is no longer an option to stay in the current state
    //  you transition immediately to that state according to the relative probablites
    //  this prevents long-tail probablities
    // The chance of each transition state plus the current state if valid will add to 100 for each cycle
    this.advancements = {
      infected: [{
          atLeast: 3,
          noMore: 14,
          chance: 25,
          next: "symptomatic"
        },
        {
          atLeast: 7,
          noMore: 14,
          chance: 75,
          next: "recovered"
        },
      ],
      symptomatic: [{
          atLeast: 7,
          noMore: 14,
          chance: 20,
          next: "hospitalized"
        },
        {
          atLeast: 7,
          noMore: 14,
          chance: 5,
          next: "dead"
        },
        {
          atLeast: 2,
          noMore: 14,
          chance: 75,
          next: "recovered"
        },
      ],
      hospitalized: [{
          atLeast: 7,
          noMore: 14,
          chance: 25,
          next: "dead"
        },
        {
          atLeast: 7,
          noMore: 14,
          chance: 75,
          next: "recovered"
        },
      ]
    };

    // defines some shortcut states
    this.contageous = ["infected", "symptomatic", "hospitalized"];
    this.susceptible = ["uninfected"];

    // build some shortcuts that are easier to iterate over
    this.statuses = [];
    this.statusColors = {
      "undefined": "Yellow"
    };
    this.statusTexts = {
      "undefined": "Undefined"
    };
    this.statusTextColors.forEach(([status, text, color]) => {
      this.statuses.push(status);
      this.statusColors[status] = color;
      this.statusTexts[status] = text;
    });

    // stores the number of cycles in each state
    this.history = {};

    // initial state
    this.status = "uninfected";

    return this;
  }
  update(cycle) {
    this.history[this.status] = this.history[this.status]+1 || 1;
    if (this.advancements[this.status]) {
      // things to make it easier
      let advancementOptions = this.advancements[this.status];
      let timeInThisState = this.history[this.status];
      // things to remember
      let originalStatus = this.status;
      // things to calculate
      let maxNoMore = 0; // this is the maximum of the noMore cycle limits
      let chances = {}; // this will be a list of the probabilities of our next state options
      advancementOptions.forEach(advancement => {
        if (timeInThisState >= advancement.atLeast) {
          chances[advancement.next] = advancement.chance / Math.max(advancement.noMore - advancement.atLeast, 1);
        }
        maxNoMore = Math.max(advancement.noMore, maxNoMore);
      });
      // get out your two-sided dice
      Object.keys(chances).forEach(next => {
        if (Utility.coinToss(chances[next])) { // congratulations, you advance a state
          this.status = next;
          return;
        }
      });

      while ((maxNoMore < timeInThisState) && (this.status === originalStatus)) { Object.keys(chances).forEach(next => {
          if (Utility.coinToss(chances[next])) { // congratulations, you advance a state
            this.status = next;
            return;
          }
        });
      }
    }
  }
  isContageous() {
    return (this.contageous.includes(this._status));
  }
  wasInfected() {
    const infected = (element) => Object.keys(this.history).includes(element);
    return (this.contageous.some(infected));
  }
  maybeInfect(otherCritter) {
    if (this.contageous.includes(this._status) && this.susceptible.includes(otherCritter.infection.status)) {
      otherCritter.infection.status = "infected";
    }
  }
  maybeReinfect() {
    return false;
  }

  get text() {
    let targetStatus = this._status;
    return (this.statusTexts[this.status]) ? this.statusTexts[this.status] : this.statusTexts["undefined"];
  }
  get color() {
    let targetStatus = this.status;
    return (this.statusColors[targetStatus]) ? this.statusColors[targetStatus] : this.statusColors["undefined"];
  }
  get status() {
    return this._status;
  }
  set status(proposedValue) {
    if (this.statuses.includes(proposedValue)) {
      if (this._status) this.history[this._status] = this.history[this._status] - 1 || 0;
      this.history[proposedValue] = this.history[proposedValue] + 1 || 1;
      this._status = proposedValue;
    } else Utility.log("set status invalid proposedValue not in " + this.statuses);
  }
}

class Critter {
  constructor(name) {
    this.name = name || Utility.getRandomInt(0, 99999999);
    this.settings = {
      x: Utility.getRandomInt(0, settings.xmax),
      y: Utility.getRandomInt(0, settings.ymax),
      directionX: (Utility.coinToss()) ? -1 : 1,
      directionY: (Utility.coinToss()) ? -1 : 1,
      roaming: Utility.getRandomInt(0, settings.roammax), // the 0<=chance<-100 they will roam
      speed: Utility.getRandomInt(0, settings.speedmax), // how 0<=fast they will move if they do move
    }
    this.infection = new CritterInfection();
  }
  move() {
    let s = this.settings;
    // if a random integer is less than the roaming integer, then move in some direction
    if (Utility.coinToss(s.roaming)) {
      let dx = Utility.getRandomInt(0, s.speed) * ((Utility.coinToss()) ? -1 : 1); //s.directionX;
      let dy = Utility.getRandomInt(0, s.speed) * ((Utility.coinToss()) ? -1 : 1);
      s.x += dx;
      if ((s.x < 0) || (s.x > settings.xmax)) {
        s.x -= dx; // undo
        s.directionX = -s.directionX; // reverse next time
      }
      s.y += dy;
      if ((s.y < 0) || (s.y > settings.ymax)) {
        s.y -= dy; // do
        s.directionY = -s.directionY; // reverse next time
      }
    }
  }

  isSame(otherCritter) {
    return this.name === otherCritter.name;
  }
  collide(otherCritter) {
    if (this.isSame(otherCritter)) return false;
    let dq = (this.x - otherCritter.x) * (this.x - otherCritter.x) + (this.y - otherCritter.y) * (this.y - otherCritter.y);
    return (dq < settings.separation); } get x() { return this.settings.x; } get y() { return this.settings.y; } get dx() { return this.settings.dx; } get dy() { return this.settings.dy; } get c() { return this.infection.color; } get status() { return this.infection.status; } } // definitions function doAnimateCritters(visualization, critters) { visualization.animateCritters(critters); } function doUpdateCritters(critters) { cycle++; // move critters.forEach(function(critter) { critter.move(); critter.infection.update(cycle); }); // look for collisions with contageous critters critters.forEach(function(critter) { if (critter.infection.isContageous()) { critters.forEach(function(othercritter) { if (critter.isSame(othercritter)) { critter.infection.maybeReinfect(); } else { if (critter.collide(othercritter)) critter.infection.maybeInfect(othercritter); } }); } }); // for debugging critters.forEach(function(critter) { graph.drawCritter(critter); // for debugging }); // for reporting report.replace(statistics.report(settings, critters, cycle)); // start the next cycle, maybe if (cycle === settings.cycles) { wrapup(); } else { // more to do if (settings.cps > 0) {
      window.setTimeout(doUpdateCritters, 1000 / settings.cps, critters); // recursion!
    } else {
      doUpdateCritters(); // unconstrained recursion!
    }
  }
}

// Part IV: Views

class TextArea {
  constructor(divID) {
    this.elementID = divID;
    this.div = document.getElementById(this.elementID);
    if (!this.div) {
      this.div = this.createDiv();
    }
  }
  createDiv() { // creates the named div if it wasn't found on the page
    let theBox = document.createElement('div');
    theBox.id = this.elementID;
    document.body.appendChild(theBox);
    return theBox;
  }
  log(message) {
    let line = document.createElement("p");
    line.innerHTML = message;
    this.div.appendChild(line);
  }
  static getColorSwatch(color, text = "&nbsp;&nbsp;") { // returns a HTML color swatch in the color specified
    return '<span style="height:1em;width:1em;background:' + color + ' !important">' + text + '</span>';
  }
  clear() {
    this.div.innerHTML = "";
  }
  replace(sections) { // messages is an array of arrays of messages
    this.clear();
    if (!Array.isArray(sections)) sections = [sections];
    sections.forEach(section => {
      if (!Array.isArray(section)) sections = [section];
      this.log(section.join("
"));
    });
  }
}

class Visualization {
  constructor(divID) {
    this.elementID = divID;
    this.width = settings.width || 400;
    this.height = settings.height || 400;
    this.xmax = settings.xmax || settings.width;
    this.ymax = settings.ymax || settings.height;
    this.fillColor = settings.fillColor || "white";
    this.borderColor = settings.borderColor || "green";
    this.borderSize = settings.borderSize || 5;
    this.pointWidth = 2; // lets you make the points big enough to see
    this.pointHeight = 2; // lets you make the points big enough to see

    this.canvas = document.getElementById(this.elementID);
    if (!this.canvas) {
      this.canvas = this.createCanvas();
    }

    this.context = this.canvas.getContext("2d");
    this.clearCanvas();
    return this;
  }
  createCanvas() {
    let theBox = document.createElement('canvas');
    theBox.id = this.elementID;
    theBox.width = this.width.toString();
    theBox.height = this.height.toString();
    document.body.appendChild(theBox);
    return theBox;
  }
  clearCanvas() {
    this.context.fillStyle = this.fillColor;
    this.context.fillRect(0, 0, this.width, this.height);

    this.context.strokeStyle = this.borderColor;
    this.context.lineWidth = this.borderSize;
    this.context.strokeRect(0, 0, this.width, this.height); // strokeRectangle
  }
  /* Quickly plots a point into the visualization.
   * Transforms the points so they fit in the boundries.
   * Assumes that all the elements that are ever drawn have the same w&h
   */
  drawPoint(color, x, y) {
    let cx = Math.floor(this.borderSize + ((this.width - 2 * this.borderSize - this.pointWidth) * x / this.xmax));
    let cy = Math.floor(this.borderSize + ((this.height - 2 * this.borderSize - this.pointHeight) * y / this.ymax));
    this.context.fillStyle = color;
    this.context.fillRect(cx, cy, this.pointWidth, this.pointHeight);
  }
  drawCritter(critter) {
    this.drawPoint(critter.c, critter.x, critter.y);
  }
  eraseCritter(critter) {
    this.drawPoint(this.fillColor, critter.x, critter.y);
  }
  animateCritters(critters) {
    this.clearCanvas();
    critters.forEach(critter => this.drawCritter(critter));
  }
}

class Statistics {
  constructor() {

  }

  report(settings, critters, cycle) {
    // summarize
    let outcomes = {}; // infectionStatus => count
    let statustimes = {}; // infectionStatus => cycles in that status
    critters.forEach(critter => {
      outcomes[critter.infection.status] = outcomes[critter.infection.status] + 1 || 1;
      Object.keys(critter.infection.history).forEach((historicalstatus, count) => {
        let statustime = critter.infection.history[historicalstatus] || 0;
        statustimes[historicalstatus] = statustimes[historicalstatus] + statustime || statustime;
      });
    });

    // the data summaries are available at this point if you want to tap them off
    // Utility.log(outcomes);
    // Utility.log(statustimes);

    // summary
    // The reporting display is looking for an array of arrays. 
    // It will put sub-arrays together in  blocks in the display.
    let acritteri = new CritterInfection(); // access to infection's colors and texts
    let sections = []; // these will be text sections
    let section = []; // this will accumulate the text in each text section.
    section.push("Cycles: planned " + settings.cycles + " completed " + cycle + " (" + Math.round(100 * cycle / settings.cycles) + "%)");
    section.push("Critters: planned " + settings.critters + " created " + critters.length + " (" + Math.round(100 * critters.length / settings.critters) + "%)");
    sections.push(section);

    section = [];
    acritteri.statuses.forEach(status => {
      if (outcomes[status]) {
        let count = outcomes[status];
        let perCount = Math.round(100 * count / critters.length);
        acritteri.status = status;
        let label = acritteri.text + ":";
        let text = Utility.withSpaces(
          TextArea.getColorSwatch(acritteri.color),
          label,
          Utility.niceNumber(count, 0, "critter"),
          Utility.nicePercent(count, critters.length, 1)
        );
        section.push(text);
      }
    });
    sections.push(section);

    section = [];
    let cumulativetime = 0;
    Object.keys(statustimes).forEach(status => {
      cumulativetime += statustimes[status]
    });
    cumulativetime /= critters.length;
    acritteri.statuses.forEach(status => {
      if (statustimes[status]) {
        let timeper = statustimes[status] / critters.length;
        acritteri.status = status;
        let label = "Time as " + acritteri.text + ":";
        let text = Utility.withSpaces(
          TextArea.getColorSwatch(acritteri.color),
          label,
          Utility.niceNumber(timeper, 1, "cycles/critter", "cycles/critter"),
          Utility.nicePercent(timeper, cumulativetime)
        );
        section.push(text);
      }
    });
    sections.push(section);
    return sections;
  }
}

// Part V: Controllers

// things start here
let visualization = new Visualization("visualization");
let graph = new Visualization("graph");
let report = new TextArea("report");
let statistics = new Statistics();

let critters = [];
for (let i = 0; i < settings.critters; i++) { critters.push(new Critter("Marlin " + i)); } critters[0].infection.status = "infected"; Utility.log("Critters: planned " + settings.critters + " created " + critters.length); // start visualization visualization.animateCritters(critters); if (settings.fps > 0) {
  var animateIntervalID = window.setInterval(doAnimateCritters, 1000 / settings.fps, visualization, critters);
}

var cycle = 0;
doUpdateCritters(critters);

// end visualization
function wrapup() { // called by updateCritters when everything is done
  // final animation
  if (settings.fps > 0) {
    clearInterval(animateIntervalID);
  }
  visualization.animateCritters(critters);
  report.replace(statistics.report(settings, critters, cycle));
  report.log("Done.");
}

The Simulator

Here’s my finished fiddle: https://jsfiddle.net/jtw90210/a4Ld25n9/.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x