diff --git a/articles/auto-thefts-2022/auto-thefts.js b/articles/auto-thefts-2022/auto-thefts.js
index 156a234..713fb40 100644
--- a/articles/auto-thefts-2022/auto-thefts.js
+++ b/articles/auto-thefts-2022/auto-thefts.js
@@ -1,3 +1,4 @@
+// establish array of location types
const locations = [
"Streets, Roads, Highways (Bicycle Path, Private Road)",
"Parking Lots (Apt., Commercial Or Non-Commercial)",
@@ -38,6 +39,7 @@ const locations = [
"Community Group Home",
];
+// show a notification when the year is changed (on mobile)
let yearTimer;
function updateDataLabel(year) {
if (["", "sm", "md"].includes(module.currentBreakpoint())) {
@@ -65,12 +67,14 @@ function updateDataLabel(year) {
}
}
+// initialize a geojson object to hold the data
const mappable = {
type: "FeatureCollection",
features: [],
};
const noGood = [];
+// collect data for the current selected year
function filterData(year) {
const thefts = mappable.features.filter((f) => f.properties.y == year);
return {
@@ -79,12 +83,14 @@ function filterData(year) {
};
}
+// update the map with the selected year's data
function updateViz(year) {
const src = module.map.getSource("auto-theft");
src.setData(filterData(year));
updateDataLabel(year);
}
+// build the year selector element
const radioYears = document.createElement("div");
radioYears.innerHTML = `
`;
+
+// build and initialize the legend
function addLegend() {
module.setLegendTitle("Toronto Auto Thefts");
module.addToLegend(radioYears);
@@ -152,6 +160,7 @@ function addLegend() {
selector.addEventListener("change", (e) => updateViz(e.target.value));
}
+// show a popup for a theft location
function showPopup(e) {
module.clearPopups();
const { l, p, n } = e.features[0].properties;
@@ -219,6 +228,7 @@ function showPopup(e) {
});
}
+// establish a consistent opacity
const opacity = [
"interpolate",
["linear"],
@@ -234,6 +244,7 @@ const opacity = [
];
function displayData() {
+ // adds point clusters at higher zoom levels
module.addVizLayer({
id: "clusters",
filter: ["has", "point_count"],
@@ -326,6 +337,7 @@ function displayData() {
fetch(url)
.then((r) => r.json())
.then((d) => {
+ // remove data points outside of Toronto (mislabelled)
d.features.forEach((f) => {
if (
f.geometry.coordinates[0] < -79.9 ||
@@ -345,6 +357,7 @@ fetch(url)
data: mappable,
type: "geojson",
});
+ // load and display data for 2022
updateViz(2022);
displayData();
})
diff --git a/articles/auto-thefts-2022/neighbourhoods.js b/articles/auto-thefts-2022/neighbourhoods.js
index aec4b18..3cd9bb0 100644
--- a/articles/auto-thefts-2022/neighbourhoods.js
+++ b/articles/auto-thefts-2022/neighbourhoods.js
@@ -1,9 +1,11 @@
+// hide selected mapbox layers to reduce visual clutter
module.hideLayer("settlement-subdivision-label");
module.hideLayer("settlement-minor-label");
module.hideLayer("settlement-major-label");
let currentYear = 2022;
+// update the visualisation based on the selected year
function updateViz(year) {
module.clearPopups();
currentYear = year;
@@ -39,6 +41,7 @@ function updateViz(year) {
]);
}
+// listen for the initial year selector to be added to the DOM and then add an event listener
const interval = setInterval(() => {
const selector = document.getElementById("year-selector");
if (selector) {
@@ -47,6 +50,7 @@ const interval = setInterval(() => {
}
}, 1000);
+// establish consistent text colours
const textColour = {
red: "text-red-700 dark:text-red-500",
orange: "text-orange-700 dark:text-orange-500",
@@ -54,6 +58,7 @@ const textColour = {
blue: "text-blue-700 dark:text-blue-500",
};
+// show the popup with the neighbourhood details
function showDetails(e) {
if (module.map.getLayer("clusters")) {
const clusters = module.map.queryRenderedFeatures(e.point, {
@@ -110,6 +115,7 @@ function showDetails(e) {
.setLngLat(center.geometry.coordinates)
.setHTML(defaultHTML),
);
+ // zoom to the neighbourhood
module.map.fitBounds(bbox, {
bearing: module.map.getBearing(),
duration: 2500,
@@ -119,11 +125,13 @@ function showDetails(e) {
});
}
+// initialise an object to store upper limits
const limits = {
highestTotal: 0,
highestPerKm: 0,
};
+// build the neighbourhood visual and add it to the map
let hoveredStateId = null;
function addNeighbourhoods() {
module.addUnderglowLayer({
@@ -207,7 +215,9 @@ function addNeighbourhoods() {
],
},
});
+ // show a popup on click
module.handleCursor("neighbourhoods-trigger", showDetails);
+ // highlight the neighbourhood boundary on hover
module.map.on("mousemove", "neighbourhoods-trigger", (e) => {
if (e.features.length > 0) {
if (hoveredStateId !== null) {
@@ -234,6 +244,7 @@ function addNeighbourhoods() {
});
}
+// get the upper limits for the data
function getLimits(features) {
features.forEach((f) => {
[2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022].forEach((year) => {
@@ -247,6 +258,7 @@ function getLimits(features) {
});
}
+// fetch the neighbourhood data and begin building the visualisation
fetch(url)
.then((r) => r.json())
.then((d) => {
diff --git a/articles/auto-thefts-2022/readme.md b/articles/auto-thefts-2022/readme.md
new file mode 100644
index 0000000..6dd7ab7
--- /dev/null
+++ b/articles/auto-thefts-2022/readme.md
@@ -0,0 +1,7 @@
+# We mapped Toronto auto thefts. Here are the hardest-hit areas.
+
+** Code and markup by Kyle Duncan**
+
+This is a data analysis and visualisation based on auto theft data from the Toronto Police Service. There are three aspects to this visualisation, an elevation-based overview of total thefts by neighbourhood, and a colour-based visual of thefts per square kilometer by neighbourhood at lower zoom levels; and a clustered count of individual thefts locations at higher zoom levels.
+
+Popups with more detail appear on click for both neighbourhoods and individual thefts, and the user can view a given year's data using the selector in the legend. Multiple thefts at a single location are collated and ordered in single popup.
diff --git a/articles/greater-toronto-area-quiz/article.html b/articles/greater-toronto-area-quiz/article.html
index 91de3e3..9a4f3a7 100644
--- a/articles/greater-toronto-area-quiz/article.html
+++ b/articles/greater-toronto-area-quiz/article.html
@@ -1 +1,28 @@
+
+
+
+ You’ve come to a site called Torontoverse, so you must have at least
+ some knowledge of the place. Or you came by mistake. Whatever.
+
+
+ The point is: The Greater Toronto and Hamilton Area is made up of 26 distinct
+ places — a mix of urban, suburban, and rural municipalities. How many of them
+ can you pick out on a map?
+
diff --git a/articles/greater-toronto-area-quiz/article.js b/articles/greater-toronto-area-quiz/article.js
index ffc3076..7da13ae 100644
--- a/articles/greater-toronto-area-quiz/article.js
+++ b/articles/greater-toronto-area-quiz/article.js
@@ -1,3 +1,4 @@
+// hide the article window
function goFullScreen() {
const fullscreen = document.querySelector(".mapctrl-fullscreen");
if (fullscreen) fullscreen.click();
@@ -5,6 +6,7 @@ function goFullScreen() {
}
goFullScreen();
+// show a popup for the intro or instructions
function showPopup(content) {
module.clearPopups();
module.showPopup(
@@ -19,6 +21,7 @@ function showPopup(content) {
);
}
+// initialize data to be used throughout the quiz
const quiz = {
// establish objects for data
answerList: [],
@@ -42,6 +45,7 @@ const quiz = {
},
};
+// initialize comments for responses and end of quiz
quiz.comments = {
result: {
best: "You’re the Greater-est. Drake should write a song about you.", // 21–25 correct
@@ -73,12 +77,13 @@ quiz.comments = {
],
};
+// return a random index of an array
quiz.getRandom = (array) => {
const randomIndex = Math.floor(Math.random() * array.length);
return array[randomIndex];
};
-// show an intro popup
+// show the intro popup
quiz.showIntro = () => {
const container = document.createElement("div");
const content = document.createElement("div");
@@ -146,16 +151,14 @@ quiz.showInstructions = () => {
.addEventListener("click", quiz.showPrompt);
};
+// create a persistent element at the bottom center of the map that shows:
+// - the next municipality to guess
+// - the time remaining
+// - the number of correct answers
+// - a button to skip to the next municipality
+// - a button to end the quiz
quiz.showPrompt = () => {
module.clearPopups();
- /*
- create a persistent element at the bottom center of the map that shows:
- - the next municipality to guess
- - the time remaining
- - the number of correct answers
- - a button to skip to the next municipality
- - a button to end the quiz
- */
const prompt = document.createElement("div");
prompt.className =
"absolute bg-map-100 dark:bg-map-800 bottom-14 lg:bottom-10 cursor-default default-popoup flex flex-col inset-x-2.5 md:inset-x-1/4 lg:inset-x-1/3 items-center justify-center";
@@ -180,12 +183,11 @@ quiz.showPrompt = () => {
-
-
+
+
`;
prompt.appendChild(content);
- // add hover states to buttons
module.map.getContainer().appendChild(prompt);
window.addEventListener("flexWindowReset", () => {
document.getElementById("promptPopup").remove();
@@ -198,10 +200,9 @@ quiz.newPrompt = () => {
quiz.prompt.innerText = quiz.currentPrompt;
};
+/* INITIALIZE QUIZ */
quiz.start = () => {
- /* INITIALIZE QUIZ */
// grab visual elements
-
// text
quiz.siienComment = document.getElementById("commentText");
quiz.prompt = document.getElementById("prompt");
@@ -215,10 +216,11 @@ quiz.start = () => {
// buttons
quiz.endButton = document.getElementById("giveUp");
quiz.skipButton = document.getElementById("skipPrompt");
-
+ // listen for responses and other elements
module.handleCursor("boundary-fills", quiz.checkAnswer);
quiz.skipButton.addEventListener("click", quiz.newPrompt);
quiz.endButton.addEventListener("click", quiz.giveUp);
+ // load and start the quiz
quiz.buildData();
quiz.newPrompt();
quiz.startTimer();
@@ -301,7 +303,6 @@ quiz.correctGuess = (answer) => {
if (quiz.correctAnswers.length === quiz.masterTotals.answers) {
setTimeout(() => quiz.end("win"), 1000);
} else quiz.newPrompt();
- // else quiz.animateSiien("bounce");
};
quiz.revealAnswer = (answer) => {
@@ -341,6 +342,7 @@ quiz.timeLeftPercentage = ({ minutes, seconds }) => {
return percentage;
};
+// make sure the shorter bar is on top of the other
quiz.checkBarZindex = () => {
const scoreWidth = +quiz.scoreBar.style.width.slice(0, -1);
const timeWidth = +quiz.timeBar.style.width.slice(0, -1);
@@ -532,8 +534,6 @@ quiz.reset = () => {
module.map.once("idle", quiz.showInstructions);
};
-// add intro content to legend/article body?
-
// add a listener to the map that shows the intro popup when the map is idle
module.map.once("idle", () => {
setTimeout(quiz.showIntro, 500);
diff --git a/articles/greater-toronto-area-quiz/readme.md b/articles/greater-toronto-area-quiz/readme.md
new file mode 100644
index 0000000..d0d812b
--- /dev/null
+++ b/articles/greater-toronto-area-quiz/readme.md
@@ -0,0 +1,5 @@
+# The great Greater Toronto and Hamilton Area municipality quiz
+
+**Code and markup by Kyle Duncan**
+
+This "article" was an experiment to try different kinds of user interaction as well as a full screen mode, i.e. no article text. At present this is achieved by manually hiding the article window on load. Initially the guesses were typed into a form element, but in adapting to mobile this was converted to a prompt of a municipality name which the user than has to correctly click or tap.
diff --git a/articles/raptors-distance-traveled/article.js b/articles/raptors-distance-traveled/article.js
index a617a02..1f51da7 100644
--- a/articles/raptors-distance-traveled/article.js
+++ b/articles/raptors-distance-traveled/article.js
@@ -1,21 +1,13 @@
-// arena center
+// arena center coordinates
const arena = {
lng: -79.3790169712448,
lat: 43.64344921310201,
};
+// initialize object to store routes
const routes = {};
-function addRoutePoint(id) {
- const next = routes[id].coordinates.shift();
- routes[id].geometry.coordinates.push(next);
- module.map.getSource(`${id}-symbol`).setData({
- type: "Point",
- coordinates: next,
- });
- return routes[id].geometry;
-}
-
+// listen for article close to abandon animation
let abort = false;
window.addEventListener(
"flexWindowReset",
@@ -31,6 +23,18 @@ window.addEventListener(
{ once: true },
);
+// add new route point data and update map
+function addRoutePoint(id) {
+ const next = routes[id].coordinates.shift();
+ routes[id].geometry.coordinates.push(next);
+ module.map.getSource(`${id}-symbol`).setData({
+ type: "Point",
+ coordinates: next,
+ });
+ return routes[id].geometry;
+}
+
+// animate route by adding new point every frame
function animateRoute(id) {
if (routes[id].coordinates.length > 0 && !abort) {
module.map.getSource(id).setData(addRoutePoint(id));
@@ -38,6 +42,7 @@ function animateRoute(id) {
}
}
+// Create and show player popup
function showPopup(e) {
const id = e.features[0].layer.id.slice(0, -7);
const { destination, kilometers, player } = routes[id];
@@ -65,6 +70,7 @@ function showPopup(e) {
}
}
+// build player routes and add to map for animation
function mapRoutes() {
for (const player in routes) {
const { geometry, image } = routes[player];
@@ -126,6 +132,7 @@ function mapRoutes() {
}
}
+// caclucates and zooms to a bounding box from all route points
const bboxRaw = [[arena.lng, arena.lat]];
function zoomToBbox() {
let minLng, minLat, maxLng, maxLat;
@@ -157,6 +164,7 @@ function zoomToBbox() {
module.map.once("idle", mapRoutes);
}
+// match players distances to a POI and store in routes object
const destinations = [];
function findMatches(distances) {
destinations.sort((a, b) => {
@@ -197,6 +205,7 @@ function findMatches(distances) {
setTimeout(zoomToBbox, 1000);
}
+// load available player images
function addImages(data) {
data.forEach((player) => {
if (player.image && !module.map.hasImage(player.image)) {
@@ -211,6 +220,7 @@ function addImages(data) {
});
}
+// get updated distances data
function getDistances() {
fetch(
"https://media.geomodul.us/articles/raptors-distance-travelled/distances.json",
@@ -223,6 +233,7 @@ function getDistances() {
.catch((e) => console.log("error:", e));
}
+// add the arena graphic to the map
function addArena() {
// add arena location as source
module.addSource("scotiabank-arena", {
@@ -279,6 +290,7 @@ function addArena() {
});
}
+// load arena graphic
if (!module.map.hasImage("scotiabank-arena")) {
module.map.loadImage(
"https://media.geomodul.us/img/scotiabank-arena-md.png",
@@ -290,6 +302,7 @@ if (!module.map.hasImage("scotiabank-arena")) {
);
} else addArena();
+// load basketball graphic for missing player images
if (!module.map.hasImage("orange-bball")) {
module.map.loadImage(
"https://media.geomodul.us/img/raptor-heads/basketball-fill.png",
@@ -300,6 +313,7 @@ if (!module.map.hasImage("orange-bball")) {
);
}
+// load route data
fetch(
"https://media.geomodul.us/articles/raptors-distance-travelled/poi-routes.json",
)
diff --git a/articles/raptors-distance-traveled/readme.md b/articles/raptors-distance-traveled/readme.md
index a3383fa..77fdd89 100644
--- a/articles/raptors-distance-traveled/readme.md
+++ b/articles/raptors-distance-traveled/readme.md
@@ -1,4 +1,14 @@
-# How to update this article
+# How far did each Raptor actually travel versus the Rockets?
+
+**Code and markup by Kyle Duncan**
+
+This article takes a set of distances travelled by Raptors players in a particular NBA game, finds an exisitng 'Place of Interest' (POI) on the Torontoverse map, and then animates images of each player along a route from the Raptor's arena to that location comparable to the distance travelled.
+
+The article was designed to be updated for different games using the public [stats from the NBA](https://www.nba.com/stats/players/speed-distance?TeamID=1610612761&LastNGames=1&Location=Home) ("Dist. Miles" by player).
+
+The routes and distance to our POI collection were calculated and stored in a static file using the [mapbox directions API](https://docs.mapbox.com/api/navigation/directions/), being manually updated as new POI were added.
+
+## How to update the game/distances
1. Get distances from [NBA stats](https://www.nba.com/stats/players/speed-distance?TeamID=1610612761&LastNGames=1&Location=Home) ("Dist. Miles" by player)
@@ -10,12 +20,12 @@
---
-# How to update POI routes
+## How to update POI routes
1. Run the following code:
```
-const token = "pk.eyJ1IjoiZ2VvbW9kdWx1cyIsImEiOiJja2hubHI2eG0wMW1jMnJxcTk1OWVycjF1In0.kKg7JGdOkhKk_tiS-Wh-Rw";
+const token = "your-mapbox-token-here";
const destinations = [];
@@ -57,3 +67,7 @@ setTimeout(() => {
3. Update the "last updated" field below.
> Last Updated: 9 November 2022
+
+---
+
+_Original article written by Craig Battle_
diff --git a/articles/yonge-subway-north-extension/readme.md b/articles/yonge-subway-north-extension/readme.md
index 909dc7a..3fb4bf9 100644
--- a/articles/yonge-subway-north-extension/readme.md
+++ b/articles/yonge-subway-north-extension/readme.md
@@ -1,8 +1,11 @@
# North to Richmond Hill: Breaking down the 8-km Yonge subway extension
**Code and markup by Kyle Duncan**
+
This article uses Torontoverse's `SceneList` module to direct the user to particular elements of the map visualisation at key points in the article scroll.
+
It fetches geojson data for the existing and proposed sections of TTC Line 1, and also for two connected major bus and train lines, GO and Viva BRT, and adds those to the map. It programatically builds a legend, identifying the different lines, stops, transit hubs or portals, and grade levels of the proposed extension.
+
Finally, the particular scenes are created and connected to elements in the article that will be triggered using the [Scroll Magic](http://scrollmagic.io/) plugin.
---