Public
Edited
Feb 7, 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
similarityMeasures = [
["Ratio of Mutually Exclusive Pixels to Common Pixels", "similarityExclusive"],
["Maximum of Pixel Difference", "similarityMaxDiff"],
["Ratio of max. Pixel Difference to Common Pixels", "similarityMaxDiffToCommon"],
["Composite (Mutually Exclusive × Maximum Difference)", "similarityComposite"],
["Maximum of Eroded Difference", "similarityMaxErodedDiff"],
["Ratio of max. Eroded Difference to Common Pixels", "similarityMaxErodedDiffToCommon"],
]
Insert cell
Insert cell
graphData = {
for (let arr of Object.values(dataset.similarityData.similars)) {
arr.sort((a,b) => b[similarityMeasure] - a[similarityMeasure]);
}

let result = {
nodes: Object.keys(dataset.similarityData.similars).map(d => ({id: d})),
links: [],
//rankedConnections: dataset.similarityData.connections.sort((a,b) => b[similarityMeasure] - a[similarityMeasure])
};
for (let k of Object.keys(dataset.similarityData.similars)) {
let count = 0;
for (let s of dataset.similarityData.similars[k]) {
if (s[similarityMeasure] >= similarityLimit &&
s.exclusive / (dataset.similarityData.resolution ** 2) <= exclusiveAreaLimit &&
s.pixelRatio >= pixelRatioLimit) {
/*
if (s.exclusiveToTotal <= exclusiveToTotalLimit &&
s.exclusiveToOverlap <= exclusiveToOverlapLimit &&
s.pixelRatio >= pixelRatioLimit) {
*/
count++;
if (count > similarCount) break;
result.links.push(Object.assign({}, s, {
source: k,
target: s.icon
}));
}
}
}
return result;
}
Insert cell
datasets = [
{
name: "Maki",
infoURL: "https://labs.mapbox.com/maki-icons/",
similarityData: parseData(await FileAttachment("results-maki_20_3.csv").text()),
iconURL: id => "https://cartolab.at/map-icons/maki/" + id + ".svg" // "http://cartolab.at/icons/maki/" + id + ".svg"
},
{
name: "Maki (subset)",
infoURL: "https://labs.mapbox.com/maki-icons/",
similarityData: parseData(await FileAttachment("results-maki-subset_20.csv").text()),
iconURL: id => "https://cartolab.at/map-icons/maki/" + id + ".svg"
},
{
name: "National Park Service",
infoURL: "https://www.nps.gov/maps/tools/symbol-library/index.html",
similarityData: parseData(await FileAttachment("results-nps_20.csv").text()),
iconURL: id => "https://cartolab.at/map-icons/nps/" + id + ".svg" // "http://cartolab.at/icons/maki/" + id + ".svg"
},
{
name: "OpenStreetMap",
infoURL: "https://wiki.openstreetmap.org/wiki/SymbolsTab",
similarityData: parseData(await FileAttachment("results-osm_20_2.csv").text()),
iconURL: id => "https://cartolab.at/map-icons/osm/" + id + ".svg" // "http://cartolab.at/icons/maki/" + id + ".svg"
},

]
Insert cell
function parseData(raw) {
//let raw = await FileAttachment("results-maki_20.csv").text();
raw = raw.split("\n");
let resolution = +raw.splice(0,1);
raw = raw.map(s => s.split(","));
let similars = {};
let connections = [];

let i = 2;
while (!isNaN(parseFloat(raw[0][i]))) {
i++;
}

let numParams = i-1;

console.log(numParams);

raw.forEach(a => {
let icon = a[0];

if (!similars[icon]) similars[icon] = [];

for (let i=1; i<a.length-4; i+=numParams) {
let r = {
icon: a[i],
icon2: icon,
exclusive: +a[i+1],
common: +a[i+2],
pixelRatio: +a[i+3],
maxDiff: +a[i+4],
};

r.similarityExclusive = r.common / (r.common + r.exclusive);
r.similarityMaxDiff = 1 - r.maxDiff / (resolution ** 2);
r.similarityMaxDiffToCommon = r.common / (r.common + r.maxDiff);
r.similarityComposite = r.similarityExclusive * r.similarityMaxDiff;
if (numParams > 5) {
r.erodedMaxDiff = +a[i+5];
r.similarityMaxErodedDiff = 1 - r.erodedMaxDiff / (resolution ** 2);
r.similarityMaxErodedDiffToCommon = r.common / (r.common + r.erodedMaxDiff);
//r.similarity = r.similarityMaxErodedDiff;
}
else {
//r.similarity = r.similarityComposite;
}
r.exclusiveArea = r.exclusive / (resolution ** 2);
similars[icon].push(r);
connections.push(r);

// make copy for "other" icon
let r2 = Object.assign({},r,{icon: icon});
if (!similars[r.icon]) similars[r.icon] = [];
similars[r.icon].push(r2);
}
});

function rank(attributeName) {
let simName = "similarity"+attributeName;
let rankName = "rank"+attributeName;
connections.sort((a,b) => b[simName] - a[simName]);
connections.forEach((c, idx) => c[rankName] = idx+1);
}

rank("Exclusive");
rank("MaxDiff");
rank("MaxDiffToCommon");
rank("Composite");
rank("MaxErodedDiff");
rank("MaxErodedDiffToCommon");

return {
resolution: resolution,
similars: similars,
connections: connections
}

}
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,
linkTitle,
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 N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
const linkTitles = linkTitle == null ? null : d3.map(links, linkTitle);
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);

// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => Object.assign({},_,{id: N[i]}));
links = d3.map(links, (_, i) => Object.assign({},_,{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().strength(-60);
const forceLink = d3.forceLink(links)
.id(({index: i}) => N[i])
//.id(d => N[d])
.distance((d,i) => {
//console.log(d, links[i]);
return (1/d[similarityMeasure]) * 20;
});
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);

const simulation = d3.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.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 link = svg.append("g")
.attr("stroke", linkStroke)
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line");
if (linkTitles) link.append("title").text(({index: i}) => linkTitles[i]);

let node = svg.append("g")
.attr("fill", nodeFill)
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("g")
.data(nodes)
.join("g")
.call(drag(simulation));

//let circle = node.append("circle").attr("r", nodeRadius)

const img = node
.append('image')
.attr(
'xlink:href',
d => dataset.iconURL(d.id)
)
.attr('width', 15)
.attr('height', 15)
.attr('x', -7)
.attr('y', -7);

if (W) link.attr("stroke-width", ({index: i}) => W[i]);
//if (G) circle.attr("fill", ({index: i}) => color(G[i]));
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("transform", d => "translate(" + d.x + "," + d.y + ")")
//.attr("cy", d => 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
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