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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more