Public
Edited
May 6
Insert cell
Insert cell
data = FileAttachment("media_bias_sentiment.csv").csv()


Insert cell
parsed = data.map(d => ({
month: d.month,
bias: d.bias,
topic: d.topic,
sentiment_mean: +d.sentiment_mean,
emotion_word_count: +d.emotion_word_count,
article_count: +d.article_count
}))

Insert cell
viewof selectedTopic = Inputs.select(
[...new Set(parsed.map(d => d.topic))],
{label: "Choose a Topic", value: "Politics"}
)

Insert cell
viewof currentMonthIndex = Inputs.range([0, months.length - 1], {
step: 1,
label: "Month",
format: i => months[i]
})


Insert cell
months = [...new Set(parsed.map(d => d.month))].sort()


Insert cell

currentMonth = months[currentMonthIndex]


Insert cell
annotations = [
{ month: "2020-03", label: "COVID Begins" },
{ month: "2020-05", label: "George Floyd Protests" },
{ month: "2020-08", label: "DNC & RNC Conventions" },
{ month: "2020-11", label: "US Election" }
]


Insert cell
tooltip = {
const div = html`<div style="
position: absolute;
pointer-events: none;
padding: 6px 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
font-size: 12px;
font-family: sans-serif;
display: none;
z-index: 1000;
"></div>`;

document.body.appendChild(div);
return div;
}

Insert cell
chart = Generators.observe((change) => {
let svg;

const draw = () => {
const width = 800, height = 400;
const margin = {top: 50, right: 160, bottom: 40, left: 60};

svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

const filtered = parsed.filter(d => d.topic === selectedTopic);

const x = d3.scalePoint()
.domain(months)
.range([margin.left, width - margin.right]);

const y = d3.scaleLinear()
.domain([0, d3.max(filtered, d => d.emotion_word_count)]).nice()
.range([height - margin.bottom, margin.top]);

const color = d3.scaleOrdinal()
.domain(["Left", "Center", "Right"])
.range(["#e41a1c", "#377eb8", "#4daf4a"]);

const line = d3.line()
.x(d => x(d.month))
.y(d => y(d.emotion_word_count));

const biasGroups = d3.groups(filtered, d => d.bias).map(([bias, values]) => ({
bias,
values: values.sort((a, b) => a.month.localeCompare(b.month))
}));

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));

svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));

svg.append("g")
.selectAll("path")
.data(biasGroups)
.join("path")
.attr("fill", "none")
.attr("stroke", d => color(d.bias))
.attr("stroke-width", 2)
.attr("d", d => line(d.values));

svg.append("g")
.selectAll("line.annotation")
.data(annotations)
.join("line")
.attr("x1", d => x(d.month))
.attr("x2", d => x(d.month))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("stroke", "red")
.attr("stroke-dasharray", "4");

svg.append("g")
.selectAll("text.annotation")
.data(annotations)
.join("text")
.attr("x", d => x(d.month))
.attr("y", margin.top - 10)
.attr("text-anchor", "middle")
.attr("fill", "red")
.style("font-size", "12px")
.text(d => d.label);

for (const group of biasGroups) {
const thisMonth = group.values.find(d => d.month === currentMonth);
if (!thisMonth) continue;

svg.append("circle")
.attr("r", 6)
.attr("fill", color(group.bias))
.attr("stroke", "#000")
.attr("cx", x(thisMonth.month))
.attr("cy", y(thisMonth.emotion_word_count))
.on("mouseover", (event) => {
tooltip.style.display = "block";
tooltip.innerHTML = `
<strong>${group.bias}</strong><br/>
Month: ${currentMonth}<br/>
Emotion words: ${thisMonth.emotion_word_count}
`;
})
.on("mousemove", (event) => {
tooltip.style.left = event.pageX + 10 + "px";
tooltip.style.top = event.pageY - 28 + "px";
})
.on("mouseout", () => {
tooltip.style.display = "none";
});
}

svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.text(`Emotion Word Count by Bias — ${selectedTopic} (${currentMonth})`);

const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 20}, ${margin.top})`);

const biases = ["Left", "Center", "Right"];
const colors = ["#e41a1c", "#377eb8", "#4daf4a"];

biases.forEach((bias, i) => {
legend.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.attr("fill", colors[i]);

legend.append("text")
.attr("x", 10)
.attr("y", i * 20 + 5)
.text(bias)
.style("font-size", "12px")
.attr("alignment-baseline", "middle");
});

change(svg.node());
};

draw();

const interval = setInterval(() => {
draw();
}, 1000);

return () => clearInterval(interval);
})

Insert cell
downloadChart = {
const svg = chart.cloneNode(true);
const blob = new Blob([svg.outerHTML], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const link = html`<a href="${url}" download="bias_emotion_chart.svg">📥 Download SVG</a>`;
return link;
}

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