Public
Edited
Feb 4, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
let plt,
{ width, height } = s24;

plt = Plot.plot({
width,
margin: 0,
aspectRatio: 1.0,
x: { nice: true },
y: { nice: true },
r: { range: [0.5, 1.0] },
style: {
padding: 10,
color: "black",
background: `url(${await FileAttachment("s-2-4.png").url()})`,
backgroundSize: "cover"
},
color: { nice: true, legend: true, width: 600, scheme: "Dark2" },
marks: [
Plot.dot(station, {
x: "lng火星",
y: "lat火星",
fill: "铁路局",
stroke: "white",
strokeWidth: 0.1,
r: (d) => (d.srcCount > 0 ? Math.log(d.srcCount) : 0.5),
tip: true
})
]
});

return plt;
}
Insert cell
s24 = FileAttachment("s-2-4.png").image()
Insert cell
Insert cell
station.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
parsed_line.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Initialize the mapbox
{
map.on("load", () => {
// Init source
map.addSource("stationSource", {
type: "geojson",
data: stationGeoJson,
cluster: true,
clusterMaxZoom: 2, // Max zoom to cluster points on
clusterRadius: 30 // Radius of each cluster when clustering points (defaults to 50)
});

map.addLayer({
id: "clusters",
type: "circle",
source: "stationSource",
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",
100,
"#f1f075",
500,
"#f28cb1"
],
"circle-opacity": 0.5,
"circle-radius": ["step", ["get", "point_count"], 20, 100, 30, 750, 40]
}
});

map.addLayer({
id: "cluster-count",
type: "symbol",
source: "stationSource",
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: "stationSource",
filter: ["!", ["has", "point_count"]],
layout: {
"text-field": ["get", "站名"],
"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: "stationSource",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": ["get", "clAuth"],
"circle-radius": 4,
"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("stationSource")
.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();
});
}
{
// Set mapbox colorby, sizeby
const target = selectColor === "Province" ? "clProv" : "clAuth";

const radius =
selectSize === "src count"
? ["log2", ["+", ["get", "srcCount"], 2]]
: 4;

map.setPaintProperty("unclustered-point", "circle-color", [
"get",
target
]);
map.setPaintProperty("unclustered-point", "circle-radius", radius);
}
});
}
Insert cell
map = {
reloadButton;
const map = new mapboxgl.Map({
container,
center: cityCenter["beijing"],
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
import { SankeyChart } from "@d3/sankey"
Insert cell
sankeyData = {
const _pathProvProv = {},
_pathProvAuth = {},
_pathAuthProv = {},
_pathAuthAuth = {};

const pathProvProv = [],
pathProvAuth = [],
pathAuthProv = [],
pathAuthAuth = [];

const _idxMap = {},
idxMap = {};

function fill(path, a, b) {
path[a] = path[a] ? path[a] : {};
path[a][b] = path[a][b] ? path[a][b] : { count: 0 };
path[a][b].count += 1;

_idxMap[a] = 1;
}

function obj2arr(_path, path) {
for (let a in _path) {
for (let b in _path[a]) {
path.push({
source: "s" + idxMap[a] + a,
target: "d" + idxMap[b] + b,
value: _path[a][b].count
});
}
}
}

parsed_line.map((line) => {
const { src, srcProv, srcAuth, dst, dstProv, dstAuth } = line;

fill(_pathProvProv, srcProv, dstProv);
fill(_pathProvAuth, srcProv, dstAuth);
fill(_pathAuthProv, srcAuth, dstProv);
fill(_pathAuthAuth, srcAuth, dstAuth);
});

var cnt = 0;
for (let n in _idxMap) {
idxMap[n] = cnt;
cnt += 1;
}

obj2arr(_pathProvProv, pathProvProv);
obj2arr(_pathProvAuth, pathProvAuth);
obj2arr(_pathAuthProv, pathAuthProv);
obj2arr(_pathAuthAuth, pathAuthAuth);

return { pathProvProv, pathProvAuth, pathAuthProv, pathAuthAuth };
}
Insert cell
stationGeoJson = {
const geoJson = { type: "FeatureCollection" };
const features = [];

var properties, geometry, coordinates, clAuth, clProv;
station.map((station) => {
const { 站名, 铁路局, 性质, , , srcCount, WGS84_Lng, WGS84_Lat } =
station;

clAuth = summary.authority.filter((e) => e.name === 铁路局)[0].color;
clProv = summary.province.filter((e) => e.name === )[0].color;

coordinates = [WGS84_Lng, WGS84_Lat];
properties = { 站名, 铁路局, 性质, , , srcCount, clAuth, clProv };
geometry = { type: "Point", coordinates };
features.push({ type: "Feature", properties, geometry });
});
return { type: "FeatureCollection", features };
}
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/force-directed-graph
function ForceGraph(
{
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{source, target}, …])
},
{
nodeId = (d) => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeTitle, // given d in nodes, a title string
nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke = "#fff", // node stroke color
nodeStrokeWidth = 1.5, // node stroke width, in pixels
nodeStrokeOpacity = 1, // node stroke opacity
nodeRadius = 5, // node radius, in pixels
nodeStrength,
linkSource = ({ source }) => source, // given d in links, returns a node identifier string
linkTarget = ({ target }) => target, // given d in links, returns a node identifier string
linkStroke = "#999", // link stroke color
linkStrokeOpacity = 0.6, // link stroke opacity
linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap = "round", // link stroke linecap
linkStrength,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}
) {
// Compute values.
const customColor = nodes.map((e) => e.color);
const province = nodes.map((e) => e.province);
const radius = nodes.map((e) => 2 * Math.log2(e.count + 1));

const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W =
typeof linkStrokeWidth !== "function"
? null
: d3.map(links, linkStrokeWidth);
const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);

// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({ id: N[i] }));
links = d3.map(links, (_, i) => ({ source: LS[i], target: LT[i] }));

// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);

// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);

// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({ index: i }) => N[i]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);

const simulation = d3
.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", forceNode)
.force("center", d3.forceCenter())
// Force the data holds on the circle
.force("radius", d3.forceRadial(d3.min([width, height]) * 0.1, 0, 0))
.on("tick", ticked);

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

const textOpacity = 1.0;
const text = svg
.append("g")
.attr("fill", nodeFill)
.attr("fill-opacity", textOpacity)
// .attr("stroke", nodeStroke)
// .attr("stroke-opacity", textOpacity)
// .attr("stroke-width", 0.1)
.selectAll("circle")
.data(nodes)
.join("text")
.text((d, i) => (province[i] === "中国" ? d.id : ""))
.attr("style", "font-weight: bold")
.attr("fill", (d, i) => customColor[i]);

const link = svg
.append("g")
.attr("stroke", typeof linkStroke !== "function" ? linkStroke : null)
.attr("stroke-opacity", linkStrokeOpacity)
.attr(
"stroke-width",
typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null
)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line");

const node = svg
.append("g")
.attr("fill", nodeFill)
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", (d, i) => radius[i])
.call(drag(simulation));

if (W) link.attr("stroke-width", ({ index: i }) => W[i]);
if (L) link.attr("stroke", ({ index: i }) => L[i]);

if (G) {
node.attr("fill", ({ index: i }) => color(G[i]));

// Setup the node fill with the customColor
node.attr("fill", (d, i) => {
const cl = customColor[i];
return cl;
});
}

if (T) node.append("title").text(({ index: i }) => T[i]);

if (invalidation != null) invalidation.then(() => simulation.stop());

function intern(value) {
return value !== null && typeof value === "object"
? value.valueOf()
: value;
}

function ticked() {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);

node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
text.attr("x", (d, i) => d.x).attr("y", (d, i) => d.y);
}

function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}

return Object.assign(svg.node(), { scales: { color } });
}
Insert cell
forceGraphData = {
const { authority, province } = summary;

const links = [];

// One node for a city,
// it links to the authority
const nodes = uniqueStation.map((station) => {
const { 站名, 铁路局, , count } = station;

links.push({
source: 站名,
target: 铁路局,
value: 1
});

return {
name: 站名,
province: ,
count,
color: province.filter((e) => e.name === )[0].color
};
});

// Links the authorities
{
nodes.push({ name: "中国", province: "root", color: "#000000" });
authority.map((auth) => {
nodes.push({
name: auth.name,
province: "中国",
color: "#000000",
count: 1
});
const { name } = auth;
links.push({ source: name, target: "中国", value: 1 });

// authority
// .filter((e) => e.name !== auth.name)
// .map((auth2) => {
// links.push({
// source: auth.name,
// target: auth2.name,
// value: 3
// });
// });
});
}

if (false) {
authority.map((auth) => {
nodes.push({ name: auth.name, province: "中国", color: "#000000" });

authority
.filter((e) => e.name !== auth.name)
.map((auth2) => {
links.push({
source: auth.name,
target: auth2.name,
value: 3
});
});
});
}

return { nodes, links };
}
Insert cell
// One node for one city
uniqueStation = {
const hist = {};
const uniqueStation = [];

var uq;
station.map((station) => {
const { , } = station;
uq = + ;

if (uq in hist) {
hist[uq] += 1;
return;
}

hist[uq] = 1;
uniqueStation.push(station);
});

uniqueStation.map((e) => {
const { , } = e;
uq = + ;
return (e.count = hist[uq]);
});

return uniqueStation;
}
Insert cell
summary = {
const authority = {};
const province = {};

station.map((station) => {
const { 铁路局, } = station;
authority[铁路局] = authority[铁路局] ? authority[铁路局] : 0;
authority[铁路局] += 1;

province[] = province[] ? province[] : 0;
province[] += 1;
});

var n, scale;

const dct2arr = (src) => {
const output = [];
for (const name in src) {
output.push({ name, count: src[name] });
}
output.sort((a, b) => b.count - a.count);
n = output.length;
scale = d3.scaleLinear().range([20, 300]).domain([0, n]);
output.map((e, i) => {
e.color = d3.hsl(scale(i), 0.5, 0.5).hex();
});
return output;
};

const _authority = dct2arr(authority);
const _province = dct2arr(province);

return { authority: _authority, province: _province };
}
Insert cell
stationSrc
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
stationSrc = station
.filter((e) => e.srcCount > 0)
.sort((a, b) => b.srcCount - a.srcCount)
Insert cell
coordtransform = require("coordtransform")
Insert cell
d3 = require("d3")
Insert cell
// import { ForceGraph } from "@d3/force-directed-graph"
Insert cell
import { ToC } from "@rmw4269/contents"
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