Public
Edited
Nov 1, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
map.setZoom(setZoom);
map.setCenter(cities.filter((e) => e.name === selectCity)[0].centroid);
return { setZoom, selectCity };
}
Insert cell
// Initialize the mapbox
{
map.on("load", () => {
// Init source
map.addSource("subwayStationSource", {
type: "geojson",
data: subwayGeoJson.station,
cluster: true,
clusterMaxZoom: 9, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});

map.addSource("subwayLineSource", {
type: "geojson",
lineMetrics: true,
data: subwayGeoJson.line
});

// the layer must be of type 'line'
map.addLayer({
type: "line",
source: "subwayLineSource",
id: "subwayLine",
paint: {
"line-color": ["get", "cl"],
"line-width": 2
},
layout: {
"line-cap": "round",
"line-join": "round"
}
});

map.addLayer({
id: "clusters",
type: "circle",
source: "subwayStationSource",
filter: ["has", "point_count"],
paint: {
// Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
"circle-color": [
"step",
["get", "point_count"],
"#51bbd6",
5,
"#f1f075",
10,
"#f28cb1"
],
"circle-opacity": 0.5,
"circle-radius": ["step", ["get", "point_count"], 20, 100, 30, 750, 40]
}
});

map.addLayer({
id: "cluster-count",
type: "symbol",
source: "subwayStationSource",
filter: ["has", "point_count"],
layout: {
"text-field": "{point_count_abbreviated}",
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 12
},
paint: {
"text-opacity": 0.5
}
});

map.addLayer({
id: "station-symbol",
type: "symbol",
source: "subwayStationSource",
filter: ["!", ["has", "point_count"]],
layout: {
"text-field": ["get", "n"],
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 12,
"text-offset": [0, 1]
},
paint: {
"text-color": "#aaaaaa"
}
});

map.addLayer({
id: "unclustered-point",
type: "circle",
source: "subwayStationSource",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": ["get", "cl"],
"circle-radius": ["*", ["get", "nLoad"], 0.05], //4,
"circle-opacity": 0.5,
"circle-stroke-width": 1,
"circle-stroke-color": "#fff"
}
});

// inspect a cluster on click
map.on("click", "clusters", (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ["clusters"]
});
const clusterId = features[0].properties.cluster_id;
map
.getSource("subwayStationSource")
.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;

map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
});
});

// When a click event occurs on a feature in
// the unclustered-point layer, open a popup at
// the location of the feature, with
// description HTML from its properties.
map.on("click", "unclustered-point", (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const mag = e.features[0].properties.mag;
const tsunami = e.features[0].properties.tsunami === 1 ? "yes" : "no";

// Ensure that if the map is zoomed out such that
// multiple copies of the feature are visible, the
// popup appears over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}

new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`magnitude: ${mag}<br>Was there a tsunami?: ${tsunami}`)
.addTo(map);
});

map.on("mouseenter", "clusters", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "clusters", () => {
map.getCanvas().style.cursor = "";
});

// Add handlers
{
map.on("dragend", () => {
console.log("dragend");
updateMapboxParamDiv();
});

map.on("zoomend", () => {
console.log("zoomend");
updateMapboxParamDiv();
});

map.on("pitchend", () => {
console.log("pitchend");
updateMapboxParamDiv();
});
}
});
}
Insert cell
map = {
reloadButton;
const map = new mapboxgl.Map({
container,
center: cities[0].centroid,
zoom: 10,
maxPitch: 80,
// style: "mapbox://styles/listenzcc/cky2j3ywf13yi15nuybjj4kem"
style: "mapbox://styles/mapbox/light-v9"
});
return map;
}
Insert cell
aspect = 9 / 16
Insert cell
height = width * aspect
Insert cell
mapboxgl = {
const gl = await require("mapbox-gl");
if (!gl.accessToken) {
gl.accessToken =
"pk.eyJ1IjoibGlzdGVuemNjIiwiYSI6ImNrMzU5MmpxZDAxMXEzbXQ0dnd4YTZ2NDAifQ.GohcgYXFsbDqfsi_7SXdpA";
const href = await require.resolve("mapbox-gl/dist/mapbox-gl.css");
document.head.appendChild(html`<link href=${href} rel=stylesheet>`);
}
return gl;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Inputs.table(subwayGeoJson.line.features.map((e) => e.properties))
Insert cell
subwayGeoJson = {
const type = "FeatureCollection";
var stationFeatures = [],
lineFeatures = [];

cities.map((city) => {
const { stGeoJson, lineGeoJson } = city.geoJson;
stationFeatures = stationFeatures.concat(stGeoJson.features);
lineFeatures = lineFeatures.concat(lineGeoJson.features);
});

return {
station: { type, features: stationFeatures },
line: { type, features: lineFeatures }
};
}
Insert cell
Insert cell
Plot.plot({
grid: true,
height: 400,
color: { type: "categorical", range: colorsHist },
y: { label: "Count" },
facet: {
data: travelStations,
y: "city",
marginRight: 90
},
marks: [
Plot.rectY(
travelStations,
Plot.binX(
{ y: "count" },
{ x: { value: "stations" }, y: "y", fill: "city" }
)
)
]
})
Insert cell
travelStations = {
const cs = cities.map((city) => {
const { simulation, name } = city;
return simulation.traces.map((t) => {
return { x: 0, stations: t.length, city: name };
});
});

var cc = [];
cs.map((d) => {
cc = cc.concat(d);
});

return cc;
}
Insert cell
Insert cell
Plot.plot({
grid: true,
height: 400,
color: { type: "categorical", range: colorsHist },
y: { label: "Count" },
facet: {
data: stationLoads,
y: "city",
marginRight: 90
},
marks: [
Plot.rectY(
stationLoads,
Plot.binX({ y: "count" }, { x: { value: "load" }, y: "y", fill: "city" })
)
]
})
Insert cell
stationLoads.filter((e) => e.city === "Tianjin")
Insert cell
stationLoads = {
const cs = cities.map((city) => {
const { simulation, name } = city;
const { stationsLoad } = simulation;
const loads = [];
for (const st in stationsLoad) {
loads.push([stationsLoad[st], st]);
}

return loads.map((ld) => {
return { x: 0, load: ld[0], st: ld[1], city: name };
});
});

var cc = [];
cs.map((d) => {
cc = cc.concat(d);
});

return cc;
}
Insert cell
cities = {
const num = 1000;
const group = [
{ sb: subwayBeijing, name: "Beijing" },
{ sb: subwayShanghai, name: "Shanghai" },
{ sb: subwayTianjin, name: "Tianjin" }
];

const data = group.map((d) => {
const { sb, name } = d;
const simulation = simulate(sb, num);
const numStations = simulation.stations.length;
const { geoJson } = simulation;
const centroid = d3.geoCentroid(geoJson.stGeoJson);
return { sb, name, simulation, geoJson, centroid, numStations };
});

return data;
}
Insert cell
colorsHist = cities.map((d, i) => colorScaleHist(i))
Insert cell
colorScaleHist = d3
.scaleSequential(d3.interpolateCool)
.domain(d3.extent(d3.range(cities.length)))
Insert cell
simulation = simulate(subwayBeijing, 1000)
Insert cell
simulate = (subway, num) => {
const geoJson = mkSubwayGeoJson(subway);
const { links } = geoJson;
const stations = [];
const stationsLoad = {};
const traces = [];

for (const st in links) {
stations.push(st);
stationsLoad[st] = 0;
}

var trace;
for (let i = 0; i < num; ++i) {
d3.shuffle(stations);
trace = dijkstra.find_path(links, stations[0], stations[1]);
trace.map((n) => (stationsLoad[n] += 1));
traces.push(trace);
}

geoJson.stGeoJson.features.map((d) => {
const { n } = d.properties;
d.properties["nLoad"] = stationsLoad[n];
});

return { links, stations, stationsLoad, traces, geoJson };
}
Insert cell
mkSubwayGeoJson = (subway) => {
const stGeoJson = { type: "FeatureCollection", features: [] };
const lineGeoJson = { type: "FeatureCollection", features: [] };
const links = {};

const { s } = subway;

var properties,
geometry,
firstSt,
latestSt,
lineFeatures = -1;
var pair, corrected, exists;

const pushLineFeatures = () => {
// If lineFeatures is not -1, it means we already have the lineFeatures
// So we push it, before init a new one
if (lineFeatures !== -1) {
// If the line is loop-line, connect the head with the tail.
if (lineFeatures.properties.lo === "1") {
lineFeatures.geometry.coordinates.push(
lineFeatures.geometry.coordinates[0]
);
}
lineGeoJson.features.push(lineFeatures);
}
};

// For each line
subway.l.map((line) => {
pushLineFeatures();

// Init new line
const { ln, cl, kn, st, lo } = line;
const numStm1 = st.length - 1;

lineFeatures = {
type: "Feature",
properties: { ln, cl: "#" + cl, kn, st, lo, s },
geometry: { coordinates: [], type: "LineString" }
};

// For each station
st.map((station, i) => {
const { n, sl } = station;

// Link the station with the previous one
// Remember the first and the last one,
// and link them with the next station,
// if lo is "1", the start and end stations are linked either
{
// Remember the first st.
if (i === 0) {
firstSt = n;
}

// Link this one with the latest
if (i > 0) {
links[latestSt] = links[latestSt] ? links[latestSt] : {};
links[latestSt][n] = 1;
links[n] = links[n] ? links[n] : {};
links[n][latestSt] = 1;
}

// Link the loop condition
if (lo === "1" && i === numStm1) {
links[firstSt] = links[firstSt] ? links[firstSt] : {};
links[firstSt][n] = 1;
links[n] = links[n] ? links[n] : {};
links[n][firstSt] = 1;
}

// Always remember this one as the latest st.
latestSt = n;
}

pair = sl.split(",").map((e) => parseFloat(e));
corrected = coordtransform.gcj02towgs84(pair[0], pair[1]);

lineFeatures.geometry.coordinates.push(corrected);

// If the station exists, attach it
exists = stGeoJson.features.filter((e) => e.properties.n == n);
if (exists.length > 0) {
exists[0].properties.ln += ", " + ln;
exists[0].properties.kn += ", " + kn;
return;
}

// Add the new station
properties = {
ln,
cl: "#" + cl,
kn,
n,
s,
sl,
corrected
};

geometry = {
type: "Point",
coordinates: corrected
};

stGeoJson.features.push({ type: "Feature", properties, geometry });
});
});

// Push the latest line
pushLineFeatures();

return { stGeoJson, lineGeoJson, links };
}
Insert cell
subwayTianjin = FileAttachment("subway-tianjin.json").json()
Insert cell
subwayShanghai = FileAttachment("subway-shanghai.json").json()
Insert cell
subwayBeijing = FileAttachment("subway.json").json()
Insert cell
coordtransform = require("coordtransform")
Insert cell
d3 = require("d3")
Insert cell
dijkstra = require("https://bundle.run/dijkstrajs@1.0.2")
Insert cell
import { Histogram } from "@d3/histogram"
Insert cell
import { Plot } from "@observablehq/plot"
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