Public
Edited
Nov 24, 2024
Insert cell
Insert cell
// Built on from Vishal Devireddy - https://observablehq.com/@twotau/serpentine-bar-chart
// source for SIPRI https://www.sipri.org/media/press-release/2024/global-military-spending-surges-amid-war-rising-tensions-and-insecurity
{
const xOffset = 15;
const svgWidth = 400; //"100%"; //680;
const barHeight = 25;
const radius = 18;
const standardVerticalSpacePerBar = 0; //40
const extraSpaceForTopBar = 335; // Increased to add padding for title and subtitle
const extraSpaceForBottom = 120; // Added space for the caption and bottom padding
const svgHeight =
(dataSnake.length - 1) * standardVerticalSpacePerBar +
extraSpaceForTopBar +
extraSpaceForBottom;

const svg = d3
.create("svg")
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`) // Adjusted svgHeight
.attr("width", "100%") //100
.attr("height", svgHeight) // Adjusted height
.style("display", "block")
.style("margin", "auto");

// Add title
svg
.append("text")
.attr("x", 40) // Centered
.attr("y", 30) // Position at the top
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.text("Global military spending in 2023 was about 8x larger")
.append("tspan")
.attr("x", 33) // Align with the same X position
.attr("dy", "1.2em") // Offset to the next line
.text("than the newly agreed annual climate finance goal*");

// Add subtitle
svg
.append("text")
.attr("x", -105) // Centered
.attr("y", 90) // Position below the title
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-style", "italic")
.text("Outcome of COP29. Excludes inflation.");

const secondLargestValue = dataSnake[1].num;
const xScale = d3
.scaleLinear()
.domain([0, 2440])
.range([xOffset, 100]) // svgWidth
.nice();

// Adjust startY calculation to include extra space for title and subtitle
dataSnake.forEach((dataSnake, index) => {
const startY =
index === 0
? radius * 2 + 20 + 70 // Add 40 to move the first bar down
: radius * 2 +
extraSpaceForTopBar +
(index - 1) * standardVerticalSpacePerBar +
20;

const { d, x, y } = serpentineBar(
dataSnake.num * 1,
300,
radius,
xOffset,
startY,
0
);

svg
.append("path")
.attr("d", d)
.attr("fill", "none")
//.attr("stroke", "#2f5d9e")
.attr("stroke", index === 0 ? "#2f5d9e" : "#e55592")
.attr("stroke-width", barHeight)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "butt");

svg
.append("text")
.attr("x", xOffset - 10)
.attr("y", startY + 5)
.text(dataSnake.course)
.attr("alignment-baseline", "middle")
.style("text-anchor", "end")
.style("font-size", "18px");
// Format the labels for trillion and billion
const formattedLabel =
dataSnake.num >= 1000
? `$${(dataSnake.num / 1000).toFixed(2)} trillion`
: `$${dataSnake.num} billion`;

svg
.append("text")
.attr("x", x + 5)
.attr("y", y + 5)
.text(formattedLabel) // Use the formatted label
.attr("alignment-baseline", "middle")
.style("text-anchor", "start")
.style("font-size", "18px");

// ignore this code - was for something else

// Directly access the UK data using its known index
const ukData = dataSnake[15]; // Access the UK's data at index 15

// Calculate startY for the UK based on its position, which is index 15 in your case
const ukStartY =
radius * 2 +
extraSpaceForTopBar +
(15 - 1) * standardVerticalSpacePerBar +
20;

//Use the ukData to draw your elements
const ukResult = serpentineBar(
2534 / 107,
500,
radius,
xOffset,
ukStartY, // Correctly calculated startY based on the UK's position
0
);

//Add annotation
svg
.append("text")
.attr("x", ukResult.x + 60)
.attr("y", ukResult.y + 5)
.text(dataSnake.annotation)
.attr("alignment-baseline", "middle")
.style("text-anchor", "start")
.style("font-size", "14px");
});

//Add caption
svg
.append("text")
.attr("x", 80)
.attr("y", svgHeight - 20) // Position at the bottom
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text(
"Source: Leo Hickman; Stockholm International Peace Research Institute (SIPRI) • Graphic: Yusuf Imaad Khan"
);

return svg.node();
}
Insert cell
dataSnake = [
["Global Military Spending (2023)", 2440],
["New Climate Finance Goal", 300]
]
.map(([course, num, annotation]) => ({ course, num, annotation }))
.sort((a, b) => b.num - a.num)
Insert cell
Insert cell
Insert cell
Insert cell
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