sgratzl / chartjs-chart-geo

Chart.js Choropleth and Bubble Maps
https://www.sgratzl.com/chartjs-chart-geo/
MIT License
347 stars 36 forks source link

Small countries as circles in choropleth map? #212

Closed PMCS64 closed 9 months ago

PMCS64 commented 9 months ago

In the below example, the country 442 being Luxembourg, it is impossible to see in the map the value of that country since it is a tiny country. Some mappers have fixed that problem by putting a small circle on top of smaller countries like the map below.

What I would like

Note how the smaller countries have been replaced by circles.

https://upload.wikimedia.org/wikipedia/commons/0/00/British_Overseas_Territories_and_Crown_Dependencies.svg

What I have done

const countries_data = {"442": 104242, "036": 249530};

fetch('https://unpkg.com/world-atlas/countries-50m.json')
  .then((r) => r.json())
  .then((data) => {
    const countries = ChartGeo.topojson.feature(data, data.objects.countries).features;

    // Match countries in the chart data with the countries_data and assign values
    countries.forEach((d) => {
      const countryCode = d.id;
      const countryValue = countries_data[countryCode];
      d.properties.value = countryValue ? countryValue : 0;
    });

    const chart = new Chart(document.getElementById("canvas").getContext("2d"), {
      type: 'choropleth',
      data: {
        labels: countries.map((d) => d.properties.name),
        datasets: [{
          label: 'Countries',
          data: countries.map((d) => ({
            feature: d,
            value: d.properties.value,
          })),
        }]
      },
      options: {
        showOutline: true,
        showGraticule: true,
        plugins: {
          legend: {
            display: false
          },
        },
        scales: {
          projection: {
            axis: 'x',
            projection: 'equalEarth'
          }
        }
      }
    });
  });
sgratzl commented 9 months ago

it depends on which map data you are using, AFAIK you are using 50m, so a 1:50m (miles) scaling factor.

PMCS64 commented 9 months ago

This looks independent of the resolution of the map. I have managed to create the intended result using d3 geoCentroid, however that's more of a "DIY" solution.

const countries_data = {
  "442": { "value": 100.00, "small": true },
  "250": { "value": 75.00, "small": false },
  "702": { "value": 50.00, "small": true },
  "756": { "value": 25.00, "small": false }
};

fetch('https://unpkg.com/world-atlas/countries-50m.json')
  .then((r) => r.json())
  .then((data) => {
    const countries = ChartGeo.topojson.feature(data, data.objects.countries).features;

    // Match countries in the chart data with the countries_data and assign values
    countries.forEach((d) => {
      const countryCode = d.id;
      const countryData = countries_data[countryCode];
      d.properties.value = countryData ? countryData.value : 0;

      // Check if the country is in the list to be represented as a circle
      if (countryData && countryData.small) {
        const centroid = d3.geoCentroid(d); // Get the centroid of the country
        const circleRadius = 1; // Set a fixed size for the circle

        // Use d3.geoCircle to create a circular shape
        const circleGenerator = d3.geoCircle().radius(circleRadius).center(centroid);
        const circleGeometry = circleGenerator();

        // Update the geometry to represent the circle
        d.geometry = {
          type: "Polygon",
          coordinates: circleGeometry.coordinates,
        };
        d.isCircle = true;
      }
    });

    countries.sort((a, b) => {
      return (a.isCircle || 0) - (b.isCircle || 0);
    });

    const chart = new Chart(document.getElementById("canvas").getContext("2d"), {
      type: 'choropleth',
      data: {
        labels: countries.map((d) => d.properties.name),
        datasets: [{
          label: 'Countries',
          data: countries.map((d) => ({
            feature: d,
            value: d.properties.value,
          })),
        }]
      },
      options: {
        showOutline: true,
        showGraticule: true,
        plugins: {
          legend: {
            display: false
          },
        },
        scales: {
          projection: {
            axis: 'x',
            projection: 'equalEarth'
          },
          color: {
            axis: 'x',
            display: false,
          },
        },
      },
    });
  });
sgratzl commented 9 months ago

sorry I misunderstood your question. while your solution sounds hacky, I don't think there is a way around it since this library just renders a map with the given geo shapes. As you figured out when you manipulate the shape you can achieve your desired effect.

PMCS64 commented 9 months ago

Sounds good - just didn't know if there was any map or any code part of chartjs-chart-geo that might have had implemented it already somehow. Thanks for the quick replies. Feel free to mark as closed.