Published unlisted
Edited
May 11, 2022
Insert cell
# Michael Asare's Data Visualization
Insert cell
<!DOCTYPE html>
<html>
<head>
<title>Need for Speed</title>
<meta name="description" content="Dynamic Data Visualization for SARC 5400 Spring 2022">
<meta name="author" content="Michael Asare">
</head>
<body>
<div id="viz-container">
<p id="header-text">Need for Speed</p>
<p id="sub-text">A visualization depicting internet speeds around the globe</p>
<svg id="graphic" height="400" width="1000">
</svg>
<div id="button-container">
<div id="direction-container">
<button id="previous-button">Previous</button>
<button id="next-button">Next</button>
</div>
<div id="restart-container">
<button id="restart-button">Restart Visualization</button>
</div>
</div>
</div>
</body>
</html>
Insert cell
<style>
@import url("https://fonts.googleapis.com/css?family=Assistant");
* {
font-family: Assistant, Arial, Helvetica, sans-serif;
}
#viz-container {
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

#direction-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0px 10px 0px;
width: 800px;
}

p {
margin: 4px;
}

#header-text {
font-weight: bold;
font-size: large;
visibility: hidden;
}

#sub-text {
font-weight: lighter;
font-size: small;
visibility: hidden;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
width: 120px;
top: 100%;
left: 50%;
margin-left: -60px;
z-index: 1;
}
.dot-group:hover .tooltiptext {
visibility: visible;
}

.rotated {
transform-origin: center center;
transform: rotate(270deg);
}
</style>
Insert cell
{
// import * as d3 from "https://cdn.skypack.dev/d3@7";

const HEIGHT = 400;
const WIDTH = 1000;

const headerText = document.getElementById("header-text");
const subText = document.getElementById("sub-text");

let timers = [];
const clearTimers = () => {
for(const timer of timers) {
clearInterval(timer);
}
timers = [];
}
function fadeIn( elem, ms )
{
if( ! elem )
return;

elem.style.opacity = 0;
elem.style.filter = "alpha(opacity=0)";

if( ms )
{
var opacity = 0;
var timer = setInterval( function() {
opacity += 50 / ms;
if( opacity >= 1 )
{
clearInterval(timer);
opacity = 1;
}
elem.style.opacity = opacity;
elem.style.filter = "alpha(opacity=" + opacity * 100 + ")";
}, 50 );
timers.push(timer);
}
else
{
elem.style.opacity = 1;
elem.style.filter = "alpha(opacity=1)";
}
}

const invis = (element) => {
element.style.visibility = "hidden";
}
const vis = (element) => {
element.style.visibility = "visible";
}

const header = (text, ms) => {
headerText.innerHTML = text;
if(!ms) {
ms = 5000;
}
fadeIn(headerText, ms);
}
const sub = (text, ms) => {
subText.innerHTML = text;
if(!ms) {
ms = 5000;
}
fadeIn(subText, ms);
}

const delaySub = (text, ms, delay) => {
let timer = setTimeout(() => {
sub(text, ms);
}, delay);
timers.push(timer);
}

const fadeButton = (button) => {
button.disabled = true;
}
const unFadeButton = (button) => {
button.disabled = false;
}


const previousButton = document.getElementById("previous-button")
const nextButton = document.getElementById("next-button")
const restartButton = document.getElementById("restart-button")

const flashNext = (delay) => {
if(delay == undefined) {
delay = 0;
}
const timer = setTimeout(() => {
const interval = setInterval(() => {
nextButton.style.backgroundColor = "lightgreen";
nextButton.style.border = "solid darkgreen 2px";
nextButton.style.borderRadius = "2px";
const secondTimer = setTimeout(() => {
resetNext();
}, 500);
timers.push(secondTimer);
}, 1000);
timers.push(interval);
}, delay)
timers.push(timer);
}

const resetNext = () => {
nextButton.style.backgroundColor = "";
nextButton.style.border = "";
nextButton.style.borderRadius = "";
}

const graphic = d3.select("#graphic")
let speed_data;
let pop_data;
(async() => {
const speed_file = FileAttachment("worldwide_speed_league_data.csv");
const pop_file = FileAttachment("popdata.csv");
speed_data = await speed_file.csv("./worldwide_speed_league_data.csv");
pop_data = await pop_file.csv("./")
})();

const clearGraphic = () => {
d3.selectAll("#graphic > *").remove();
}

const clearFilters = () => {
d3.selectAll("#filter > *").remove();
}

const cleanUpPanel = () => {
clearTimers();
clearGraphic();
clearFilters();
resetNext();
}

const objectExists = (id) => {
return document.getElementById(id) != null;
}

let BLUE_DOT;
const grabBlueDot = () => {
const blueDotId = "blue-dot"
if(!objectExists(blueDotId)) {
BLUE_DOT = graphic.append("circle")
.attr("r", 5)
.attr("fill", "blue")
.attr("id", blueDotId);
}
return BLUE_DOT;
}

const grabBlueDotAt = (x, y) => {
if(!x) {
x = WIDTH/2;
}
if(!y) {
y = HEIGHT/2;
}
return grabBlueDot().attr("cx", x).attr("cy", y);
}

let GRAY_LINE;
const grabGrayLine = () => {
const grayLineId = "gray-line"
if(!objectExists(grayLineId)) {
GRAY_LINE = graphic.append("line")
.attr("x1", 200)
.attr("y1", 200)
.attr("x2", WIDTH - 200)
.attr("y2", 200)
.attr("stroke", "lightgray")
.attr("stroke-width", "5px")
.attr("id", grayLineId);
}
return GRAY_LINE;
}

const moveRight = (object, duration, delay) => {
if(duration == undefined) {
duration = 1000;
}
if(delay == undefined) {
delay = 0;
}
object
.transition()
.ease(d3.easeLinear)
.delay(delay)
.duration(duration)
.attr("cx", WIDTH - 200)
}

const simDownload = (object, speed, delay, size) => {
if(size == undefined) {
size = 3; // Photograph
}
if(delay == undefined) {
delay = 1000;
}
if(speed == undefined) {
speed = 92.42/8;
}
const duration = size / speed;
moveRight(object, duration*1000, delay);
}

const startupSequence = () => {
cleanUpPanel();
invis(headerText);
invis(subText);
fadeButton(previousButton);
unFadeButton(nextButton);
vis(headerText);
vis(subText);
header("Need for Speed", 3000);
sub("A visualization depicting internet speeds around the globe", 4000);
flashNext(4500);
}

const photoExplainPage = () => {
cleanUpPanel();
invis(headerText);
vis(subText);
const delay = 1000;
sub("An average smartphone photo takes up 3 MB.", delay);
const imgsrc = "./img/smartphone-image.jpg"
const imgHeight = 500
const imgWidth = 500
const img = graphic.append("image")
.attr("opacity", 0)
.attr("href", imgsrc)
.attr("height", imgHeight)
.attr("width", imgWidth)
.attr("x", WIDTH/2 - imgHeight/2)
.attr("y", HEIGHT/2 - imgWidth/2)
.transition()
.ease(d3.easeLinear)
.duration(1000)
.delay(delay)
.attr("opacity", 1);
flashNext(3000);
}

const blueDotPage = () => {
cleanUpPanel();
invis(headerText);
vis(subText);
const delay = 2000;
sub("This blue dot represents 30 MBs.", delay);
const blueDot = grabBlueDotAt(WIDTH/2, 100)
.attr("opacity", 0)
.transition()
.ease(d3.easeLinear)
.duration(1000)
.attr("opacity", 1);
flashNext(4000);
}

const firstSlider = () => {
cleanUpPanel();
invis(headerText);
vis(subText);
const grayLine = grabGrayLine()
.attr("opacity", 0)
.transition()
.ease(d3.easeLinear)
.delay(6000)
.duration(1500)
.attr("opacity", 1);
const blueDot = grabBlueDotAt(WIDTH/2, 100)
.transition()
.ease(d3.easeQuadOut)
.delay(2000)
.duration(3000)
.attr("cx", 200)
.attr("cy", 200);
sub("The average US download speed is 11.55 MBs/second.", 2000);
delaySub("This blue dot will travel across the gray line in the time it takes for 3 MBs to be downloaded over that connection speed.", 1000, 11000);
delaySub("Ready?", 1000, 18000);
flashNext(20000);
}

const firstDemo = () => {
cleanUpPanel();
vis(headerText);
invis(subText);
const regionName = "US";
const speedInMbs = 94.22;
const speed = (speedInMbs / 8.0).toFixed(2);
header(`${regionName}: ${speed} MBs/second.`, 1000);
const grayLine = grabGrayLine();
const blueDot = grabBlueDotAt(200, 200);
const size = 30;
simDownload(blueDot, speed, 4000, size);
const timer = setTimeout(() => {
vis(subText);
sub("Not too shabby.", 1000);
}, 6000);
timers.push(timer);
flashNext(8000);
}

const nextSeveralExplained = () => {
cleanUpPanel();
invis(headerText);
vis(subText);
sub("For the next several slides, I'll demo some other download speeds.", 1000);
flashNext(3000);
}

const countryDemo = (regionName, speedInMbs, text) => {
cleanUpPanel();
vis(headerText);
invis(subText);
const speed = (speedInMbs / 8.0).toFixed(2);
header(`${regionName}: ${speed} MBs/second.`, 1000);
const grayLine = grabGrayLine();
const blueDot = grabBlueDotAt(200, 200);
const size = 30;
simDownload(blueDot, speed, 4000, size);
vis(subText);
sub(text, 1000);
const durationTillFlash = 1000*(size/speed) + 5000;
flashNext(durationTillFlash);
}

let PAGE_NUMBER = 0;

const demoRegions = [
{"name": "Virginia", "speed": 193.10, "subtext": "Consistently rated in the top 3 states for internet speeds."},
{"name": "US (Rural)", "speed": 45.90, "subtext": "Internet access is far more limited in these areas."},
{"name": "Charlottsville", "speed": 169.80, "subtext": "Good ol' Albemarle County."},
{"name": "High Speed Gigabit Internet", "speed": 1000.00, "subtext": "See Ting Internet."},
{"name": "Canada", "speed": 79.96, "subtext": "Our partners up north are just a little behind us."},
{"name": "Mexico", "speed": 18.83, "subtext": "Mexico's internet infrastructure is sub-optimal in many places in the country."},
{"name": "United Kingdom", "speed": 51.48, "subtext": "Take a look across the pond."},
{"name": "Australia", "speed": 40.50, "subtext": "Surprisingly slow for a modernized nation."},
{"name": "Liechtenstein", "speed": 211.26, "subtext": "A small european country with blazing internet speeds."},
];
const countryDemoRouter = () => {
cleanUpPanel();
const PAGES_BEFORE_DEMOS = 5;
const i = PAGE_NUMBER - PAGES_BEFORE_DEMOS;
countryDemo(demoRegions[i]["name"], demoRegions[i]["speed"], demoRegions[i]["subtext"]);
}

const dynamicSpeedTest = () => {
cleanUpPanel();
invis(headerText);
invis(subText);
sub("", 1);
vis(subText);
const d3SubText = d3.selectAll("#sub-text");
const datalist = d3SubText.append("datalist")
.attr("id", "countries");
speed_data.forEach(d => {
const country = d["Country"];
if(country != "") {
datalist.append("option").attr("value", country);
}
})
d3SubText.append("label")
.attr("for", "countries")
const input = d3SubText.append("input")
.attr("list", "countries")
.attr("name", "countries")
.attr("id", "countries")
.attr("class", "selected-input");
d3SubText.append("button")
.style("left-margin", "5px")
.text("Visualize")
.on("click", () => {
const currentCountry = document.getElementsByClassName("selected-input")[0].value;
let speedInMbs;
speed_data.forEach(d => {
const country = d["Country"];
if(currentCountry == country) {
speedInMbs = d["Mean download speed (Mbps)"];
}
})
if(speedInMbs != undefined) {
const speed = (speedInMbs / 8.0).toFixed(2);
header(`${currentCountry}: ${speed} MBs/second.`, 1);
vis(headerText);
const grayLine = grabGrayLine();
const blueDot = grabBlueDotAt(200, 200);
const size = 30;
simDownload(blueDot, speed, 0, size);
}
});

}

const dynamicGraph = () => {
cleanUpPanel();
invis(headerText);
invis(subText);
sub("", 1);
header("Download Speed, Population, and Land Area", 1);
vis(headerText);
vis(subText);
const d3SubText = d3.selectAll("#sub-text");
let maxSpeedInMbs;
speed_data.forEach(d => {
const some_speed = d["Mean download speed (Mbps)"]
maxSpeedInMbs = maxSpeedInMbs != undefined ? Math.max(some_speed, maxSpeedInMbs) : 0;
})
console.log(maxSpeedInMbs);
const default_min = 0;
const default_max = maxSpeedInMbs + 2;
d3SubText
.append("p")
.text("Min Speed in Mbps");
d3SubText
.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", maxSpeedInMbs)
.attr("value", default_min)
.attr("name", "min-slider")
.attr("id", "min-slider");
d3SubText
.append("label")
.attr("for", "min-slider")
.attr("id", "min-slider-value")
.text(default_min);
d3SubText
.append("p")
.text("Max Speed in Mbps");
d3SubText
.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", maxSpeedInMbs + 2)
.attr("value", default_max)
.attr("name", "max-slider")
.attr("id", "max-slider");
d3SubText
.append("label")
.attr("id", "max-slider-value")
.attr("for", "max-slider")
.text(default_max);

const margin = {top: 10, right: 30, bottom: 30, left: 60};
const ADJ_WIDTH = WIDTH - margin.left - margin.right;
const ADJ_HEIGHT = HEIGHT - margin.top - margin.bottom;
graphic.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
const x = d3.scaleLog().domain([1, 18000000]).range([0, ADJ_WIDTH])
const y = d3.scaleLog().domain([1, 1500000]).range([ADJ_HEIGHT, 0])
const x_transform_string = "translate(50," + ADJ_HEIGHT + ")";
const y_transform_string = "translate(50," + 0 + ")";
graphic.append("g").attr("transform", x_transform_string).call(d3.axisBottom(x));
graphic.append("g").attr("transform", y_transform_string).call(d3.axisLeft(y));

graphic.append("text")
.attr("x", WIDTH/2)
.attr("y", HEIGHT-10)
.text("Land Area km^2 (Log Scale)")
.style("text-anchor", "middle")
.style("font-size", "11");
graphic.append("text")
.attr("x", WIDTH/2)
.attr("y", -280)
.text("Population (Log Scale)")
.style("font-size", "11")
.attr("class", "rotated");

const colors = {
"ASIA (EX. NEAR EAST)": "#E5989B",
"BALTICS": "#C0BDA5",
"CARIBBEAN": "#F4D35E",
"CENTRAL AMERICA": "#FF3CC7",
"CIS (FORMER USSR)": "#95969D",
"EASTERN EUROPE": "#464D77",
"NEAR EAST": "#95A472",
"NORTHERN AMERICA": "#FF7F11",
"NORTHERN AFRICA": "#9EE493",
"OCEANIA": "#336699",
"SOUTH AMERICA": "#F4E04D",
"SUB-SAHARAN AFRICA": "#35281D",
"WESTERN EUROPE": "#B49FCC",
}

let legend_x = WIDTH/16;
let legend_y = HEIGHT/16;

for(const region of Object.keys(colors)) {
const color = colors[region];
graphic.append("text")
.attr("x", legend_x + 3)
.attr("y", legend_y)
.attr("alignment-baseline","middle")
.style("fill", color)
.style("font-size", "11px")
.text(region)
legend_y += 12;
}

const valid_countries = new Set()
let country_regions = {}
let country_speeds = {}
speed_data.forEach(d => {
const country_name = d["Country"];
valid_countries.add(country_name);
const region = d["Region"];
country_regions[country_name] = region;

const speed = d["Mean download speed (Mbps)"];
country_speeds[country_name] = speed;
});

const g = graphic.append('g');
pop_data.filter(d => valid_countries.has(d["name"])).forEach(d => {
const div = g.append('g').attr('class', 'dot-group').attr("speed", country_speeds[d["name"]]);
div.append('rect')
.attr('x', () => {
return x(d['area']);
})
.attr('y', () => {
return y(d['pop2022']);
})
.attr('width', 10)
.attr('height', 10)
.style("fill", () => {
const country = d["name"];
const region = country_regions[country];
return colors[region];
})
.style("opacity", 0.7)
.style("transform-box", "fill-box")
.style("transform-origin", "50% 50%")
.style("animation-duration", () => {
const country = d["name"];
const speed = country_speeds[country]/8;
const size = 10;
const duration = size/speed;
return `${duration}s`
})
.style("animation-name", "rotate")
.style("animation-iteration-count", "infinite")
.attr("value", () => {
const country = d["name"];
return country_speeds[country];
})
.attr("country", () => d["name"])
.attr("transform", y_transform_string)
.attr("class", "dot")
div.append('text')
.attr("class", "tooltiptext")
.attr("x", WIDTH/4)
.attr("y", HEIGHT/4)
.text(() => {
const country_name = d["name"];
const country_speed = (country_speeds[country_name]/8).toFixed(2);
return `${country_name}: ${country_speed}MB/s`
});
})
const max_slider = document.getElementById("max-slider");
const min_slider = document.getElementById("min-slider");

const filter_dots = () => {
const groups = document.getElementsByClassName("dot-group");
const max_speed = Number(max_slider.value);
const min_speed = Number(min_slider.value);
console.log(max_speed)
console.log(min_speed)
for(const group of groups) {
const speed_val = Number(group.getAttribute("speed"));
console.log(speed_val)
if(speed_val >= min_speed && speed_val <= max_speed ) {
group.style.visibility = "";
} else {
group.style.visibility = "hidden";
}
}
}

max_slider.oninput = () => {
document.getElementById("max-slider-value").innerHTML = max_slider.value;
filter_dots();
}
min_slider.oninput = () => {
document.getElementById("min-slider-value").innerHTML = min_slider.value;
filter_dots();
}
}

const pageFunctions = [
startupSequence,
blueDotPage,
firstSlider,
firstDemo,
nextSeveralExplained,
...Array(demoRegions.length).fill(countryDemoRouter),
dynamicSpeedTest,
dynamicGraph,
];
const MAX_PAGES = pageFunctions.length;

const previousPage = () => {
if(PAGE_NUMBER > 0) {
PAGE_NUMBER -= 1;
unFadeButton(nextButton);
if(PAGE_NUMBER == 0) {
fadeButton(previousButton);
}
pageFunctions[PAGE_NUMBER]();
}
};

const nextPage = () => {
if(PAGE_NUMBER < MAX_PAGES - 1) {
PAGE_NUMBER += 1;
unFadeButton(previousButton);
if(PAGE_NUMBER == MAX_PAGES - 1) {
fadeButton(nextButton);
}
pageFunctions[PAGE_NUMBER]();
}
};

const restart = () => {
PAGE_NUMBER = 0;
startupSequence();
};


previousButton.addEventListener("click", previousPage);
nextButton.addEventListener("click", nextPage);
restartButton.addEventListener("click", restart);

if(document.readyState !== "loading") {
startupSequence();
} else {
document.addEventListener("DOMContentLoaded", startupSequence);
}


}
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