Skip to main content

3 posts tagged with "d3"

View All Tags

Light Speed Visualization

· 4 min read

Light is the fastest thing in the universe, but how fast is it really? Scroll through the demo below to see how how fast it really is.

Demo

Sources

Source Code

View source

LightSpeed.tsx
export default function LightSpeed() {
let { withBaseUrl } = useBaseUrlUtils();

useEffect(() => {
const c = 299792458;
const width = 500;
const height = 350;
const mpsToMph = 2.23694;
const margin = { top: 10, bottom: 30, left: 10, right: 10 };

let dataContainer: string | any[];
let canvas: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
objects: d3.Selection<SVGGElement, unknown, SVGGElement, unknown>,
gX: d3.Selection<SVGGElement, unknown, HTMLElement, any>;

let x = d3.scaleLog().domain([6e1, c]).range([0, width]);
let xAxis = d3.axisBottom(x);

let currentScale = 10;
let currentTranslateX = 0;
let imageRatios;
let hsize = 80;
let scaleFactor = 12;
let objectNumber;
let alreadyLoaded = false;
let wrapper = d3.select(`.${styles.wrapper}`);

let svg = wrapper
.append("svg")
.attr(
"viewBox",
"0 0 " +
(width + margin.left + margin.right) +
" " +
(height + margin.top + margin.bottom)
)
.attr("width", "100%");
canvas = svg
.append("g")
.attr("id", "canvas")
.attr("width", width)
.attr("height", height)
.attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")"
);
let tooltip = wrapper
.append("div")
.attr("class", styles.tooltip)
.style("opacity", 0);

function updateGraph() {
let newTransform = d3.zoomIdentity
.translate(currentTranslateX, height / 2)
.scale(currentScale);
let new_x = newTransform.rescaleX(x);
gX.call(xAxis.scale(new_x));
objects.data(dataContainer).attr("transform", (d) => {
return "translate(" + new_x(d.speed) + "," + height / 2 + ")";
});
}

function motion(event: WheelEvent) {
let dx = event.deltaX;
let dy = event.deltaY;
let amount: number;
if (Math.abs(dx) > Math.abs(dy)) {
amount = -dx;
} else {
amount = -dy;
}
currentTranslateX += amount;
currentTranslateX = Math.max(
-490 * currentScale,
currentTranslateX
);
currentTranslateX = Math.min(40, currentTranslateX);
event.preventDefault();
event.stopPropagation();
updateGraph();
}

function updateRectanglesIfComplete() {
let proceed = true;
for (let i = 0; i < dataContainer.length; i++) {
proceed = proceed && imageRatios[dataContainer[i].name] != null;
}
if (proceed) {
if (alreadyLoaded) return;
alreadyLoaded = true;

objects
.append("rect")
.attr("height", hsize)
.attr("width", function (d: any) {
return imageRatios[d.name] * hsize;
})
.attr("stroke", "white")
.style("stroke-width", 2)
.attr("y", function (d, i) {
return (
-hsize / 2 +
scaleFactor * i -
(scaleFactor * objectNumber) / 2
);
});

objects
.append("image")
.attr("height", hsize)
.attr("width", function (d: any) {
return imageRatios[d.name] * hsize;
})
.attr("y", function (d, i) {
return (
-hsize / 2 +
scaleFactor * i -
(scaleFactor * objectNumber) / 2
);
})
.attr("href", (d: any) => {
return withBaseUrl(`light-speed/${d.image}`);
});
} else {
setTimeout(updateRectanglesIfComplete, 1000);
}
}

d3.json(withBaseUrl("light-speed/FastThings.json")).then(
(data: any[]) => {
dataContainer = JSON.parse(JSON.stringify(data));
for (let i = 0; i < dataContainer.length; i++) {
dataContainer[i].speed =
Math.round(dataContainer[i].speed * mpsToMph * 100) /
100.0;
}
imageRatios = {};
for (let i = 0; i < dataContainer.length; i++) {
let img = new Image();
img.onload = function (this: HTMLImageElement, ev: Event) {
imageRatios[dataContainer[i].name] =
this.width / this.height;
updateRectanglesIfComplete();
};
img.src = withBaseUrl(
"light-speed/" + dataContainer[i].image
);
}

objects = canvas
.selectAll(".objects")
.data(data, function (d: any) {
return d.name;
})
.enter()
.append("g")
.attr("class", "objects")
.attr("transform", (d) => {
return (
"translate(" + x(d.speed) + "," + height / 2 + ")"
);
})
.on("mouseover", function (evt: MouseEvent, d) {
let tooltipText =
'<span style="font-weight: bold; font-size: 12px">' +
d.name +
"</span><br/>" +
'<span style="font-style: italic">' +
d.speed +
"</span>" +
" mph" +
"</br>" +
"</br>" +
d.description;
tooltip
.transition()
.duration(200)
.style("opacity", 0.9);
tooltip
.html(tooltipText)
.style("left", evt.offsetX + "px")
.style("top", evt.offsetY - 28 + "px");
})
.on("mouseout", function (d) {
tooltip.transition().duration(500).style("opacity", 0);
});

objectNumber = data.length;

gX = canvas
.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(xAxis);
updateGraph();
svg.on("wheel.zoom", motion);
}
);
}, []);

return <div className={styles.wrapper} />;
}

Double Pendulum Chaos Visualization

· 15 min read

Introduction

What is chaos? Chaos is seen a crowded room, a shattered glass and a uncontrollable dog. At its deepest level, chaos is disorder and unpredictability.

In mathematics and physics, chaos means something very specific

  • sensitive to starting conditions
  • non-repeating

These two ideas mean that chaotic problems are impossible to solve exactly and difficult to solve approximately. But they're very important problems to work on because chaos theory is the reason that things like the weather, the stock market and fluid flow is so hard to predict. If we can get better at predicting hurricanes or stock market crashes, we can do a better job preparing for them.

Stable Double Pendulum

The atmosphere and the stock market are large systems with many moving parts, but we can find chaos in very simple systems. Pendulums are the essence of regularity. They swing so consistently that they were used by Galileo to make the earliest reliable clock. In the diagram directly below, we add a second pendulum to the end of the first. This adds a slight disruption to the oscillation, but it still swings regularly.

Unstable Double Pendulum

If we increase the length of oscillation, the system takes a few swings, but it soon loses all regularity and becomes unstable. Below is a demonstration of how sensitive and unpredictable double pendulums are. This system starts out with 50 pendulums almost exactly on top of each other. Their bobs are just 0.03 degrees apart at the furthest (If you zoom into the image, you should be able to see the edges of the ones in the back). After just a few seconds, these pendulums split and take a totally unique path.

Pendulum Paths

There is a certain beauty to the chaos. The few moments of instability before the bobs diverge is mesmerizing. In the demo below, the paths of the pendulums are plotted behind them.

Adjusting the Parameters

Below I added a demonstration where you can play with some of the parameters.

Pendulum Number: 60
Trail Length: 0
Rod A Length: 8
Rod B Length: 6
Mass A: 5
Mass B: 5
Angle A Initial: 1.6
Angle B Initial: 3.1

In this final demo, we can observer how difficult these problems are to solve even with the assistance of a computer. To process these simulations fast enough on a normal computer for you to see them, the solution loses much accuracy. This loss can be seen by making rod lengths or masses significantly different. When one rod is much longer than the other, the errors become significant enough to notice with the naked eye. You can see the second pendulum dragging behind or lunging forward unnaturally.

These errors can be reduced, but not without a lot of time or a larger computer.

Sources

Source Code

View source

DoublePendulum.jsx
import { useEffect, useId, useMemo, useState } from "react";
import * as d3 from "d3";

import styles from "./styles.module.css";

let goodColorScales = [
d3.interpolateOranges,
d3.interpolateInferno,
d3.interpolatePlasma,
];

let baseConstants = {
l1: 8,
m1: 5,
l2: 6,
m2: 5,
phi1Init: 0.5,
phi2Init: 0.5,
pendulumNumber: 1,
deviation: 0,
trailUpdateInterval: 3,
trailLength: 60,
trails: false,
colorscale: goodColorScales[1],
explain: true,
caption: "A double pendulum with a small oscillation",
};

export class AnimationLock {
locked;
constructor() {
this.locked = false;
}
request() {
if (this.locked) {
return true;
}
this.locked = true;
return false;
}
release() {
this.locked = false;
}
}

class DoublePendulum {
stepSize = 0.001;
repeats = 32;
randomness = 0.4;
started = false;

currentCoords;
currentTime;
currentCartCoords;
width;
height;

origin;
dots1;
lines1;
dots2;
lines2;
trails;

continueLooping = false;
restartable = false;
colorscaleVariable = 0;
currentStep = 0;
lineResizer = 0.3;
dotResizer = 0.1;

isReset;

lineGenerator = d3.line().curve(d3.curveNatural);

constructor(constantsDict, animationLock) {
this.isReset = true;
this.lock = animationLock;
this.constants = [
constantsDict.l1,
constantsDict.m1,
constantsDict.l2,
constantsDict.m2,
];
this.phi1Init = constantsDict.phi1Init;
this.phi2Init = constantsDict.phi2Init;
this.pendulumNumber = constantsDict.pendulumNumber;
this.deviation = constantsDict.deviation;
this.location = constantsDict.location;
this.trailUpdateInterval = constantsDict.trailUpdateInterval;
this.trailLength = constantsDict.trailLength;
this.useTrails = constantsDict.trails;
this.trailPaths = new Array(this.pendulumNumber);
this.explanation = constantsDict.explanation;
this.trailPathsNumeric = new Array(this.pendulumNumber);
this.trailCounter = 0;
this.windowSize = 100 / (2 * 1.05 * (this.l1_ + this.l2_));
this.dotsResizer = 0.1 / this.windowSize;
this.caption = constantsDict.caption;
if ("colorScale" in constantsDict) {
this.colorScale = constants.colorScale;
} else {
this.colorScale = d3.interpolateInferno;
}
this.currentTime = 0;
this.initialCoords = this.getInitialCoords();
this.lastCoords = this.initialCoords.clone();
this.initialCoordsCart = convertToCoordinates(
this.initialCoords,
this.constants
);

this.lastTrailUpdateCoords = this.initialCoordsCart.slice(2);
let location = document.getElementById(this.location);
this.svg = d3
.select(location)
.append("svg")
.attr("viewBox", "0 0 100 100");
let explanation = "Hover to play. Click to restart";
// Check if has tap controls or can't hover
if ("ontouchstart" in window || navigator.maxTouchPoints > 0) {
explanation = "Tap to play. Double tap to restart";
}
if (this.explanation) {
explanation = this.explanation;
}
let caption = d3
.select(location)
.append("p")
.attr("class", styles.demoCaption)
.html(this.caption);
if (constantsDict.explain || this.explanation) {
caption
.append("div")
.attr("class", styles.demoExplanation)
.html(explanation);
}
}

get l1_() {
return this.constants[0];
}
get m1_() {
return this.constants[1];
}
get l2_() {
return this.constants[2];
}
get m2_() {
return this.constants[3];
}
set l1_(newValue) {
this.constants[0] = newValue;
this.windowSize = 100 / (2 * 1.05 * (this.l1_ + this.l2_));
this.dotsResizer = 0.1 / this.windowSize;
this.restart.bind(this)();
}
set m1_(newValue) {
this.constants[1] = newValue;
this.restart.bind(this)();
}
set l2_(newValue) {
this.constants[2] = newValue;
this.windowSize = 100 / (2 * 1.05 * (this.l1_ + this.l2_));
this.dotsResizer = 0.1 / this.windowSize;
this.restart.bind(this)();
}
set m2_(newValue) {
this.constants[3] = newValue;
this.restart.bind(this)();
}
set pendulumNumber_(newValue) {
this.pendulumNumber = newValue;
this.initialCoords = this.getInitialCoords();
this.currentCoords = this.initialCoords.clone();
this.currentCartCoords = convertToCoordinates(
this.currentCoords,
this.constants
);
this.initialCoordsCart = convertToCoordinates(
this.initialCoords,
this.constants
);
// this.restart.bind(this)();
}
set phi1Init_(newValue) {
this.phi1Init = newValue;
// this.restart.bind(this)();
}
set phi2Init_(newValue) {
this.phi2Init = newValue;
// this.restart.bind(this)();
}
set trailLength_(newValue) {
this.trailLength = newValue;
// this.restart.bind(this)();
}
get pendulumNumber_() {
return this.pendulumNumber;
}
get phi1Init_() {
return this.phi1Init;
}
get phi2Init_() {
return this.phi2Init;
}
get trailLength_() {
return this.trailLength;
}

start() {
if (this.lock.request()) {
return;
}
if (!this.continueLooping) {
this.continueLooping = true;
const demoContext = this;
requestAnimationFrame(() => demoContext.updateDisplay());
}
}

stop() {
this.continueLooping = false;
}

restart() {
this.stop();
this.initialCoords = this.getInitialCoords();
this.initialCoordsCart = convertToCoordinates(
this.initialCoords,
this.constants
);
this.currentCoords = this.initialCoords.clone();
this.lastCoords = this.currentCoords.clone();
this.currentCartCoords = this.initialCoordsCart.slice();
this.trailCounter = 0;
this.currentStep = 0;

for (let i = 0; i < this.pendulumNumber; i++) {
this.trailPaths[i] =
"M " +
(this.initialCoordsCart[i][2] * this.windowSize).toPrecision(
5
) +
" " +
(this.initialCoordsCart[i][3] * this.windowSize).toPrecision(
5
) +
" ";
}

this.svg.selectAll("*").remove();
this.init();
setTimeout(() => {
this.start();
}, 100);
}

getInitialCoords() {
return initPendulums(
this.phi1Init + (Math.random() - 0.5) * this.randomness,
this.phi2Init + (Math.random() - 0.5) * this.randomness,
this.deviation,
this.pendulumNumber
);
}

init() {
for (let i = 0; i < this.pendulumNumber; i++) {
this.trailPaths[i] =
"M " +
(this.initialCoordsCart[i][2] * this.windowSize).toPrecision(
5
) +
" " +
(this.initialCoordsCart[i][3] * this.windowSize).toPrecision(
5
) +
" ";
}
this.origin = this.svg
.append("g")
.attr("transform", "translate(" + 50 + "," + 50 + ")")
.attr("class", "origin");

this.trails = this.origin
.selectAll(".trails")
.data(this.initialCoordsCart)
.enter()
.append("path")
.attr("fill", "transparent")
.attr("class", "trails")
.attr("stroke-width", this.lineResizer)
.attr("d", (d) => {
return (
"M " +
(d[2] * this.windowSize).toPrecision(5) +
" " +
(d[3] * this.windowSize).toPrecision(5) +
" "
);
})
.attr("stroke", this.colorScaleFunc.bind(this));

let pendulums = this.origin
.selectAll(".pendulums")
.data(this.initialCoordsCart)
.enter()
.append("g")
.attr("class", "pendulums");

this.lines1 = pendulums
.append("line")
.attr("class", "pendulumLine1")
.attr("x1", 0)
.attr("y1", 0)
.attr("stroke-width", 2 * this.lineResizer)
.attr("x2", (d) => {
return d[0] * this.windowSize;
})
.attr("y2", (d) => {
return d[1] * this.windowSize;
})
.attr("stroke", this.colorScaleFunc.bind(this));

this.lines2 = pendulums
.append("line")
.attr("class", "pendulumLine2")
.attr("stroke-width", 2 * this.lineResizer)
.attr("x1", (d) => {
return d[0] * this.windowSize;
})
.attr("y1", (d) => {
return d[1] * this.windowSize;
})
.attr("x2", (d) => {
return d[2] * this.windowSize;
})
.attr("y2", (d) => {
return d[3] * this.windowSize;
})
.attr("stroke", this.colorScaleFunc.bind(this));

this.dots1 = pendulums
.append("circle")
.attr("class", "dots1")
.attr("fill", this.colorScaleFunc.bind(this))
.attr("cx", (d) => {
return d[0] * this.windowSize;
})
.attr("cy", (d) => {
return d[1] * this.windowSize;
})
.attr("r", this.constants[1] * this.dotResizer * this.windowSize);

this.dots2 = pendulums
.append("circle")
.attr("class", "dots2")
.attr("fill", this.colorScaleFunc.bind(this))
.attr("cx", (d) => {
return d[2] * this.windowSize;
})
.attr("cy", (d) => {
return d[3] * this.windowSize;
})
.attr("r", this.constants[3] * this.dotResizer * this.windowSize);

this.origin
.append("circle")
.attr("class", "origin-mark")
.attr(
"r",
Math.max(this.constants[2], this.constants[1]) *
1.5 *
this.dotResizer *
this.windowSize
)
.style(
"fill",
this.colorScaleFunc.bind(this)(
null,
(this.pendulumNumber - 1) / (this.pendulumNumber + 5)
)
);

this.currentCoords = this.initialCoords.clone();
}

colorScaleFunc(d, i) {
if (this.pendulumNumber == 1) {
if (d != null) return this.colorScale(0.95);
return d3.rgb(this.colorScale(0.95)).hex();
}
if (d == null) return d3.rgb(this.colorScale(0.95)).hex();
return this.colorScale(
i / (this.pendulumNumber + 5) + 2 / this.pendulumNumber
);
}

updateTrailDataWith(newCoords) {
this.trailCounter++;
if (this.trailLength == 0) return;
if (this.trailCounter > this.trailLength) {
for (let i = 0; i < this.pendulumNumber; i++) {
this.trailPaths[i] =
"M" +
this.trailPaths[i].slice(
this.trailPaths[i].indexOf("L") + 1
) +
"L " +
(newCoords[i][2] * this.windowSize).toPrecision(5) +
" " +
(newCoords[i][3] * this.windowSize).toPrecision(5) +
" ";
}
} else {
for (let i = 0; i < this.pendulumNumber; i++) {
this.trailPaths[i] +=
"L " +
(newCoords[i][2] * this.windowSize).toPrecision(5) +
" " +
(newCoords[i][3] * this.windowSize).toPrecision(5) +
" ";
}
}
this.lastCartCoords = this.newCoords;
}

updateDisplay() {
this.lastCoords = this.currentCoords.clone();
for (let i = 0; i < this.repeats; i++) {
this.currentCoords = RK4LA(
derivativeLA,
this.stepSize,
0,
this.currentCoords,
this.constants
);
}
this.currentCartCoords = convertToCoordinates(
this.currentCoords,
this.constants
);

this.dots1
.data(this.currentCartCoords)
.attr("cx", (d) => {
return d[0] * this.windowSize;
})
.attr("cy", (d) => {
return d[1] * this.windowSize;
});
this.dots2
.data(this.currentCartCoords)
.attr("cx", (d) => {
return d[2] * this.windowSize;
})
.attr("cy", (d) => {
return d[3] * this.windowSize;
});

this.lines1
.data(this.currentCartCoords)
.attr("x2", (d) => {
return d[0] * this.windowSize;
})
.attr("y2", (d) => {
return d[1] * this.windowSize;
});

this.lines2
.data(this.currentCartCoords)
.attr("x1", (d) => {
return d[0] * this.windowSize;
})
.attr("y1", (d) => {
return d[1] * this.windowSize;
})
.attr("x2", (d) => {
return d[2] * this.windowSize;
})
.attr("y2", (d) => {
return d[3] * this.windowSize;
});

if (this.useTrails && !(this.currentStep % this.trailUpdateInterval)) {
this.lastTrailUpdateCoords = this.currentCartCoords.slice(2);
this.updateTrailDataWith(this.currentCartCoords);
this.trails.data(this.trailPaths).attr("d", (d) => {
return d;
});
}
this.currentStep += 1;
if (this.continueLooping) {
requestAnimationFrame(() => this.updateDisplay());
} else {
this.lock.release();
}
}
}
const g = 9.81;

function concat(a, b, c, d) {
let destination = new Float64Array(
a.length + b.length + c.length + d.length
);
destination.set(a);
destination.set(b, a.length);
destination.set(c, a.length + b.length);
destination.set(d, a.length + b.length + c.length);
return destination;
}

function linspace(start, stop, number) {
let arr = new Float64Array(number);
let step;
if (number > 1) step = (stop - start) / (number - 1);
else step = 0;
for (let i = 0; i < number; i++) {
arr[i] = start + step * i;
}
return arr;
}

function initPendulums(phi1, phi2, deviation, number) {
// Phi1 Array contains all the values of phi1 (no deviation)
// Phi2 Array contains all the values of phi2 but deviated such that the
// average value is phi2, and the difference between any 2 phi2's is
// deviation
const phi1Array = linspace(phi1, phi1, number);
let phi2Array;
if (number > 1) {
phi2Array = linspace(
phi2 - (deviation / 2) * (number - 1),
phi2 + (deviation / 2) * (number - 1),
number
);
} else {
phi2Array = linspace(
phi2 - deviation / 2,
phi2 + deviation / 2,
number
);
}
const p1Array = linspace(0, 0, number);
const p2Array = p1Array.clone();
const output = concat(phi1Array, p1Array, phi2Array, p2Array);
return output;
}

function RK4LA(f, h, t, p, constants) {
const k1 = f(t, p.clone(), constants).scalarMul(h);
const k2 = f(
t + h / 2,
p.clone().add(k1.clone().scalarMul(0.5)),
constants
).scalarMul(h);
const k3 = f(
t + h / 2,
p.clone().add(k2.clone().scalarMul(0.5)),
constants
).scalarMul(h);
const k4 = f(t + h, p.clone().add(k3), constants).scalarMul(h);
return p.clone().add(
k1
.clone()
.add(k2)
.add(k3)
.add(k4)
.scalarMul(1 / 6)
);
}

function derivativeLA(t, p, constants) {
// p: phi1, p1, phi2, p2
// constants: l1, m1, l2, m2
const l1 = constants[0],
m1 = constants[1],
l2 = constants[2],
m2 = constants[3];
const vectorLength = p.length / 4;
const phi1 = p.slice(0, vectorLength),
p1 = p.slice(vectorLength, vectorLength * 2),
phi2 = p.slice(vectorLength * 2, vectorLength * 3),
p2 = p.slice(vectorLength * 3, vectorLength * 4);

const cosdif = phi1.clone().sub(phi2).cos();
const sindif = phi1.clone().sub(phi2).sin();
const divisor = sindif.clone().square().scalarMul(m2).scalarAdd(m1);

const h1 = p1
.clone()
.mul(p2)
.mul(sindif)
.div(divisor.clone().scalarMul(l1 * l2));
const h2 = p1
.clone()
.square()
.scalarMul(m2 * l2 * l2)
.add(
p2
.clone()
.square()
.scalarMul(l1 * l1 * (m1 + m2))
)
.sub(
cosdif
.clone()
.mul(p1)
.mul(p2)
.scalarMul(2 * m2 * l2 * l2)
)
.div(
divisor
.clone()
.square()
.scalarMul(2 * l1 * l1 * l2 * l2)
);

const dphi1 = p1
.clone()
.scalarMul(l2)
.sub(p2.clone().scalarMul(m1).mul(cosdif))
.div(divisor.clone().scalarMul(l1 * l1 * l2));
const dphi2 = p2
.clone()
.scalarMul(l1 * (m1 + m2))
.sub(
p1
.clone()
.mul(cosdif)
.scalarMul(m2 * l2)
)
.div(divisor.clone().scalarMul(m2 * l1 * l2 * l2));

const dp1 = h2
.clone()
.mul(sindif)
.mul(cosdif)
.scalarMul(2)
.sub(h1)
.sub(
phi1
.clone()
.sin()
.scalarMul(g * l1 * (m1 + m2))
);
const dp2 = h1
.clone()
.sub(h2.clone().mul(sindif).mul(cosdif).scalarMul(2))
.sub(
phi2
.clone()
.sin()
.scalarMul(g * l2 * m2)
);

return concat(dphi1, dp1, dphi2, dp2);
}

function convertToCoordinates(p, constants) {
const l1 = constants[0],
m1 = constants[1],
l2 = constants[2],
m2 = constants[3];
const vectorLength = p.length / 4;
const phi1 = p.slice(0, vectorLength),
p1 = p.slice(vectorLength, vectorLength * 2),
phi2 = p.slice(vectorLength * 2, vectorLength * 3),
p2 = p.slice(vectorLength * 3, vectorLength * 4);

const x1 = phi1.clone().sin().scalarMul(l1),
y1 = phi1.clone().cos().scalarMul(l1);

const x2 = x1.clone().add(phi2.clone().sin().scalarMul(l2)),
y2 = y1.clone().add(phi2.clone().cos().scalarMul(l2));
return [x1, y1, x2, y2].transpose();
}

Float64Array.prototype.clone = function () {
let destination = new Float64Array(this.length);
destination.set(this);
return destination;
};

Array.prototype.transpose = function () {
let newArray = new Array(this[0].length);
for (var i = 0; i < this[0].length; i++) {
let rowArray = new Float64Array(this.length);
for (var j = 0; j < this.length; j++) {
rowArray[j] = this[j][i];
}
newArray[i] = rowArray;
}
return newArray;
};

Float64Array.prototype.add = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] += b[i];
}
return destination;
};

Float64Array.prototype.mul = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] *= b[i];
}
return destination;
};

Float64Array.prototype.div = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] /= b[i];
}
return destination;
};

Float64Array.prototype.sub = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] -= b[i];
}
return destination;
};

Float64Array.prototype.scalarAdd = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] += b;
}
return destination;
};

Float64Array.prototype.scalarMul = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] *= b;
}
return destination;
};

Float64Array.prototype.scalarDiv = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] /= b;
}
return destination;
};

Float64Array.prototype.scalarSub = function (b) {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] -= b;
}
return destination;
};

Float64Array.prototype.square = function () {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] = destination[i] * destination[i];
}
return destination;
};

Float64Array.prototype.cos = function () {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] = Math.cos(destination[i]);
}
return destination;
};

Float64Array.prototype.sin = function () {
let destination = new Float64Array(this.length);
destination.set(this);
for (let i = 0; i < this.length; i++) {
destination[i] = Math.sin(destination[i]);
}
return destination;
};

let lock = new AnimationLock();

export function DoublePendulumDemo(constantOverrides) {
let constants = useMemo(() => {
return { ...baseConstants, ...constantOverrides };
}, [constantOverrides]);

let id = useId();

let key = useMemo(() => {
return Math.random();
}, [constants, id]);

useEffect(() => {
let demo = new DoublePendulum({ ...constants, location: id }, lock);
demo.init();
const demoLocation = document.getElementById(id);
if (!demoLocation) return;

// Check for hover vs touch device
if ("ontouchstart" in window || navigator.maxTouchPoints > 0) {
demoLocation.onclick = function (d) {
if (d.target != d.currentTarget) return;
if (demo.continueLooping) {
demo.stop.bind(demo)();
}
};
demoLocation.ondblclick = demo.restart.bind(demo);
} else {
demoLocation.onmouseenter = demo.start.bind(demo);
demoLocation.onmouseleave = demo.stop.bind(demo);
demoLocation.onclick = demo.restart.bind(demo);
}
}, [constants, id]);

return <div className={styles.wrapper} id={id} key={key} />;
}

export function DoublePendulumSliderDemo(constantOverrides) {
let [sliderOverrides, setSliderOverrides] = useState({
...baseConstants,
...constantOverrides,
});

const slidersConstants = [
["Pendulum Number", "pendulumNumber", 1, 200, true],
["Trail Length", "trailLength", 0, 200, true],
["Rod A Length", "l1", 3, 10, false],
["Rod B Length", "l2", 3, 10, false],
["Mass A", "m1", 1, 10, false],
["Mass B", "m2", 1, 10, false],
["Angle A Initial", "phi1Init", 0, 2 * Math.PI, false],
["Angle B Initial", "phi2Init", 0, 2 * Math.PI, false],
];

return (
<>
<DoublePendulumDemo {...sliderOverrides} />
<div className={styles.sliderWrapper}>
{slidersConstants.map((item, i) => (
<Slider
key={i}
id={item[1]}
name={item[0]}
startValue={item[2]}
stopValue={item[3]}
isInteger={item[4]}
value={
sliderOverrides[item[1]] ?? (item[2] + item[3]) / 2
}
onChange={(name, value) => {
setSliderOverrides({
...sliderOverrides,
[name]: value,
});
}}
/>
))}
</div>
</>
);
}

const Slider = ({
name,
id,
startValue,
stopValue,
isInteger,
value,
onChange,
}) => {
const handleInputChange = (event) => {
const newValue = isInteger
? parseInt(event.target.value)
: parseFloat(event.target.value);
onChange(id, newValue);
};

return (
<div className="sliderWrapper">
<div>
{name}: <span style={{ fontWeight: "bold" }}>{value}</span>
</div>
<input
type="range"
min={startValue}
max={stopValue}
value={value}
step={isInteger ? 1 : 0.1}
className="slider"
onChange={handleInputChange}
/>
</div>
);
};

COVID-19 Early Spread in South Carolina

· 7 min read

COVID-19 Early Spread in South Carolina

Hover over bubbles to see details

Sources

Source Code

View source
CovidSpreadDemo.tsx
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
import { useEffect, useId } from "react";
import * as d3 from "d3";
import styles from "./styles.module.css";
import * as topojson from "topojson-client";

export default function LightSpeed() {
let { withBaseUrl } = useBaseUrlUtils();
let id = useId();

useEffect(() => {
const width = 420,
height = 360,
maxRadius = 28;

let maxData = 100;

let wrapper = document.getElementById(id);

let svg = d3
.select(wrapper)
.append("svg")
.attr("width", width)
.attr("id", "SVGMain")
.attr("height", height);

let sc_projection = d3
.geoAlbers()
.parallels([32 + 30 / 60, 34 + 50 / 60])
.rotate([81, 0])
.scale(6000)
.translate([155, -340]);

let path = d3.geoPath().projection(sc_projection);

let tooltip = d3
.select(wrapper)
.append("div")
.attr("class", styles.tooltip)
.style("opacity", 0);

let radius;

let playButton;
let stopButton;

let sizeLegend;
let colorLegend;
let timeLegend;

let virusData;
let deathData;
let centroids = {};
let totalData = {};

let dateOptions = {
month: "long",
day: "numeric",
} satisfies Intl.DateTimeFormatOptions;
let dataLoaded = false;
let currentDay = 0;
let timerID: NodeJS.Timeout | null = null;
let isPlaying = false;

function getDate(day) {
return new Date(2020, 2, 6 + day);
}

function drawButtons() {
let buttonGroup = svg
.append("g")
.attr(
"transform",
"translate(" +
(width - 50) +
"," +
(height - 20 - (2 * maxRadius + 10)) +
")"
)
.attr("class", styles.button);

playButton = buttonGroup
.append("g")
.attr("class", styles.button)
.attr("transform", "translate(0," + (maxRadius - 20) + ")")
.on("click", playAnimation);
playButton
.append("rect")
.attr("rx", 10)
.attr("ry", 10)
.attr("width", maxRadius * 2)
.attr("height", maxRadius)
.attr("x", -maxRadius);

playButton
.append("text")
.attr("x", -maxRadius / 2)
.attr("dy", "1.2em")
.text("Play");

stopButton = buttonGroup
.append("g")
.attr("class", styles.button)
.attr("transform", "translate(0," + (maxRadius * 2 - 10) + ")")
.on("click", stopAnimation);

stopButton
.append("rect")
.attr("rx", 10)
.attr("ry", 10)
.attr("width", maxRadius * 2)
.attr("height", maxRadius)
.attr("x", -maxRadius);

stopButton
.append("text")
.attr("x", -maxRadius / 2 - 2)
.attr("dy", "1.2em")
.text("Stop");
}

function drawLegend() {
let d1 = maxData / 2;
let d2 = maxData / 4;
sizeLegend = svg
.append("g")
.attr("class", styles.sizeLegend)
.attr(
"transform",
"translate(" + 60 + "," + (height - 20) + ")"
)
.selectAll("g")
.data([
Math.floor(d1 / 10) * 10,
Math.floor(d2 / 10) * 10,
Math.floor(maxData / 10) * 10,
])
.enter()
.append("g");

sizeLegend
.append("circle")
.attr("cy", function (d) {
return -radius(d);
})
.attr("r", radius);

sizeLegend
.append("text")
.attr("y", function (d) {
return -2 * radius(d);
})
.attr("x", function (d) {
return -radius(d) / 100;
})
.attr("dy", "1.2em")
.text(d3.format(".1s"));

colorLegend = svg
.append("g")
.attr("class", styles.colorLegend)
.attr(
"transform",
"translate(" +
60 +
"," +
(height - (20 + 2 * maxRadius)) +
")"
)
.selectAll("g")
.data([
[0, "Cases", "#aa1111"],
[1, "Deaths", "#000"],
])
.enter()
.append("g");

colorLegend
.append("circle")
.attr("cx", function (d) {
return d[0] * 60 - 30;
})
.attr("cy", -maxRadius)
.attr("r", maxRadius)
.style("fill", function (d) {
return d[2];
});

colorLegend
.append("text")
.attr("x", function (d) {
return d[0] * 60 - 30;
})
.attr("y", -maxRadius - 10)
.attr("dy", "1.2em")
.text(function (d) {
return d[1];
});
timeLegend = svg
.append("g")
.attr("class", styles.timeLegend)
.attr(
"transform",
"translate(" +
60 +
"," +
(height - (20 + 32 + 4 * maxRadius)) +
")"
)
.data([0]);

timeLegend
.append("rect")
.attr("width", maxRadius * 2 + 4)
.attr("rx", 10)
.attr("ry", 10)
.attr("height", maxRadius)
.attr("x", -maxRadius - 2);
timeLegend
.append("text")
.attr("y", maxRadius / 2 + 3)
.text(function (d) {
return getDate(d).toLocaleDateString("en-US", dateOptions);
});
}

function load_initial_data() {
maxData = 0;
for (let day in virusData.columns) {
let dayData = {};
for (let county in virusData[day]) {
dayData[county] = [
virusData[day][county],
deathData[day][county],
];
maxData = Math.max(maxData, virusData[day][county]);
}
totalData[day] = dayData;
}
radius = d3.scaleSqrt().domain([0, maxData]).range([0, maxRadius]);
drawLegend();
drawButtons();
fill_initial_data();
}

function inputInitialData() {
d3.csv(
withBaseUrl("covid-spread-sc/SC_VIRUS_TOTAL_CUMULATIVE.csv")
).then(function (total_data) {
virusData = total_data;
if (dataLoaded) {
load_initial_data();
} else {
dataLoaded = true;
}
});
d3.csv(
withBaseUrl("covid-spread-sc/SC_VIRUS_DEATH_CUMULATIVE.csv")
).then(function (total_data) {
deathData = total_data;
if (dataLoaded) {
load_initial_data();
} else {
dataLoaded = true;
}
});
}

function fill_initial_data() {
let dVList = Object.entries(totalData[0]).map(([key, value]) => {
return { key, value };
});
svg.selectAll(`path.${styles.countyFill}`)
.data(dVList)
.style("fill-opacity", function (d: any) {
if (+d.value[0] > 0) return 0;
return 1;
})
.on("mouseover", function (evt: MouseEvent, d: any) {
if (+d.value[0] >= 1) return;
let firstS = "";
let secondS = "";
if (d.value[0] != 1) firstS = "s";
if (d.value[1] != 1) secondS = "s";
let tooltipText =
"<bold>" +
d.key +
"</bold>" +
"<br/>" +
+d.value[0] +
" case" +
firstS +
"<br/>" +
+d.value[1] +
" death" +
secondS;
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip
.html(tooltipText)
.style("left", evt.pageX + "px")
.style("top", evt.pageY - 28 + "px");
})
.on("mouseout", function (d) {
tooltip.transition().duration(500).style("opacity", 0);
});
let countyLocations = svg
.append("g")
.attr("class", styles.bubbleContainer)
.selectAll(".county-centroid")
.data(dVList)
.enter()
.append("g")
.attr("class", "county-centroid")
.attr("transform", function (d: any) {
return "translate(" + centroids[d.key] + ")";
});

let cases = countyLocations
.append("circle")
.attr("class", styles.case)
.attr("r", function (d: any) {
return radius(+d.value[0]);
});

countyLocations
.append("circle")
.attr("class", styles.death)
.attr("r", function (d: any) {
return radius(+d.value[1]);
});

countyLocations
.append("circle")
.attr("class", styles.caseBoundary)
.attr("r", function (d: any) {
return radius(+d.value[0]);
})
.on("mouseover", function (evt: MouseEvent, d: any) {
let firstS = "";
let secondS = "";
if (d.value[0] != 1) firstS = "s";
if (d.value[1] != 1) secondS = "s";
let tooltipText =
"<bold>" +
d.key +
"</bold>" +
"<br/>" +
+d.value[0] +
" case" +
firstS +
"<br/>" +
+d.value[1] +
" death" +
secondS;
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip
.html(tooltipText)
.style("left", evt.pageX + "px")
.style("top", evt.pageY - 28 + "px");
})
.on("mouseout", function (d) {
tooltip.transition().duration(500).style("opacity", 0);
});
}

function playAnimation() {
if (isPlaying) return;
isPlaying = true;
if (currentDay >= Object.keys(virusData).length - 2) {
currentDay = 0;
transitionToDay(currentDay);
}
timerID = setInterval(() => updateForNextDay(), 605);
}

function stopAnimation() {
if (!isPlaying) return;
if (timerID != null) {
clearInterval(timerID);
}
isPlaying = false;
}

function updateForNextDay() {
currentDay = (currentDay + 1) % (Object.keys(totalData).length - 2);
transitionToDay(currentDay);
}

function transitionToDay(day) {
if (day >= Object.keys(virusData).length - 2) {
stopAnimation();
}

let dVList = Object.entries(totalData[day]).map(([key, value]) => {
return { key, value };
});

let countyLocations = svg
.selectAll(".county-centroid")
.data(dVList);
let cases = countyLocations.select(`circle.${styles.case}`);
let deaths = countyLocations.select(`circle.${styles.death}`);
let boundaries = countyLocations.select(
`circle.${styles.caseBoundary}`
);

svg.select(`.${styles.timeLegend}`)
.data([day])
.select("text")
.text(function (d) {
return getDate(d).toLocaleDateString("en-US", dateOptions);
});

if (day == 0) {
cases.attr("r", function (d: any) {
return radius(+d.value[0]);
});

deaths.attr("r", function (d: any) {
return radius(+d.value[1]);
});

boundaries.attr("r", function (d: any) {
return radius(+d.value[0]);
});

svg.selectAll(`path.${styles.countyFill}`)
.data(dVList)
.style("fill-opacity", function (d: any) {
if (+d.value[0] >= 1) return 0;
return 1;
});
} else {
let transitionEase = d3.easeLinear;
let duration = 600;
cases
.transition()
.duration(duration)
.ease(transitionEase)
.attr("r", function (d: any) {
return radius(+d.value[0]);
});

deaths
.transition()
.duration(duration)
.ease(transitionEase)
.attr("r", function (d: any) {
return radius(+d.value[1]);
});

boundaries
.transition()
.duration(duration)
.ease(transitionEase)
.attr("r", function (d: any) {
return radius(+d.value[0]);
});

svg.selectAll(`path.${styles.countyFill}`)
.data(dVList)
.transition()
.ease(transitionEase)
.duration(duration)
.style("fill-opacity", function (d: any) {
if (+d.value[0] >= 1) return 0;
return 1;
});
}
}

d3.json(withBaseUrl("covid-spread-sc/states.json")).then(function (
sc: any
) {
let state = topojson.feature(sc, sc.objects.places);

svg.append("path")
.datum(state)
.attr("d", path)
.attr("class", styles.state);
});

d3.json(withBaseUrl("covid-spread-sc/counties.json")).then(function (
counties: any
) {
let features = topojson.feature(
counties,
counties.objects.places
).features;
svg.append("g")
.selectAll(`path.${styles.countyFill}`)
.data(
features.sort(function (a, b) {
if (b.properties.NAME > a.properties.NAME) return -1;
if (b.properties.NAME < a.properties.NAME) return 1;
return 0;
})
)
.enter()
.append("path")
.attr("d", path)
.attr("class", styles.countyFill);

svg.append("path")
.datum(
topojson.mesh(
counties,
counties.objects.places,
function (a, b) {
return a !== b;
}
)
)
.attr("d", path)
.attr("class", styles.countyBoundary);

svg.append("path")
.datum(
topojson.mesh(
counties,
counties.objects.places,
function (a, b) {
return a === b;
}
)
)
.attr("d", path)
.attr("class", styles.countyBoundary);
for (let i = 0; i < features.length; i++) {
let centroid = path.centroid(features[i]);
if (features[i].properties.NAME == "Charleston") {
centroid = [centroid[0] - 15, centroid[1] + 12];
}
centroids[features[i].properties.NAME] = centroid;
}
inputInitialData();
});
}, [id]);

return <div className={styles.wrapper} id={id} />;
}