{
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);
}
}