function createElevatedChart(elevatedData) {
const width = 1600;
const height = 1000;
const margin = {
top: 80,
right: 300,
bottom: 250,
left: 80
};
const fontSize = {
title: "28px",
yearLabel: "26px",
axisLabel: "24px",
tickLabel: "16px",
legend: "20px",
siteLabel: "18px"
};
const container = d3
.create("div")
.style("width", "100%")
.style("max-width", "1600px")
.style("margin", "0 auto");
const svg = container
.append("svg")
.attr("viewBox", [0, 0, width, height])
.style("max-width", "100%")
.style("height", "auto");
const years = Array.from(new Set(elevatedData.map((d) => d.year))).sort();
const allSpecies = Array.from(new Set(elevatedData.map((d) => d.spec_name)));
// Create scales
const xScale = d3
.scaleBand()
.padding(0.5)
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear().range([height - margin.bottom, margin.top]);
const colorScale = d3
.scaleOrdinal()
.domain(allSpecies)
.range(d3.schemeCategory10);
// Add title
svg
.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.style("font-size", fontSize.title)
.text("Elevated Phytoplankton Samples by Location");
// Create axes groups
const xAxis = svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`);
const yAxis = svg
.append("g")
.attr("transform", `translate(${margin.left},0)`);
// Add axis labels
svg
.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.style("font-size", fontSize.axisLabel)
.text("Sampling Site");
svg
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", margin.left / 3)
.attr("text-anchor", "middle")
.style("font-size", fontSize.axisLabel)
.text("Count of Elevated Samples");
// Create year text
const yearText = svg
.append("text")
.attr("x", width / 2)
.attr("y", margin.top - 10)
.attr("text-anchor", "middle")
.style("font-size", fontSize.yearLabel);
// Create legend group
const legend = svg
.append("g")
.attr("class", "legend")
.attr(
"transform",
`translate(${width - margin.right + 20}, ${margin.top})`
);
// Update function
function update(selectedYear) {
const yearData = d3.group(
elevatedData.filter((d) => d.year === selectedYear),
(d) => d.sampl_site
);
const sites = Array.from(yearData.keys());
const activeSpecies = Array.from(
new Set(
elevatedData
.filter((d) => d.year === selectedYear)
.map((d) => d.spec_name)
)
);
const stackedData = d3
.stack()
.keys(activeSpecies)
.value((d, key) => {
const matches = d[1].filter((item) => item.spec_name === key);
return matches.length;
})(Array.from(yearData.entries()));
xScale.domain(sites);
yScale.domain([
0,
Math.ceil(d3.max(stackedData, (d) => d3.max(d, (d) => d[1])))
]);
// Update x-axis with rotated and wrapped text
xAxis
.call(d3.axisBottom(xScale))
.selectAll(".tick text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-size", fontSize.siteLabel)
.each(function (d) {
const text = d3.select(this);
const words = text
.text()
.split(/\s+|(?=-)/)
.filter((d) => d !== "-");
text.text(null);
let tspan = text.append("tspan").attr("x", 0).attr("dy", "0em");
let line = [];
let lineNumber = 0;
const maxWidth = 200;
for (let word of words) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > maxWidth) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", 0)
.attr("dy", "1.2em")
.text(word);
lineNumber++;
}
}
});
// Update y-axis
yAxis
.call(
d3
.axisLeft(yScale)
.ticks(Math.ceil(yScale.domain()[1]))
.tickFormat(d3.format("d"))
)
.selectAll(".tick text")
.style("font-size", fontSize.tickLabel);
yearText.text(selectedYear);
// Update legend
legend.selectAll("*").remove();
activeSpecies.forEach((spec, i) => {
const legendRow = legend
.append("g")
.attr("transform", `translate(0, ${i * 40})`);
legendRow
.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", colorScale(spec));
legendRow
.append("text")
.attr("x", 24)
.attr("y", 10)
.style("font-size", fontSize.legend)
.text(spec);
});
// Create tooltip div if it doesn't exist
const tooltip = d3
.select("body")
.selectAll(".tooltip")
.data([null])
.join("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "1px")
.style("border-radius", "5px")
.style("padding", "10px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("box-shadow", "3px 3px 5px rgba(0, 0, 0, 0.2)");
// Update bars with tooltips
const series = svg
.selectAll(".series")
.data(stackedData)
.join("g")
.attr("class", "series")
.attr("fill", (d) => colorScale(d.key));
series
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("x", (d) => xScale(d.data[0]))
.attr("y", (d) => yScale(d[1]))
.attr("height", (d) => yScale(d[0]) - yScale(d[1]))
.attr("width", xScale.bandwidth())
.on("mouseover", function (event, d) {
const speciesName = d3.select(this.parentNode).datum().key;
const siteCount = d[1] - d[0];
tooltip.style("visibility", "visible").html(`
<strong>Location:</strong> ${d.data[0]}<br/>
<strong>Species:</strong> ${speciesName}<br/>
<strong>Elevated Samples:</strong> ${siteCount}
`);
})
.on("mousemove", function (event) {
tooltip
.style("top", event.pageY - 10 + "px")
.style("left", event.pageX + 10 + "px");
})
.on("mouseout", function () {
tooltip.style("visibility", "hidden");
});
}
// Create slider
const slider = container
.append("input")
.attr("type", "range")
.attr("min", d3.min(years))
.attr("max", d3.max(years))
.attr("value", d3.min(years))
.attr("step", 1)
.style("width", "80%")
.style("margin", "20px 10%")
.on("input", function () {
if (isPlaying) {
pause();
}
update(+this.value);
});
// Add playback controls
const playbackControls = container
.append("div")
.style("text-align", "center")
.style("margin", "10px 0");
// Add play button
const playButton = playbackControls
.append("button")
.text("Play")
.style("margin", "0 5px")
.style("padding", "5px 15px");
// Add speed control
const speedControl = playbackControls
.append("select")
.style("margin", "0 5px");
speedControl
.selectAll("option")
.data([
{ label: "Slow", value: 2000 },
{ label: "Medium", value: 1000 },
{ label: "Fast", value: 500 }
])
.enter()
.append("option")
.text((d) => d.label)
.attr("value", (d) => d.value);
// Animation variables
let interval;
let isPlaying = false;
// Play function
function play() {
if (!isPlaying) {
isPlaying = true;
playButton.text("Pause");
interval = setInterval(() => {
let currentYear = +slider.node().value;
let nextYear = currentYear + 1;
// Reset to first year if we reach the end
if (nextYear > d3.max(years)) {
nextYear = d3.min(years);
}
// Update slider and visualization
slider.node().value = nextYear;
update(nextYear);
}, +speedControl.node().value);
} else {
pause();
}
}
// Pause function
function pause() {
if (isPlaying) {
isPlaying = false;
playButton.text("Play");
clearInterval(interval);
}
}
// Add event listeners
playButton.on("click", play);
speedControl.on("change", function () {
if (isPlaying) {
clearInterval(interval);
play();
}
});
// Initial update
update(d3.min(years));
return container.node();
}