Public
Edited
Jun 18
Insert cell
Insert cell
mutable selectedMovies = []
Insert cell
movies = {
const genres = ["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Romance", "Thriller"];
const titles = [
"Stellar Wars", "Ocean's Revenge", "The Last Knight", "Comedy Central", "Love Actually",
"Space Odyssey", "Dark Night", "Summer Love", "Fast Track", "Mind Games",
"Lost World", "Happy Days", "Thriller Night", "Romance Road", "Action Hero",
"Sci-Fi Future", "Horror House", "Comedy Club", "Drama Queen", "Adventure Time",
"Galaxy Quest", "Beach Party", "Mountain High", "City Lights", "Forest Deep",
"Desert Storm", "Ocean Blue", "Sky High", "Earth Below", "Fire Storm",
"Ice Age", "Thunder Strike", "Lightning Fast", "Storm Chaser", "Wind Walker",
"Rain Dance", "Snow Fall", "Sun Rise", "Moon Light", "Star Bright",
"Dream Big", "Hope High", "Faith Strong", "Love Deep", "Joy Full",
"Peace Now", "Time Traveler", "Space Explorer", "Deep Sea", "High Sky"
];
return Array.from({length: 50}, (_, i) => ({
id: i + 1,
title: titles[i],
genre: genres[Math.floor(Math.random() * genres.length)],
year: Math.floor(Math.random() * 34) + 1990, // 1990-2023
budget: Math.floor(Math.random() * 180) + 20, // $20M - $200M
revenue: Math.floor(Math.random() * 800) + 50, // $50M - $850M
rating: Math.round((Math.random() * 6 + 4) * 10) / 10, // 4.0 - 10.0
duration: Math.floor(Math.random() * 90) + 90 // 90-180 minutes
}));
}

Insert cell
Insert cell
Insert cell
viewof genreFilter = Inputs.checkbox(
["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Romance", "Thriller"],
{
label: "📽️ Genre Filter (Multi-select)",
value: ["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Romance", "Thriller"]
}
)
Insert cell
viewof yearRange = Inputs.range(
[1990, 2023],
{
step: 1,
label: "📅 Year Range",
value: [1990, 2023],
format: d => d,
value: 1995
}
)
Insert cell
viewof ratingThreshold = Inputs.range(
[4, 10],
{
step: 0.1,
label: "⭐ Minimum Rating",
value: 4,
format: d => d.toFixed(1)
}
)
Insert cell
viewof sortBy = Inputs.select(
["budget", "revenue", "rating", "year"],
{
label: "🔄 Sort By",
value: "revenue"
}
)
Insert cell
viewof chartType = Inputs.select(
["scatter", "bar", "histogram"],
{
label: "📊 Chart Type",
value: "scatter"
}
)

Insert cell
viewof resetButton = Inputs.button("🔄 Reset All Filters", {
reduce: () => {
// Clear selected movies when reset is clicked
mutable selectedMovies = [];
return Math.random(); // Return random value to trigger updates
}
})

Insert cell
filteredMovies = {
resetButton;
// Ensure yearRange is valid
const minYear = Array.isArray(yearRange) ? yearRange[0] : 1990;
const maxYear = Array.isArray(yearRange) ? yearRange[1] : 2023;
let filtered = movies.filter(d =>
genreFilter.includes(d.genre) &&
d.year >= minYear && d.year <= maxYear &&
d.rating >= ratingThreshold
);
filtered.sort((a, b) => b[sortBy] - a[sortBy]);
return filtered;
}
Insert cell
Insert cell
Insert cell
chart = {
const width = 800;
const height = 500;
const margin = {top: 40, right: 120, bottom: 60, left: 80};
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("max-width", "100%");

if (chartType === "scatter") {
// SCATTER PLOT IMPLEMENTATION (INLINE TO ACCESS MUTABLE STATE)
const xScale = d3.scaleLinear()
.domain(d3.extent(filteredMovies, d => d.budget))
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain(d3.extent(filteredMovies, d => d.revenue))
.range([height - margin.bottom, margin.top]);
const colorScale = d3.scaleOrdinal()
.domain(["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Romance", "Thriller"])
.range(d3.schemeCategory10);
const sizeScale = d3.scaleSqrt()
.domain(d3.extent(filteredMovies, d => d.rating))
.range([4, 12]);

// Add axes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale))
.append("text")
.attr("x", width / 2)
.attr("y", 35)
.attr("fill", "black")
.style("text-anchor", "middle")
.text("Budget ($ millions)");

svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -50)
.attr("x", -height / 2)
.attr("fill", "black")
.style("text-anchor", "middle")
.text("Revenue ($ millions)");

// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Movie Budget vs Revenue");

// Create tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background", "rgba(0,0,0,0.8)")
.style("color", "white")
.style("padding", "10px")
.style("border-radius", "5px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("z-index", "1000");

// Add circles with interactions
const circles = svg.selectAll("circle")
.data(filteredMovies)
.join("circle")
.attr("cx", d => xScale(d.budget))
.attr("cy", d => yScale(d.revenue))
.attr("r", d => sizeScale(d.rating))
.attr("fill", d => colorScale(d.genre))
.attr("stroke", "#333")
.attr("stroke-width", 0.5)
.style("opacity", 0.7)
.style("cursor", "pointer");

// Add hover interactions
circles
.on("mouseover", function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("r", d => sizeScale(d.rating) * 1.5)
.style("opacity", 1)
.attr("stroke-width", 2);
tooltip
.style("visibility", "visible")
.html(`
<strong>${d.title}</strong><br/>
Genre: ${d.genre}<br/>
Year: ${d.year}<br/>
Budget: $${d.budget}M<br/>
Revenue: $${d.revenue}M<br/>
Rating: ${d.rating}/10<br/>
Duration: ${d.duration} min
`);
})
.on("mousemove", function(event) {
tooltip
.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("r", d => sizeScale(d.rating))
.style("opacity", 0.7)
.attr("stroke-width", 0.5);
tooltip.style("visibility", "hidden");
})
.on("click", function(event, d) {
// Toggle selection - this now has access to mutable selectedMovies
const currentSelected = mutable selectedMovies;
const isSelected = currentSelected.some(movie => movie.id === d.id);
if (isSelected) {
mutable selectedMovies = currentSelected.filter(movie => movie.id !== d.id);
} else {
mutable selectedMovies = [...currentSelected, d];
}
// Update visual feedback
d3.select(this)
.attr("stroke", isSelected ? "#333" : "#ff0000")
.attr("stroke-width", isSelected ? 0.5 : 3);
});

// Add legend
const legend = svg.selectAll(".legend")
.data(colorScale.domain())
.join("g")
.attr("class", "legend")
.attr("transform", (d, i) => `translate(${width - 100}, ${50 + i * 20})`);

legend.append("circle")
.attr("r", 6)
.attr("fill", d => colorScale(d));

legend.append("text")
.attr("x", 12)
.attr("y", 4)
.style("font-size", "12px")
.text(d => d);

} else if (chartType === "bar") {
// BAR CHART IMPLEMENTATION
const genreData = d3.rollup(filteredMovies,
v => d3.sum(v, d => d.revenue),
d => d.genre
);
const barData = Array.from(genreData, ([genre, revenue]) => ({genre, revenue}))
.sort((a, b) => b.revenue - a.revenue);

const xScale = d3.scaleBand()
.domain(barData.map(d => d.genre))
.range([margin.left, width - margin.right])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(barData, d => d.revenue)])
.range([height - margin.bottom, margin.top]);

const colorScale = d3.scaleOrdinal()
.domain(["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Romance", "Thriller"])
.range(d3.schemeCategory10);

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

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

// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Total Revenue by Genre");

// Add bars
svg.selectAll("rect")
.data(barData)
.join("rect")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.revenue))
.attr("width", xScale.bandwidth())
.attr("height", d => yScale(0) - yScale(d.revenue))
.attr("fill", d => colorScale(d.genre))
.style("opacity", 0.8);


// Add legend for bar chart
const legend = svg.selectAll(".legend")
.data(barData)
.join("g")
.attr("class", "legend")
.attr("transform", (d, i) => `translate(${width - 100}, ${50 + i * 20})`);

legend.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", d => colorScale(d.genre));

legend.append("text")
.attr("x", 16)
.attr("y", 9)
.style("font-size", "12px")
.text(d => d.genre);

} else {
// HISTOGRAM IMPLEMENTATION
const xScale = d3.scaleLinear()
.domain(d3.extent(filteredMovies, d => d.rating))
.range([margin.left, width - margin.right]);

const bins = d3.histogram()
.value(d => d.rating)
.domain(xScale.domain())
.thresholds(20)(filteredMovies);

const yScale = d3.scaleLinear()
.domain([0, d3.max(bins, d => d.length)])
.range([height - margin.bottom, margin.top]);

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

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

// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Distribution of Movie Ratings");

// Add bars
svg.selectAll("rect")
.data(bins)
.join("rect")
.attr("x", d => xScale(d.x0))
.attr("y", d => yScale(d.length))
.attr("width", d => Math.max(0, xScale(d.x1) - xScale(d.x0) - 1))
.attr("height", d => yScale(0) - yScale(d.length))
.attr("fill", "steelblue")
.style("opacity", 0.8);
}

return svg.node();
}
Insert cell
Insert cell
summaryStats = {
const count = filteredMovies.length;
const avgBudget = d3.mean(filteredMovies, d => d.budget).toFixed(1);
const avgRevenue = d3.mean(filteredMovies, d => d.revenue).toFixed(1);
const avgRating = d3.mean(filteredMovies, d => d.rating).toFixed(1);
const totalRevenue = d3.sum(filteredMovies, d => d.revenue);
const profitMargin = ((avgRevenue - avgBudget) / avgBudget * 100).toFixed(1);
return {
count,
avgBudget,
avgRevenue,
avgRating,
totalRevenue,
profitMargin
};
}
Insert cell
Insert cell
Insert cell
selectedMoviesTable = {
// Access the mutable variable directly
const selected = mutable selectedMovies;
if (selected.length === 0) {
return html`<p style="color: #666; font-style: italic;">Click on points in the scatter plot to select movies and see them here!</p>`;
}
return Inputs.table(selected, {
columns: ["title", "genre", "year", "budget", "revenue", "rating"],
header: {
title: "Movie Title",
genre: "Genre",
year: "Year",
budget: "Budget ($M)",
revenue: "Revenue ($M)",
rating: "Rating"
},
width: {
title: 200,
genre: 100,
year: 80,
budget: 100,
revenue: 100,
rating: 80
}
});
}
Insert cell
selectedMoviesTable
Insert cell
Insert cell
d3 = require("d3@7")
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