push demos for today
This commit is contained in:
parent
98114b9f7d
commit
1a7eec7baa
BIN
13.js-mapping/XYZ_Tiles.png
Normal file
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
188
13.js-mapping/d3.html
Normal 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>
|
96
13.js-mapping/leaflet.html
Normal file
96
13.js-mapping/leaflet.html
Normal 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
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
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
142
13.js-mapping/maplibre.html
Normal 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 © <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
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
176
13.js-mapping/slides.md
Normal 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
BIN
13.js-mapping/tiles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
Loading…
Reference in New Issue
Block a user