Fourth example of a map drawn with Svelte and the d3 projections. Shows how to use elements on the map (circles in this case) that get transformed too.
Check this blog post from Geoexamples for more explanations.
To test it, clone the standard svelte template by
npx degit sveltejs/template svelte-app cd svelte-app
And copy the App.svelte, Feature.svelte and cities.js files into the src directory.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Svelte app</title>
<link rel="stylesheet" href="bundle.css" />
<script defer src="bundle.js"></script>
</head>
<body></body>
</html>
<script>
import { geoAlbers, geoPath, geoProjection } from "d3-geo";
import { geoAlbersUk } from "d3-composite-projections";
import { scaleLinear, scaleSqrt } from "d3-scale";
import { extent } from "d3-array";
import { onMount } from "svelte";
import { feature } from "topojson";
import { tweened } from "svelte/motion";
import { interpolate } from "d3-interpolate";
import Feature from "./Feature.svelte";
import { cities } from "./cities";
let data = [];
let colorScale = () => {};
const width = "960";
const height = "500";
const projectionAlbers = geoAlbers()
.rotate([4.4, 0.8])
.center([0, 55.4])
.parallels([50, 60])
.scale(3800)
.translate([width / 2, (1.8 * height) / 2]);
const projectionAlbersUk = geoAlbersUk()
.translate([width / 2, (1.85 * height) / 2])
.scale(5200);
const projectionTween = (projection0, projection1) => {
return function(t) {
function project(λ, φ) {
(λ *= 180 / Math.PI), (φ *= 180 / Math.PI);
var p0 = projection0([λ, φ]),
p1 = projection1([λ, φ]);
if (!p0 || !p1) return [0, 0];
return [(1 - t) * p0[0] + t * p1[0], (1 - t) * -p0[1] + t * -p1[1]];
}
return geoProjection(project)
.scale(1)
.translate([0, 0]);
};
};
const currentProj = tweened(projectionAlbers, {
duration: 1000,
interpolate: projectionTween
});
$: path = geoPath().projection($currentProj);
const opacity = tweened(0, {
duration: 1000
});
const circleScale = scaleSqrt()
.domain([
0,
Math.max.apply(
Math,
cities.map(function(o) {
return o.population;
})
)
])
.range([2, 15]);
const circleColorScale = scaleSqrt()
.domain([
0,
Math.max.apply(
Math,
cities.map(function(o) {
return o.population;
})
)
])
.range(["#ffffff", "#5555ff"]);
onMount(async function() {
const response = await fetch(
"https://gist.githubusercontent.com/rveciana/27272a581e975835aaa321ddf816d726/raw/c40062a328843322208b8e98c2104dc8f6ad5301/uk-counties.json"
);
const json = await response.json();
const topoData = feature(json, json.objects.UK);
const land = {
...topoData,
features: topoData.features.filter(
d => d.properties.NAME_1 === "Scotland"
)
};
const namesExtent = extent(land.features, d => d.properties.NAME_2.length);
colorScale = scaleLinear()
.domain(namesExtent)
.range(["#feedde", "#fd8d3c"]);
data = land.features;
});
</script>
<style>
svg {
width: 100%;
height: calc(100% - 5em);
background-color: "#eeeeee";
}
.borders {
fill: #ddd;
}
.city {
stroke: #777777;
}
</style>
<button
on:click={() => {
currentProj.set($currentProj === projectionAlbers ? projectionAlbersUk : projectionAlbers);
opacity.set($currentProj === projectionAlbers ? 1 : 0);
}}>
Change projection
</button>
<svg width="960" height="500">
<path
class="borders"
d={projectionAlbersUk.getCompositionBorders()}
style="opacity: {$opacity}" />
{#each data as feature}
<Feature
featurePath={path(feature)}
initialColor={colorScale(feature.properties.NAME_2.length)} />
{/each}
{#each cities as city}
<circle
class="city"
cx={$currentProj([city.lon, city.lat])[0]}
cy={$currentProj([city.lon, city.lat])[1]}
r={circleScale(city.population)}
fill={circleColorScale(city.population)} />
{/each}
</svg>
<script>
import { tweened } from "svelte/motion";
import { interpolateLab } from "d3-interpolate";
import { rgb } from "d3-color";
export let featurePath;
export let initialColor;
const color = tweened(initialColor, {
duration: 300,
interpolate: interpolateLab
});
</script>
<style>
.provinceShape {
stroke: #444444;
stroke-width: 0.5;
}
</style>
<path
d={featurePath}
class="provinceShape"
fill={$color}
on:mouseover={() => {
color.set(rgb(initialColor).brighter(0.3));
}}
on:mouseout={() => {
color.set(initialColor);
}} />
svg.svelte-1xdlizl{width:960;height:500;background-color:"#eeeeee"}.borders.svelte-1xdlizl{fill:#ddd}.city.svelte-1xdlizl{stroke:#777777}
.provinceShape.svelte-116txg3{stroke:#444444;stroke-width:0.5}
/*# sourceMappingURL=bundle.css.map */
export const cities = [
{ name: "Perth", lat: 56.396999, lon: -3.437, population: 47180 },
{ name: "Glasgow", lat: 55.860916, lon: -4.251433, population: 598830 },
{ name: "Dundee", lat: 56.462002, lon: -2.9707, population: 148270 },
{ name: "Dundee", lat: 56.462002, lon: -2.9707, population: 148270 },
{ name: "Elgin", lat: 57.653484, lon: -3.335724, population: 23128 },
{ name: "Edinburgh", lat: 55.953251, lon: -3.188267, population: 482005 },
{ name: "Edinburgh", lat: 55.953251, lon: -3.188267, population: 482005 },
{ name: "Inverness", lat: 57.477772, lon: -4.224721, population: 46870 },
{ name: "Lerwick", lat: 60.154167, lon: -1.148611, population: 6958 }
];
html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}