Skip to content

Instantly share code, notes, and snippets.

@badosa
Last active April 29, 2022 08:04
Show Gist options
  • Save badosa/45855280509edd73300af7240de3524a to your computer and use it in GitHub Desktop.
Save badosa/45855280509edd73300af7240de3524a to your computer and use it in GitHub Desktop.
EU Dashboard

This example tries to show how to address common issues when retrieving data from Eurostat's API.

  • Not all the data you want to combine belong to the same dataset: Several API queries are required. fetch, await and async to the rescue.

  • Each country has time series of different length: They must be trimmed individually to keep as much information as possible.

  • Data must be analyzed in a time but also in a space dimension: This is achieved using two external dataviz libraries: jQuery Sparklines and Idescat Visual. This example is not about a particular visualization library. You can use others. The point here is that in many scenarios you will need to use several libraries (in the example, one for sparklines and one for maps). Which leads us to the next issue:

  • Data must be sliced and diced to meet external libraries requirements: The JSON-stat Toolkit allows you to transform, extract and adapt the data to any library interface.

See also GDP per capita in Europe.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>EU Dashboard</title>
<link href="https://fonts.googleapis.com/css?family=Lato&display=swap" rel="stylesheet" />
<link href="/d/45855280509edd73300af7240de3524a/styles.css" rel="stylesheet" />
<!-- DO NOT DO THIS IN PRODUCTION -->
<!-- github.io IS NOT A CDN -->
<link href="https://visual.js.org/visual.css" rel="stylesheet" type="text/css" />
<script src="https://visual.js.org/lazyvisualsetup.js"></script>
<!-- /DO NOT DO THIS IN PRODUCTION -->
<script src="https://cdn.jsdelivr.net/npm/jsonstat@0.13.13"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-sparkline@2.4.0/jquery.sparkline.min.js"></script>
<script>
/* Include your preferred indicators here */
const sources=[
{
id: "gdp",
label: "GDP per capita (€)",
descr: "Gross domestic product at current market prices, euro per capita",
dataset: "tec00001?na_item=B1GQ&precision=1&unit=CP_EUR_HAB",
isPercent: false
},
{
id: "prices",
label: "HICP (2015=100)",
descr: "Harmonised Indices of Consumer Prices, annual average index",
dataset: "tec00027?precision=1&unit=INX_A_AVG&coicop=CP00",
isPercent: false
},
{
id: "employ",
label: "Employment rate (%)",
descr: "Total employment (resident population concept) from 20 to 64 year, percentage of total population",
dataset: "tesem010?precision=1&sex=T&indic_em=EMP_LFS&unit=PC_POP&age=Y20-64",
isPercent: true
},
{
id: "unemploy",
label: "Unemployment rate (%)",
descr: "Unemployed persons aged 15 to 74, percentage of active population",
dataset: "tps00203?precision=1&sex=T&unit=PC_ACT&age=Y15-74",
isPercent: true
},
{
id: "berd",
label: "BERD (€/inh.)",
descr: "Business expenditure on R&D, euro per inhabitant",
dataset: "rd_e_berdindr2?precision=1&unit=EUR_HAB&nace_r2=TOTAL",
isPercent: false
},
{
id: "internet",
label: "Internet access (% of hh.)",
descr: "Percentage of households with internet access",
dataset: "isoc_ci_in_h?hhtyp=TOTAL&precision=1&unit=PC_HH",
isPercent: true
},
{
id: "ecommerce",
label: "Online purchases (% of indv.)",
descr: "Last online purchase in the last 3 months, percentage of individuals",
dataset: "isoc_ec_ibuy?ind_type=IND_TOTAL&precision=1&unit=PC_IND&indic_is=I_BUY3",
isPercent: true
},
{
id: "airpol",
label: "Greenhouse gases (grams/€)",
descr: "Grams per euro (current prices value added, gross)",
dataset: "env_ac_aeint_r2?airpol=GHG&na_item=B1G&precision=1&unit=G_EUR_CP&nace_r2=TOTAL",
isPercent: false
}
];
</script>
</head>
<body>
<main></main>
<div id="visual" class="visual">
<p id="msg">Click on an indicador&rsquo;s title to see its distribution by EU countries. Then click on a map area to see the indicador&rsquo;s sparkline for the selected country. To retrieve the EU sparkline, click on the country (brown) code.</p>
</div>
<script>
if(typeof fetch!=="function"){
window.alert("Sorry, a modern browser is required!");
}
const
main=document.querySelector("main"),
sparkOptions={lineColor: "#548ac3", width: "150px", height: "60px", fillColor: false, lineWidth: 2, spotRadius: 3, highlightSpotColor: "#000", spotColor: "#900", maxSpotColor: "#900", minSpotColor: "#900", highlightLineColor: "#c00"}
;
function nonNull(d){
const
values=d.map(e=>e.value),
len=values.length,
first=values.findIndex(e=>e!==null);
last=len-values.reverse().findIndex(e=>e!==null),
filtered=d.slice(first, last)
;
return {
time: filtered.map(e=>e.time),
value: filtered.map(e=>e.value)
};
}
function indic(i, country){
const
source=sources[i],
id=source.id,
ds=source.jsonstat,
series=nonNull( ds.toTable({type: "arrobj", content: "id"}).filter(e=>e.geo===country) ),
first=series.time.slice(0,1)[0],
last=series.time.slice(-1)[0],
geo=ds.Dimension("geo"),
countries=geo.id,
data=series.value,
$id=$("#"+id),
$ref=$id.children(".ref"),
change=(function(d){
const
two=d.slice(-2),
dif=Math.abs(two[1]-two[0]),
plus=two[1]<two[0] ? "↓" : "↑"
;
$id.addClass(plus==="↑" ? "up" : "down");
return (source.isPercent) ? "<strong>"+plus+"</strong>" + dif.toFixed(1) : "<strong>"+plus+"</strong>" + ((dif*100)/two[0]).toFixed(1) + " %";
}(data))
;
$id.children(".change").html( change );
$id.children(".value").text(data.slice(-1).toLocaleString("en-EN"));
$id.children(".viz").sparkline(data, sparkOptions);
$id.children(".ref").html('<abbr title="'+geo.Category(country).label+'">' + (country==="EU27_2020"? "EU27" : country) + "</abbr> " + first + "&ndash;" + last);
if(country!=="EU27_2020"){
const callback=function(){
indic(i, "EU27_2020");
$ref
.removeClass("changed")
.attr("tabindex", "-1")
;
};
$ref
.addClass("changed")
.attr("tabindex", "0")
.trigger("focus")
.click(callback)
.keypress(e=>{
if(e.which===13){
callback();
}
})
;
}
const callback=function(){
if(!$id.hasClass("sel")){
$(".indic.sel").removeClass("sel");
$id.addClass("sel");
visual({
fixed: [450, 525],
lang: "en",
legend: true,
range: 0,
type: "cmap",
by: "eu27",
data: ds.Data( { "time": last } ).map( (e, i)=>({ id: countries[i], val: e.value })),
click: function(area){
indic(i, area.id);
}
})
}
};
$id.children("h2")
.text(source.label)
.attr("title", source.descr)
.click(callback)
.keypress(e=>{
if(e.which===13){
callback();
}
})
;
}
sources.forEach(async (source, i)=>{
main.innerHTML=main.innerHTML+'<div class="indic" id="'+source.id+'"><h2 tabindex="0"></h2><span class="viz"></span><span class="value"></span><p class="change"></p><p class="ref"></p></div>';
const
service="https://ec.europa.eu/eurostat/wdds/rest/data/v2.1/json/en/",
time="&lastTimePeriod=10",
geo="&geo=AT&geo=BE&geo=BG&geo=CY&geo=CZ&geo=DE&geo=DK&geo=EE&geo=EL&geo=ES&geo=FI&geo=FR&geo=HR&geo=HU&geo=IE&geo=IT&geo=LT&geo=LU&geo=LV&geo=MT&geo=NL&geo=PL&geo=PT&geo=RO&geo=SE&geo=SI&geo=SK&geo=EU27_2020",
res=await fetch(service+source.dataset+time+geo),
json=await res.json()
;
source.jsonstat=JSONstat(json);
indic(i, "EU27_2020");
});
</script>
</body>
</html>
body {
text-align: center;
padding-top: 2px!important;
}
main {
display: inline-block;
width: 500px;
vertical-align: top;
column-count: 2;
column-gap: 4px;
}
#visual, #msg {
display: inline-block;
width: 410px;
height: 525px;
margin: 0;
}
#msg {
display: table-cell;
text-align: left;
vertical-align: middle;
line-height: 2;
padding: 0 16px;
font-size: 190%;
}
.indic {
text-align: center;
font-family: 'Lato', sans-serif;
margin-bottom: 4px;
padding-bottom: 2px;
background-color: #f2f2f2;
min-height: 118px;
break-inside: avoid;
border: 1px solid #444;
border-top: none;
border-radius: 10px;
}
.indic h2 {
font-size: 12px;
margin: 0 0 8px 0;
padding: 4px;
color: #fff;
font-weight: normal;
background-color: #444;
cursor: pointer;
min-height: 15px;
outline: none;
border-radius: 10px 10px 0 0;
}
.indic h2:focus{
color: yellow;
}
.indic.sel {
background-color: #ecf2f8;
border: 1px solid #294d74;
border-top: none;
}
.indic.sel h2 {
background-color: #294d74;
}
.indic p {
margin: -9px 0 0 0;
}
.indic p.ref {
font-size: 12px;
margin-top: -1px;
outline: none;
}
.change {
font-size: 110%;
font-weight: bold;
}
.change strong {
padding-right: 2px;
padding-top: 0;
}
.value {
font-size: 14px;
}
.changed {
color: #900;
cursor: pointer;
}
p.ref.changed:focus {
color: #294d74;
}
abbr[title] {
text-decoration: none;
}
.viz {
padding-right: 4px;
}
#jqstooltip {
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0,0,0,.2);
padding: 4px 0 6px 4px;
background-color: #f6f6f6;
opacity: 0.90;
}
#jqstooltip .jqsfield {
color: #000;
font-weight: bold;
font-size: 11px;
font-family: Verdana,Arial,Helvetica,sans-serif;
}
#jqstooltip .jqsfield span {
display: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment