Public
Edited
May 27
Fork of Untitled
Insert cell
Insert cell
combined_movies_data = FileAttachment("combined_movies_data.csv").csv()
Insert cell
customGeoJSON = FileAttachment("custom.geo.json").json()
Insert cell
geoCountryNames = new Set(customGeoJSON.features.map(f => f.properties.name))
Insert cell
rawCountryNames = {
const countries = new Set();

for (const d of combined_movies_data) {
if (!d.countries_origin) continue;

let parsed;
try {
parsed = eval(d.countries_origin);
} catch {
continue;
}

for (let c of parsed) {
countries.add(c);
}
}

return Array.from(countries).sort();
}
Insert cell
unmatchedCountryNames = rawCountryNames.filter(c => !geoCountryNames.has(c))
Insert cell
Inputs.table(unmatchedCountryNames.map(d => ({ Unmatched: d })))
Insert cell
countryNameFixes = {
return {
"Bosnia and Herzegovina": "Bosnia and Herz.",
"Cayman Islands": "Cayman Is.",
"Central African Republic": "Central African Rep.",
"Czech Republic": "Czechia",
"Czechoslovakia": "Czechia", // or "Slovakia" — best guess
"Dominican Republic": "Dominican Rep.",
"East Germany": "Germany",
"Federal Republic of Yugoslavia": "Serbia",
"Gibraltar": "United Kingdom",
"Guadeloupe": "France",
"Korea": "Korea, South", // assume South by default
"Martinique": "France",
"Netherlands Antilles": "Netherlands",
"North Vietnam": "Viet Nam",
"Occupied Palestinian Territory": "Palestine",
"Reunion": "France",
"Saint Kitts and Nevis": "St. Kitts and Nevis",
"Serbia and Montenegro": "Serbia",
"Siam": "Thailand",
"Soviet Union": "Russia",
"Swaziland": "Eswatini",
"The Democratic Republic of Congo": "Democratic Republic of the Congo",
"United States": "United States of America",
"West Germany": "Germany",
"Yugoslavia": "Serbia" // or "Slovenia", "Croatia", etc.
};
}
Insert cell
// movieCountsByYear = {
// const counts = {};
// for (const d of combined_movies_data) {
// const year = +d.Year;
// const rating = +d.Rating;
// if (!year || !d.countries_origin || isNaN(rating) || rating < minRating) continue;

// let countries;
// try {
// countries = eval(d.countries_origin);
// } catch {
// continue;
// }

// if (!counts[year]) counts[year] = {};
// for (let country of countries) {
// country = countryNameFixes[country] || country;
// if (!counts[year][country]) counts[year][country] = 0;
// counts[year][country]++;
// }
// }
// return counts;
// }

movieCountsByYear = {
const counts = {};

for (const d of combined_movies_data) {
const year = +d.Year;
const rating = +d.Rating;
const revenueStr = d.grossWorldWWide;
const voteStr = d.Votes;

if (!year || !d.countries_origin || isNaN(rating) || rating < minRating) continue;

// Parse vote count
let votes = 0;
if (voteStr) {
const cleaned = voteStr.trim().toUpperCase();
if (cleaned.endsWith("K")) {
votes = parseFloat(cleaned.replace("K", "")) * 1000;
} else if (cleaned.endsWith("M")) {
votes = parseFloat(cleaned.replace("M", "")) * 1000000;
} else {
votes = parseFloat(cleaned.replace(/[^0-9.]/g, ""));
}
}

// Apply vote filter
if (isNaN(votes) || votes < minVotes) continue;

// Parse revenue
let revenue = 0;
if (revenueStr) {
const cleanedRevenue = revenueStr.replace(/[^0-9.]/g, "");
revenue = parseFloat(cleanedRevenue) || 0;
}

// Parse countries
let countries;
try {
countries = eval(d.countries_origin);
} catch {
continue;
}

if (!counts[year]) counts[year] = {};
for (let country of countries) {
country = countryNameFixes[country] || country;
if (!counts[year][country]) {
counts[year][country] = {
count: 0,
revenue: 0,
ratingSum: 0,
ratingCount: 0
};
}
counts[year][country].count++;
counts[year][country].revenue += revenue;
counts[year][country].ratingSum += rating;
counts[year][country].ratingCount++;
}
}

return counts;
}
Insert cell
viewof selectedYear = Inputs.range([1925, 2025], {
step: 1,
label: "Select Year",
value: 2025
})
Insert cell
viewof minRating = Inputs.range([0, 10], {
step: 0.1,
value: 7,
label: "Minimum IMDb Rating"
})
Insert cell
viewof mapMetric = Inputs.radio(["Movie Count", "Average Rating"], {
label: "Map Metric",
value: "Movie Count"
})
Insert cell
viewof minVotes = Inputs.range([0, 100000], {
step: 1000,
value: 10000,
label: "Minimum IMDb Votes"
})
Insert cell
// mapChart = {
// const width = 940, height = 650;

// const svg = d3.create("svg")
// .attr("width", width)
// .attr("height", height)
// .style("border", "1px solid #ccc");

// const g = svg.append("g"); // for zoomable map group

// const projection = d3.geoMercator().scale(150).translate([width / 2, height / 1.5]);
// const path = d3.geoPath(projection);

// const data = movieCountsByYear[selectedYear] || {};
// const totalMovies = d3.sum(Object.values(data));
// const maxCount = d3.max(Object.values(data));
// const color = d3.scaleSequential(d3.interpolateOranges).domain([0, maxCount || 1]);

// g.selectAll("path")
// .data(customGeoJSON.features)
// .join("path")
// .attr("d", path)
// .attr("fill", d => {
// const count = data[d.properties.name] || 0;
// return count > 0 ? color(count) : "#eee";
// })
// .attr("stroke", "#333")
// .append("title")
// .text(d => {
// const name = d.properties.name;
// const count = data[name] || 0;
// const percent = totalMovies > 0 ? ((count / totalMovies) * 100).toFixed(2) : "0.00";
// return `${name}\n${count} movie${count !== 1 ? 's' : ''} (${percent}%)`;
// });

// // Add zoom behavior
// svg.call(d3.zoom()
// .scaleExtent([1, 8]) // min/max zoom level
// .on("zoom", (event) => {
// g.attr("transform", event.transform);
// })
// );

// return svg.node();
// }

mapChart = {
const width = 940, height = 650;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid #ccc");

const g = svg.append("g");

const projection = d3.geoMercator().scale(150).translate([width / 2, height / 1.5]);
const path = d3.geoPath(projection);

const data = movieCountsByYear[selectedYear] || {};
const totalCount = d3.sum(Object.values(data), d => d.count || 0);
const maxCount = d3.max(Object.values(data), d => d.count || 0);
let maxValue;
let color;
if (mapMetric === "Movie Count") {
maxValue = d3.max(Object.values(data), d => d.count || 0);
color = d3.scaleSequential(d3.interpolateOranges).domain([0, maxValue || 1]);
} else {
maxValue = 10; // IMDb ratings cap at 10
color = d3.scaleSequential(d3.interpolateBlues).domain([0, maxValue]);
}

g.selectAll("path")
.data(customGeoJSON.features)
.join("path")
.attr("d", path)
.attr("fill", d => {
const record = data[d.properties.name];
if (!record) return "#eee";
if (mapMetric === "Movie Count") return color(record.count);
if (record.ratingCount > 0) {
const avgRating = record.ratingSum / record.ratingCount;
return color(avgRating);
}
return "#eee";
})
.attr("stroke", "#333")
.append("title")
.text(d => {
const name = d.properties.name;
const record = data[name];
if (!record) return `${name}\nNo data`;
const { count, revenue, ratingSum, ratingCount } = record;
const percent = totalCount > 0 ? ((count / totalCount) * 100).toFixed(2) : "0.00";
const revenueDisplay = revenue > 0 ? `$${revenue.toLocaleString()}` : "Unknown";
const avgRating = ratingCount > 0 ? (ratingSum / ratingCount).toFixed(2) : "N/A";
return `${name}
Movies: ${count} (${percent}%)
Revenue: ${revenueDisplay}
Avg IMDb Rating: ${avgRating}`;
});

svg.call(d3.zoom()
.scaleExtent([1, 8])
.on("zoom", (event) => {
g.attr("transform", event.transform);
})
);

return svg.node();
}
Insert cell
import {legend} from "@d3/color-legend"
Insert cell
movieLegend = {
const legendScale = d3.scaleSequential(d3.interpolateOranges)
.domain([0, d3.max(Object.values(movieCountsByYear[selectedYear] || {})) || 1]);

return legend({
color: legendScale,
title: "Movie Count",
width: 260
});
}
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