Published
Edited
Feb 4, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = BarChart(data, {
x: (d) => d.value,
y: (d) => d.country_name,
yDomain: d3.groupSort(
data,
([d]) => -d.value,
(d) => d.country_name
), // sort by descending frequency
width: myWidth,
height,
color: "steelblue",
marginLeft: 120,
marginTop: 50,
avgDatum: avgEu
})
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/horizontal-bar-chart
function BarChart(
data,
{
x = (d) => d, // given d in data, returns the (quantitative) x-value
y = (d, i) => i, // given d in data, returns the (ordinal) y-value
title, // given d in data, returns the title text
marginTop = 30, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 10, // the bottom margin, in pixels
marginLeft = 1, // the left margin, in pixels
width = 640, // the outer width of the chart, in pixels
height, // outer height, in pixels
xType = d3.scaleLinear, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
xFormat, // a format specifier string for the x-axis
xLabel, // a label for the x-axis
yPadding = 0.1, // amount of y-range to reserve to separate bars
yDomain, // an array of (ordinal) y-values
yRange, // [top, bottom]
color = "currentColor", // bar fill color
titleColor = "white", // title fill color when atop bar
titleAltColor = "currentColor", // title fill color when atop background
avgDatum // Percentage wher line is drawn
} = {}
) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);

// Compute default domains, and unique the y-domain.
if (xDomain === undefined) xDomain = [0, d3.max(X)];
if (yDomain === undefined) yDomain = Y;
yDomain = new d3.InternSet(yDomain);

// Omit any data not present in the y-domain.
const I = d3.range(X.length).filter((i) => yDomain.has(Y[i]));

// Compute the default height.
if (height === undefined)
height =
Math.ceil((yDomain.size + yPadding) * 25) + marginTop + marginBottom;
if (yRange === undefined) yRange = [marginTop, height - marginBottom];

// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = d3.scaleBand(yDomain, yRange).padding(yPadding);
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);

// Compute titles.
if (title === undefined) {
const formatValue = xScale.tickFormat(100, xFormat);
title = (i) => `${formatterNumber(formatValue(X[i]))}`;
} else {
const O = d3.map(data, (d) => d);
const T = title;
title = (i) => T(O[i], i, data);
}

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

svg
.append("g")
.attr("class", "rects")
.attr("fill", color)
.selectAll("rect")
.data(I)
.join("rect")
.attr("x", xScale(0))
.attr("y", (i) => yScale(Y[i]))
.attr("width", (i) => xScale(X[i]) - xScale(0))
.attr("height", yScale.bandwidth())
.classed("myCountry", (i) => data[i].country_code === myCountry)
.on("mouseover", (el, i) => {
d3.select(el.target).classed("hover", true);
d3.select(".labels")
.selectAll("text")
.filter((idNode) => i === idNode)
.classed("visible", true);
d3.select(".y.axes")
.selectAll(".tick")
.filter((countryName) => countryName === data[i].country_name)
.classed("visible", true);
})
.on("mouseout", (el, i) => {
d3.select(el.target).classed("hover", false);
d3.select(".labels")
.selectAll("text")
.filter((idNode) => i === idNode)
.classed("visible", false);

d3.select(".y.axes").selectAll(".tick").classed("visible", false);
});

svg
.append("g")
.attr("class", "labels")
.attr("fill", titleColor)
.attr("text-anchor", "end")
.attr("font-size", 10)
.selectAll("text")
.data(I)
.join("text")
.attr("x", (i) => xScale(X[i] - 3))
.attr("y", (i) => yScale(Y[i]) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("dx", -4)
.text(title)
.classed("myCountry", (i) => data[i].country_code === myCountry)
.call((text) =>
text
.filter((i) => xScale(X[i]) - xScale(0) < 20) // short bars
.attr("dx", +4)
.attr("fill", titleAltColor)
.attr("text-anchor", "start")
);

svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.attr("class", "y axes")
.call(yAxis)
.selectAll(".tick")
.filter((countryName, i) => {
const countryObj = data.filter((d) => d.country_name === countryName)[0];
return countryObj.country_code === myCountry;
})
.classed("myCountry", true);

svg
.append("g")
.attr("class", "avgLine")
.append("line")
.attr("x1", xScale(avgDatum))
.attr("x2", xScale(avgDatum))
.attr("y1", 22)
.attr("y2", height - marginBottom)
.attr("stroke", "black")
.attr("stroke-dasharray", "5, 5");

svg
.select(".avgLine")
.append("text")
.attr("x", xScale(avgDatum) + 3)
.attr("y", 20)
.text(locale[languageSelector].avgLine(formatterNumber(avgEu)))
.attr("text-anchor", "end");

svg
.append("rect")
.attr("x", xScale(30) - 10)
.attr("y", 30)
.attr("width", 20)
.attr("height", 15)
.attr("fill", "white");

svg
.append("g")
.attr("transform", `translate(0,${marginTop})`)
.attr("class", "x axes")
.call(xAxis)
.call((g) => g.select(".domain").remove())
.call((g) =>
g
.selectAll(".tick line")
.clone()
.attr("y2", height - marginTop - marginBottom)
.attr("stroke-opacity", 0.1)
)
.call((g) =>
g
.append("text")
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel)
);

return svg.node();
}
Insert cell
Insert cell
height = isMobile ? 600 : 600
Insert cell
maxWidth = 320
Insert cell
mediaQueryLimit = 600
Insert cell
myWidth = deviceSelection === "desktop" ? +Math.min(width, maxWidth) : 320 // 👈🏻 Just for testing
Insert cell
isMobile = myWidth < mediaQueryLimit
Insert cell
Insert cell
Insert cell
// Reference: https://observablehq.com/@planemad/mapbox-static-image-generator
/*geoIp = await fetch("https://freegeoip.app/json/")
.then((res) => res.json())
.catch(() => null)*/
Insert cell
Insert cell
// Trying another IP service, as the previous one failed due to a CORS problem at some point after publishing the article
geoIp2 = await fetch("https://freegeoip.live/json/")
.then((res) => res.json())
.catch(() => null)
Insert cell
Insert cell
Insert cell
myCountry = {
if (geoIp2) {
const geoIpDetected = geoIp2.country_code;
const isOnTheMap = myCountries.includes(geoIpDetected);

// If the country detected is not in the map => mark our default country
const myIPCountry = isOnTheMap ? geoIpDetected : defaultCountry;
return myIPCountry;
} else {
// If the IP fetch fails, returns our default country
return defaultCountry;
}
}
Insert cell
Insert cell
Insert cell
data = d3.csv(
`https://data.civio.es/medicamentalia/access-ART/context_mean-age-women_eu.csv?q=${currentTimestamp}`,
(d) => {
if (d["2019_data"] !== ":" && d["2019_data"] !== "n/a") {
return {
country_name: locale[languageSelector].countries[d.country_code],
country_code: d.country_code,
value: +d["2019_data"].replace(",", ".")
};
}
}
)
Insert cell
Insert cell
Insert cell
Insert cell
styles = html`
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap" rel="stylesheet">
<style>
svg, #title {
font-family: 'Lato', sans-serif;
}
svg {
font-size: 13px;
}
g path {
display: none;
}
.y.axes line {
display: none;
}
.y.axes text {
opacity: 0.7;
font-size: 12px;
}
.x.axes line {
opacity: 0.8;
}
.rects rect {
opacity: 0.6;
fill: #08A6BF;
}
rect.myCountry, .labels text.myCountry, .labels text.visible, rect.hover, .y.axes text.hover {
display: block;
opacity: 0.9;
font-weight: bold;
}
.labels text {
display: none;
pointer-events: none;
font-size: 13px;
}
.avgLine {
pointer-events: none;
}
.avgLine line.visible {
opacity: 1;
}
#title {
font-size: 1.25rem;
text-align: center;
margin-top: 0;
}
.tick.visible text, .tick.myCountry text {
opacity: 1;
font-weight: bold;
}
</style>
`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more