push demos for today

This commit is contained in:
James Turk 2024-11-19 13:40:06 -06:00
parent 98114b9f7d
commit 1a7eec7baa
9 changed files with 757 additions and 0 deletions

BIN
13.js-mapping/XYZ_Tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

188
13.js-mapping/d3.html Normal file
View File

@ -0,0 +1,188 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
/* style for tooltip as well as the base map container */
.tooltip {
position: absolute;
padding: 8px;
background: white;
border: 1px solid #333333;
border-radius: 4px;
pointer-events: none;
font-family: sans-serif;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container {
max-width: 1000px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="container">
<div id="map"></div>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/topojson@3"></script>
<script>
const euMemberStates = {
Belgium: 1,
France: 1,
Germany: 1,
Italy: 1,
Luxembourg: 1,
Netherlands: 1,
Denmark: 2,
Ireland: 2,
Greece: 3,
Portugal: 4,
Spain: 4,
Austria: 5,
Finland: 5,
Sweden: 5,
Cyprus: 6,
"Czech Republic": 6,
Estonia: 6,
Hungary: 6,
Latvia: 6,
Lithuania: 6,
Malta: 6,
Poland: 6,
Slovakia: 6,
Slovenia: 6,
Bulgaria: 7,
Romania: 7,
Croatia: 8,
};
const width = 800;
const height = 600;
const svg = d3
.select("#map")
.append("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);
// d3 tooltip pattern: create a placeholder div which will be
// hidden at first & revealed/moved as needed
const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// color function used for choropleth
const colorScale = d3
.scaleSequential()
.domain([8, 1]) // Reversed domain so earlier members are darker
.interpolator(d3.interpolateViridis);
// set up projection centered on Europe
const projection = d3
// DEMO: change projection
// https://d3js.org/d3-geo/conic
// https://github.com/d3/d3-geo-projection
.geoMercator()
//.geoConicEqualArea()
.center([15, 54])
.scale(600)
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
// load TopoJSON
d3.json(
// DEMO change to 110m/10m
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json",
).then((data) => {
// using topojson 3rd party library to convert to GeoJSON which D3 understands
const geojson = topojson.feature(data, data.objects.countries);
// entire map render happens after data is loaded, in the then() function
const europeCountries = geojson.features.filter(
(d) => euMemberStates[d.properties.name] !== undefined,
);
svg
.selectAll("path")
.data(europeCountries)
.join("path")
.attr("d", path)
.attr("fill", (d) => {
const score = euMemberStates[d.properties.name];
return score ? colorScale(score) : "#ccc";
})
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.on("mouseover", function (event, d) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip
.html(
`
<strong>${d.properties.name}</strong><br/>
Wave: ${euMemberStates[d.properties.name]}
`,
)
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(500).style("opacity", 0);
});
// example of a legend, not related to map topics
const legendWidth = 200;
const legendHeight = 20;
const legendScale = d3
.scaleLinear()
.domain([1, 8])
.range([0, legendWidth]);
const legendAxis = d3
.axisBottom(legendScale)
.ticks(8)
.tickFormat((d) => d);
const legend = svg
.append("g")
.attr(
"transform",
`translate(${width - legendWidth - 20}, ${height - 50})`,
);
const defs = svg.append("defs");
const gradient = defs
.append("linearGradient")
.attr("id", "legend-gradient")
.attr("x1", "0%")
.attr("x2", "100%")
.attr("y1", "0%")
.attr("y2", "0%");
// Add gradient stops
const stops = d3.range(0, 8);
stops.forEach((stop, i) => {
gradient
.append("stop")
.attr("offset", `${(i / (stops.length - 1)) * 100}%`)
.attr("stop-color", colorScale(stop));
});
legend
.append("rect")
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#legend-gradient)");
legend
.append("g")
.attr("transform", `translate(0, ${legendHeight})`)
.call(legendAxis);
});
</script>
</body>
</html>

View File

@ -0,0 +1,96 @@
<!--
See Also:
https://leafletjs.com/examples/quick-start/
https://leafletjs.com/examples.html
-->
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
/>
<style>
#map-goes-here {
height: 500px;
width: 100%;
}
#info {
margin: 10px 0;
padding: 10px;
border: 1px solid #ccc;
background: #f9f9f9;
}
#controls {
margin: 10px 0;
}
</style>
</head>
<body>
<div id="map-goes-here"></div>
<div id="info">placeholder</div>
<div id="controls">
<button id="zoom-fit">Fit All Points</button>
<button id="zoom-us">Fit US</button>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const chicago = [41.8781, -87.6298];
const initialZoom = 12;
// Leaflet makes a global variable "L" available.
// this creates a map in the div above
const map = L.map("map-goes-here").setView(chicago, initialZoom);
// OSM base layer
// DEMO change of base layer
const watercolorURL =
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg";
const osmURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
L.tileLayer(osmURL, {
maxZoom: 19,
attribution: "© OpenStreetMap contributors",
}).addTo(map);
// points could be loaded from GeoJSON or any other source
// usage (below) does not depend on format
const points = [
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
];
// Add points to the map
const markers = [];
points.forEach((point) => {
const marker = L.marker([point.lat, point.lng]).addTo(map);
// adds a popup to the marker, can use HTML
marker.bindPopup(`<strong>${point.name}</strong>`);
// add custom events to markers in typical JS fashion
marker.on("click", function () {
const infoDiv = document.getElementById("info");
infoDiv.textContent = `Selected Location: ${point.name}`;
});
markers.push(marker);
});
// example of binding button events to modify the map
document.getElementById("zoom-fit").addEventListener("click", () => {
const groupBounds = new L.featureGroup(markers).getBounds();
map.fitBounds(groupBounds);
});
document.getElementById("zoom-us").addEventListener("click", () => {
const usBounds = [
[24.396308, -125.0], // Southwest corner (latitude, longitude)
[49.384358, -66.93457], // Northeast corner (latitude, longitude)
];
map.fitBounds(usBounds);
});
</script>
</body>
</html>

BIN
13.js-mapping/map1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
13.js-mapping/map2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

142
13.js-mapping/maplibre.html Normal file
View File

@ -0,0 +1,142 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- maplibre needs custom CSS loaded to style its maps and controls -->
<link
href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css"
rel="stylesheet"
/>
<style>
#map-goes-here {
height: 500px;
width: 100%;
}
#info {
margin: 10px 0;
padding: 10px;
border: 1px solid #ccc;
background: #f9f9f9;
}
#controls {
margin: 10px 0;
}
</style>
</head>
<body>
<div id="map-goes-here"></div>
<div id="info">placeholder</div>
<div id="controls">
<button id="zoom-fit">Fit All Points</button>
<button id="zoom-us">Fit US</button>
</div>
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
<script>
const watercolor = {
version: 8,
sources: {
"raster-tiles": {
type: "raster",
tiles: [
// NOTE: Layers from Stadia Maps do not require an API key for localhost development or most production
// web deployments. See https://docs.stadiamaps.com/authentication/ for details.
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
],
tileSize: 256,
attribution:
'Map tiles by <a target="_blank" href="https://stamen.com">Stamen Design</a>; Hosting by <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>. Data &copy; <a href="https://www.openstreetmap.org/about" target="_blank">OpenStreetMap</a> contributors',
},
},
layers: [
{
id: "simple-tiles",
type: "raster",
source: "raster-tiles",
minzoom: 0,
maxzoom: 22,
},
],
};
const tileJSON =
"https://tiles.stadiamaps.com/styles/alidade_smooth.json";
const map = new maplibregl.Map({
container: "map-goes-here",
center: [-87.6298, 41.8781],
zoom: 12,
style: tileJSON,
// DEMO toggle style
style: watercolor,
});
const points = [
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
];
// some features expect GeoJSON in the format shown here.
// in practice we may load from an API or source file
// if possible, it is OK to embed data directly like this
// (Outside of this example I would probably move this to a function for reuse.)
const geojson = {
type: "FeatureCollection",
features: points.map((point) => ({
type: "Feature",
// this should be one of the OGR geometries (Point/Line/Polygon/etc.)
geometry: {
type: "Point",
coordinates: [point.lng, point.lat],
},
// can put arbitrary data here in properties
properties: {
name: point.name,
},
})),
};
// looping over the GeoJSON, we add a marker & popup for each
// point in the data. the parameter here is a GeoJSON object
geojson.features.forEach((feature) => {
// these variables are just extracting data from the format seen above
// which is GeoJSON's representation of a geometry
const coordinates = feature.geometry.coordinates;
const name = feature.properties.name;
// add a marker for each point
const marker = new maplibregl.Marker()
.setLngLat(coordinates)
.addTo(map);
// add a popup with the point name
const popup = new maplibregl.Popup().setText(name);
marker.setPopup(popup);
// bind event listener to marker (identical to leaflet)
marker.getElement().addEventListener("click", () => {
const infoDiv = document.getElementById("info");
infoDiv.textContent = `Selected Location: ${name}`;
});
});
// fit all points -- a bit more complicated than Leaflet's version
document.getElementById("zoom-fit").addEventListener("click", () => {
const bounds = geojson.features.reduce((bounds, feature) => {
return bounds.extend(feature.geometry.coordinates);
}, new maplibregl.LngLatBounds());
map.fitBounds(bounds, { padding: 20 });
});
// fit entire US
document.getElementById("zoom-us").addEventListener("click", () => {
const usBounds = [
[-125.0, 24.396308], // Southwest corner [lng, lat]
[-66.93457, 49.384358], // Northeast corner [lng, lat]
];
map.fitBounds(usBounds, { padding: 20 });
});
</script>
</body>
</html>

155
13.js-mapping/slides.html Normal file

File diff suppressed because one or more lines are too long

176
13.js-mapping/slides.md Normal file
View File

@ -0,0 +1,176 @@
# Mapping in JavaScript
---
## Today
- Discuss types of maps & appropriate library choices.
- Explore examples in D3, Leaflet, and MapLibreGL.
---
## Which Tool?
**What kind of map?**
![bg left fit](map1.png)
Abstract/geometric representation of data.
D3 will provide the most flexibility & can use existing tools for color scales/interactivity/etc.
---
## Map w/ Cartographic Features
![bg right fit](map2.png)
A different library will be necessary. Depending on needs **raster** or **vector** tiles.
---
## Choropleths in D3
<https://d3-graph-gallery.com/graph/choropleth_basic.html>
### Projection
<https://d3-graph-gallery.com/graph/backgroundmap_changeprojection.html>
---
## Leaflet and MapLibreGL Choropleths
- Projection choice constrained by tile layers.
- Need additional libraries (or pure JS) for interactivity & other graphics.
Demo/Tutorial: <https://leafletjs.com/examples/choropleth/>
---
## Base Maps
Instead of starting with a blank slate, it can be helpful to have a base map, but a base map is often comprised of many feature layers:
- Country/Ocean borders
- National Sub-divisions
- Roads
- Terrain
- Points of Interest
- ...
Drawing all of these layers adds up.
---
## Raster Tiles
An innovation that made web mapping scalable in the mid-2000s was pre-rendering base layers into image tiles.
![](XYZ_Tiles.png)
---
## Raster Tiles
### Pros
- Simple to serve: just a lot of images.
- Each image at a zoom level ~approx same size.
- 100% consistent rendering.
- Any kind of imagery (watercolor, satellite maps, etc.)
- Minimal client-side processing needed.
- Good for dense data.
---
### Cons
- Fixed zoom levels and projection.
- Large file sizes for large Z.
- Need to create variants for all desired permutations of features.
- No modifications to styling after the fact.
---
## Vector Tiles
Instead of images, data is sent as images.
- Can create multiple variations per JSON, change road colors/etc without regenerating millions of images.
- Can be more efficient on bandwidth.
- Significantly more complexity on client and server side.
- Older devices may struggle to render them.
- Rendering relies on client library, can vary.
- Not suitable for dense data such as satellite imagery.
Blog Post from *today* on OSM switch to Vector tiles: <https://tech.marksblogg.com/osm-mvt-vector-tiles.html>
---
## Leaflet
**Raster Tiles w/ Vector Layer on Top**
<https://leafletjs.com/reference.html>
(Has plugins to use Vector Tiles via MapLibreGL)
---
## MapLibreGL
Vector-tile based library. Community fork of MapboxGL.
Mapbox is an innovator in the space, but also quite expensive.
MaplibreGL/MapboxGL are mostly compatible.
---
## General Workflow
- Use library to render map(s) to div(s) on page.
- Pick base layer, either raster or vector.
- If vector, possibly apply additional styling to base.
- Add additional vector layers based on your data & its features.
- Attach event handlers for interactivity on your features and/or input elements adjacent to the map.
---
## Pop-ups
Leaflet/MapLibre offer a simple API for adding pop-ups to features.
Reminder: hover/pop-up alone is **not enough interactivity** for final product.
---
## Examples
Leaflet & MapLibre examples
---
## Custom Base Layers
- [Stadia Maps Hosted Tiles](https://stadiamaps.com/products/map-tiles/), free for noncommercial/academic use. You will need an account though.
- [OpenMapTiles](https://openmaptiles.org/styles/) - free, meant for self hosting.
- [Mapbox](https://www.mapbox.com) - another paid solution, $$$.
---
## Other Libraries
- [Deck.gl](https://deck.gl) - Alternative renderer similar to Map*GL. TypeScript/React focused mostly, hard to use with JS we covered.
- [OpenLayers](https://openlayers.org) -
- [Cesium](https://cesium.com) - 3D-focused geospatial.
- Note: You can also get 3D rendering w/ D3 or MapLibreGL.
---
## More Examples
- <https://projects.fivethirtyeight.com/redistricting-maps/alabama/#Competitive>
- <https://projects.fivethirtyeight.com/partisan-gerrymandering-north-carolina/>

BIN
13.js-mapping/tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB