block by curran 29d8ad97087177924b759b1f1db85a0f

Map with circles

Full Screen

Visualizing population by country using circles on a map! The area of each circle corresponds to the population of the country it represents. You can also pan & zoom, and hover over each country for more information.

index.js

import {
  select,
  geoPath,
  geoCentroid,
  geoNaturalEarth1,
  zoom,
  event,
  scaleSqrt,
  max,
  format
} from 'd3';
import { loadAndProcessData } from './loadAndProcessData';
import { sizeLegend } from './sizeLegend';

const svg = select('svg');

const projection = geoNaturalEarth1();
const pathGenerator = geoPath().projection(projection);
const radiusValue = d => d.properties['2018'];

const g = svg.append('g');

const colorLegendG = svg.append('g')
    .attr('transform', `translate(40,310)`);

g.append('path')
    .attr('class', 'sphere')
    .attr('d', pathGenerator({type: 'Sphere'}));

svg.call(zoom().on('zoom', () => {
  g.attr('transform', event.transform);
}));

const populationFormat = format(',');

loadAndProcessData().then(countries => {
  
  const sizeScale = scaleSqrt()
    .domain([0, max(countries.features, radiusValue)])
    .range([0, 33]);
  
  g.selectAll('path').data(countries.features)
    .enter().append('path')
      .attr('class', 'country')
      .attr('d', pathGenerator)
      .attr('fill', d => d.properties['2018'] ? '#e8e8e8' : '#fecccc')
    .append('title')
      .text(d => 
        isNaN(radiusValue(d))
          ? 'Missing data'
          : [
            d.properties['Region, subregion, country or area *'],
            populationFormat(radiusValue(d))
          ].join(': ')
      );
  
  countries.featuresWithPopulation.forEach(d => {
    d.properties.projected = projection(geoCentroid(d));
  });
  
  g.selectAll('circle').data(countries.featuresWithPopulation)
    .enter().append('circle')
      .attr('class', 'country-circle')
      .attr('cx', d => d.properties.projected[0])
      .attr('cy', d => d.properties.projected[1])
      .attr('r', d => sizeScale(radiusValue(d)));

  g.append('g')
    .attr('transform', `translate(45,215)`)
    .call(sizeLegend, {
      sizeScale,
      spacing: 45,
      textOffset: 10,
      numTicks: 5,
      tickFormat: populationFormat
    })
    .append('text')
      .attr('class', 'legend-title')
      .text('Population')
      .attr('y', -45)
      .attr('x', -30);

});

index.html

<!DOCTYPE html>
<html>                         
  <head>
    <title>Circles on a Map</title>
    <link rel="stylesheet" href="styles.css">
    <script src="https://unpkg.com/d3@5.6.0/dist/d3.min.js"></script>
    <script src="https://unpkg.com/topojson@3.0.2/dist/topojson.min.js"></script>
  </head>
  <body>
    <svg width="960" height="500"></svg>
    <script src="bundle.js"></script>
  </body>
</html>

bundle.js

(function (topojson,d3) {
  'use strict';

  const loadAndProcessData = () => 
    Promise
      .all([
        d3.csv('https://vizhub.com/curran/datasets/un-population-estimates-2017-medium-variant.csv'),
        d3.json('https://unpkg.com/visionscarto-world-atlas@0.0.4/world/50m.json')
      ])
      .then(([unData, topoJSONdata]) => {
       
        const rowById = unData.reduce((accumulator, d) => {
          accumulator[d['Country code']] = d;      
          return accumulator;
        }, {});

        const countries = topojson.feature(topoJSONdata, topoJSONdata.objects.countries);

        countries.features.forEach(d => {
          Object.assign(d.properties, rowById[+d.id]);
        });
        
        const featuresWithPopulation = countries.features
          .filter(d => d.properties['2018'])
          .map(d => {
            d.properties['2018'] = +d.properties['2018'].replace(/ /g, '') * 1000;
            return d;
          });

        return {
          features: countries.features,
          featuresWithPopulation
        };
      });

  const sizeLegend = (selection, props) => {
    const {
      sizeScale,
      spacing,
      textOffset,
      numTicks,
      tickFormat
    } = props;
    
    const ticks = sizeScale.ticks(numTicks)
      .filter(d => d !== 0)
      .reverse();

    const groups = selection.selectAll('g').data(ticks);
    const groupsEnter = groups
      .enter().append('g')
        .attr('class', 'tick');
    groupsEnter
      .merge(groups)
        .attr('transform', (d, i) =>
          `translate(0, ${i * spacing})`
        );
    groups.exit().remove();
    
    groupsEnter.append('circle')
      .merge(groups.select('circle'))
        .attr('r', sizeScale);
    
    groupsEnter.append('text')
      .merge(groups.select('text'))
        .text(tickFormat)
        .attr('dy', '0.32em')
        .attr('x', d => sizeScale(d) + textOffset);
    
  };

  const svg = d3.select('svg');

  const projection = d3.geoNaturalEarth1();
  const pathGenerator = d3.geoPath().projection(projection);
  const radiusValue = d => d.properties['2018'];

  const g = svg.append('g');

  const colorLegendG = svg.append('g')
      .attr('transform', `translate(40,310)`);

  g.append('path')
      .attr('class', 'sphere')
      .attr('d', pathGenerator({type: 'Sphere'}));

  svg.call(d3.zoom().on('zoom', () => {
    g.attr('transform', d3.event.transform);
  }));

  const populationFormat = d3.format(',');

  loadAndProcessData().then(countries => {
    
    const sizeScale = d3.scaleSqrt()
      .domain([0, d3.max(countries.features, radiusValue)])
      .range([0, 33]);
    
    g.selectAll('path').data(countries.features)
      .enter().append('path')
        .attr('class', 'country')
        .attr('d', pathGenerator)
        .attr('fill', d => d.properties['2018'] ? '#e8e8e8' : '#fecccc')
      .append('title')
        .text(d => 
          isNaN(radiusValue(d))
            ? 'Missing data'
            : [
              d.properties['Region, subregion, country or area *'],
              populationFormat(radiusValue(d))
            ].join(': ')
        );
    
    countries.featuresWithPopulation.forEach(d => {
      d.properties.projected = projection(d3.geoCentroid(d));
    });
    
    g.selectAll('circle').data(countries.featuresWithPopulation)
      .enter().append('circle')
        .attr('class', 'country-circle')
        .attr('cx', d => d.properties.projected[0])
        .attr('cy', d => d.properties.projected[1])
        .attr('r', d => sizeScale(radiusValue(d)));

    g.append('g')
      .attr('transform', `translate(45,215)`)
      .call(sizeLegend, {
        sizeScale,
        spacing: 45,
        textOffset: 10,
        numTicks: 5,
        tickFormat: populationFormat
      })
      .append('text')
        .attr('class', 'legend-title')
        .text('Population')
        .attr('y', -45)
        .attr('x', -30);

  });

}(topojson,d3));
//# sourceMappingURL=bundle.js.map

bundle.js.map

{"version":3,"file":"bundle.js","sources":["loadAndProcessData.js","sizeLegend.js","index.js"],"sourcesContent":["import { feature } from 'topojson';\nimport { csv, json } from 'd3';\n\nexport const loadAndProcessData = () => \n  Promise\n    .all([\n      csv('https://vizhub.com/curran/datasets/un-population-estimates-2017-medium-variant.csv'),\n      json('https://unpkg.com/visionscarto-world-atlas@0.0.4/world/50m.json')\n    ])\n    .then(([unData, topoJSONdata]) => {\n     \n      const rowById = unData.reduce((accumulator, d) => {\n        accumulator[d['Country code']] = d;      \n        return accumulator;\n      }, {});\n\n      const countries = feature(topoJSONdata, topoJSONdata.objects.countries);\n\n      countries.features.forEach(d => {\n        Object.assign(d.properties, rowById[+d.id]);\n      });\n      \n      const featuresWithPopulation = countries.features\n        .filter(d => d.properties['2018'])\n        .map(d => {\n          d.properties['2018'] = +d.properties['2018'].replace(/ /g, '') * 1000;\n          return d;\n        });\n\n      return {\n        features: countries.features,\n        featuresWithPopulation\n      };\n    });\n","export const sizeLegend = (selection, props) => {\n  const {\n    sizeScale,\n    spacing,\n    textOffset,\n    numTicks,\n    tickFormat\n  } = props;\n  \n  const ticks = sizeScale.ticks(numTicks)\n    .filter(d => d !== 0)\n    .reverse();\n\n  const groups = selection.selectAll('g').data(ticks);\n  const groupsEnter = groups\n    .enter().append('g')\n      .attr('class', 'tick');\n  groupsEnter\n    .merge(groups)\n      .attr('transform', (d, i) =>\n        `translate(0, ${i * spacing})`\n      );\n  groups.exit().remove();\n  \n  groupsEnter.append('circle')\n    .merge(groups.select('circle'))\n      .attr('r', sizeScale);\n  \n  groupsEnter.append('text')\n    .merge(groups.select('text'))\n      .text(tickFormat)\n      .attr('dy', '0.32em')\n      .attr('x', d => sizeScale(d) + textOffset);\n  \n}","import {\n  select,\n  geoPath,\n  geoCentroid,\n  geoNaturalEarth1,\n  zoom,\n  event,\n  scaleSqrt,\n  max,\n  format\n} from 'd3';\nimport { loadAndProcessData } from './loadAndProcessData';\nimport { sizeLegend } from './sizeLegend';\n\nconst svg = select('svg');\n\nconst projection = geoNaturalEarth1();\nconst pathGenerator = geoPath().projection(projection);\nconst radiusValue = d => d.properties['2018'];\n\nconst g = svg.append('g');\n\nconst colorLegendG = svg.append('g')\n    .attr('transform', `translate(40,310)`);\n\ng.append('path')\n    .attr('class', 'sphere')\n    .attr('d', pathGenerator({type: 'Sphere'}));\n\nsvg.call(zoom().on('zoom', () => {\n  g.attr('transform', event.transform);\n}));\n\nconst populationFormat = format(',');\n\nloadAndProcessData().then(countries => {\n  \n  const sizeScale = scaleSqrt()\n    .domain([0, max(countries.features, radiusValue)])\n    .range([0, 33]);\n  \n  g.selectAll('path').data(countries.features)\n    .enter().append('path')\n      .attr('class', 'country')\n      .attr('d', pathGenerator)\n      .attr('fill', d => d.properties['2018'] ? '#e8e8e8' : '#fecccc')\n    .append('title')\n      .text(d => \n        isNaN(radiusValue(d))\n          ? 'Missing data'\n          : [\n            d.properties['Region, subregion, country or area *'],\n            populationFormat(radiusValue(d))\n          ].join(': ')\n      );\n  \n  countries.featuresWithPopulation.forEach(d => {\n    d.properties.projected = projection(geoCentroid(d));\n  });\n  \n  g.selectAll('circle').data(countries.featuresWithPopulation)\n    .enter().append('circle')\n      .attr('class', 'country-circle')\n      .attr('cx', d => d.properties.projected[0])\n      .attr('cy', d => d.properties.projected[1])\n      .attr('r', d => sizeScale(radiusValue(d)));\n\n  g.append('g')\n    .attr('transform', `translate(45,215)`)\n    .call(sizeLegend, {\n      sizeScale,\n      spacing: 45,\n      textOffset: 10,\n      numTicks: 5,\n      tickFormat: populationFormat\n    })\n    .append('text')\n      .attr('class', 'legend-title')\n      .text('Population')\n      .attr('y', -45)\n      .attr('x', -30);\n\n});\n"],"names":["csv","json","feature","select","geoNaturalEarth1","geoPath","zoom","event","format","scaleSqrt","max","geoCentroid"],"mappings":"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"}

colorLegend.js

export const colorLegend = (selection, props) => {
  const {                      
    colorScale,                
    circleRadius,
    spacing,                   
    textOffset,
    backgroundRectWidth        
  } = props;                   
  
  const backgroundRect = selection.selectAll('rect')
    .data([null]);             
  const n = colorScale.domain().length; 
  backgroundRect.enter().append('rect')
    .merge(backgroundRect)
      .attr('x', -circleRadius * 2)   
      .attr('y', -circleRadius * 2)   
      .attr('rx', circleRadius * 2)   
      .attr('width', backgroundRectWidth)
      .attr('height', spacing * n + circleRadius * 2) 
      .attr('fill', 'white')
      .attr('opacity', 0.8);
  

  const groups = selection.selectAll('.tick')
    .data(colorScale.domain());
  const groupsEnter = groups
    .enter().append('g')
      .attr('class', 'tick');
  groupsEnter
    .merge(groups)
      .attr('transform', (d, i) =>    
        `translate(0, ${i * spacing})`  
      );
  groups.exit().remove();
  
  groupsEnter.append('circle')
    .merge(groups.select('circle')) 
      .attr('r', circleRadius)
      .attr('fill', colorScale);      
  
  groupsEnter.append('text')
    .merge(groups.select('text'))   
      .text(d => d)
      .attr('dy', '0.32em')
      .attr('x', textOffset);
}

loadAndProcessData.js

import { feature } from 'topojson';
import { csv, json } from 'd3';

export const loadAndProcessData = () => 
  Promise
    .all([
      csv('https://vizhub.com/curran/datasets/un-population-estimates-2017-medium-variant.csv'),
      json('https://unpkg.com/visionscarto-world-atlas@0.0.4/world/50m.json')
    ])
    .then(([unData, topoJSONdata]) => {
     
      const rowById = unData.reduce((accumulator, d) => {
        accumulator[d['Country code']] = d;      
        return accumulator;
      }, {});

      const countries = feature(topoJSONdata, topoJSONdata.objects.countries);

      countries.features.forEach(d => {
        Object.assign(d.properties, rowById[+d.id]);
      });
      
      const featuresWithPopulation = countries.features
        .filter(d => d.properties['2018'])
        .map(d => {
          d.properties['2018'] = +d.properties['2018'].replace(/ /g, '') * 1000;
          return d;
        });

      return {
        features: countries.features,
        featuresWithPopulation
      };
    });

package-lock.json

{
  "requires": true,
  "lockfileVersion": 1,
  "dependencies": {
    "@types/estree": {
      "version": "0.0.39",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
      "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
      "dev": true
    },
    "@types/node": {
      "version": "10.12.9",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.9.tgz",
      "integrity": "sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==",
      "dev": true
    },
    "rollup": {
      "version": "0.67.3",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.67.3.tgz",
      "integrity": "sha512-TyNQCz97rKuVVbsKUTXfwIjV7UljWyTVd7cTMuE+aqlQ7WJslkYF5QaYGjMLR2BlQtUOO5CAxSVnpQ55iYp5jg==",
      "dev": true,
      "requires": {
        "@types/estree": "0.0.39",
        "@types/node": "*"
      }
    }
  }
}

package.json

{
  "scripts": {
    "build": "rollup -c"
  },
  "devDependencies": {
    "rollup": "latest"
  }
}

rollup.config.js

export default {
  input: 'index.js',
  external: ['d3', 'topojson'],
  output: {
    file: 'bundle.js',
    format: 'iife',
    sourcemap: true,
    globals: {
      d3: 'd3',
      topojson: 'topojson'
    }
  }
};

sizeLegend.js

export const sizeLegend = (selection, props) => {
  const {
    sizeScale,
    spacing,
    textOffset,
    numTicks,
    tickFormat
  } = props;
  
  const ticks = sizeScale.ticks(numTicks)
    .filter(d => d !== 0)
    .reverse();

  const groups = selection.selectAll('g').data(ticks);
  const groupsEnter = groups
    .enter().append('g')
      .attr('class', 'tick');
  groupsEnter
    .merge(groups)
      .attr('transform', (d, i) =>
        `translate(0, ${i * spacing})`
      );
  groups.exit().remove();
  
  groupsEnter.append('circle')
    .merge(groups.select('circle'))
      .attr('r', sizeScale);
  
  groupsEnter.append('text')
    .merge(groups.select('text'))
      .text(tickFormat)
      .attr('dy', '0.32em')
      .attr('x', d => sizeScale(d) + textOffset);
  
}

styles.css

body {
  margin: 0px;
  overflow: hidden;
}

.sphere {
  fill: #f2f2f2;
}

.country {
  stroke: black;
  stroke-width: 0.08px;
}

.country:hover {
  fill: #74e1d8;
}

.tick text {
  font-size: 1.3em;
  fill: #444444;
  font-family: sans-serif;
}

.tick circle, .country-circle {
  fill: #00d1bf;
  fill-opacity: 0.489216;
  stroke: black;
  stroke-width: 0.3;
  pointer-events: none;
}

.legend-title {
  font-size: 2.2em;
  fill: #444444;
  font-family: sans-serif;
}