viewof kickstarterComparison = {
const container = html`
<div style="font-family: Arial, sans-serif; max-width: 900px; text-align: center;">
<style>
h1 { text-align: center; font-size: 20px; margin-bottom: 10px; }
.control-container { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-bottom: 15px; }
select, input { padding: 5px; font-size: 14px; border: 1px solid #ccc; }
svg { display: block; margin: auto; }
.tooltip { position: absolute; background: rgba(0, 0, 0, 0.8); color: white; padding: 8px; border-radius: 5px; font-size: 12px; visibility: hidden; pointer-events: none; }
.dot { cursor: pointer; fill: white; stroke: black; stroke-width: 1.5px; }
</style>
<h1>📊 Kickstarter Success Rate & Trends</h1>
<p>Compare Kickstarter project success rates across different categories, subcategories, and years.</p>
<div class="control-container">
<label>Category 1: <select id="categoryFilter"></select></label>
<label>Subcategory 1: <select id="subcategoryFilter" disabled></select></label>
<label>Category 2: <select id="categoryFilter2"></select></label>
<label>Subcategory 2: <select id="subcategoryFilter2" disabled></select></label>
<label>Year: <input type="range" id="yearSlider" min="2009" max="2023" step="1" value="2023"></label>
<span id="selectedYear">2023</span>
</div>
<svg width="800" height="500"></svg>
<div class="tooltip"></div>
</div>`;
const svg = d3.select(container).select("svg");
const tooltip = d3.select(container).select(".tooltip");
const margin = { top: 50, right: 50, bottom: 50, left: 80 };
const width = +svg.attr("width") - margin.left - margin.right;
const height = +svg.attr("height") - margin.top - margin.bottom;
const gMain = svg.append("g").attr("transform", `translate(${margin.left}, ${margin.top})`);
const xScale = d3.scaleLinear().domain([2009, 2023]).range([0, width]);
gMain.append("g").attr("transform", `translate(0,${height})`).call(d3.axisBottom(xScale).tickFormat(d3.format("d")));
async function initialize() {
const kickstarterData = await FileAttachment("Kickstarter_processed.tsv").text();
const parsedData = d3.tsvParse(kickstarterData, d => {
d.YEAR = +d.YEAR;
d.CATEGORY = (d.CATEGORY || "Unknown").trim();
d.SUBCATEGORY = (d.SUBCATEGORY || "General").trim();
d.SUCCESS_RATE = Math.min(+d.SUCCESS_RATE, 1);
return d;
});
const aggregatedCategoryData = Array.from(d3.group(parsedData, d => `${d.CATEGORY}_${d.YEAR}`))
.map(([key, values]) => {
const [category, year] = key.split("_");
return {
CATEGORY: category,
YEAR: +year,
SUCCESS_RATE: d3.mean(values, d => d.SUCCESS_RATE)
};
});
const categories = [...new Set(parsedData.map(d => d.CATEGORY))].sort();
const subcategories = d3.group(parsedData, d => d.CATEGORY, d => d.SUBCATEGORY);
function populateDropdown(dropdownId, options) {
const dropdown = d3.select(dropdownId);
dropdown.html("<option value=''>All</option>");
options.forEach(option => {
dropdown.append("option").attr("value", option).text(option);
});
}
function updateSubcategories(categoryDropdownId, subcategoryDropdownId) {
const selectedCategory = d3.select(categoryDropdownId).property("value");
const subcategoryDropdown = d3.select(subcategoryDropdownId);
if (!selectedCategory) {
subcategoryDropdown.html("<option value=''>All</option>").property("disabled", true);
} else {
const subcats = Array.from(subcategories.get(selectedCategory)?.keys() || []);
populateDropdown(subcategoryDropdownId, subcats);
subcategoryDropdown.property("disabled", false);
}
}
populateDropdown("#categoryFilter", categories);
populateDropdown("#categoryFilter2", categories);
d3.select("#categoryFilter").on("change", function () {
updateSubcategories("#categoryFilter", "#subcategoryFilter");
updateChart();
});
d3.select("#categoryFilter2").on("change", function () {
updateSubcategories("#categoryFilter2", "#subcategoryFilter2");
updateChart();
});
d3.select("#subcategoryFilter").on("change", updateChart);
d3.select("#subcategoryFilter2").on("change", updateChart);
d3.select("#yearSlider").on("input", function () {
d3.select("#selectedYear").text(this.value);
updateChart();
});
function updateChart() {
const selectedCategory1 = d3.select("#categoryFilter").property("value");
const selectedSubcategory1 = d3.select("#subcategoryFilter").property("value");
const selectedCategory2 = d3.select("#categoryFilter2").property("value");
const selectedSubcategory2 = d3.select("#subcategoryFilter2").property("value");
const selectedYear = +d3.select("#yearSlider").property("value");
d3.select("#selectedYear").text(selectedYear);
function fillMissingYears(data) {
const years = d3.range(2009, selectedYear + 1);
return years.map(year => {
const found = data.find(d => d.YEAR === year);
return found ? found : { YEAR: year, SUCCESS_RATE: 0 };
});
}
let filteredData1 = selectedSubcategory1
? parsedData.filter(d => d.YEAR <= selectedYear && d.CATEGORY === selectedCategory1 && d.SUBCATEGORY === selectedSubcategory1)
: aggregatedCategoryData.filter(d => d.YEAR <= selectedYear && d.CATEGORY === selectedCategory1);
let filteredData2 = selectedSubcategory2
? parsedData.filter(d => d.YEAR <= selectedYear && d.CATEGORY === selectedCategory2 && d.SUBCATEGORY === selectedSubcategory2)
: aggregatedCategoryData.filter(d => d.YEAR <= selectedYear && d.CATEGORY === selectedCategory2);
filteredData1 = fillMissingYears(filteredData1.sort((a, b) => a.YEAR - b.YEAR));
filteredData2 = fillMissingYears(filteredData2.sort((a, b) => a.YEAR - b.YEAR));
const allSuccessRates = [...filteredData1, ...filteredData2].map(d => d.SUCCESS_RATE);
const yMax = allSuccessRates.length ? Math.max(...allSuccessRates) : 1;
const yScale = d3.scaleLinear().domain([0, yMax + 0.1]).range([height, 0]);
gMain.selectAll("*").remove();
gMain.append("g").attr("transform", `translate(0,${height})`).call(d3.axisBottom(xScale).tickFormat(d3.format("d")));
gMain.append("g").call(d3.axisLeft(yScale));
// X Axis Label
gMain.append("text")
.attr("text-anchor", "middle")
.attr("x", width / 2)
.attr("y", height + 40)
.attr("font-size", "14px")
.text("Year");
// Y Axis Label
gMain.append("text")
.attr("text-anchor", "middle")
.attr("transform", `rotate(-90)`)
.attr("x", -height / 2)
.attr("y", -50)
.attr("font-size", "14px")
.text("Success Rate");
function drawLine(data, color, className) {
if (data.length === 0) return;
const line = d3.line()
.x(d => xScale(d.YEAR))
.y(d => yScale(d.SUCCESS_RATE));
const path = gMain.append("path")
.datum(data)
.attr("class", className)
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", 3)
.attr("d", line);
const totalLength = path.node().getTotalLength(); // Get path length
path.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1500)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
// Add dots with tooltips
gMain.selectAll(".dot-" + className)
.data(data)
.enter().append("circle")
.attr("class", "dot " + className)
.attr("cx", d => xScale(d.YEAR))
.attr("cy", d => yScale(d.SUCCESS_RATE))
.attr("r", 5)
.style("fill", "white")
.style("stroke", color)
.style("stroke-width", 2)
.on("mouseover", (event, d) => {
tooltip.style("visibility", "visible")
.html(`
<b>📅 Year:</b> ${d.YEAR} <br>
<b>📂 Category:</b> ${d.CATEGORY} <br>
<b>📌 Subcategory:</b> ${d.SUBCATEGORY} <br>
<b>✅ Success Rate:</b> ${(d.SUCCESS_RATE * 100).toFixed(2)}% <br>
<b>📊 Total Projects:</b> ${d.total_projects || "N/A"} <br>
<b>🏆 Successful Projects:</b> ${d.successful_projects || "N/A"}
`)
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 40}px`);
})
.on("mouseout", () => tooltip.style("visibility", "hidden"));
}
drawLine(filteredData1, "steelblue"); // First category (Blue)
drawLine(filteredData2, "red"); // Second category (Red)
}
updateChart();
}
initialize();
return container;
}