I have created a World Bubble Map where the bubbles are formed according to geolocation of the countries and bubble radius (size) should change according to selected parameters from radio button.
For instance, if population is selected, the bubble size should be formed according to population size of every nation and by selecting the next button it should change as per selected parameter.
So far I have managed to form a Bubble Map which reads the data from JSON file to plot the first bubbles, but I am stuck on how to make it read my other parameters from my CSV file, as all the necessary data that needs to be visualized is within a separate CSV file to the JSON file that the geolocation data comes from.
Is there a way, that I can link my CSV file to create the bubbles as per the countries geolocation and parameters from CSV file. All my coding has been done in this Observable Notebook
Yes there is, you have already demonstrated code that actively re-renders when the selected value from a radio button list is changed. The problem with your current code has nothing to do with bubbles or radius, it is a simple data referencing issue, importantly, how to merge arrays of data from multiple sources in javascript.
NOTE: A post in this format doesn't work very well on SO, a link to a fiddle or observable is a great secondary resource, but your post on SO needs to be focused on the specific issue. Going to that effort usually helps you identify the solution as part of the process. In this solution I'll take you through my debugging effort.
First, lets have a look at your current attempt, to show the value of the currently selected property from the radio buttons. We start here because it shows your current understanding of the code and how the data is loaded into memory. Before worrying about the bubbles, that you have already demonstrated how to render lets solve the data issue first.
Your current tooltip content is not displaying the value for the selected property (in yellow), it is however displaying the label of the selected option (blue underline). lets dig a bit closer, this is the current content logic:
<div>
<h3>${hover.NAME}</h3>
<div style="margin: 10px">
<h4>CountryCode : ${hover.ISO_A2} </h4>
<h4> ContinentCode : ${hover.CONTINENT} </h>
<h4>${parameter } : ${d3.format(",")(hover[p])}</h4>
</div>
</div>
in this case hover
is an object that has properties ISO_A2
and CONTINENT
which are not properties from your referenced CSV file. This is properties from the countries.json
To confirm this and to quickly explore the properties that are available we can make this simple change to the tooltip content, we want to verify the value of p
so we can make sure it exists in the expected dataset, then we want to explore the structure of hover
. When I need to quickly explore the structure of objects at a point in time in the code and I can't easily step through and debug the script, using JSON.stringify()
to include content in the output is a quick and dirty solution:
<div>
<h3>${hover.NAME}</h3>
<div style="margin: 10px">
<h4>CountryCode : ${hover.ISO_A2} </h4>
<h4> ContinentCode : ${hover.CONTINENT} </h>
<h4>${parameter } : ${d3.format(",")(hover[p])}</h4>
<h4>Expected Property: ${p}</h4>
<smaller>${JSON.stringify(hover)}</smaller>
</div>
</div>
console.log()
is a better option if you need to capture the data rather than simply dumping it into the screen as content to visually inspect it.
It is clear that the original intent was to have the data available as part of the data feed available at that point, it is also clear that nothing has been done to merge these two datasets, so lets do that now.
My preference in this case is to use forEach
to iterate over countries
and modify each entry to include the entire matching record from the csv file. You could use map
but in this case we are already loading heaps of data into memory, we don't need to retain the unchanged version of the countries JSON as well as the new output that map
would have produced.
The other reason that I recommend injecting the data from the CSV into the array of countries is that we can avoid the overhead of a lookup when we need to access the data. Arranging the csv data and the country data into arrays of the same length and sorted so that the same indexes correlate to the same country in each file incurs the same over head as injecting the data and makes it more deterministic. It is also easier to support scenarios where one array has more values that the other.
All we need to know is the property mapping to identity the unique record in each file. We can see that the csv file is using 2 letter country codes:
country_id | CountryName | CountryCode | ContinentCode | CenterLongitude | CenterLatitude | areasqkm | population | airports | gdpgrowthrate | inflationrate | unemploymentrate | popnbelowpoverty | medianage |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
12 | Australia | AU | OC | 135.0 | -25.0 | 7741220 | 26141369 | 418 | 1.84 | 1.60 | 5.16 | 0.00 | 37.50 |
13 | Austria | AT | EU | 13.33 | 47.33 | 83871 | 8913088 | 50 | 1.42 | 1.50 | 7.35 | 13.30 | 44.50 |
114 | India | IN | AS | 77.0 | 20.0 | 3287263 | 1389637446 | 346 | 4.86 | 3.70 | 8.50 | 21.90 | 28.70 |
115 | Indonesia | ID | AS | 120.0 | -5.0 | 1904569 | 277329163 | 673 | 5.03 | 2.80 | 5.31 | 9.40 | 31.10 |
I have picked two pairs Australia, Austria and India, Indonesia for comparison as they cover common matching issues, Most countries have a 2 letter postal code, and it is common for people to pick this first, however India's postal code is 3 letters, Indonesia's is 4 and Austria have 1. I find this dataset particularly interesting for how close the name terms are lexicographically yet how different their unique codes are, so I always check with these specific cases first before running a test or proof across an entire dataset.
The feature property metadata in the json file has a lot of information, importantly, it also has the ISO_A2
value, which is the ISO standard 2 letter country code.
featurecla | scalerank | LABELRANK | SOVEREIGNT | SOV_A3 | ADM0_DIF | LEVEL | TYPE | TLC | ADMIN | ADM0_A3 | GEOU_DIF | GEOUNIT | GU_A3 | SU_DIF | SUBUNIT | SU_A3 | BRK_DIFF | NAME | NAME_LONG | BRK_A3 | BRK_NAME | BRK_GROUP | ABBREV | POSTAL | FORMAL_EN | FORMAL_FR | NAME_CIAWF | NOTE_ADM0 | NOTE_BRK | NAME_SORT | NAME_ALT | MAPCOLOR7 | MAPCOLOR8 | MAPCOLOR9 | MAPCOLOR13 | POP_EST | POP_RANK | POP_YEAR | GDP_MD | GDP_YEAR | ECONOMY | INCOME_GRP | FIPS_10 | ISO_A2 | ISO_A2_EH | ISO_A3 | ISO_A3_EH | ISO_N3 | ISO_N3_EH | UN_A3 | WB_A2 | WB_A3 | WOE_ID | WOE_ID_EH | WOE_NOTE | ADM0_ISO | ADM0_DIFF | ADM0_TLC | ADM0_A3_US | ADM0_A3_FR | ADM0_A3_RU | ADM0_A3_ES | ADM0_A3_CN | ADM0_A3_TW | ADM0_A3_IN | ADM0_A3_NP | ADM0_A3_PK | ADM0_A3_DE | ADM0_A3_GB | ADM0_A3_BR | ADM0_A3_IL | ADM0_A3_PS | ADM0_A3_SA | ADM0_A3_EG | ADM0_A3_MA | ADM0_A3_PT | ADM0_A3_AR | ADM0_A3_JP | ADM0_A3_KO | ADM0_A3_VN | ADM0_A3_TR | ADM0_A3_ID | ADM0_A3_PL | ADM0_A3_GR | ADM0_A3_IT | ADM0_A3_NL | ADM0_A3_SE | ADM0_A3_BD | ADM0_A3_UA | ADM0_A3_UN | ADM0_A3_WB | CONTINENT | REGION_UN | SUBREGION | REGION_WB | NAME_LEN | LONG_LEN | ABBREV_LEN | TINY | HOMEPART | MIN_ZOOM | MIN_LABEL | MAX_LABEL | LABEL_X | LABEL_Y | NE_ID | WIKIDATAID | NAME_AR | NAME_BN | NAME_DE | NAME_EN | NAME_ES | NAME_FA | NAME_FR | NAME_EL | NAME_HE | NAME_HI | NAME_HU | NAME_ID | NAME_IT | NAME_JA | NAME_KO | NAME_NL | NAME_PL | NAME_PT | NAME_RU | NAME_SV | NAME_TR | NAME_UK | NAME_UR | NAME_VI | NAME_ZH | NAME_ZHT | FCLASS_ISO | TLC_DIFF | FCLASS_TLC | FCLASS_US | FCLASS_FR | FCLASS_RU | FCLASS_ES | FCLASS_CN | FCLASS_TW | FCLASS_IN | FCLASS_NP | FCLASS_PK | FCLASS_DE | FCLASS_GB | FCLASS_BR | FCLASS_IL | FCLASS_PS | FCLASS_SA | FCLASS_EG | FCLASS_MA | FCLASS_PT | FCLASS_AR | FCLASS_JP | FCLASS_KO | FCLASS_VN | FCLASS_TR | FCLASS_ID | FCLASS_PL | FCLASS_GR | FCLASS_IT | FCLASS_NL | FCLASS_SE | FCLASS_BD | FCLASS_UA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Admin-0 country | 1 | 2 | Australia | AU1 | 1 | 2 | Country | 1 | Australia | AUS | 0 | Australia | AUS | 0 | Australia | AUS | 0 | Australia | Australia | AUS | Australia | Auz. | AU | Commonwealth of Australia | Australia | Australia | 1 | 2 | 2 | 7 | 25364307 | 15 | 2019 | 1396567 | 2019 | 2. Developed region: nonG7 | 1. High income: OECD | AS | AU | AU | AUS | AUS | 36 | 36 | 36 | AU | AUS | -90 | 23424748 | Includes Ashmore and Cartier Islands (23424749) and Coral Sea Islands (23424790). | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | AUS | -99 | -99 | Oceania | Oceania | Australia and New Zealand | East Asia & Pacific | 9 | 9 | 4 | -99 | 1 | 0 | 1.7 | 5.7 | 134.04972 | -24.129522 | 1159320355 | Q408 | أستراليا | অস্ট্রেলিয়া | Australien | Australia | Australia | استرالیا | Australie | Αυστραλία | אוסטרליה | ऑस्ट्रेलिया | Ausztrália | Australia | Australia | オーストラリア | 오스트레일리아 | Australië | Australia | Austrália | Австралия | Australien | Avustralya | Австралія | آسٹریلیا | Úc | 澳大利亚 | 澳大利亞 | Admin-0 country | Admin-0 country | ||||||||||||||||||||||||||||||||||||||
Admin-0 country | 1 | 4 | Austria | AUT | 0 | 2 | Sovereign country | 1 | Austria | AUT | 0 | Austria | AUT | 0 | Austria | AUT | 0 | Austria | Austria | AUT | Austria | Aust. | A | Republic of Austria | Austria | Austria | 3 | 1 | 3 | 4 | 8877067 | 13 | 2019 | 445075 | 2019 | 2. Developed region: nonG7 | 1. High income: OECD | AU | AT | AT | AUT | AUT | 40 | 40 | 40 | AT | AUT | 23424750 | 23424750 | Exact WOE match as country | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | AUT | -99 | -99 | Europe | Europe | Western Europe | Europe & Central Asia | 7 | 7 | 5 | -99 | 1 | 0 | 3 | 8 | 14.130515 | 47.518859 | 1159320379 | Q40 | النمسا | অস্ট্রিয়া | Österreich | Austria | Austria | اتریش | Autriche | Αυστρία | אוסטריה | ऑस्ट्रिया | Ausztria | Austria | Austria | オーストリア | 오스트리아 | Oostenrijk | Austria | Áustria | Австрия | Österrike | Avusturya | Австрія | آسٹریا | Áo | 奥地利 | 奧地利 | Admin-0 country | Admin-0 country | ||||||||||||||||||||||||||||||||||||||
Admin-0 country | 1 | 2 | India | IND | 0 | 2 | Sovereign country | 1 | India | IND | 0 | India | IND | 0 | India | IND | 0 | India | India | IND | India | India | IND | Republic of India | India | India | 1 | 3 | 2 | 2 | 1366417754 | 18 | 2019 | 2868929 | 2019 | 3. Emerging region: BRIC | 4. Lower middle income | IN | IN | IN | IND | IND | 356 | 356 | 356 | IN | IND | 23424848 | 23424848 | Exact WOE match as country | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | IND | -99 | -99 | Asia | Asia | Southern Asia | South Asia | 5 | 5 | 5 | -99 | 1 | 0 | 1.7 | 6.7 | 79.358105 | 22.686852 | 1159320847 | Q668 | الهند | ভারত | Indien | India | India | هند | Inde | Ινδία | הודו | भारत | India | India | India | インド | 인도 | India | Indie | Índia | Индия | Indien | Hindistan | Індія | بھارت | Ấn Độ | 印度 | 印度 | Admin-0 country | Admin-0 country | ||||||||||||||||||||||||||||||||||||||
Admin-0 country | 3 | 2 | Indonesia | IDN | 0 | 2 | Sovereign country | 1 | Indonesia | IDN | 0 | Indonesia | IDN | 0 | Indonesia | IDN | 0 | Indonesia | Indonesia | IDN | Indonesia | Indo. | INDO | Republic of Indonesia | Indonesia | Indonesia | 6 | 6 | 6 | 11 | 270625568 | 17 | 2019 | 1119190 | 2019 | 4. Emerging region: MIKT | 4. Lower middle income | ID | ID | ID | IDN | IDN | 360 | 360 | 360 | ID | IDN | 23424846 | 23424846 | Exact WOE match as country | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | IDN | -99 | -99 | Asia | Asia | South-Eastern Asia | East Asia & Pacific | 9 | 9 | 5 | -99 | 1 | 0 | 1.7 | 6.7 | 101.892949 | -0.954404 | 1159320845 | Q252 | إندونيسيا | ইন্দোনেশিয়া | Indonesien | Indonesia | Indonesia | اندونزی | Indonésie | Ινδονησία | אינדונזיה | इंडोनेशिया | Indonézia | Indonesia | Indonesia | インドネシア | 인도네시아 | Indonesië | Indonezja | Indonésia | Индонезия | Indonesien | Endonezya | Індонезія | انڈونیشیا | Indonesia | 印度尼西亚 | 印度尼西亞 | Admin-0 country | Admin-0 country |
there are lots of different ways to do this, I'm just going to find
the matching record and stuff it into a new property called csv
using a verbose method to make it easy to follow:
// Convert the TopoJSON to GeoJSON
countries = {
const geo = topojson.feature(map_layer, map_layer.objects.countries);
geo.features.forEach(feature => {
if(feature.geometry) {
feature.centroid = centroid(feature);
}
let code = feature.properties.ISO_A2;
let cData = country_data.find(x => x.CountryCode === code);
feature.properties.csv = cData ?? {
areasqkm: "",
population: "",
airports: "",
gdpgrowthrate: "",
inflationrate: "",
unemploymentrate: "",
popnbelowpoverty: "",
medianage: ""
};
return feature;
});
return geo;
}
Now the data is available, including a null record to simplify error handling later. Lets add this to the tooltip content first, to verify that it works, i'll dump all the extended data just so we can explore the different values:
<div>
<h3>${hover.NAME}</h3>
<div style="margin: 10px">
<h4>CountryCode : ${hover.ISO_A2} </h4>
<h4> ContinentCode : ${hover.CONTINENT} </h>
<h4>${parameter } : ${d3.format(",")(hover.csv[p])}</h4>
<hr style="padding:0px"/>
<smaller>
Area Sqkm: ${d3.format(",")(hover.csv['areasqkm'])}<br/>
Population: ${d3.format(",")(hover.csv['population'])}<br/>
Airports: ${hover.csv['airports']}<br/>
GDP Growth Rate: ${d3.format(",")(hover.csv['gdpgrowthrate'])}<br/>
Inflation Rate: ${d3.format(",")(hover.csv['inflationrate'])}<br/>
Unemployment Rate: ${d3.format(",")(hover.csv['unemploymentrate'])}<br/>
Pop. Below Poverty: ${d3.format(",")(hover.csv['popnbelowpoverty'])}<br/>
Median Age: ${hover.csv['medianage']}<br/>
<smaller>
</div>
</div>
Now you just need to apply those property mapping paths to your bubble logic, I'm actually going to leave that logic out here, the main issue was how to make the data available, there are practicality issues to using the direct value to render proportional shapes. Very quickly you end up with some items so small you can't click or see them, other shapes will have such large discrepancies that functions like forceCollide()
will just make a mess... have a look at zimbabwe's inflation rate in this bubble map:
https://observablehq.com/@chrisschaller/worldbubblemap
It is often more practical to map values to specific size domains, a scale of 1-5 is usually enough but you can go to 10, where 1 is the smallest on the map, and 10 the largest, then trim the data range into standard deviations so that outliers below all get 1 and outliers above all get the same max.
The techniques and reasoning are well outside of this discussion space.