Public
Edited
Apr 12, 2024
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const margin = { top: 30, right: 40, bottom: 30, left: 25 };
const totalWidth = 960;
const height = 350 - margin.top - margin.bottom;
const dataByDecade = d3.group(data, (d) => d.decade);
const dur = 1400;

// label
const data2023 = data.filter((d) => d.year === 2023);
const lastData2023 = data2023[data2023.length - 1];
const data2024 = data.filter((d) => d.year === 2024);
const lastData2024 = data2024[data2024.length - 1];

const svg = d3
.create("svg")
.attr("viewBox", `0 0 ${totalWidth} ${height + margin.top + margin.bottom}`)
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");

let facetWidth =
(totalWidth - margin.left - margin.right) / dataByDecade.size;

const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.ano_pi)])
.range([height, 0]);

const minAnoPi = d3.min(data, (d) => d.ano_pi);
const maxAnoPi = d3.max(data, (d) => d.ano_pi);

const x = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.dummy_date))
.range([0, facetWidth - margin.right]);

const xSuperimposed = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.dummy_date))
.range([0, totalWidth - margin.left - margin.right]);

const lineGenerator = d3
.line()
.x((d) => x(d.dummy_date)) // Initially set to use the 'x' scale
.y((d) => y(d.ano_pi));

let isMerged = false;

function updateFacets() {
facetWidth = isMerged
? totalWidth - margin.left - margin.right
: (totalWidth - margin.left - margin.right) / dataByDecade.size;

const currentXScale = isMerged ? xSuperimposed : x;
lineGenerator.x((d) => currentXScale(d.dummy_date));

svg
.selectAll(".facet")
.transition()
.duration(dur)
.attr("transform", (_, i) =>
isMerged
? `translate(${margin.left}, ${margin.top})`
: `translate(${margin.left + i * facetWidth}, ${margin.top})`
);

svg
.selectAll(".decade-label")
.transition()
.duration(dur)
.style("opacity", isMerged ? 0 : 1); // Hide labels when merged

svg
.selectAll(".twentythree-label")
.transition()
.duration(dur)
.attr("x", currentXScale(lastData2023.dummy_date)) // Use currentXScale for positioning
.attr("y", y(lastData2023.ano_pi));

svg
.selectAll(".twentyfour-label")
.transition()
.duration(dur)
.attr("x", currentXScale(lastData2024.dummy_date)) // Use currentXScale for positioning
.attr("y", y(lastData2024.ano_pi));

// Hide or show y-axis gridlines based on isMerged
svg
.selectAll(".facet .tick line") // Assuming facetGroup has the class "facet"
.transition()
.duration(dur)
.style("opacity", isMerged ? 0 : 1);

svg
.selectAll(".x-axis")
.transition()
.duration(dur)
.call(
d3
.axisBottom(currentXScale)
.tickFormat(d3.timeFormat("%B"))
.tickValues([
new Date("2000-01-01"),
new Date("2000-06-01"),
new Date("2000-12-01")
])
)
.call((g) => g.select(".domain").attr("stroke", "none")) //.remove())
.call((g) => g.selectAll(".tick line").attr("stroke", "#777"));

svg
.selectAll(".line")
.transition()
.duration(dur)
.attr("d", (d) => lineGenerator(d)); // Pass original data points

// Debugging console.log statements
console.log("Facet Width:", facetWidth);
console.log("Is Merged:", isMerged);
}

// Uncomment for click version
//svg.on("click", function () {
//isMerged = !isMerged;
//updateFacets();
//});

// comment out if click version
setInterval(() => {
isMerged = !isMerged; // Toggle
updateFacets(); // Update the facets according to the new state
}, 3000);

dataByDecade.forEach((values, decade, i) => {
const xOffset = margin.left + i * facetWidth;

const facetGroup = svg
.append("g")
.attr("class", "facet")
.attr("transform", `translate(${xOffset}, ${margin.top})`);

facetGroup
.append("text")
.attr("class", "decade-label")
.attr("x", facetWidth / 2) // Center the label
.attr("y", 0 - margin.top / 2) // Adjust y position
.text(decade + "s")
.style("font-size", "16px") // Adjust font size
.attr("text-anchor", "middle");

facetGroup
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height})`)
.call(
d3
.axisBottom(x)
.tickFormat(d3.timeFormat("%b"))
.tickValues([
new Date("2000-01-01"),
new Date("2000-06-01"),
new Date("2000-12-01")
])
)
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll(".tick line").attr("stroke", "#777"));

facetGroup
.append("g")
.call(d3.axisLeft(y).tickValues([0, 0.5, 1, 1.5, 2]))
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll(".tick line").attr("stroke", "#777"))
.call((g) =>
g
.selectAll("line")
.attr("x2", facetWidth - margin.right)
.attr("stroke", "#ddd")
);

const line = d3
.line()
.x((d) => x(d.dummy_date))
.y((d) => y(d.ano_pi));

// Set the gradient
const gradient = svg
.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0)
.attr("y1", y(minAnoPi))
.attr("x2", 0)
.attr("y2", y(maxAnoPi))
.selectAll("stop")
.data([
{ offset: "5%", color: "#CCCCCC" },
{ offset: "15%", color: "#FFC300" },
{ offset: "40%", color: "#FF5733" },
{ offset: "70%", color: "#C70039" },
{ offset: "90%", color: "#900C3F" },
{ offset: "98%", color: "#581845" }
])
.enter()
.append("stop")
.attr("offset", function (d) {
return d.offset;
})
.attr("stop-color", function (d) {
return d.color;
});

const dataByYearWithinDecade = d3.group(values, (d) => d.year);

dataByYearWithinDecade.forEach((yearValues, year) => {
// For each year within the decade, create a separate line
facetGroup
.selectAll(`.line-${year}`)
.data([yearValues])
.join("path")
.attr("class", `line line-${year}`)
.attr("fill", "none")
.attr("stroke", "url(#line-gradient)")
.attr("d", lineGenerator);
});

// Check if this facet includes the year 2023 or 2024, and then redraw those lines
if (decade === 2020) {
// Assuming this matches how your decades are determined
const highlightYears = [2024]; // Define which years to highlight
highlightYears.forEach((year) => {
const yearData = data.filter((d) => d.year === year);
if (yearData.length > 0) {
// Draw the wider white line (outline)
facetGroup
.selectAll(`.line-outline-${year}`)
.data([yearData])
.join("path")
.attr("class", `line line-outline-${year}`)
.attr("fill", "none")
.attr("stroke", "white") // White outline
.attr("stroke-width", 6) // Make this line wider than the colored line
.attr("d", lineGenerator);
facetGroup
.selectAll(`.line-highlight-${year}`)
.data([yearData])
.join("path")
.attr("class", `line line-highlight-${year}`)
.attr("fill", "none")
.attr("stroke", "url(#line-gradient)")
.attr("stroke-width", 3.5) // Make the line wider
//.style("filter", "url(#drop-shadow)")
.attr("d", lineGenerator);
}
});
}

if (decade === 2020) {
facetGroup
.append("text")
.attr("class", "twentythree-label")
.attr("x", x(lastData2023.dummy_date)) // Position at the last data point's date
.attr("y", y(lastData2023.ano_pi)) // Position at the last data point's value
.attr("dy", "0px")
.attr("dx", "5px")
.attr("text-anchor", "start")
.style("font-size", "16px")
.style("fill", "black")
.style("font-weight", "bold")
.text("2023");

facetGroup
.append("text")
.attr("class", "twentyfour-label")
.attr("x", x(lastData2024.dummy_date))
.attr("y", y(lastData2024.ano_pi))
.attr("dy", "0px")
.attr("dx", "5px")
.attr("text-anchor", "start")
.style("font-size", "16px")
.style("fill", "black")
.style("font-weight", "bold")
.style(
"text-shadow",
"-1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff"
)
.text("2024");
}
});

updateFacets();
return svg.node();
}
Insert cell
data = FileAttachment("clean_data.csv").csv({ typed: true }) // source: https://sites.ecmwf.int/data/c3sci/bulletin/202402/press_release/ - daily anomalies. Some slight edits in R for format/dropping variables (I should do that all here)
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more