Unlisted
Edited
Feb 16, 2024
Insert cell
Insert cell
chart = htl.html`
${titleCard1}
${chartLegend}
${chartOnly}
`
Insert cell
titleCard1 = titleCard({
title:
"Indonesia, China and India are the primary markets for Indonesian palm oil",
subtitle:
"Due to their large demand for Indonesian palm oil, and their relatively high deforestation-risk intensity, these three markets purchased palm oil representing 60% of Indonesian palm oil's deforestation risk in 2018-2020 (Source: Trase Supply Chains).",
extra: [viewof selected_value, viewof bubbleYearSelect],
footnote:
"Unknown country in 2020 is associated with approximately 2.2 million tons of CPO-eq palm oil missing from the trade data",
width: 1
})
Insert cell
chartLegend = legend({
colour: d3.scaleThreshold([...thresholds], colourSchemeRev),
title: "Share of production covered by ZDC exporters"
})
Insert cell
chartOnly = {
const id = DOM.uid("chart").id;
const svg = d3
.create("svg")
.attr("class", id)
.attr("viewBox", [0, 0, width, height])
.call((svg) => svg.append("defs").html(fonts(`.${id}`)));

const g = svg.append("g").attr("text-anchor", "middle");

let text = g.selectAll("text");

const tooltip = new Tooltip();
svg.append(() => tooltip.node);

const t = svg.transition().duration(800);

const root = pack(
d3
.hierarchy({
children: data.sort((a, b) => b[selected_value] - a[selected_value])
})
.sum((d) => d[selected_value])
);

const thresholds = [0.005, 0.025, 0.05, 0.1, 0.8].map((d) =>
Math.floor(d * d3.max(data, (d) => d[selected_value]))
);

const colour = d3.scaleThreshold().domain(thresholds).range(colourScheme);

// let circles = g.selectAll("circle");
// circles = circles
// .data(root.leaves(), (d) => d.data.key)
// .join(
// (enter) =>
// enter
// .append("circle")
// .attr("cx", (d) => d.x)
// .attr("cy", (d) => d.y)
// .attr("r", (d) => d.r)
// .attr("fill", (d) => colourSchemeZdc(d))
// // (update) =>
// // update.call((update) =>
// // update
// // .transition(t)
// // .attr("cx", (d) => d.x)
// // .attr("cy", (d) => d.y)
// // .attr("r", (d) => d.r)
// // .attr("fill", (d) => colourSchemeZdc(d))
// // ),
// // (exit) => exit.remove()
// );

let circles = g
.selectAll("circle")
.data(root.leaves(), (d) => d.data.key)
.join("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("r", (d) => d.r)
.attr("fill", (d) => colourSchemeZdc(d));
// .on("mouseout", () => tooltip.hide())
// .on("mouseover", (d) => tooltip.show(d.data.key, d))
// .on("mousemove", function () {
// let [xm, ym] = d3.clientPoint(this.closest("svg"), d3.event);
// if (navigator.userAgent.includes("Firefox")) {
// xm = d3.select(this).attr("cx");
// ym = d3.select(this).attr("cy");
// }
// xm += 10;
// if (xm + tooltip.width > width) xm -= tooltip.width + 20;
// if (ym + tooltip.height > height - margin.bottom)
// ym = height - margin.bottom - tooltip.height - 10;
// tooltip.position(xm, ym);
// });

// text = text
// .data(root.leaves(), (d) => d.data.key)
// .join(
// (enter) =>
// enter
// .append("text")
// .attr("transform", (d) => `translate(${d.x}, ${d.y})`)
// // (update) =>
// // update.call((update) =>
// // update
// // .transition(t)
// // .attr("transform", (d) => `translate(${d.x}, ${d.y})`)
// // ),
// // (exit) => exit.remove()
// );

// text
// .style("fill", (d) => {
// const bgColor = colourSchemeZdc(d);
// return pickTextColorBasedOnBackground(bgColor);
// })
// .selectAll("tspan")
// .data((d) => formatBubbleText(d))
// .join("tspan")
// .attr("x", 0)
// .attr("y", (d, i, nodes) =>
// i === nodes.length - 1
// ? `${i - nodes.length / 2 + 1.1}em`
// : `${i - nodes.length / 2 + 0.8}em`
// )
// .attr("fill-opacity", (d, i, nodes) =>
// i === nodes.length - 1 ? 0.7 : null
// )
// .style("font", (d, i, nodes) =>
// i === nodes.length - 1
// ? "14px var(--trase-mono)"
// : "bold 18px var(--trase-sans-serif)"
// )
// .style("letter-spacing", (d, i, nodes) =>
// i === nodes.length - 1 ? "0.03em" : null
// )
// .text((d) => d)
// .style("pointer-events", "none");

text = g
.selectAll("text")
.data(root.leaves(), (d) => d.data.key)
.join("text")
.attr("transform", (d) => `translate(${d.x}, ${d.y})`)
.style("fill", (d) => {
const bgColor = colourSchemeZdc(d);
return pickTextColorBasedOnBackground(bgColor);
})
.selectAll("tspan")
.data((d) => formatBubbleText(d))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, nodes) =>
i === nodes.length - 1
? `${i - nodes.length / 2 + 1.1}em`
: `${i - nodes.length / 2 + 0.8}em`
)
.attr("fill-opacity", (d, i, nodes) =>
i === nodes.length - 1 ? 0.7 : null
)
.style("font", (d, i, nodes) =>
i === nodes.length - 1
? "14px var(--trase-mono)"
: "bold 18px var(--trase-sans-serif)"
)
.style("letter-spacing", (d, i, nodes) =>
i === nodes.length - 1 ? "0.03em" : null
)
.text((d) => d)
.style("pointer-events", "none");

// circles
// .on("mouseout", () => tooltip.hide())
// .on("mouseover", (d) => tooltip.show(d.data.key, d))
// .on("mousemove", function () {
// let [xm, ym] = d3.clientPoint(this.closest("svg"), d3.event);
// if (navigator.userAgent.includes("Firefox")) {
// xm = d3.select(this).attr("cx");
// ym = d3.select(this).attr("cy");
// }
// xm += 10;
// if (xm + tooltip.width > width) xm -= tooltip.width + 20;
// if (ym + tooltip.height > height - margin.bottom)
// ym = height - margin.bottom - tooltip.height - 10;
// tooltip.position(xm, ym);
// });

svg.append("g").call(annotate);

circles.call(hover, svg, selected_value);

return svg.node();
}
Insert cell
canvasChart = {
const container = document.createElement("div");
container.style.position = "relative";
container.style.width = `${width}px`;
container.style.height = `${height}px`;

const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);

const tooltip = new Tooltip();
document.body.appendChild(tooltip.node);

const root = pack(
d3
.hierarchy({
children: data.sort((a, b) => b[selected_value] - a[selected_value])
})
.sum((d) => d[selected_value])
);

const ctx = canvas.getContext("2d");

root.leaves().forEach((d) => {
ctx.beginPath();
ctx.fillStyle = colourSchemeZdc(d);
ctx.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
});

root.leaves().forEach((d) => {
const textLines = formatBubbleText(d);
const textContainer = document.createElement("div");
textContainer.style.position = "absolute";
textContainer.style.left = `${d.x}px`;
textContainer.style.top = `${d.y}px`;
textContainer.style.transform = "translate(-50%, -50%)";
container.appendChild(textContainer);

if (textLines.length > 0 && Array.isArray(textLines)) {
textLines.forEach((line, i) => {
const textElement = document.createElement("div");
textElement.textContent = line;
textElement.style.font =
i === textLines.length - 1
? "14px var(--trase-mono)"
: "bold 18px var(--trase-sans-serif)";
textElement.style.color = pickTextColorBasedOnBackground(
colourSchemeZdc(d)
);
textElement.style.textAlign = "center";
textElement.style.letterSpacing =
i === textLines.length - 1 ? "0.03em" : "normal";
textContainer.appendChild(textElement);
});
}
});

const tooltipDiv = document.createElement("div");
// const tooltipSvg = document.createElementNS(
// "http://www.w3.org/2000/svg",
// "svg"
// );
// tooltipSvg.setAttribute("width", "400"); // Set SVG width
// tooltipSvg.setAttribute("height", "300"); // Set SVG height
// tooltipDiv.appendChild(tooltipSvg);

container.appendChild(tooltipDiv);
container.addEventListener("mousemove", function (event) {
const rect = canvas.getBoundingClientRect();
let xm = event.clientX - rect.left;
let ym = event.clientY - rect.top;
const hoveredNode = root.leaves().find((d) => {
const dx = xm - d.x;
const dy = ym - d.y;
return dx * dx + dy * dy < d.r * d.r;
});
if (hoveredNode) {
let [xPosition, yPosition] = d3.pointer(event, container);

tooltipDiv.style.position = "absolute";
tooltipDiv.style.left = `${xPosition}px`;
tooltipDiv.style.top = `${yPosition}px`;
tooltipDiv.style.transform = "translate(-50%, -50%)";

// tooltipSvg.appendChild(tooltipKeyValueMultiple(hoveredNode));
// tooltipDiv.textContent = hoveredNode.data.key;
tooltipDiv.innerHTML = htl.html`
<div>
<p>${hoveredNode.data.key}</p>
</div>
`;
} else {
// tooltip.hide();
}
});

return container;
}
Insert cell
Insert cell
viewof bubbleYearSelect = Inputs.select([2018, 2019, 2020], {
step: 1,
value: 2018,
label: "Select year:",
format: (d) => d
})
Insert cell
viewof selected_value = Inputs.radio(metrics, {
value: "volume"
})
Insert cell
metrics = new Map([
["Volume (tonnes)", "volume"],
["Deforestation risk (ha)", "deforestation"]
])
Insert cell
// draw = chart.update(keys[0])
Insert cell
keys = Object.keys(data[0]).filter((d) => d != "key" && d != "zdc_share")
Insert cell
legendItems = [
{
type: "dot",
stroke: "#bdbdbd",
fill: "#bdbdbd",
text: "No data"
}
]
Insert cell
height = width * 0.8
Insert cell
width
Insert cell
margin = ({ top: 5, bottom: 5, right: 5, left: 5 })
Insert cell
// colourScheme = traseOranges[6]
colourScheme = traseOranges[7]
Insert cell
colourSchemeRev = colourScheme.reverse()
Insert cell
colourSchemeZdc = (d) => {
if ((d.data.key === "DOMESTIC") | (d.data.key === "UNKNOWN COUNTRY"))
return "#bdbdbd";
return d3.scaleThreshold().domain(thresholds).range(colourSchemeRev)(
d.data[colourKey]
);
}
Insert cell
id_palm_zdc_shr_vols_imp_ctry1 = FileAttachment("id_palm_zdc_shr_vols_imp_ctry@4.csv").csv()
Insert cell
colourKey = "zdc_share"
Insert cell
defriskKey = "deforestation"
Insert cell
root = d3
.hierarchy(
d3.rollup(
data,
(v) => ({ value: d3.sum(v, (d) => d.value), data: v }),
...keys.map((k) => (d) => d[k])
)
)
.sum(([, d]) => d.value)
.sort((a, b) => b.value - a.value)

Insert cell
pack = d3
.pack()
.size([
width - (margin.left + margin.right),
height - (margin.top + margin.bottom)
])
.padding(3)
Insert cell
thresholds = [0.2, 0.4, 0.6, 0.8, 0.9, 0.95].map((d) =>
Math.floor(d * d3.max(data, (d) => d.zdc_share))
)
Insert cell
full = FileAttachment("id_palm_zdc_shr_vols_imp_ctry@4.csv").csv({
typed: true
})
Insert cell
data = full
.filter((d) => d.year === bubbleYearSelect)
.map((d) => ({
key: d.key,
volume: d.volume,
deforestation: d.def_risk,
zdc_share: d.zdc_share
}))
Insert cell
format = d3.format(".03s")
Insert cell
formatBubbleText = (d) => {
let key = d.data.key;
const length = getLabelLength(key, "font: 10px var(--trase-sans-serif)");
const perimeter = d.r * 2 - 20;
const prop = (length - perimeter) / length;
if (perimeter < 50) return "";
if (length > perimeter)
key =
key.substring(0, Math.floor(key.length * (1 - prop)) - 3).trim() + "...";
return [...key.split(" "), format(d.value)];
}
Insert cell
annotate = g => {}
Insert cell
function hover(bubbles, svg, selected_value) {
const tooltip = new Tooltip();
const tooltipKey = selected_value.replace(/_/g, " ");
bubbles
.style("cursor", "pointer")
.on("touchstart", (event) => event.preventDefault())
.on("pointerenter", (event, d) => {
tooltip.position(
...tooltipOffset(tooltip, d3.pointer(event, svg.node()), width, height)
);
tooltip.show(d.data.key, tooltipKeyValueMultiple(d));
})
.on("pointermove", function (event) {
let [x, y] = tooltipOffset(
tooltip,
d3.pointer(event, svg.node()),
width,
height
);
tooltip.position(x, y);
this.releasePointerCapture(event.pointerId);
})
.on("pointerout", () => tooltip.hide());
svg.append(() => tooltip.node);
}
Insert cell
tooltipKeyValueMultiple = (d) => svg`<text>
<tspan font-size="10px" fill="#839095" x="0" dy="0em">VOLUME:</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="0" dy="1.4em">${format(
d.data.volume
)} t</tspan>
<tspan font-size="10px" fill="#839095" x="0" dy="2.2em">DEFORESTATION RISK:</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="0" dy="1.4em">${d3.format(
".03s"
)(d.data.deforestation)} ha</tspan>
<tspan font-size="10px" fill="#839095" x="0" dy="2.2em">SHARE COVERED BY ZDC:</tspan>
<tspan font-family="var(--trase-sans-serif)" font-size="14px" font-weight="700" fill="#31464e" x="0" dy="1.4em">${
d.data[colourKey] >= 0 ? d3.format(".1%")(d.data[colourKey] / 100) : "N/A"
}</tspan>
</text>`
Insert cell
formatTooltipValue = d3.format(".03s")
Insert cell
import {
traseColours,
traseCategory1,
fonts,
traseOranges,
trasePurples,
traseGreens,
traseOrPu
} from "@trase/visual-id"
Insert cell
import { getLabelLength, legend, swatches } from "@trase/legends@376"
Insert cell
import { Tooltip, tooltipKeyValue, tooltipOffset } from "@trase/tooltip@440";
Insert cell
import { pickTextColorBasedOnBackground } from "@trase/pick-text-color-based-on-bg"
Insert cell
import { slider, select, checkbox, radio } from "@jashkenas/inputs"
Insert cell
import { titleCard } from "@trase/title-card"
Insert cell
d3 = require("d3@6")
Insert cell
htl = require("htl@0.2")
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