block by rveciana 653784cfab5d859610926733cfb14773

Svelte mapping: circles

Full Screen

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.

index.html

<!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>

App.svelte

<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>

Feature.svelte

<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);
  }} />

bundle.css

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 */

cities.js

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 }
];

global.css

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;
}