Public
Edited
May 6
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function threeColumnScroller() {
const container = html`<div style="display: flex; width: 100vw; max-height: 100vh;"></div>`;

const left = html`<div style="flex: 1; max-height: background: #f0f0f0; padding: 1rem;">Left column</div>`;
const center = html`<div style="flex: 2; overflow-y: auto; height: 100vh;"></div>`;
const right = html`<div style="flex: 1; background: #f0f0f0; padding: 1rem;">Right column</div>`;

container.appendChild(left);
container.appendChild(center);
container.appendChild(right);

const scroller = customScroller4Func(); // Your original function
center.appendChild(scroller); // Drop it in the middle column

return container;
}

Insert cell
customScroller6 = {

const width = window.innerWidth;
const height = 875;
const margin = {top: 10, right: 0, bottom: 40, left: 0};

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
}

const div = html`<div></div>`;

const initialYear = 2000;

const initialData = [
{ name: "Drama", value: 25 },
{ name: "Comedy", value: 25 }
];

const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;

const x = d3.scaleBand()
.domain(initialData.map(d => d.name))
.range([chartWidth / 3, width - chartWidth / 3])
.padding(0.25);

const y = d3.scaleLinear()
.domain([0, 25]) // adjust this based on your data range
.range([0, chartHeight]);

const xAxis = d3.axisBottom(x).ticks(2);

const blockHeight = 25;
const blockGap = 5;
const offSet = {
"Drama": - 2 * x.bandwidth() / 2,
"Comedy": x.bandwidth() / 3
};

const divSel = d3.select(div)
.style("overflow-y", "auto")
.style("max-width", `${width}px`)
.style("max-height", `${height}px`)
.style("border", "1px solid #ccc");

const svg = divSel.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.style("top", "0px")
.style("left", "0px")
.style("pointer-events", "none");

// const leftImg = d3.select(div)
// .append("img")
// .attr("class", "left-img")
// .style("position", "absolute")
// .style("top", "50%")
// .style("left", "0")
// .style("transform", "translateY(-50%)")
// .style("width", "400px")
// .style("height", "auto")
// .style("pointer-events", "none")
// .attr("src", "https://image.tmdb.org/t/p/w1280/2koX1xLkpTQM4IZebYvKysFW1Nh.jpg");
// // Add right image
// const rightImg = d3.select(div)
// .append("img")
// .attr("class", "right-img")
// .style("position", "absolute")
// .style("top", "50%")
// .style("right", "0")
// .style("transform", "translateY(-50%)")
// .style("width", "400px")
// .style("height", "auto")
// .style("pointer-events", "none")
// .attr("src", "https://image.tmdb.org/t/p/w1280/2koX1xLkpTQM4IZebYvKysFW1Nh.jpg");

const chartGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

[5, 10, 15, 20, 25].forEach(refVal => {
const y_val = chartHeight - (blockHeight + blockGap) * refVal - 2.5;
chartGroup.append("line")
.attr("x1", width / 2 + offSet["Drama"])
.attr("x2", width / 2 + 2.5 * offSet["Comedy"])
.attr("y1", y_val)
.attr("y2", y_val)
.attr("stroke", "#999")
.attr("stroke-dasharray", "4 2")
.attr("stroke-width", 1)
.lower();
chartGroup.append("text")
.attr("x", chartWidth/2 - 10) // center within chartGroup
.attr("y", y_val - 5) // nudge up slightly for spacing
.attr("text-anchor", "middle")
.style("font", "16px")
.style("fill", "#666")
.text(`${refVal} episodes`);
});

const referenceBlocksGroup = chartGroup.selectAll(".reference-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "reference-group")
.attr("transform", d => `translate(0, 0)`);
referenceBlocksGroup.each(function(d) {
const group = d3.select(this);
group.selectAll("rect")
.data(d3.range(d.value)) // create an array [0, 1, ..., value-1]
.enter()
.append("rect")
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", "#d3d3d3");
});

const blocksGroup = chartGroup.selectAll(".block-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate($0, 0)`);

const label = svg.append("text")
.attr("x", width / 2 - 10)
.attr("y", 40)
.attr("text-anchor", "middle")
.style("fill", "#666")
.style("font", "32px serif");

const nSteps = 27;
const stepSize = 300;
const scrollHeight = stepSize * nSteps;

const innerDiv = divSel.append("div")
.style("width", `${width}px`)
.style("height", `${scrollHeight}px`);

const updateBars = (year) => {
const newData = [
{ name: "Drama", value: showDataByYear[year]?.drama_episodes || 0 },
{ name: "Comedy", value: showDataByYear[year]?.comedy_episodes || 0 }
];
label.text(`${year}`);
const blocks = chartGroup.selectAll(".block-group")
blocks.data(newData, d => d.name); // key by genre
blocks.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate(0, 0)`)
.merge(blocks)
.each(function(d) {
const group = d3.select(this);
// JOIN
const rects = group.selectAll("rect")
.data(d3.range(d.value));
// EXIT
rects.exit().remove();
// UPDATE + ENTER
rects.enter()
.append("rect")
.merge(rects)
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", colors[d.name])
.attr("opacity", d.value/25);
});
blocks.exit().remove();
};

let pos = 0;
let currentStep = 0;

divSel.on("scroll", function() {
const newPos = divSel.property("scrollTop");
const newStep = Math.floor(newPos / stepSize);
const year = initialYear + newStep;

if (newStep !== currentStep) {
currentStep = newStep;
updateBars(year);
}

pos = newPos;
});

// Initial render
updateBars(initialYear);

return div;
}

Insert cell
customScroller7 = {

const width = window.innerWidth;
const height = 850;
const margin = {top: 10, right: 0, bottom: 10, left: 0};

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
}

const div = html`<div></div>`;

const initialYear = 2000;

const initialData = [
{ name: "Drama", value: 25 },
{ name: "Comedy", value: 25 }
];

const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;

const x = d3.scaleBand()
.domain(initialData.map(d => d.name))
.range([chartWidth / 3, width - chartWidth / 3])
.padding(0.25);

const y = d3.scaleLinear()
.domain([0, 25]) // adjust this based on your data range
.range([0, chartHeight]);

const xAxis = d3.axisBottom(x).ticks(2);

const blockHeight = 25;
const blockGap = 5;
const offSet = {
"Drama": - 2 * x.bandwidth() / 2,
"Comedy": x.bandwidth() / 3
};

const divSel = d3.select(div)
.style("overflow-y", "auto")
.style("max-width", `${width}px`)
.style("max-height", `${height}px`)
.style("border", "1px solid #ccc");

const svg = divSel.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.style("top", "0px")
.style("left", "0px")
.style("pointer-events", "none");

const leftImg = d3.select(div)
.append("img")
.attr("class", "left-img")
.style("position", "absolute")
.style("top", "50%")
.style("left", "0")
.style("transform", "translateY(-50%)")
.style("width", "400px")
.style("height", "auto")
.style("pointer-events", "none")
.attr("src", "https://image.tmdb.org/t/p/w1280/2koX1xLkpTQM4IZebYvKysFW1Nh.jpg");
// Add right image
const rightImg = d3.select(div)
.append("img")
.attr("class", "right-img")
.style("position", "absolute")
.style("top", "50%")
.style("right", "0")
.style("transform", "translateY(-50%)")
.style("width", "400px")
.style("height", "auto")
.style("pointer-events", "none")
.attr("src", "https://image.tmdb.org/t/p/w1280/2koX1xLkpTQM4IZebYvKysFW1Nh.jpg");

const chartGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

[5, 10, 15, 20, 25].forEach(refVal => {
const y_val = chartHeight - (blockHeight + blockGap) * refVal - 2.5;
chartGroup.append("line")
.attr("x1", width / 2 + offSet["Drama"])
.attr("x2", width / 2 + 2.5 * offSet["Comedy"])
.attr("y1", y_val)
.attr("y2", y_val)
.attr("stroke", "#999")
.attr("stroke-dasharray", "4 2")
.attr("stroke-width", 1)
.lower();
chartGroup.append("text")
.attr("x", chartWidth/2 - 10) // center within chartGroup
.attr("y", y_val - 5) // nudge up slightly for spacing
.attr("text-anchor", "middle")
.style("font", "16px")
.style("fill", "#666")
.text(`${refVal} episodes`);
});

const referenceBlocksGroup = chartGroup.selectAll(".reference-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "reference-group")
.attr("transform", d => `translate(0, 0)`);
referenceBlocksGroup.each(function(d) {
const group = d3.select(this);
group.selectAll("rect")
.data(d3.range(d.value)) // create an array [0, 1, ..., value-1]
.enter()
.append("rect")
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", "#d3d3d3");
});

const blocksGroup = chartGroup.selectAll(".block-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate($0, 0)`);

const label = svg.append("text")
.attr("x", width / 2 - 10)
.attr("y", 40)
.attr("text-anchor", "middle")
.style("fill", "#666")
.style("font", "32px serif");

const nSteps = 27;
const stepSize = 300;
const scrollHeight = stepSize * nSteps;

const innerDiv = divSel.append("div")
.style("width", `${width}px`)
.style("height", `${scrollHeight}px`);

const updateBars = (year) => {
const newData = [
{ name: "Drama", value: showDataByYear2[year]["Drama"]["episodes"] || 0 },
{ name: "Comedy", value: showDataByYear2[year]["Comedy"]["episodes"] || 0 }
];
label.text(`${year}`);
const blocks = chartGroup.selectAll(".block-group")
blocks.data(newData, d => d.name); // key by genre
blocks.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate(0, 0)`)
.merge(blocks)
.each(function(d) {

if (d.name == "Drama") {
leftImg.attr("src", showDataByYear2[year][d.name]["poster_path"]);
}
else {
rightImg.attr("src", showDataByYear2[year][d.name]["poster_path"]);
}
const group = d3.select(this);
// JOIN
const rects = group.selectAll("rect")
.data(d3.range(d.value));
// EXIT
rects.exit().remove();
// UPDATE + ENTER
rects.enter()
.append("rect")
.merge(rects)
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", colors[d.name])
.attr("opacity", d.value/25);
});
blocks.exit().remove();
};

let pos = 0;
let currentStep = 0;

divSel.on("scroll", function() {
const newPos = divSel.property("scrollTop");
const newStep = Math.floor(newPos / stepSize);
const year = initialYear + newStep;

if (newStep !== currentStep) {
currentStep = newStep;
updateBars(year);
}

pos = newPos;
});

// Initial render
updateBars(initialYear);

return div;
}

Insert cell
customScroller8 = {

const width = window.innerWidth;
const height = 850;
const margin = {top: 10, right: 0, bottom: 30, left: 0};

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
}

const div = html`<div></div>`;

const initialYear = 2000;

const initialData = [
{ name: "Drama", value: 25 },
{ name: "Comedy", value: 25 }
];

const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;

const x = d3.scaleBand()
.domain(initialData.map(d => d.name))
.range([chartWidth / 3, width - chartWidth / 3])
.padding(0.25);

const y = d3.scaleLinear()
.domain([0, 25]) // adjust this based on your data range
.range([0, chartHeight]);

const xAxis = d3.axisBottom(x).ticks(2);

const blockHeight = 25;
const blockGap = 5;
const offSet = {
"Drama": - 2 * x.bandwidth() / 2,
"Comedy": x.bandwidth() / 3
};

const divSel = d3.select(div)
.style("overflow-y", "auto")
.style("max-width", `${width}px`)
.style("max-height", `${height}px`)
.style("border", "1px solid #ccc");

const svg = divSel.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.style("top", "0px")
.style("left", "0px")
.style("pointer-events", "none");

const chartGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

[5, 10, 15, 20, 25].forEach(refVal => {
const y_val = chartHeight - (blockHeight + blockGap) * refVal - 2.5;
chartGroup.append("line")
.attr("x1", width / 2 + offSet["Drama"])
.attr("x2", width / 2 + 2.5 * offSet["Comedy"])
.attr("y1", y_val)
.attr("y2", y_val)
.attr("stroke", "#999")
.attr("stroke-dasharray", "4 2")
.attr("stroke-width", 1)
.lower();
chartGroup.append("text")
.attr("x", chartWidth/2 - 10) // center within chartGroup
.attr("y", y_val - 5) // nudge up slightly for spacing
.attr("text-anchor", "middle")
.style("font", "16px")
.style("fill", "#666")
.text(`${refVal} episodes`);
});

const referenceBlocksGroup = chartGroup.selectAll(".reference-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "reference-group")
.attr("transform", d => `translate(0, 0)`);
referenceBlocksGroup.each(function(d) {
const group = d3.select(this);
group.selectAll("rect")
.data(d3.range(d.value)) // create an array [0, 1, ..., value-1]
.enter()
.append("rect")
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", "#d3d3d3");
});

const blocksGroup = chartGroup.selectAll(".block-group")
.data(initialData)
.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate($0, 0)`);

const yearLabel = svg.append("text")
.attr("x", width / 2 - 10)
.attr("y", margin.top * 3)
.attr("text-anchor", "middle")
.style("fill", "#666")
.style("font", "32px serif")
.style("font-weight", "bold");

const bestDrama = svg.append("text")
.attr("x", width / 2 + offSet["Drama"] + x.bandwidth() / 4)
.attr("y", height - margin.bottom / 2)
.attr("text-anchor", "middle")
.style("fill", "#666")
.style("font", "20px serif")
.style("font-weight", "bold")
.text("Best drama");

const bestComedy = svg.append("text")
.attr("x", width / 2 + offSet["Comedy"] + x.bandwidth() / 4)
.attr("y", height - margin.bottom / 2)
.attr("text-anchor", "middle")
.style("fill", "#666")
.style("font", "20px serif")
.style("font-weight", "bold")
.text("Best comedy");

const bestDramaName = svg.append("text");
const bestComedyName = svg.append("text");
// const title = svg.append("text")
// .attr("x", width / 2 - 10)
// .attr("y", 40)
// .attr("text-anchor", "middle")
// .style("fill", "#666")
// .style("font", "32px serif")
// .text("Over the years, Emmy Awards have gone to TV seasons with fewer episodes");

const nSteps = 27;
const stepSize = 300;
const scrollHeight = stepSize * nSteps;

const innerDiv = divSel.append("div")
.style("width", `${width}px`)
.style("height", `${scrollHeight}px`);

const updateBars = (year) => {
const newData = [
{ name: "Drama", value: showDataByYear[year]?.drama_episodes || 0 },
{ name: "Comedy", value: showDataByYear[year]?.comedy_episodes || 0 }
];
yearLabel.text(`${year}`);
const blocks = chartGroup.selectAll(".block-group")
blocks.data(newData, d => d.name); // key by genre
blocks.enter()
.append("g")
.attr("class", "block-group")
.attr("transform", d => `translate(0, 0)`)
.merge(blocks)
.each(function(d) {
const group = d3.select(this);
// JOIN
const rects = group.selectAll("rect")
.data(d3.range(d.value));
// EXIT
rects.exit().remove();
// UPDATE + ENTER
rects.enter()
.append("rect")
.merge(rects)
.attr("x", width / 2 + offSet[d.name])
.attr("y", (d, i) => chartHeight - (i + 1) * (blockHeight + blockGap))
.attr("width", x.bandwidth() / 2)
.attr("height", blockHeight)
.attr("fill", colors[d.name])
.attr("opacity", d.value/25);
});

bestComedyName
.attr("x", 10 + width / 2 + offSet["Comedy"] + x.bandwidth() / 2)
.attr("y", chartHeight - (newData[1].value - 1) * (blockHeight + blockGap))
.text(`${showDataByYear[year].comedy_show_name}, Season ${showDataByYear[year].comedy_season_number}`)
.style("fill", "#666")
.style("font", "24px serif")
.style("font-weight", "bold");

bestDramaName
.attr("x", width / 2 + offSet["Drama"] - 10)
.attr("y", chartHeight - (newData[0].value - 1) * (blockHeight + blockGap))
.text(`${showDataByYear[year].drama_show_name}, Season ${showDataByYear[year].drama_season_number}`)
.style("fill", "#666")
.attr("text-anchor", "end")
.style("font", "24px serif")
.style("font-weight", "bold");
blocks.exit().remove();
};

let pos = 0;
let currentStep = 0;

divSel.on("scroll", function() {
const newPos = divSel.property("scrollTop");
const newStep = Math.floor(newPos / stepSize);
const year = initialYear + newStep;

if (newStep !== currentStep) {
currentStep = newStep;
updateBars(year);
}

pos = newPos;
});

// Initial render
updateBars(initialYear);

return div;
}

Insert cell
data = FileAttachment("emmy_winners_by_year_and_type_simple@4.json").json()
Insert cell
chart7 = {

const container = html`<div style="display: flex; align-items: flex-start;"></div>`;

const width = window.innerWidth;
const height = 600;
const margin = { top: 20, right: 20, bottom: 30, left: 20 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
}

const opacities = {
"Yes": 1,
"No": 0.25,
}

const svg = d3.create("svg")
.attr("width", width * .75)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(years))
.range([margin.left, (width * .75) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", ".5em");;

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

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {

filteredData = filteredData.filter(d => d.winner === "Yes");
}

if ((colorBy == "Award type") && (typeFilter !== "Both")) {

filteredData = filteredData.filter(d => d.type === typeFilter);
}

container.append(svg.node());
const img = html`<img src="https://via.placeholder.com/200x300" style="margin-left: 50px; max-width:${width*.25}px">`;

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes) + (Math.random() - 0.5) * 10)
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("opacity", d => opacities[d.winner])
.on("mouseover", (event, d) => {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("opacity", 1)
.html(`${d.name}<br>Year: ${d.year}<br>Number of episodes: ${d.episodes}`);
})
.on("mouseout", () => tooltip.style("opacity", 0))
.on("click", (event, d) => {
img.src = d.poster_path
})


container.append(img);
d3.select(document.body).append(() => tooltip.node());

return container;
}

Insert cell
chart1 = {
const container = html`<div style="display: flex; align-items: flex-start;"></div>`;

const width = window.innerWidth;
const height = 600;
const margin = { top: 20, right: 20, bottom: 30, left: 20 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
};

const opacities = {
"Yes": 1,
"No": 0.25,
};

const svg = d3.create("svg")
.attr("width", width * 0.75)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(years))
.range([margin.left, (width * 0.75) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", ".5em");

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

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {
filteredData = filteredData.filter(d => d.winner === "Yes");
}

if (typeFilter !== "Both") {
filteredData = filteredData.filter(d => d.type === typeFilter);
}

// Create the image element to the right of the chart
const img = html`<img src="https://via.placeholder.com/200x300" style="margin-left: 20px;">`;

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes) + (Math.random() - 0.5) * 10)
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("opacity", d => opacities[d.winner])
.on("mouseover", (event, d) => {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("opacity", 1)
.html(`${d.name}<br>Year: ${d.year}<br>Number of episodes: ${d.episodes}`);
})
.on("mouseout", () => tooltip.style("opacity", 0))
.on("click", d => {
// Change this line based on your data
img.attr("src", d.poster_path || "https://via.placeholder.com/200x300");
});

d3.select(document.body).append(() => tooltip.node());

container.append(svg.node());
container.append(img);

return container;
}

Insert cell
viewof container = {
const container = html`<div style="display: flex; align-items: flex-start;">`;

const width = window.innerWidth;
const height = 600;
const margin = { top: 20, right: 20, bottom: 30, left: 20 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
};

const opacities = {
"Yes": 1,
"No": 0.25,
};

// Create SVG chart
const svg = d3.create("svg")
.attr("width", width * 0.7)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([margin.left, (width * 0.7) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", ".5em");

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

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {
filteredData = filteredData.filter(d => d.winner === "Yes");
}

if (typeFilter !== "Both") {
filteredData = filteredData.filter(d => d.type === typeFilter);
}

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes) + (Math.random() - 0.5) * 10)
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("opacity", d => opacities[d.winner])
.on("mouseover", (event, d) => {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("opacity", 1)
.html(`${d.name}<br>Year: ${d.year}<br>Number of episodes: ${d.episodes}`);
})
.on("mouseout", () => tooltip.style("opacity", 0))
.on("click", (event, d) => {
img.src = d.poster_path; // assumes d.image is the new image URL
});


// Vertical line as a styled div
const divider = html`<div style="
width: 1px;
background-color: black;
height: 100%;
margin: 0 10px;
">`;

// Image
const img = html`<img src="https://via.placeholder.com/200" style="height: 400px;">`;

container.append(svg.node(), divider, img);
return container;
}

Insert cell
viewof chart = {
const width = window.innerWidth;
const height = 600;
const margin = { top: 40, right: 20, bottom: 40, left: 20 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
};

const opacities = {
"Yes": 1,
"No": 0.25,
};

// Create flex container
const container = DOM.element("div");
container.style.display = "flex";
container.style.alignItems = "flex-start";
container.style.gap = "5px";

// Create SVG chart
const svg = d3.create("svg")
.attr("width", width * 0.7)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([margin.left, (width * 0.7) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", ".5em");

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

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {
filteredData = filteredData.filter(d => d.winner === "Yes");
}

if (typeFilter !== "Both") {
filteredData = filteredData.filter(d => d.type === typeFilter);
}

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes) + (Math.random() - 0.5) * 10)
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("opacity", d => opacities[d.winner])
.on("mouseover", (event, d) => {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("opacity", 1)
.html(`<b>${d.name} </b><em>Click to expand</em><br>Year: ${d.year}<br>Number of episodes: ${d.episodes}<br>`);
})
.on("mouseout", () => tooltip.style("opacity", 0))
.on("click", (event, d) => {
img.src = d.poster_path; // assumes d.image is the new image URL
next_div.style.backgroundColor = "steelblue";
});

d3.select(document.body).append(() => tooltip.node());

// Append SVG chart first (left side)
container.appendChild(svg.node());

// Create and append the IMAGE (right side)
const next_div = DOM.element("div");
next_div.style.backgroundColor = "lightblue";
const img = DOM.element("img");
img.src = "";
img.style.maxWidth = width * .3 - margin.left - margin.right + "px";
img.style.display = "block";
next_div.appendChild(img);
container.appendChild(next_div);

return container;
}

Insert cell
Insert cell
viewof chart6 = {
const width = window.innerWidth;
const height = 600;
const margin = { top: 40, right: 20, bottom: 40, left: 20 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
};

const opacities = {
"Yes": 1,
"No": 0.25,
};

const winnerObj = {
"Yes": "Best ",
"No": "Nominee for ",
};

// Create flex container
const container = DOM.element("div");
container.style.display = "flex";
container.style.alignItems = "flex-start";
container.style.gap = "5px";

// Create SVG chart
const svg = d3.create("svg")
.attr("width", width * 0.7)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([margin.left, (width * 0.7) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", ".5em");

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

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {
filteredData = filteredData.filter(d => d.winner === "Yes");
}

if (typeFilter !== "Both") {
filteredData = filteredData.filter(d => d.type === typeFilter);
}

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes))
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("fill-opacity", d => opacities[d.winner])
.on("mouseover", function(event, d) {
d3.select(this)
.attr("stroke", "black")
.attr("stroke-opacity", 1)
.attr("stroke-width", 2);
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("font-size", "16px")
.style("opacity", 1)
.html(`<b>${d.name}</b><br>Season ${d.season_number} (${d.year})<br>Number of episodes: ${d.episodes}<br>`);
})
.on("mouseout", function() {
d3.select(this)
.attr("stroke", "none");
tooltip.style("opacity", 0);
})
.on("click", (event, d) => {
img.src = d.poster_path; // assumes d.image is the new image URL
const mc = d.median_color
// next_div.style.backgroundColor = scaleToLuminance(`rgb(${mc[0]},${mc[1]},${mc[2]})`, 0.3); // Change background color on click
next_div.style.backgroundColor = "#d3d3d3"
caption.textContent = `${winnerObj[d.winner]} ${d.type}, ${d.year}`;
num.textContent = `${d.episodes} episodes`;
});

d3.select(document.body).append(() => tooltip.node());

// Append SVG chart first (left side)
container.appendChild(svg.node());

// Create and append the IMAGE container (right side)
const next_div = DOM.element("div");
const imgWidth = width * 0.3 - margin.left - margin.right;
// next_div.style.backgroundColor = "lightblue"; // Default background color
next_div.style.width = `${imgWidth}px`; // Ensure it fits
// next_div.style.height = `${height - margin.bottom}px`; // Match chart height
next_div.style.display = "block";
next_div.style.justifyContent = "center"; // Center image horizontally
next_div.style.overflow = "hidden";
next_div.style.borderRadius = "10px";
next_div.style.textAlign = "right";

const img = DOM.element("img");
// img.src = ""; // Empty image source initially
img.style.maxWidth = "100%"; // Make sure the image fits within the container
img.style.maxHeight = imgWidth * 1.5 + "px";
img.style.display = "block"; // Block display to make it behave like a block-level element

const caption = DOM.element("div");
caption.style.fontSize = "20px";
caption.style.marginTop = "5px";
caption.style.marginLeft = "5px";
caption.style.marginRight = "5px";
caption.style.fontWeight = "600";

const num = DOM.element("div");
num.style.fontSize = "20px";
num.style.color = "#414141";
num.style.fontWeight = "900";
num.style.marginLeft = "5px";
num.style.marginRight = "5px";
num.style.marginBottom = "5px";
next_div.appendChild(img);
next_div.appendChild(caption);
next_div.appendChild(num);
container.appendChild(next_div);

return container;
}

Insert cell
function lightenRGB(rgbStr, factor = 0.5) {
const match = rgbStr.match(/\d+/g);
if (!match) return rgbStr;

const [r, g, b] = match.map(Number);
const lighten = (c) => Math.round(c + (255 - c) * factor);

return `rgb(${lighten(r)}, ${lighten(g)}, ${lighten(b)})`;
}

Insert cell
// Converts sRGB component to linear
function srgbToLinear(c) {
c /= 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
Insert cell
// Converts linear component back to sRGB
function linearToSrgb(c) {
return Math.round(255 * (c <= 0.0031308
? 12.92 * c
: 1.055 * Math.pow(c, 1 / 2.4) - 0.055));
}
Insert cell
// Computes current luminance (L)
function getLuminance(r, g, b) {
const R = srgbToLinear(r);
const G = srgbToLinear(g);
const B = srgbToLinear(b);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
Insert cell

// Scales color to target luminance
function scaleToLuminance(rgbStr, targetL) {
const match = rgbStr.match(/\d+/g);
if (!match) return rgbStr;

let [r, g, b] = match.map(Number);
const currentL = getLuminance(r, g, b);

if (currentL === 0) return `rgb(0,0,0)`; // Avoid divide by zero

else if (currentL < 0.3) {

const scale = targetL / currentL;
const newR = Math.min(255, linearToSrgb(srgbToLinear(r) * scale));
const newG = Math.min(255, linearToSrgb(srgbToLinear(g) * scale));
const newB = Math.min(255, linearToSrgb(srgbToLinear(b) * scale));
return `rgb(${newR}, ${newG}, ${newB})`;
} else {

return rgbStr
}

}
Insert cell
viewof chartLabeled = {
const width = window.innerWidth;
const height = 600;
const margin = { top: 20, right: 20, bottom: 50, left: 50 };

const colors = {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
};

const opacities = {
"Yes": 1,
"No": 0.25,
};

const winnerObj = {
"Yes": "Best ",
"No": "Nominee for ",
};

// Create flex container
const container = DOM.element("div");
container.style.display = "flex";
container.style.alignItems = "flex-start";
container.style.gap = "5px";

// Create SVG chart
const svg = d3.create("svg")
.attr("width", width * 0.7)
.attr("height", height)
.style("font", "10px sans-serif");

const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([margin.left, (width * 0.7) - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.episodes))
.range([height - margin.bottom, margin.top]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom + 10})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.selectAll("text")
.style("text-anchor", "middle");

svg.append("text")
.attr("text-anchor", "middle")
.attr("x", (width * 0.7) / 2)
.attr("y", height - 5)
.style("font-size", "16px")
.style("fill", "#000")
.text("Award year");

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

svg.append("text")
.attr("text-anchor", "middle")
.attr("transform", `rotate(-90)`)
.attr("x", -(height / 2))
.attr("y", 15) // Distance from the left edge
.style("font-size", "16px")
.style("fill", "#000")
.text("Number of episodes");

const tooltip = d3.select(DOM.element("div"))
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "12px sans-serif")
.style("opacity", 0);

let filteredData = data;

if (winnerFilter) {
filteredData = filteredData.filter(d => d.winner === "Yes");
}

if (typeFilter !== "Both") {
filteredData = filteredData.filter(d => d.type === typeFilter);
}

svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => x(d.year) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.episodes))
.attr("r", 6)
.attr("fill", d => colors[d.type])
.attr("fill-opacity", d => opacities[d.winner])
.on("mouseover", function(event, d) {
d3.select(this)
.attr("stroke", "black")
.attr("stroke-opacity", 1)
.attr("stroke-width", 2);
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("font-size", "16px")
.style("opacity", 1)
.html(`<b>${d.name}</b><br>Season ${d.season_number} (${d.year})<br>Number of episodes: ${d.episodes}<br>`);
})
.on("mouseout", function() {
d3.select(this)
.attr("stroke", "none");
tooltip.style("opacity", 0);
})
.on("click", (event, d) => {
img.src = d.poster_path;
const mc = d.median_color;
next_div.style.backgroundColor = "#d3d3d3";
caption.textContent = `${winnerObj[d.winner]} ${d.type}, ${d.year}`;
num.textContent = `${d.episodes} episodes`;
});

d3.select(document.body).append(() => tooltip.node());

// Append SVG chart first (left side)
container.appendChild(svg.node());

// Create and append IMAGE container
const next_div = DOM.element("div");
const imgWidth = width * 0.3 - margin.left - margin.right;
next_div.style.width = `${imgWidth}px`;
next_div.style.display = "block";
next_div.style.justifyContent = "center";
next_div.style.overflow = "hidden";
next_div.style.borderRadius = "10px";
next_div.style.textAlign = "right";

const img = DOM.element("img");
img.style.maxWidth = "100%";
img.style.maxHeight = imgWidth * 1.5 + "px";
img.style.display = "block";

const caption = DOM.element("div");
caption.style.fontSize = "20px";
caption.style.marginTop = "7.5px";
caption.style.marginLeft = "7.5px";
caption.style.marginRight = "7.5px";
caption.style.fontWeight = "600";

const num = DOM.element("div");
num.style.fontSize = "20px";
num.style.color = "#414141";
num.style.fontWeight = "900";
num.style.marginLeft = "7.5px";
num.style.marginRight = "7.5px";
num.style.marginBottom = "7.5px";

next_div.appendChild(img);
next_div.appendChild(caption);
next_div.appendChild(num);
container.appendChild(next_div);

// title element
const title = DOM.element("div");
title.textContent = "Emmy Awards are favoring pithier seasons";
title.style.fontSize = "40px";
title.style.fontWeight = "bold";
title.style.marginBottom = "10px";
title.style.textAlign = "center";
title.style.width = "100%";

// Combine title and content
const outer = DOM.element("div");
outer.style.display = "flex";
outer.style.flexDirection = "column";
outer.style.alignItems = "center";
outer.appendChild(title);
outer.appendChild(container);

return outer;
}

Insert cell
function jitterPoints(data, radius = 5) {
const groups = {};
data.forEach((item, index) => {
const key = `${item.year}_${item.episodes}`;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push({ ...item, originalIndex: index });
});

const jittered = [...data];

Object.values(groups).forEach(group => {
const count = group.length;
if (count === 1) {
jittered[group[0].originalIndex].xJitter = 0;
jittered[group[0].originalIndex].yJitter = 0;
} else if (count === 2) {
jittered[group[0].originalIndex].xJitter = -radius / 2;
jittered[group[0].originalIndex].yJitter = 0;

jittered[group[1].originalIndex].xJitter = radius / 2;
jittered[group[1].originalIndex].yJitter = 0;
} else {
for (let i = 0; i < count; i++) {
const angle = (2 * Math.PI * i) / count;
const xOffset = radius * Math.cos(angle);
const yOffset = radius * Math.sin(angle);
jittered[group[i].originalIndex].xJitter = (2 * count / 10) * xOffset;
jittered[group[i].originalIndex].yJitter = (2 * count / 10) * yOffset;
}
}
});

return jittered;
}

Insert cell
Insert cell
networkColors = { return {
'HBO': '#1E3A8A', // Dark blue
'ABC': '#FF0000', // Red
'NBC': '#0084B4', // Blue
'CBS': '#FF8C00', // Orange
'FOX': '#00A1E4', // Light blue
'Showtime': '#9B1B30', // Dark red
'FX': '#DC3545', // Red
'AMC': '#00B140', // Green
'PBS': '#4F4A33', // Dark brown
'Netflix': '#E50914', // Netflix Red
'Amazon': '#FF9900', // Amazon Orange
'USA Network': '#006F91', // Blue
'Hulu': '#1CE783', // Green
'BBC America': '#BBC0C0', // Light grey
'Pop TV': '#FC71A1', // Pink
'Disney+': '#003F87', // Disney Blue
'HBO Max': '#F56C6C', // Light Red (Max)
'Apple TV+': '#A1B3B4' // Apple Grey
}
}
Insert cell
awardColors = { return {
"Drama": "#7B3F74",
"Comedy": "#FF5F1F"
}};

Insert cell
scatColors = { return {
"Award type": awardColors,
"Network": networkColors
}}
Insert cell
function getColor(award, network) {
if (colorBy == "Award type") {
return scatColors[colorBy][award]
} else {
return scatColors[colorBy][network]
}
}
Insert cell
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