infinite-agents-public/d3_test/d3_viz_3.html

564 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3 Visualization 3: Global Coffee Production Analysis</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
#visualization-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
h1 {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
color: white;
padding: 30px;
text-align: center;
font-size: 2em;
letter-spacing: 1px;
}
#controls {
padding: 20px 30px;
background: #f7fafc;
border-bottom: 2px solid #e2e8f0;
display: flex;
gap: 20px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-weight: 600;
color: #2d3748;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
button:hover {
background: #5a67d8;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button.active {
background: #48bb78;
}
#viz {
padding: 40px;
display: flex;
justify-content: center;
}
#insights {
padding: 30px;
background: #edf2f7;
border-top: 3px solid #cbd5e0;
}
#insights h3 {
color: #2d3748;
margin-bottom: 15px;
font-size: 1.3em;
}
.insight-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 15px;
}
.insight-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.insight-card h4 {
color: #667eea;
margin-bottom: 8px;
}
.insight-card p {
color: #4a5568;
line-height: 1.6;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 16px;
border-radius: 6px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.tooltip.visible {
opacity: 1;
}
.bar {
transition: all 0.3s ease;
cursor: pointer;
}
.bar:hover {
opacity: 0.8;
filter: brightness(1.1);
}
.axis text {
font-size: 12px;
color: #4a5568;
}
.axis path,
.axis line {
stroke: #cbd5e0;
}
.axis-label {
font-size: 14px;
font-weight: 600;
fill: #2d3748;
}
footer {
margin-top: 0;
padding: 30px;
background: #2d3748;
color: #e2e8f0;
}
footer h3 {
color: #a0aec0;
margin-bottom: 15px;
border-bottom: 2px solid #4a5568;
padding-bottom: 10px;
}
footer ul {
list-style: none;
}
footer li {
margin-bottom: 12px;
line-height: 1.6;
}
footer strong {
color: #90cdf4;
}
footer a {
color: #90cdf4;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div id="visualization-container">
<h1>Global Coffee Production by Country (2024)</h1>
<div id="controls">
<div class="control-group">
<label>Sort by:</label>
<button id="sortDefault" class="active">Default</button>
<button id="sortAsc">Production ↑</button>
<button id="sortDesc">Production ↓</button>
</div>
<div class="control-group">
<button id="toggleColors">Toggle Color Theme</button>
</div>
</div>
<div id="viz">
<!-- D3 bar chart renders here -->
</div>
<div id="insights">
<h3>Key Insights from Coffee Production Data</h3>
<div class="insight-grid">
<div class="insight-card">
<h4>🏆 Top Producer</h4>
<p id="topProducer">Loading...</p>
</div>
<div class="insight-card">
<h4>📊 Total Production</h4>
<p id="totalProduction">Loading...</p>
</div>
<div class="insight-card">
<h4>📈 Average Production</h4>
<p id="avgProduction">Loading...</p>
</div>
<div class="insight-card">
<h4>🌍 Market Concentration</h4>
<p id="marketShare">Loading...</p>
</div>
</div>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
// ============================================
// DATA: Global Coffee Production (Million 60kg bags)
// ============================================
const coffeeData = [
{ country: "Brazil", production: 69.0, region: "South America" },
{ country: "Vietnam", production: 29.0, region: "Asia" },
{ country: "Colombia", production: 14.5, region: "South America" },
{ country: "Indonesia", production: 11.5, region: "Asia" },
{ country: "Ethiopia", production: 8.2, region: "Africa" },
{ country: "Honduras", production: 7.8, region: "Central America" },
{ country: "India", production: 5.8, region: "Asia" },
{ country: "Uganda", production: 5.5, region: "Africa" },
{ country: "Peru", production: 5.2, region: "South America" },
{ country: "Guatemala", production: 4.0, region: "Central America" },
{ country: "Mexico", production: 3.8, region: "North America" },
{ country: "Nicaragua", production: 3.2, region: "Central America" },
{ country: "Côte d'Ivoire", production: 2.1, region: "Africa" },
{ country: "Costa Rica", production: 1.5, region: "Central America" },
{ country: "Tanzania", production: 1.3, region: "Africa" },
{ country: "El Salvador", production: 0.7, region: "Central America" }
];
// ============================================
// MARGIN CONVENTION (from Observable tutorial)
// ============================================
const margin = { top: 20, right: 30, bottom: 70, left: 80 };
const width = 1000;
const height = 500;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// ============================================
// COLOR SCHEMES (toggleable)
// ============================================
const colorSchemes = {
gradient: d3.interpolateRgbBasis(["#f093fb", "#4facfe"]),
earth: d3.interpolateRgbBasis(["#fa709a", "#fee140"]),
ocean: d3.interpolateRgbBasis(["#667eea", "#764ba2"])
};
let currentScheme = "gradient";
// ============================================
// CREATE SVG WITH VIEWBOX (responsive design)
// ============================================
const svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
// Main chart group with margin transform
const chart = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// ============================================
// SCALES (Synthesis of Iteration 2 learnings)
// ============================================
// X Scale: Band scale for categorical data (countries)
const xScale = d3.scaleBand()
.domain(coffeeData.map(d => d.country))
.range([0, innerWidth])
.padding(0.2);
// Y Scale: Linear scale for production values
const yScale = d3.scaleLinear()
.domain([0, d3.max(coffeeData, d => d.production)])
.range([innerHeight, 0])
.nice();
// Color Scale: For visual encoding based on production value
const colorScale = d3.scaleSequential()
.domain([0, d3.max(coffeeData, d => d.production)])
.interpolator(colorSchemes[currentScheme]);
// ============================================
// AXES (from Observable bar chart pattern)
// ============================================
const xAxis = d3.axisBottom(xScale)
.tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale)
.ticks(height / 40)
.tickFormat(d => d + "M");
// Append X Axis
const xAxisGroup = chart.append("g")
.attr("class", "axis x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");
// Append Y Axis
chart.append("g")
.attr("class", "axis y-axis")
.call(yAxis);
// Axis Labels
svg.append("text")
.attr("class", "axis-label")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.text("Country");
svg.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.text("Production (Million 60kg bags)");
// ============================================
// TOOLTIP (for interactivity)
// ============================================
const tooltip = d3.select("#tooltip");
// ============================================
// BARS (Synthesis of Iterations 1, 2, & 3)
// Uses selections (Iteration 1), scales (Iteration 2),
// and bar chart patterns (Iteration 3)
// ============================================
function renderBars(data) {
// DATA JOIN: bind data to rect elements
const bars = chart.selectAll(".bar")
.data(data, d => d.country);
// EXIT: remove bars that no longer have data
bars.exit()
.transition()
.duration(750)
.attr("y", innerHeight)
.attr("height", 0)
.remove();
// ENTER + UPDATE: create new bars and update existing
bars.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.country))
.attr("y", innerHeight)
.attr("width", xScale.bandwidth())
.attr("height", 0)
.attr("fill", d => colorScale(d.production))
.merge(bars)
.on("mouseenter", handleMouseEnter)
.on("mousemove", handleMouseMove)
.on("mouseleave", handleMouseLeave)
.transition()
.duration(750)
.attr("x", d => xScale(d.country))
.attr("y", d => yScale(d.production))
.attr("width", xScale.bandwidth())
.attr("height", d => innerHeight - yScale(d.production))
.attr("fill", d => colorScale(d.production));
}
// ============================================
// INTERACTIVITY: Tooltip handlers
// ============================================
function handleMouseEnter(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 0.7);
tooltip
.html(`
<strong>${d.country}</strong><br/>
Production: ${d.production}M bags<br/>
Region: ${d.region}
`)
.classed("visible", true);
}
function handleMouseMove(event) {
tooltip
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
}
function handleMouseLeave(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 1);
tooltip.classed("visible", false);
}
// ============================================
// SORTING CONTROLS
// ============================================
let currentData = [...coffeeData];
document.getElementById("sortDefault").addEventListener("click", function() {
currentData = [...coffeeData];
updateChart();
updateActiveButton(this);
});
document.getElementById("sortAsc").addEventListener("click", function() {
currentData = [...coffeeData].sort((a, b) => a.production - b.production);
updateChart();
updateActiveButton(this);
});
document.getElementById("sortDesc").addEventListener("click", function() {
currentData = [...coffeeData].sort((a, b) => b.production - a.production);
updateChart();
updateActiveButton(this);
});
function updateActiveButton(button) {
document.querySelectorAll("#controls button").forEach(btn => {
btn.classList.remove("active");
});
button.classList.add("active");
}
function updateChart() {
// Update X scale domain with new order
xScale.domain(currentData.map(d => d.country));
// Update X axis
chart.select(".x-axis")
.transition()
.duration(750)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");
// Re-render bars
renderBars(currentData);
}
// ============================================
// COLOR THEME TOGGLE
// ============================================
const colorSchemeKeys = Object.keys(colorSchemes);
let colorSchemeIndex = 0;
document.getElementById("toggleColors").addEventListener("click", function() {
colorSchemeIndex = (colorSchemeIndex + 1) % colorSchemeKeys.length;
currentScheme = colorSchemeKeys[colorSchemeIndex];
colorScale.interpolator(colorSchemes[currentScheme]);
chart.selectAll(".bar")
.transition()
.duration(500)
.attr("fill", d => colorScale(d.production));
});
// ============================================
// INSIGHTS CALCULATION
// ============================================
function calculateInsights() {
const total = d3.sum(coffeeData, d => d.production);
const avg = d3.mean(coffeeData, d => d.production);
const topProducer = coffeeData[0];
const topThreeTotal = d3.sum(coffeeData.slice(0, 3), d => d.production);
const marketShare = (topThreeTotal / total * 100).toFixed(1);
document.getElementById("topProducer").textContent =
`${topProducer.country} leads with ${topProducer.production}M bags`;
document.getElementById("totalProduction").textContent =
`${total.toFixed(1)}M bags globally`;
document.getElementById("avgProduction").textContent =
`${avg.toFixed(1)}M bags per country`;
document.getElementById("marketShare").textContent =
`Top 3 countries control ${marketShare}% of global production`;
}
// ============================================
// INITIALIZE
// ============================================
renderBars(currentData);
calculateInsights();
</script>
<footer>
<h3>Iteration 3 - Web Learning Applied: Complete Bar Chart Visualization</h3>
<ul>
<li><strong>Web Source:</strong> <a href="https://observablehq.com/@d3/bar-chart" target="_blank">https://observablehq.com/@d3/bar-chart</a></li>
<li><strong>Topic:</strong> D3 Bar Charts - Creating complete data visualizations with axes, scales, and interactivity</li>
<li><strong>Techniques Learned:</strong>
<ol style="margin-left: 20px; margin-top: 8px;">
<li><strong>Margin Convention:</strong> Proper SVG layout using margin object to create space for axes and labels</li>
<li><strong>Band Scales:</strong> Using scaleBand() for categorical data positioning with automatic spacing</li>
<li><strong>Axis Creation:</strong> Creating and positioning axes with axisBottom() and axisLeft(), removing outer ticks for cleaner appearance</li>
</ol>
</li>
<li><strong>What This Demonstrates:</strong> A complete, interactive bar chart showing global coffee production by country. Features dynamic sorting (default, ascending, descending), color theme toggling, smooth transitions, hover tooltips, and automated insights calculation.</li>
<li><strong>Knowledge Synthesis:</strong>
<ul style="margin-left: 20px; margin-top: 8px;">
<li><strong>From Iteration 1 (Selections):</strong> Used data joins with enter/exit/update pattern, event handlers for interactivity, and dynamic DOM manipulation</li>
<li><strong>From Iteration 2 (Scales):</strong> Applied scaleBand for X-axis categorical positioning, scaleLinear for Y-axis numerical mapping, and scaleSequential for color encoding</li>
<li><strong>From Iteration 3 (Bar Charts):</strong> Implemented margin convention, responsive SVG with viewBox, proper axis creation and positioning, and bar height calculation using scale inversion</li>
</ul>
</li>
<li><strong>Improvement over Previous:</strong> This is a complete, production-ready visualization combining all learned concepts. Unlike isolated examples of selections (Iteration 1) or scales (Iteration 2), this demonstrates a fully functional chart with professional styling, multiple interaction patterns, data-driven insights, and responsive design. The bar chart brings together DOM manipulation, data transformation, visual encoding, and user interactivity into a cohesive whole.</li>
</ul>
</footer>
</body>
</html>