tSNE in 1D is supposed to give interesting results, maybe an approximate solution to the travelling salesman's problem
trying to reproduce http://gis.stackexchange.com/questions/15528/one-dimensional-map-of-the-world/15567#15567
See also the USA version.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<style>
body {
margin: 0;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
path.link {
stroke: rgba(0,0,0,0.3);
fill: none;
}
</style>
</head>
<body>
<script>
const width = 960,
height = 500,
scalepop = d3.scaleSqrt().domain([0, 100000]).range([2.5, 6]),
scalecountry = d3.scaleOrdinal(d3.schemeCategory10);
d3.csv('cities.csv', function (cities) {
const data = cities
.sort((a, b) => d3.descending(+a[2015], +b[2015]))
.map((d, i) => {
return {
lon: +d.Longitude,
lat: +d.Latitude,
name: d['Urban Agglomeration'],
r: scalepop(+d[2015]),
color: scalecountry(+d['Country Code'])
};
})
//.slice(0, 1000);
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append('text')
.attr('class', 'total')
.attr('transform', 'translate(20,20)');
const projection = d3.geoBertin1953()
.fitExtent([[0,0], [width, height]], {
type: "MultiPoint",
coordinates: data.map(d => [d.lon, d.lat])
}),
path = d3.geoPath(projection);
// 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]);
drawsvg(svg, data);
function drawsvg(svg, nodes) {
svg.selectAll('circle.city')
.data(nodes)
.enter()
.append('circle')
.classed('city', true)
.attr('r', d => d.r)
.attr('fill', d => d.color)
.attr('transform', d => `translate(${projection([d.lon, d.lat])})`);
}
function drawlinks(svg, pos){
let p = pos
.map((d,i) => {
d.index = i;
let k = data[i];
d.coords = [k.lon, k.lat];
d.name = k.name;
return d;
})
.sort((a,b) => d3.ascending(a[0], b[0]));
p = p
.map((d,i) => {
d.next = p[(i+1) % pos.length].index;
d.nextname = p[(i+1) % pos.length].name;
return d;
});
//console.log(p.map(d=>d.name + ':' + d.nextname));
svg.selectAll('.link')
.data(p.slice(0,-1))
.enter()
.append('path')
.attr('class', 'link')
svg.selectAll('.link')
.attr('d', (d,i) => path({
type: 'LineString',
coordinates: [d.coords, p[(i+1) % pos.length].coords]
}));
let distance = p.map((d,i) => d3.geoDistance(d.coords, p[(i+1) % pos.length].coords))
.reduce((a,b) => a+b, 0);
d3.select('.total')
.text(
d3.format('0.2f')(distance)
)
}
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: 10000,
dim: 1,
perplexity: 80.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 < 20000 && e.data.cost > 1e-2) {
worker.postMessage({
maxIter: e.data.done + 10,
});
}
drawlinks(svg, pos);
};
});
});
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-geo-projection.v2.min.js