Public
Edited
Apr 15, 2023
1 star
Insert cell
Insert cell
metroStaions = {
const gridSize = 8;
const textSize = 10;
const customWidth = 800; // Use a multiple of gridSize for best results.
const customHeight = 512; // Use a multiple of gridSize for best results.
const arrWidth = customWidth / gridSize;
const arrHeight = customHeight / gridSize;
const gridVector = new Victor(gridSize, gridSize);
const paddingGrids = 32;
const lineAvoidance = 4;
const lineSelfAvoidance = -2; // Make it cheaper for a line to double back on itself.
const stationAvoidance = 3;
const stationRadius = 2;
const octolinearPathing = false;
const useOrderedLinks = false;
const useStringPulling = true;
const stringPullingLookahead = 16;
const stringPullingShortcutThreshold = 0;
const drawLines = true;
const drawStations = true;
const drawStationNames = false;
// const svg = d3.create('svg').attr('viewBox', [0, 0, customWidth, customHeight]);

//tooltip hover
const container = d3.select(
html`<div style="position:relative;">${tooltipTemplate}</div>`
);
const tooltipDiv = container.select(".tooltip");
const svg = container.append("svg").attr('viewBox', [0, 0, customWidth, customHeight]);
// const svg2 = container.append("svg").attr("viewBox", `0 0 ${width} ${height}`);
// Create links / nodes from simulationData.
const nodes = simulationData.stations.map(d => Object.create(d));
var linkPairs = [];
for (let lineIndex = 0; lineIndex < simulationData.lines.length; lineIndex++) {
for (let start = 0; start < simulationData.lines[lineIndex].stations.length - 1; start++) {
if (useOrderedLinks) {
// Only create links between stations in the order they are listed.
linkPairs.push({
source: simulationData.lines[lineIndex].stations[start],
target: simulationData.lines[lineIndex].stations[start + 1]
});
} else {
// Create links between every station in every line.
for (let end = start; end < simulationData.lines[lineIndex].stations.length; end++) {
linkPairs.push({
source: simulationData.lines[lineIndex].stations[start],
target: simulationData.lines[lineIndex].stations[end]
});
}
}
}
}
const links = linkPairs.map(d => Object.create(d));
// Run a simulation until it settles.
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody());
simulation.restart();
for (let i = 0; i < 1000; i++) {
simulation.tick();
if (simulation.alpha() < 0.005) {
break;
}
}
simulation.stop();
// Create a bounding box around the resulting nodes.
var top = Number.MAX_VALUE;
var bottom = 0;
var left = Number.MAX_VALUE;
var right = 0;
for (var i = 0; i < nodes.length; i++) {
var d = nodes[i];
if (d.y > bottom) bottom = Math.round(d.y);
if (d.y < top) top = Math.round(d.y);
if (d.x > right) right = Math.round(d.x);
if (d.x < left) left = Math.round(d.x);
}
top -= paddingGrids;
bottom += paddingGrids;
left -= paddingGrids;
right += paddingGrids;
// Create a cost array.
const arr = new Array2D(arrWidth, arrHeight, 1);
arr.defaultValue = 1000;
// Place each station within the cost array.
for (var i = 0; i < nodes.length; i++) {
var d = nodes[i];
let index = simulationData.stations.findIndex(function(station) {return station.id == d.id});
let x = Math.round((d.x - left) / (right - left) * arrWidth);
let y = Math.round((d.y - top) / (bottom - top) * arrHeight);
simulationData.stations[index].position = new Victor(x, y);
arr.incrementBox(simulationData.stations[index].position, stationRadius, stationAvoidance, true);
}
// This is the expensive part. Generate curves to define each line.
for (var i = 0; i < simulationData.lines.length; i++) {
var line = simulationData.lines[i];
var stationsOnLine = [];
for (var j = 0; j < line.stations.length; j++) {
var station = simulationData.stations.find(function(station) { return station.id == line.stations[j] });
stationsOnLine.push(station);
}
var antColony = new AntColonyOptimization(stationsOnLine)
var lineSegments = antColony.simulateColony().path;
// Pathfind for each of the paths in this line.
const curve = d3.line().curve(d3.curveBundle.beta(1))
var allPointsOnLine = [];
for (var j = 0; j < lineSegments.length; j++) {
let start = simulationData.stations.find(function(station) { return station.id == lineSegments[j].source });
let end = simulationData.stations.find(function(station) { return station.id == lineSegments[j].target });
try {
let pointsOnSegment = arr.pathfind(start.position, end.position, octolinearPathing);
// Optimize the path a little bit before continuing.
var optimizedPoints = pointsOnSegment;
if (useStringPulling) {
optimizedPoints = arr.stringPull(pointsOnSegment, stringPullingLookahead, stringPullingShortcutThreshold);
}
arr.incrementList(optimizedPoints, lineSelfAvoidance, true);
for (let i = 0; i < optimizedPoints.length; i++) {
allPointsOnLine.push(optimizedPoints[i].clone());
optimizedPoints[i] = optimizedPoints[i].multiply(gridVector).toArray();
}
// Go ahead and draw the curve while we're looping through the line.
if (drawLines) {
svg
.append('path')
.attr('d', curve(optimizedPoints))
.attr('stroke', line.color)
.attr('stroke-width', 6)
.attr('fill', 'none')
.attr('stroke-linecap', 'round');
}
} catch(e) {
console.warn("Pathing failed between", start.name, "and", end.name);
}
}
arr.incrementList(allPointsOnLine, lineAvoidance, true);
}
// Draw all of the stations and station names.
for (var i = 0; i < simulationData.stations.length; i++) {
var station = simulationData.stations[i];

if (drawStations) {
svg.append('circle')
.attr('fill', '#ffffff')
.attr('stroke', '#2c2c2c')
.attr('r', 8)
.attr('stroke-width', 4)
.attr('cx', station.position.x * gridVector.x)
.attr('cy', station.position.y * gridVector.y)
.data([station])
.call(tooltip, tooltipDiv);

// svg2.append("g")
// .attr("transform", `translate(${margin.left}, ${margin.top})`)
// .selectAll("circle")
// .data(simulationData)
// .join("circle")
// .attr("station", d => d.stations)
// .attr("line", d => d.lines)
// .attr("r", 5)
// .attr("fill", "#333")
// .call(tooltip, tooltipDiv);
if (drawStationNames) {
svg.append('text')
.attr('x', station.position.x * gridVector.x)
.attr('y', station.position.y * gridVector.y + 16)
.attr('dy', '.35em')
.attr('text-anchor', 'middle')
.attr('font-family', 'helvetica')
.attr('font-weight', 'bold')
.attr('font-size', textSize)
.attr('fill', '#2c2c2c')
.text(station.name.toUpperCase());
}
}
}
return container.node();
}
Insert cell
Insert cell
Insert cell
simulationData= JSON.parse('{ "stations": [ { "id": 1, "name": "Shangshuangtang" }, { "id": 2, "name": "Zongxin Square " }, { "id": 3, "name": "Huangtuling" }, { "id": 4, "name": "Houjiatang" }, { "id": 5, "name": "Wuyi Square" }, { "id": 6, "name": "Peiyuan Bridge" }, { "id": 7, "name": "Wenchang Pavilion" }, { "id": 8, "name": "Kaifu District Government" }, { "id": 9, "name": "end of line1" }, { "id": 10, "name": "East of Meixi Lake" }, { "id": 11, "name": "East Lake Park" }, { "id": 12, "name": "Yingbing Road" }, { "id": 13, "name": "Changsha Railway Station" }, { "id": 14, "name": "Wanjiali Square" }, { "id": 15, "name": "Renmin East Road" }, { "id": 16, "name": "Shawan Park" }, { "id": 17, "name": "Guangda" }, { "id": 18, "name": "Start of line3" }, { "id": 19, "name": "Shantang" }, { "id": 20, "name": "Fubu River" }, { "id": 21, "name": "Chaoyang Village" }, { "id": 22, "name": "North Yuehu Park" }, { "id": 23, "name": "Guangsheng" }, { "id": 24, "name": "Start of line4" }, { "id": 25, "name": "Guanziling" }, { "id": 26, "name": "Third Xiangya Hospital" }, { "id": 27, "name": "Guitang" }, { "id": 28, "name": "Changsha South Railway Station" }, { "id": 29, "name": "Dujiaping" }, { "id": 30, "name": "end of line4" }, { "id": 31, "name": "start of line5" }, { "id": 32, "name": "Maozhutang" }, { "id": 33, "name": "Furong District Government" }, { "id": 34, "name": "Shuidu River" }, { "id": 35, "name": "end of line5" }, { "id": 36, "name": "Xiejia Bridge" }, { "id": 37, "name": "Zhongtang" }, { "id": 39, "name": "Huanghua Airport" } ], "lines": [ { "color": "#f38051", "name": "Line1", "stations": [1,2,3,4,5,6,7,8,9] }, { "color": "#6fcda9", "name": "Line2", "stations": [10,11,5,12,13,14,15,16,17] }, { "color": "#d6d6d6", "name": "Line3-1", "stations": [18,19] }, { "color": "#90ba49", "name": "Line3", "stations": [19,20,4,21,13,22,23] }, { "color": "#d6d6d6", "name": "Line4-1", "stations": [24,25] }, { "color": "#bbd5f6", "name": "Line4-2", "stations": [25,26,11,20,3,27,16,28,17,29] }, { "color": "#d6d6d6", "name": "Line4-3", "stations": [29,30] }, { "color": "#d6d6d6", "name": "Line5-1", "stations": [31,2] }, { "color": "#d6d6d6", "name": "Line5-2", "stations": [2,32] }, { "color": "#ffeb99", "name": "Line5-3", "stations": [32,27,33,14,22,34] }, { "color": "#d6d6d6", "name": "Line5-4", "stations": [34,35] }, { "color": "#466bd1", "name": "Line6", "stations": [36,37,26,6,12,21,33,15,39] } ] }');
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
function setStyle(selection) {
selection.attr("fill", "#5783ec");
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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