Public
Edited
Apr 10, 2024
Insert cell
Insert cell
Insert cell
L = {
const L = await require("leaflet@1/dist/leaflet.js")
if (!L._style) {
const href = await require.resolve("leaflet@1/dist/leaflet.css")
document.head.appendChild(L._style = html`<link href=${href} rel=stylesheet>`)
}
return L
}
Insert cell
Insert cell
Insert cell
stations = FileAttachment("stations.geojson").json()
Insert cell
stations2011 = geo.filter(stations, d => d.StartYear === 2011)
Insert cell
stations2020 = geo.filter(stations, d => d.StartYear === 2020)
Insert cell
Insert cell
Insert cell
map = {
// You'll often see Leaflet examples initializing a map like L.map('map'),
// which tells the library to look for a div with the id 'map' on the page.
// In Observable, we instead create a div from scratch in this cell (called "map")
let container = DOM.element('div', {style: `width:${width}px;height:${width/1.6}px`})
// This component utilizes "yield" which pauses the execution of this code block
// returns the value of container back to the notebook which allows the
// div to be placed on the page. This is important, because Leaflet uses
// the div's .offsetWidth and .offsetHeight (used to get current size of the div)
// to size the map. If I were to only return the container at the end of this method,
// Leaflet might get the wrong idea about the map's size.
yield container
// Now we create a map object and add a layer to it.
let map = L.map(container).setView([37.4357395, -122.1378595], 12); // initializes the map, sets zoom & coordinates
let CartoDB_Positron = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
maxZoom: 20
}).addTo(map);
//This adds the tile layer to the map. The map dynamically loads tiles based on current view the experience smooth and seamless. Make sure to add attribution to your map.
}
Insert cell
Insert cell
Insert cell
circles = {
//define our map size
const height = 400
// This is a Observables specific convenience method which creates an SVG in the DOM.
const svg = d3.select(DOM.svg(width, height))
//projection this projects out points into space and fits the viewport based on
const projection = d3.geoMercator().fitExtent([[50,50], [width-50, height-50]], stations2020) ;

const label = svg
.append("text")
.attr("display", "none")
const points = svg.selectAll('circle')//Selects all circles (haven't been created yet, I know its confusing)
.data(stations2020.features) //loads the data we need - In this case our flat array of objects of features
.join('circle') //creates a circle for each element in the array
.attr("fill", "steelblue") // colors it blue
.attr("cx", d => projection(d.geometry.coordinates)[0]) // forEach element takes the coordinates and *projects*
.attr("cy", d => projection(d.geometry.coordinates)[1]) // then we index this array for the long(x) and lat(y)
.attr("r", 5) //radius of the circle
.attr("opacity", "0.5")
.on('mouseover', function(e, d) { //function to add mouseover event

const [xm, ym] = d3.pointer(e)
d3.select(this).transition() //D3 selects the object we have moused over in order to perform operations on it
.duration('150') //how long we are transitioning between the two states (works like keyframes)
.attr("fill", "red") //change the fill
.attr('r', 10) //change radius

const labelText = d.properties["Station Name"]

label
.attr("display", null)
.attr("font-size", 15)
.attr("font-weight", "bold")
.style("opacity", 1)
.attr("transform", `translate(${xm}, ${ym})`)
.attr("dx", 12)
.attr("dy", -9)
.text(d => labelText)
})
.on('mouseout', function() { //reverse the action based on when we mouse off the the circle
d3.select(this).transition()
.duration('150')
.attr("fill", "steelblue")
.attr('r', 5)

label
.attr("display", "none")
})
return svg.node();
}
Insert cell
Insert cell
Insert cell
overlayMap = {
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;

let map = L.map(container).setView([53.2527, -123.1207], 5).fitBounds(L.geoJson(stations).getBounds());
let CartoDB_Positron = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
maxZoom: 20
}).addTo(map);
const svg = d3.select(map.getPanes().overlayPane)
.append("svg")
.style("position", "relative")

const g = svg
.append("g")
.attr("class", "leaflet-zoom-hide")

const label = g
.append("text")
.attr("display", "none")
const Dots = g.selectAll('circle')
.attr("class", "Dots")
.data(stations2020.features)
.join('circle')
.attr("id", "dotties")
.attr("fill", "steelblue")
//Leaflet has to take control of projecting points. Here we are feeding the latitude and longitude coordinates to
//leaflet so that it can project them on the coordinates of the view. Notice, we have to reverse lat and lon.
//Finally, the returned conversion produces an x and y point. We have to select the the desired one using .x or .y
.attr("cx", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).x)
.attr("cy", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).y)
.attr("r", 5)
.attr("opacity", 0.5)
.on('mouseover', function(e, d) { //function to add mouseover event
const [xm, ym] = d3.pointer(e)
d3.select(this).transition() //D3 selects the object we have moused over in order to perform operations on it
.duration('150') //how long we are transitioning between the two states (works like keyframes)
.attr("fill", "red") //change the fill
.attr('r', 10) //change radius

const labelText = d.properties["Station Name"]

label
.attr("display", null)
.attr("font-size", 15)
.attr("font-weight", "bold")
.style("opacity", 1)
.attr("transform", `translate(${xm}, ${ym})`)
.attr("dx", 12)
.attr("dy", -9)
.text(d => labelText)
})
.on('mouseout', function() { //reverse the action based on when we mouse off the the circle
d3.select(this).transition()
.duration('150')
.attr("fill", "steelblue")
.attr('r', 5)

label
.attr("display", "none")
})
}
Insert cell
Insert cell
Insert cell
Insert cell
zoomMap = {
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;

let map = L.map(container).setView([53.2527, -123.1207], 5).fitBounds(L.geoJson(stations).getBounds());
let CartoDB_Positron = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
maxZoom: 20
}).addTo(map);

//initialize svg to add to map
L.svg().addTo(map)
const svg = d3.select(map.getPanes().overlayPane)
.select('svg')

const label = svg
.append("text")
.attr("display", "none")
const Dots = svg.selectAll('circle')
.attr("class", "Dots")
.data(stations2020.features)
.join('circle')
.attr("id", "dotties")
.attr("fill", "steelblue")
//Leaflet has to take control of projecting points. Here we are feeding the latitude and longitude coordinates to
//leaflet so that it can project them on the coordinates of the view. Notice, we have to reverse lat and lon.
//Finally, the returned conversion produces an x and y point. We have to select the the desired one using .x or .y
.attr("cx", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).x)
.attr("cy", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).y)
.attr("r", 5)
.attr("stroke", "black")
.attr("opacity", 0.5)
.on('mouseover', function(e, d) { //function to add mouseover event
const [xm, ym] = d3.pointer(e)
d3.select(this).transition() //D3 selects the object we have moused over in order to perform operations on it
.duration('150') //how long we are transitioning between the two states (works like keyframes)
.attr("fill", "red") //change the fill
.attr('r', 10) //change radius

const labelText = d.properties["Station Name"]

label
.attr("display", null)
.attr("font-size", 15)
.attr("font-weight", "bold")
.style("opacity", 1)
.attr("transform", `translate(${xm}, ${ym})`)
.attr("dx", 12)
.attr("dy", -9)
.text(d => labelText)
})
.on('mouseout', function() { //reverse the action based on when we mouse off the the circle
d3.select(this).transition()
.duration('150')
.attr("fill", "steelblue")
.attr('r', 5)

label
.attr("display", "none")
})
const update = () => Dots
.attr("cx", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).x)
.attr("cy", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).y)

map.on("zoomend", update)

}
Insert cell
Insert cell
Insert cell
interactionMap = {
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;

let map = L.map(container).setView([53.2527, -123.1207], 5).fitBounds(L.geoJson(stations).getBounds());
let osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

//initialize svg to add to map
L.svg({clickable: true}).addTo(map)
const svg = d3.select(map.getPanes().overlayPane)
.select('svg')
.attr("pointer-events", "auto")
const tooltip = svg.append("g").style("display", "none")
const path = tooltip.append("path")
const text = tooltip.append("text")
const Dots = svg.selectAll('circle')
.attr("class", "Dots")
.data(stations2020.features)
.join('circle')
.attr("id", "dotties")
.attr("fill", "yellow")
//Leaflet has to take control of projecting points. Here we are feeding the latitude and longitude coordinates to
//leaflet so that it can project them on the coordinates of the view. Notice, we have to reverse lat and lon.
//Finally, the returned conversion produces an x and y point. We have to select the the desired one using .x or .y
.attr("cx", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).x)
.attr("cy", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).y)
.attr("r", 5)
.attr("stroke", "black")
.attr("opacity", 0.8)
.on('mouseover', function(e, d) { //function to add mouseover event
const [xm, ym] = d3.pointer(e)
d3.select(this).transition() //D3 selects the object we have moused over in order to perform operations on it
.duration('150') //how long we are transitioning between the two states (works like keyframes)
.ease(d3.easeBounce)
.attr("fill", "red") //change the fill
.attr('r', 15) //change radius

// tooltip
tooltip
.transition()
.duration(200)
.style("display", null)
tooltip.attr("transform", `translate(${xm - 4},${ym + 3})`);
path.attr("fill", "white").attr("stroke", "black");
const textLabel = d.properties["Station Name"]
text
.text(textLabel)
.attr("font-size", 10)
size(text, path)
})
.on('mouseout', function() { //reverse the action based on when we mouse off the the circle
d3.select(this).transition()
.duration('150')
.ease(d3.easeBounce)
.attr("fill", "yellow")
.attr('r', 5)

tooltip
.style("display", "none")
})

// Wraps the text with a callout path of the correct size, as measured in the page.
function size(text, path) {
const {x, y, width: w, height: h} = text.node().getBBox();
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10}, 5H-5l5, -5l5, 5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
const update = () => Dots
.attr("cx", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).x)
.attr("cy", d => map.latLngToLayerPoint([d.geometry.coordinates[1],d.geometry.coordinates[0]]).y)

map.on("zoomend", update)

}
Insert cell
Insert cell
import {toc} from "@jonfroehlich/collapsible-toc"
Insert cell
import {textcolor} from "@observablehq/text-color-annotations-in-markdown"
Insert cell
geo = require("geotoolbox@2")
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