Public
Edited
Jun 11, 2024
Insert cell
Insert cell
chart = htl.html`
${titleCard1}
${legendOnly}
${chartOnly}
`
Insert cell
titleCard1 = titleCard({
title: "European union share of global tropical beef deforestation",
subtitle:
"Total tropical beef deforestation from global imports to EU countries and share of global deforestation from global cattle imports in relation to EU imports (source: Pendrill et al., 2022).",
width: 1
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
lineCols = [
{ name: "Global share (%)", key: "global_share" }
]
Insert cell
data = {
const rows = [];
for (const col of lineCols) {
rows.push(
dataRaw.map((d) => {
const obj = {};
obj[dateCol] = d[dateCol];
obj["share"] = d[col.key];
obj["metric"] = col.name;
obj[barCol] = d["total_EU"];
return obj;
})
);
}
return rows.flat();
}
Insert cell
valueCol = "share"
Insert cell
keyCol = "metric"
Insert cell
dateCol = "year"
Insert cell
barCol = "total_EU"
Insert cell
valueTitle = "Deforestation"
Insert cell
valueUnits = "%"
Insert cell
valueTitleBars = "Beef deforestation"
Insert cell
valueUnitsBars = "ha"
Insert cell
showValueLabels = true
Insert cell
showValueLabelsBars = true
Insert cell
keyLabelFont = "bold 12px var(--trase-sans-serif)"
Insert cell
dateFormat = "%Y"
Insert cell
dateFormatIn = "%Y-%m-%d" // what is the format of dates in your input dataset?
Insert cell
parseDate = (d) => {
if (typeof d.getMonth === "function") {
return d;
} // is already a date
if (String(d)?.length === 4) {
return d3.utcParse(dateFormatIn)(`${d}-01-01`);
} // year without month / day
return d3.utcParse(dateFormatIn)(d); // parse the date
}
Insert cell
typeof data[0][dateCol].getMonth === "function"
Insert cell
biggestKeyLabel = d3.max([
...keys.map((d) => {
return getLabelLength(d, "font: " + keyLabelFont);
}),
...data.map((d) => {
return getLabelLength(
`${formatTooltipValue(Number(d[valueCol]))} ${
valueUnits ? valueUnits.toUpperCase() : ""
}`,
"font: 11px var(--trase-mono)"
);
})
])
Insert cell
keys = d3
.rollups(
data,
(v) => d3.sum(v, (d) => Number(d[valueCol])),
(d) => d[keyCol]
)
.sort(([, a], [, b]) => b - a)
.map(([key]) => key)
Insert cell
values = d3.rollup(
data,
([d]) => Number(d[valueCol]),
(d) => d[keyCol],
(d) => +parseDate(d[dateCol])
)
Insert cell
dates = [...new Set(data.map((d) => parseDate(d[dateCol]).getTime()))]
.map(d3.utcDay)
.sort((a, b) => a - b)
Insert cell
series = keys.map((k) => ({
key: k,
values: dates.map((d) => values.get(k).get(+d))
}))
Insert cell
barWidth = xBar.bandwidth()
Insert cell
barPaddingWidth = xBar.bandwidth() * barPadding
Insert cell
barAndWidthCenter = barWidth / 2 + barPaddingWidth
Insert cell
x = d3
.scaleUtc()
.domain(d3.extent(dates))
.range([
margin.left + barAndWidthCenter,
width - margin.right - barAndWidthCenter
])
Insert cell
barPadding = 0.1
Insert cell
xBar = d3
.scaleBand()
.domain(data.map((d) => d[dateCol]))
.rangeRound([margin.left, width - margin.right])
.padding(barPadding)
Insert cell
y = d3
.scaleLinear()
.domain([0, d3.max(series.flatMap((d) => d3.extent(d.values)))])
.nice()
.range([height - margin.bottom, margin.top])
Insert cell
yBar = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d[barCol])])
.nice()
.rangeRound([height - margin.bottom, margin.top])
Insert cell
line = d3
.line()
.defined((d) => !isNaN(d))
.x((d, i) => x(dates[i]))
.y((d) => y(d))
Insert cell
line(series[0].values)
Insert cell
series[0].values
Insert cell
xAxisTicks = Math.min(dates.length, width / 50)
Insert cell
xAxis = (g) =>
g
.attr("transform", `translate(0,${height - margin.bottom})`)
.style("font", "bold 12px var(--trase-mono)")
.style("color", traseColours.get("moss"))
.attr("stroke-width", 1.5)
.call(
d3
.axisBottom(xBar)
.ticks(width / 50)
.tickSizeOuter(0)
)
.call((g) => g.selectAll(".tick line").remove())
.call((g) =>
g
.selectAll(".tick text")
.attr("y", 15)
.attr("x", 15)
.attr("dy", ".45em")
.attr("transform", "rotate(360)")
.attr("text-anchor", "end")
)
Insert cell
yAxis = (g) =>
g
.attr("transform", `translate(${width - margin.right},0)`)
.style("font", "12px var(--trase-mono)")
.style("color", traseColours.get("moss"))
// .call(d3.axisRight(y).ticks(Math.max(height / 100, 2), formatY))
.call(d3.axisRight(y).ticks(0, formatY))
.call((g) => g.selectAll(".tick line").remove())
.call((g) => g.select(".domain").remove())
.call((g) => {
if (valueTitle) {
g.select(".tick:last-of-type text")
.clone()
.attr("x", 0)
.attr("text-anchor", "end")
.text(
`${valueTitle.toUpperCase()} ${
valueUnits ? `(${valueUnits.toUpperCase()})` : ""
}`
);
}
})
Insert cell
yAxisBars = (g) =>
g
.attr("transform", `translate(${margin.left},0)`)
.style("font", "12px var(--trase-mono)")
.style("color", traseColours.get("moss"))
// .call(d3.axisLeft(yBar).ticks(Math.max(height / 100, 2), formatY))
.call(d3.axisLeft(yBar).ticks(0, formatY))
.call((g) => g.selectAll(".tick line").remove())
.call((g) => g.select(".domain").remove())
.call((g) => {
if (valueTitleBars) {
g.select(".tick:last-of-type text")
.clone()
.attr("x", 0)
.attr("text-anchor", "start")
.text(
`${valueTitleBars.toUpperCase()} ${
valueUnitsBars ? `(${valueUnitsBars.toUpperCase()})` : ""
}`
);
}
})
Insert cell
grid = (g) =>
g
.attr("transform", `translate(${margin.left},0)`)
// .call(d3.axisLeft(y).ticks(height / 100, formatY))
.call(d3.axisLeft(y).ticks(1, formatY))
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll(".tick text").remove())
.call((g) =>
g
.selectAll(".tick line")
.attr("stroke", traseColours.get("lightgrey"))
.attr("x1", (d, i, g) =>
i === g.length - 1 && valueTitle
? getLabelLength(
valueTitle + valueUnits,
"font: 12px var(--trase-mono)"
) + 5
: 0
)
.attr("x2", width - margin.left - margin.right)
)
Insert cell
colour = d3
.scaleOrdinal()
.domain(keys)
// .range(traseCategory1)
.range([/*traseColours.get("darkred"),*/ traseColours.get("red")])
Insert cell
barColor = "#8ba194"
Insert cell
colourLegend = d3
.scaleOrdinal()
.domain([valueTitleBars, ...keys])
// .range(traseCategory1)
.range([barColor, /*traseColours.get("darkred"),*/ traseColours.get("red")])
Insert cell
keys
Insert cell
numberFormat = ",.2r"
Insert cell
formatX = d3.utcFormat(dateFormat)
Insert cell
formatY = d3.format(numberFormat)
Insert cell
annotate = g => {}
Insert cell
// adapted from https://observablehq.com/@d3/multi-line-chart
function hover(g, svg) {
// create dot
const dot = svg.append("g").attr("display", "none");
dot
.append("circle")
.attr("r", 6)
.attr("stroke-width", 3)
.attr("fill", "white");

// add tooltip
const tooltip = new Tooltip();
tooltip.node.style = "transition: all 200ms";
svg
.style("-webkit-tap-highlight-color", "transparent")
.style("cursor", "pointer")
.on("touchstart", (event) => event.preventDefault())
.on("pointerenter pointermove", enter)
.on("pointerout", exit);
svg.append(() => tooltip.node);

function enter(event) {
event.preventDefault();
const pointer = d3.pointer(event, this);
const xm = x.invert(pointer[0]);
const ym = y.invert(pointer[1]);
const i1 = d3.bisectLeft(dates, xm);
const i0 = i1 - 1;
const i = xm - dates[i0] > dates[i1] - xm ? i1 : i0;
const s = d3.least(series, (d) => Math.abs(d.values[i] - ym));
let xp = x(dates[i]);
let yp = y(s.values[i]);
g.attr("stroke", (d) =>
// d === s ? colour(d.key) : traseColours.get("lightgrey")
colour(d.key)
)
.filter((d) => d === s)
.raise();
dot
.attr("display", null)
.attr("stroke", colour(s.key))
.attr("transform", `translate(${xp},${yp})`);
// tooltip.show(
// s.key,
// tooltipKeyValue(
// formatTooltipKey(dates[i]),
// `${formatTooltipValue(s.values[i])} ${valueUnits ? valueUnits : ""}`
// )
// );
tooltip.show(s.key, tooltipKeyValueMultiple(s.key, dates[i], i));
xp += 10;
if (xp + tooltip.width > width) xp -= tooltip.width + 20;
if (yp + tooltip.height > height - margin.bottom)
yp = height - margin.bottom - tooltip.height - 10;
tooltip.position(xp, yp);
}

function exit() {
// g.style("mix-blend-mode", "multiply").attr("stroke", ({ key }) =>
// colour(key)
// );
dot.attr("display", "none");
tooltip.hide();
}
}
Insert cell
`${series.map((s, i) => {
return `
<circle cx="5" cy="-5" r="5" transform="translate(0, ${47 * i})" fill="${colour(
s.key
)}" />
`;
})}`
Insert cell
tooltipKeyValueMultiple = (valueName, year, yearIndex) => {
const yearFormatted = d3.utcFormat(dateFormat)(year);
const marginLeft = 17;
return svg`
${series.map((s, i) => {
return svg`
<circle cx="5" cy="-5" r="5" transform="translate(0, ${47 * i})" fill="${colour(
s.key
)}" />
`;
})}
<circle cx="5" cy="-5" r="5" transform="translate(0, 47)" fill=${barColor} />
<text>
<tspan font-size="12px" fill="#839095" x="${marginLeft}" dy="0em">${series[0].key?.toUpperCase()}
</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="${marginLeft}" dy="1.4em">${formatTooltipValue(
series[0].values[yearIndex]
)} ${valueUnits}</tspan>



<tspan font-size="12px" fill="#839095" x="${marginLeft}" dy="2.2em">${valueTitleBars?.toUpperCase()}</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="${marginLeft}" dy="1.4em">${formatTooltipValueBars(
data.find((x) => String(x[dateCol]) === String(yearFormatted))[barCol]
)} ha </tspan>
</text>`;
}

/* <tspan font-size="12px" fill="#839095" x="${marginLeft}" dy="2.2em">${series[1].key?.toUpperCase()}</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="${marginLeft}" dy="1.4em">${formatTooltipValue(
series[1].values[yearIndex]
)} ${valueUnits}</tspan>*/
Insert cell
data
Insert cell
function hoverBars(bars, svg) {
const tooltip = new Tooltip();
bars
.style("cursor", "pointer")
.on("touchstart", (event) => event.preventDefault())
.on("pointerenter", (event, d) => {
tooltip.position(
...tooltipOffset(tooltip, d3.pointer(event), width, height)
);
tooltip.show(
valueTitleBars,
tooltipKeyValue(
valueTitleBars.toUpperCase(),
formatTooltipValue(d[1] - d[0]),
d
)
);
})
.on("pointermove", function (event) {
let [x, y] = tooltipOffset(tooltip, d3.pointer(event), width, height);
tooltip.position(x, y);
this.releasePointerCapture(event.pointerId);
})
.on("pointerout", () => tooltip.hide());
svg.append(() => tooltip.node);
}
Insert cell
formatTooltipKey = d3.utcFormat(dateFormat)
Insert cell
formatTooltipValue = d3.format(".2f")
Insert cell
formatTooltipValueBars = d3.format(",.2r")
Insert cell
import {
traseColours,
traseCategory1,
fonts,
traseReds
} from "@trase/visual-id@1366"
Insert cell
import { swatches, getLabelLength } from "@trase/legends-2-0"
Insert cell
import { Tooltip, tooltipKeyValue, tooltipOffset } from "@trase/tooltip@440"
Insert cell
import { titleCard } from "@trase/title-card"
Insert cell
d3 = require("d3@6")
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