Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

2 layer doughnut chart using Chart.js

I am trying to create a pie chart that displays an array of countries at the first level and the cities for each country at the second level.

I have a JSON file (below) with data that I modified in order to get closer to what I am trying to achieve but it does not seem to work (I'm probably way off...)

[
{city: "Budapest", country: "Hungary"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Istanbul", country: "Turkey"},
{city: "Ho Chi Minh", country: "Vietnam"},
{city: "Shenzen", country: "China"},
{city: "Budapes", country: "Hungary"},
{city: "Budapest", country: "Hungary"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Istanbul", country: "Turkey"},
{city: "Budapest", country: "Hungary"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Shenzen", country: "China"},
{city: "Istanbul", country: "Turkey"},
]

Below is the modified data that I am using:

[
 {country: "Hungary", cities: ["Budapest", "Budapes", "Budapest", "Budapest"]},
 {country: "Chine", cities: ["Shenzen", "Shenzen", "Shenzen"]},
 {country: "Turkey", cities: ["Istanbul", "Istanbul", "Istanbul"]},
 {country: "Vietnam", cities: ["Ho Chi Minh"]},
]

Essentially, I am trying to make a pie chart that shows the 4 countries in the middle and each slices is then broken into the cities for each country. Any help would be highly appreciated.

Code I used for modifying the data:

  let countries: any = [];
  let intermediete: any = [
    ...new Set(data.map((col: any) => col.country)),
  ].reduce((a: any, v: any) => ({ ...a, [v]: [] }), {});

  data.forEach((location: any) => {
    intermediete[location.country].push(location.city);
  });

  for (let i = 0; i < Object.keys(intermediete).length; i++) {
    countries.push({
      country: Object.keys(intermediete)[i],
      cities: Object.values(intermediete)[i],
    });
  }

  data = countries;
  console.log(data);

  let cuntries_count: any = [];
  let cities_count: any = [];

  data
    .map((col: any) => col.cities)
    .forEach((element: any) => {
      cuntries_count.push(element.length);

      cities_count.push([...new Set(element)].length);
    });

  this.locations_pie_data = {
    labels: Object.keys(intermediete),

    datasets: [
      {
        type: 'doughnut',
        data: cities_count,
        backgroundColor: [...new Set(data.map(() => this.randomHEX()))],
      },
      {
        type: 'doughnut',
        data: cuntries_count,
        backgroundColor: [...new Set(data.map(() => this.randomHEX()))],
      },
    ],

    options: {
      rotation: 0,
      circumference: 90,

      plugins: {
        legend: {
          position: 'right',
        },
      },
    },
  };
});

The desired output should look something like the image below

(inner circle > countries, outer circle > cities in each country) enter image description here

like image 802
Daniel H. Avatar asked Jun 19 '26 10:06

Daniel H.


1 Answers

Please take a look at below runnable code and see how it could be done.

When it comes to the legend, you'll probably have to implement a solution as explained in this answer.

let data = [
  {city: "Budapest", country: "Hungary"},
  {city: "Shenzen", country: "China"},
  {city: "Beijing", country: "China"},
  {city: "Shenzen", country: "China"},
  {city: "Istanbul", country: "Turkey"},
  {city: "Ho Chi Minh", country: "Vietnam"},
  {city: "Shenzen", country: "China"},
  {city: "Debrecen", country: "Hungary"},
  {city: "Budapest", country: "Hungary"},
  {city: "Shenzen", country: "China"},
  {city: "Shenzen", country: "China"},
  {city: "Shenzen", country: "China"},
  {city: "Istanbul", country: "Turkey"},
  {city: "Budapest", country: "Hungary"},
  {city: "Beijing", country: "China"},
  {city: "Shenzen", country: "China"},
  {city: "Shenzen", country: "China"},
  {city: "Istanbul", country: "Turkey"}
];
const colors = ['204, 0, 0', '0, 0, 255', '0, 153, 0', '153, 51, 255'];

data = data
  .sort((o1, o2) => o1.country.localeCompare(o2.country));
const countries = data
  .reduce((acc, o) => {
    let country = acc.find(v => v.country == o.country);
    if (!country) {
      country = { country: o.country, cities: 0 };
      acc.push(country);       
    }
    ++country.cities;   
    return acc;
  }, []);
countries.forEach((c, i) => c.color = colors[i]);
const cities = data
  .reduce((acc, o) => {
    let city = acc.find(v => v.city == o.city);
    if (!city) {
      city = { country: o.country, city: o.city, count: 0 };
      acc.push(city);       
    }
    ++city.count;   
    return acc;
  }, []);
cities.forEach(c => c.color = countries.find(o => o.country == c.country).color); 


Chart.register(ChartDataLabels);
new Chart('myChart', {
  type: 'doughnut',
  data: {
    datasets: [{
        data: cities.map(o => o.count),
        labels: cities.map(o => o.city),
        backgroundColor: cities.map(c => 'rgba(' + c.color + ', 0.2)'),
        borderWidth: 3
      },
      {
        data: countries.map(c => c.cities),
        labels: countries.map(c => c.country),
        backgroundColor: countries.map(c => 'rgb(' + c.color + ', 0.4)'),
        borderWidth: 3
      }
    ]
  },
  options: {
    cutout: '40%',
    plugins: {
      datalabels: {
        textAlign: 'center',
        formatter: (v, ctx) => {
          const dataset = ctx.chart.data.datasets[ctx.datasetIndex];
          return dataset.labels[ctx.dataIndex] + ': ' + dataset.data[ctx.dataIndex];
        }
      }
    }
  }
});
canvas {
  max-height: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<canvas id="myChart"></canvas>
like image 121
uminder Avatar answered Jun 21 '26 00:06

uminder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!