800 city names, grouped according to their first 2 letters:
Levenshtein distance
Original work by Philippe Rivière for Visionscarto.net. Comments and variants very welcome!
Forked from tsne world
forked from Fil's block: t-SNE with Levenshtein distances
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
margin: 0;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
font-family: monospace;
}
</style>
</head>
<body>
<script>
const width = 960,
height = 500,
margin = { top: 40, bottom: 40, left: 140, right: 140 },
scalepop = d3.scaleSqrt().domain([0, 100]).range([2.2, 34]),
scalecountry = d3.scaleOrdinal(d3.schemeCategory10),
centerx = d3.scaleLinear()
.range([margin.left, width - margin.right]),
centery = d3.scaleLinear()
.range([margin.top, height - margin.bottom]);
console.clear()
d3.queue()
.defer(d3.csv,'alexa-upstream.csv')
.defer(d3.csv,'sites-wolfram-alpha.csv')
.await(function (err, sites, wolfram) {
tailles = {};
parents = {};
wolfram.forEach(d => {
tailles[d.site] = +d.visits;
if (d.parent) parents[d.site] = d.parent;
})
console.log(parents)
scaleTailles = d3.scalePow()
.exponent(1/2.5)
.domain(d3.extent(d3.values(tailles)))
.range([11, 50])
const data = sites
.map((d, i) => {
return {
lon: Math.random(),
lat: Math.random(),
name: d['site'],
r: scaleTailles(tailles[d.site] || 10),
color: scalecountry(parents[d['site']]),
up1: d.up1,
up2: d.up2,
up3: d.up3,
up4: d.up4,
up5: d.up5,
parent: parents[d['site']] || Math.random(),
};
})
.slice(0, 800);
/*
const canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
*/
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// pos is the array of positions that will be updated by the tsne worker
// start with the geographic coordinates as is (plate-carrée)
// random or [0,0] is fine too
let pos = data.map(d => [d.lon, -d.lat]);
const forcetsne = d3.forceSimulation(
data.map(d => (d.x = width / 2, d.y = height / 2, d))
)
.alphaDecay(0.005)
.alpha(0.1)
.force('tsne', function (alpha) {
centerx.domain(d3.extent(pos.map(d => d[0])));
centery.domain(d3.extent(pos.map(d => d[1])));
data.forEach((d, i) => {
d.x += alpha * (centerx(pos[i][0]) - d.x);
d.y += alpha * (centery(pos[i][1]) - d.y);
});
})
.force('collide', d3.forceCollide().radius(d => 1.5 + d.r))
.on('tick', function () {
// drawcanvas(canvas, data);
drawsvg(svg, data);
});
function drawcanvas(canvas, nodes) {
let context = canvas.node().getContext("2d");
context.clearRect(0, 0, width, width);
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i];
context.beginPath();
context.moveTo(node.x, node.y);
context.arc(node.x, node.y, node.r, 0, 2 * Math.PI);
context.lineWidth = 0.5;
context.fillStyle = node.color;
context.fill();
}
}
function drawsvg(svg, nodes) {
const g = svg.selectAll('g.city')
.data(nodes);
var enter = g.enter().append('g').classed('city', true);
enter.append('circle')
.attr('r', d => d.r)
.attr('fill', d => d.color)
.append('title')
.text(d => d.name);
enter
.filter(d => d.r > 7)
.append('text')
.attr('fill', 'white')
.style('font-size', d => d.r > 9 ? '12px' : '9px')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('pointer-events', 'none')
.text(d => d.name.substring(0,4));
g.attr('transform', d => `translate(${d.x},${d.y})`);
}
d3.queue()
.defer(d3.text, 'tsne.js')
.defer(d3.text, 'https://unpkg.com/d3-geo')
.defer(d3.text, 'worker.js')
.awaitAll(function (err, scripts) {
const worker = new Worker(
window.URL.createObjectURL(
new Blob(scripts, {
type: "text/javascript"
})
)
);
worker.postMessage({
maxIter: 10,
dim: 2,
perplexity: 30.0,
data: data
});
worker.onmessage = function (e) {
if (e.data.log) console.log.apply(this, e.data.log);
if (e.data.pos) pos = e.data.pos;
if (e.data.done && e.data.done < 10000 && e.data.cost > 1e-2) {
worker.postMessage({
maxIter: e.data.done + 10,
});
}
};
});
});
</script>
</body>
https://d3js.org/d3.v4.min.js