564 lines
20 KiB
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>
|