749 lines
30 KiB
HTML
749 lines
30 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 2: Multi-Scale Temperature Analysis</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
#visualization-container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
}
|
|
|
|
h1 {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
font-size: 2.2em;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
#controls {
|
|
padding: 20px 40px;
|
|
background: #f8f9fa;
|
|
border-bottom: 2px solid #e9ecef;
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.control-group label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.control-group select {
|
|
padding: 8px 12px;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.control-group select:hover {
|
|
border-color: #667eea;
|
|
}
|
|
|
|
#viz {
|
|
padding: 40px;
|
|
min-height: 600px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 40px;
|
|
}
|
|
|
|
.chart-section {
|
|
background: #f8f9fa;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 1.4em;
|
|
color: #495057;
|
|
margin-bottom: 10px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.chart-description {
|
|
color: #6c757d;
|
|
margin-bottom: 20px;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
#insights {
|
|
padding: 30px 40px;
|
|
background: #f8f9fa;
|
|
border-top: 2px solid #e9ecef;
|
|
}
|
|
|
|
#insights h2 {
|
|
color: #495057;
|
|
margin-bottom: 15px;
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.insight-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.insight-card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.insight-card h3 {
|
|
color: #667eea;
|
|
margin-bottom: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.insight-card p {
|
|
color: #6c757d;
|
|
line-height: 1.6;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.scale-legend {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background: white;
|
|
border-radius: 6px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 30px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
footer {
|
|
margin-top: 0;
|
|
padding: 30px 40px;
|
|
background: #343a40;
|
|
color: white;
|
|
}
|
|
|
|
footer h3 {
|
|
color: #667eea;
|
|
margin-bottom: 20px;
|
|
font-size: 1.6em;
|
|
}
|
|
|
|
footer ul {
|
|
list-style: none;
|
|
}
|
|
|
|
footer li {
|
|
margin: 12px 0;
|
|
padding-left: 20px;
|
|
position: relative;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
footer li:before {
|
|
content: "▸";
|
|
position: absolute;
|
|
left: 0;
|
|
color: #667eea;
|
|
}
|
|
|
|
footer strong {
|
|
color: #a5b4fc;
|
|
}
|
|
|
|
.axis {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.axis path,
|
|
.axis line {
|
|
stroke: #495057;
|
|
}
|
|
|
|
.axis text {
|
|
fill: #495057;
|
|
}
|
|
|
|
.bar {
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.bar:hover {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.data-point {
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.data-point:hover {
|
|
r: 8;
|
|
stroke-width: 3;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="visualization-container">
|
|
<h1>Multi-Scale Temperature Analysis Dashboard</h1>
|
|
|
|
<div id="controls">
|
|
<div class="control-group">
|
|
<label for="scaleType">Time Scale Type:</label>
|
|
<select id="scaleType">
|
|
<option value="linear">Linear Scale</option>
|
|
<option value="time" selected>Time Scale</option>
|
|
</select>
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="colorScheme">Color Scheme:</label>
|
|
<select id="colorScheme">
|
|
<option value="sequential" selected>Sequential (Cool to Hot)</option>
|
|
<option value="diverging">Diverging (Below/Above Average)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="viz">
|
|
<div class="chart-section">
|
|
<div class="chart-title">1. Time Scale + Sequential Color Scale</div>
|
|
<div class="chart-description">
|
|
Using <strong>scaleTime()</strong> for temporal data mapping and <strong>scaleSequential()</strong> for temperature color encoding
|
|
</div>
|
|
<svg id="timeChart"></svg>
|
|
</div>
|
|
|
|
<div class="chart-section">
|
|
<div class="chart-title">2. Band Scale + Linear Scale + Threshold Color Scale</div>
|
|
<div class="chart-description">
|
|
Using <strong>scaleBand()</strong> for categorical positioning, <strong>scaleLinear()</strong> for height, and <strong>scaleThreshold()</strong> for discrete color ranges
|
|
</div>
|
|
<svg id="barChart"></svg>
|
|
</div>
|
|
|
|
<div class="chart-section">
|
|
<div class="chart-title">3. Point Scale + Log Scale</div>
|
|
<div class="chart-description">
|
|
Using <strong>scalePoint()</strong> for precise categorical positioning and <strong>scaleLog()</strong> for exponential temperature variance display
|
|
</div>
|
|
<svg id="pointChart"></svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="insights">
|
|
<h2>Scale Mappings Explained</h2>
|
|
<div class="insight-grid">
|
|
<div class="insight-card">
|
|
<h3>Time Scale (scaleTime)</h3>
|
|
<p>Maps JavaScript Date objects to pixel positions. Domain: [earliest date, latest date] → Range: [0, chartWidth]. Automatically handles date arithmetic and tick formatting.</p>
|
|
</div>
|
|
<div class="insight-card">
|
|
<h3>Sequential Color Scale</h3>
|
|
<p>Maps continuous temperature values to smooth color gradients. Domain: [minTemp, maxTemp] → Range: color interpolation. Perfect for showing intensity variations.</p>
|
|
</div>
|
|
<div class="insight-card">
|
|
<h3>Band Scale (scaleBand)</h3>
|
|
<p>Maps categorical city names to positions with automatic padding. Divides available space evenly with configurable inner/outer padding for bar charts.</p>
|
|
</div>
|
|
<div class="insight-card">
|
|
<h3>Linear Scale (scaleLinear)</h3>
|
|
<p>Standard numeric mapping. Domain: [0, maxTemp] → Range: [chartHeight, 0]. Inverted for SVG coordinate system where y=0 is at top.</p>
|
|
</div>
|
|
<div class="insight-card">
|
|
<h3>Threshold Scale</h3>
|
|
<p>Maps continuous input to discrete output using breakpoints. Temperature thresholds: [10°, 20°, 30°] create 4 color categories for "cold", "cool", "warm", "hot".</p>
|
|
</div>
|
|
<div class="insight-card">
|
|
<h3>Log Scale (scaleLog)</h3>
|
|
<p>Maps wide-range data using logarithmic transformation. Domain: [1, maxVariance] → Range: [0, height]. Compresses large values, expands small ones for better visibility.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Embedded realistic temperature data for multiple cities over time
|
|
const rawData = [
|
|
{ date: new Date(2024, 0, 1), city: 'New York', temp: 2, variance: 5 },
|
|
{ date: new Date(2024, 1, 1), city: 'New York', temp: 4, variance: 8 },
|
|
{ date: new Date(2024, 2, 1), city: 'New York', temp: 10, variance: 12 },
|
|
{ date: new Date(2024, 3, 1), city: 'New York', temp: 16, variance: 15 },
|
|
{ date: new Date(2024, 4, 1), city: 'New York', temp: 22, variance: 10 },
|
|
{ date: new Date(2024, 5, 1), city: 'New York', temp: 27, variance: 8 },
|
|
{ date: new Date(2024, 6, 1), city: 'New York', temp: 30, variance: 6 },
|
|
{ date: new Date(2024, 7, 1), city: 'New York', temp: 28, variance: 7 },
|
|
{ date: new Date(2024, 8, 1), city: 'New York', temp: 23, variance: 11 },
|
|
{ date: new Date(2024, 9, 1), city: 'New York', temp: 16, variance: 14 },
|
|
{ date: new Date(2024, 10, 1), city: 'New York', temp: 9, variance: 18 },
|
|
{ date: new Date(2024, 11, 1), city: 'New York', temp: 3, variance: 9 },
|
|
|
|
{ date: new Date(2024, 0, 1), city: 'Los Angeles', temp: 15, variance: 3 },
|
|
{ date: new Date(2024, 1, 1), city: 'Los Angeles', temp: 16, variance: 4 },
|
|
{ date: new Date(2024, 2, 1), city: 'Los Angeles', temp: 17, variance: 5 },
|
|
{ date: new Date(2024, 3, 1), city: 'Los Angeles', temp: 19, variance: 4 },
|
|
{ date: new Date(2024, 4, 1), city: 'Los Angeles', temp: 21, variance: 3 },
|
|
{ date: new Date(2024, 5, 1), city: 'Los Angeles', temp: 24, variance: 2 },
|
|
{ date: new Date(2024, 6, 1), city: 'Los Angeles', temp: 26, variance: 2 },
|
|
{ date: new Date(2024, 7, 1), city: 'Los Angeles', temp: 27, variance: 2 },
|
|
{ date: new Date(2024, 8, 1), city: 'Los Angeles', temp: 25, variance: 3 },
|
|
{ date: new Date(2024, 9, 1), city: 'Los Angeles', temp: 22, variance: 4 },
|
|
{ date: new Date(2024, 10, 1), city: 'Los Angeles', temp: 18, variance: 5 },
|
|
{ date: new Date(2024, 11, 1), city: 'Los Angeles', temp: 15, variance: 4 },
|
|
|
|
{ date: new Date(2024, 0, 1), city: 'Chicago', temp: -5, variance: 12 },
|
|
{ date: new Date(2024, 1, 1), city: 'Chicago', temp: -2, variance: 15 },
|
|
{ date: new Date(2024, 2, 1), city: 'Chicago', temp: 6, variance: 18 },
|
|
{ date: new Date(2024, 3, 1), city: 'Chicago', temp: 13, variance: 16 },
|
|
{ date: new Date(2024, 4, 1), city: 'Chicago', temp: 20, variance: 12 },
|
|
{ date: new Date(2024, 5, 1), city: 'Chicago', temp: 26, variance: 9 },
|
|
{ date: new Date(2024, 6, 1), city: 'Chicago', temp: 29, variance: 7 },
|
|
{ date: new Date(2024, 7, 1), city: 'Chicago', temp: 27, variance: 8 },
|
|
{ date: new Date(2024, 8, 1), city: 'Chicago', temp: 21, variance: 11 },
|
|
{ date: new Date(2024, 9, 1), city: 'Chicago', temp: 14, variance: 15 },
|
|
{ date: new Date(2024, 10, 1), city: 'Chicago', temp: 5, variance: 20 },
|
|
{ date: new Date(2024, 11, 1), city: 'Chicago', temp: -3, variance: 14 },
|
|
|
|
{ date: new Date(2024, 0, 1), city: 'Miami', temp: 20, variance: 4 },
|
|
{ date: new Date(2024, 1, 1), city: 'Miami', temp: 21, variance: 5 },
|
|
{ date: new Date(2024, 2, 1), city: 'Miami', temp: 23, variance: 6 },
|
|
{ date: new Date(2024, 3, 1), city: 'Miami', temp: 25, variance: 5 },
|
|
{ date: new Date(2024, 4, 1), city: 'Miami', temp: 28, variance: 4 },
|
|
{ date: new Date(2024, 5, 1), city: 'Miami', temp: 30, variance: 3 },
|
|
{ date: new Date(2024, 6, 1), city: 'Miami', temp: 32, variance: 3 },
|
|
{ date: new Date(2024, 7, 1), city: 'Miami', temp: 32, variance: 3 },
|
|
{ date: new Date(2024, 8, 1), city: 'Miami', temp: 30, variance: 4 },
|
|
{ date: new Date(2024, 9, 1), city: 'Miami', temp: 27, variance: 5 },
|
|
{ date: new Date(2024, 10, 1), city: 'Miami', temp: 24, variance: 5 },
|
|
{ date: new Date(2024, 11, 1), city: 'Miami', temp: 21, variance: 4 },
|
|
|
|
{ date: new Date(2024, 0, 1), city: 'Seattle', temp: 6, variance: 8 },
|
|
{ date: new Date(2024, 1, 1), city: 'Seattle', temp: 8, variance: 9 },
|
|
{ date: new Date(2024, 2, 1), city: 'Seattle', temp: 11, variance: 10 },
|
|
{ date: new Date(2024, 3, 1), city: 'Seattle', temp: 13, variance: 11 },
|
|
{ date: new Date(2024, 4, 1), city: 'Seattle', temp: 16, variance: 9 },
|
|
{ date: new Date(2024, 5, 1), city: 'Seattle', temp: 19, variance: 7 },
|
|
{ date: new Date(2024, 6, 1), city: 'Seattle', temp: 22, variance: 6 },
|
|
{ date: new Date(2024, 7, 1), city: 'Seattle', temp: 23, variance: 6 },
|
|
{ date: new Date(2024, 8, 1), city: 'Seattle', temp: 19, variance: 8 },
|
|
{ date: new Date(2024, 9, 1), city: 'Seattle', temp: 14, variance: 10 },
|
|
{ date: new Date(2024, 10, 1), city: 'Seattle', temp: 9, variance: 11 },
|
|
{ date: new Date(2024, 11, 1), city: 'Seattle', temp: 6, variance: 9 }
|
|
];
|
|
|
|
// Calculate average temperatures by city for bar chart
|
|
const cityAverages = d3.rollup(
|
|
rawData,
|
|
v => ({
|
|
avgTemp: d3.mean(v, d => d.temp),
|
|
maxVariance: d3.max(v, d => d.variance)
|
|
}),
|
|
d => d.city
|
|
);
|
|
|
|
const barData = Array.from(cityAverages, ([city, stats]) => ({
|
|
city,
|
|
avgTemp: stats.avgTemp,
|
|
maxVariance: stats.maxVariance
|
|
}));
|
|
|
|
// CHART 1: Time Scale + Sequential Color Scale
|
|
function renderTimeChart() {
|
|
const margin = { top: 20, right: 30, bottom: 50, left: 60 };
|
|
const width = 1200 - margin.left - margin.right;
|
|
const height = 400 - margin.top - margin.bottom;
|
|
|
|
// Clear existing chart
|
|
d3.select('#timeChart').selectAll('*').remove();
|
|
|
|
const svg = d3.select('#timeChart')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// SCALE 1: Time Scale - maps dates to x positions
|
|
const xScale = d3.scaleTime()
|
|
.domain(d3.extent(rawData, d => d.date))
|
|
.range([0, width]);
|
|
|
|
// SCALE 2: Linear Scale - maps temperature to y positions
|
|
const yScale = d3.scaleLinear()
|
|
.domain([
|
|
d3.min(rawData, d => d.temp) - 5,
|
|
d3.max(rawData, d => d.temp) + 5
|
|
])
|
|
.range([height, 0]);
|
|
|
|
// SCALE 3: Sequential Color Scale - maps temperature to colors
|
|
const colorScale = d3.scaleSequential()
|
|
.domain([d3.min(rawData, d => d.temp), d3.max(rawData, d => d.temp)])
|
|
.interpolator(d3.interpolateRdYlBu)
|
|
.unknown('#ccc');
|
|
|
|
// Group data by city for line rendering
|
|
const cities = d3.group(rawData, d => d.city);
|
|
|
|
// Draw lines for each city
|
|
const line = d3.line()
|
|
.x(d => xScale(d.date))
|
|
.y(d => yScale(d.temp))
|
|
.curve(d3.curveMonotoneX);
|
|
|
|
cities.forEach((values, city) => {
|
|
svg.append('path')
|
|
.datum(values)
|
|
.attr('fill', 'none')
|
|
.attr('stroke', '#667eea')
|
|
.attr('stroke-width', 2)
|
|
.attr('opacity', 0.3)
|
|
.attr('d', line);
|
|
});
|
|
|
|
// Draw data points with color scale
|
|
svg.selectAll('.data-point')
|
|
.data(rawData)
|
|
.join('circle')
|
|
.attr('class', 'data-point')
|
|
.attr('cx', d => xScale(d.date))
|
|
.attr('cy', d => yScale(d.temp))
|
|
.attr('r', 5)
|
|
.attr('fill', d => colorScale(d.temp))
|
|
.attr('stroke', 'white')
|
|
.attr('stroke-width', 2)
|
|
.append('title')
|
|
.text(d => `${d.city}\n${d.date.toLocaleDateString()}\n${d.temp}°C`);
|
|
|
|
// Add X axis using time scale
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(d3.axisBottom(xScale).ticks(12))
|
|
.selectAll('text')
|
|
.attr('transform', 'rotate(-45)')
|
|
.style('text-anchor', 'end');
|
|
|
|
// Add Y axis
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.call(d3.axisLeft(yScale))
|
|
.append('text')
|
|
.attr('transform', 'rotate(-90)')
|
|
.attr('y', -45)
|
|
.attr('x', -height / 2)
|
|
.attr('fill', '#495057')
|
|
.style('text-anchor', 'middle')
|
|
.text('Temperature (°C)');
|
|
|
|
// Color legend
|
|
const legendWidth = 300;
|
|
const legendHeight = 20;
|
|
const legendScale = d3.scaleLinear()
|
|
.domain(colorScale.domain())
|
|
.range([0, legendWidth]);
|
|
|
|
const legendGroup = svg.append('g')
|
|
.attr('transform', `translate(${width - legendWidth - 10}, ${height + 40})`);
|
|
|
|
// Gradient for legend
|
|
const defs = svg.append('defs');
|
|
const gradient = defs.append('linearGradient')
|
|
.attr('id', 'temp-gradient');
|
|
|
|
gradient.selectAll('stop')
|
|
.data(d3.range(0, 1.01, 0.1))
|
|
.join('stop')
|
|
.attr('offset', d => `${d * 100}%`)
|
|
.attr('stop-color', d => colorScale(
|
|
colorScale.domain()[0] + d * (colorScale.domain()[1] - colorScale.domain()[0])
|
|
));
|
|
|
|
legendGroup.append('rect')
|
|
.attr('width', legendWidth)
|
|
.attr('height', legendHeight)
|
|
.style('fill', 'url(#temp-gradient)')
|
|
.attr('stroke', '#495057');
|
|
|
|
legendGroup.append('g')
|
|
.attr('transform', `translate(0, ${legendHeight})`)
|
|
.call(d3.axisBottom(legendScale).ticks(5).tickFormat(d => `${d}°C`));
|
|
}
|
|
|
|
// CHART 2: Band Scale + Linear Scale + Threshold Color Scale
|
|
function renderBarChart() {
|
|
const margin = { top: 20, right: 30, bottom: 60, left: 60 };
|
|
const width = 1200 - margin.left - margin.right;
|
|
const height = 350 - margin.top - margin.bottom;
|
|
|
|
d3.select('#barChart').selectAll('*').remove();
|
|
|
|
const svg = d3.select('#barChart')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// SCALE 4: Band Scale - maps cities to x positions with automatic spacing
|
|
const xScale = d3.scaleBand()
|
|
.domain(barData.map(d => d.city))
|
|
.range([0, width])
|
|
.padding(0.2); // Automatic padding between bars
|
|
|
|
// SCALE 5: Linear Scale - maps temperature to bar height
|
|
const yScale = d3.scaleLinear()
|
|
.domain([0, d3.max(barData, d => d.avgTemp) + 5])
|
|
.range([height, 0]);
|
|
|
|
// SCALE 6: Threshold Scale - discrete color categories
|
|
const colorScale = d3.scaleThreshold()
|
|
.domain([10, 20, 30]) // Temperature breakpoints
|
|
.range(['#3b82f6', '#10b981', '#f59e0b', '#ef4444']); // cold, cool, warm, hot
|
|
|
|
// Draw bars
|
|
svg.selectAll('.bar')
|
|
.data(barData)
|
|
.join('rect')
|
|
.attr('class', 'bar')
|
|
.attr('x', d => xScale(d.city))
|
|
.attr('y', d => yScale(d.avgTemp))
|
|
.attr('width', xScale.bandwidth())
|
|
.attr('height', d => height - yScale(d.avgTemp))
|
|
.attr('fill', d => colorScale(d.avgTemp))
|
|
.attr('stroke', '#495057')
|
|
.attr('stroke-width', 2)
|
|
.append('title')
|
|
.text(d => `${d.city}: ${d.avgTemp.toFixed(1)}°C`);
|
|
|
|
// Add value labels on bars
|
|
svg.selectAll('.bar-label')
|
|
.data(barData)
|
|
.join('text')
|
|
.attr('class', 'bar-label')
|
|
.attr('x', d => xScale(d.city) + xScale.bandwidth() / 2)
|
|
.attr('y', d => yScale(d.avgTemp) - 5)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#495057')
|
|
.attr('font-weight', 'bold')
|
|
.attr('font-size', '14px')
|
|
.text(d => `${d.avgTemp.toFixed(1)}°C`);
|
|
|
|
// Add axes
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(d3.axisBottom(xScale))
|
|
.selectAll('text')
|
|
.attr('transform', 'rotate(-15)')
|
|
.style('text-anchor', 'end');
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.call(d3.axisLeft(yScale))
|
|
.append('text')
|
|
.attr('transform', 'rotate(-90)')
|
|
.attr('y', -45)
|
|
.attr('x', -height / 2)
|
|
.attr('fill', '#495057')
|
|
.style('text-anchor', 'middle')
|
|
.text('Average Temperature (°C)');
|
|
|
|
// Threshold legend
|
|
const thresholds = [
|
|
{ range: '< 10°C', color: '#3b82f6', label: 'Cold' },
|
|
{ range: '10-20°C', color: '#10b981', label: 'Cool' },
|
|
{ range: '20-30°C', color: '#f59e0b', label: 'Warm' },
|
|
{ range: '> 30°C', color: '#ef4444', label: 'Hot' }
|
|
];
|
|
|
|
const legend = svg.append('g')
|
|
.attr('transform', `translate(${width - 150}, 20)`);
|
|
|
|
thresholds.forEach((t, i) => {
|
|
const item = legend.append('g')
|
|
.attr('transform', `translate(0, ${i * 25})`);
|
|
|
|
item.append('rect')
|
|
.attr('width', 20)
|
|
.attr('height', 20)
|
|
.attr('fill', t.color)
|
|
.attr('stroke', '#495057');
|
|
|
|
item.append('text')
|
|
.attr('x', 28)
|
|
.attr('y', 15)
|
|
.attr('font-size', '12px')
|
|
.attr('fill', '#495057')
|
|
.text(`${t.label}: ${t.range}`);
|
|
});
|
|
}
|
|
|
|
// CHART 3: Point Scale + Log Scale
|
|
function renderPointChart() {
|
|
const margin = { top: 20, right: 30, bottom: 60, left: 80 };
|
|
const width = 1200 - margin.left - margin.right;
|
|
const height = 350 - margin.top - margin.bottom;
|
|
|
|
d3.select('#pointChart').selectAll('*').remove();
|
|
|
|
const svg = d3.select('#pointChart')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// SCALE 7: Point Scale - precise categorical positioning
|
|
const xScale = d3.scalePoint()
|
|
.domain(barData.map(d => d.city))
|
|
.range([0, width])
|
|
.padding(0.5);
|
|
|
|
// SCALE 8: Log Scale - logarithmic mapping for variance
|
|
const yScale = d3.scaleLog()
|
|
.domain([1, d3.max(barData, d => d.maxVariance) + 5])
|
|
.range([height, 0])
|
|
.clamp(true);
|
|
|
|
// Size scale for visual emphasis
|
|
const sizeScale = d3.scaleLinear()
|
|
.domain([0, d3.max(barData, d => d.avgTemp)])
|
|
.range([5, 20]);
|
|
|
|
// Draw connecting line
|
|
svg.append('path')
|
|
.datum(barData)
|
|
.attr('fill', 'none')
|
|
.attr('stroke', '#667eea')
|
|
.attr('stroke-width', 3)
|
|
.attr('opacity', 0.3)
|
|
.attr('d', d3.line()
|
|
.x(d => xScale(d.city))
|
|
.y(d => yScale(d.maxVariance))
|
|
);
|
|
|
|
// Draw points
|
|
svg.selectAll('.variance-point')
|
|
.data(barData)
|
|
.join('circle')
|
|
.attr('class', 'variance-point data-point')
|
|
.attr('cx', d => xScale(d.city))
|
|
.attr('cy', d => yScale(d.maxVariance))
|
|
.attr('r', d => sizeScale(d.avgTemp))
|
|
.attr('fill', '#764ba2')
|
|
.attr('stroke', '#fff')
|
|
.attr('stroke-width', 2)
|
|
.append('title')
|
|
.text(d => `${d.city}\nVariance: ${d.maxVariance}°C\nAvg Temp: ${d.avgTemp.toFixed(1)}°C`);
|
|
|
|
// Add axes
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(d3.axisBottom(xScale))
|
|
.selectAll('text')
|
|
.attr('transform', 'rotate(-15)')
|
|
.style('text-anchor', 'end');
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.call(d3.axisLeft(yScale).ticks(5, '.0f'))
|
|
.append('text')
|
|
.attr('transform', 'rotate(-90)')
|
|
.attr('y', -60)
|
|
.attr('x', -height / 2)
|
|
.attr('fill', '#495057')
|
|
.style('text-anchor', 'middle')
|
|
.text('Temperature Variance (°C) - Log Scale');
|
|
|
|
// Add note about log scale
|
|
svg.append('text')
|
|
.attr('x', width / 2)
|
|
.attr('y', -5)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#6c757d')
|
|
.attr('font-size', '13px')
|
|
.attr('font-style', 'italic')
|
|
.text('Point size represents average temperature | Log scale compresses high variance values');
|
|
}
|
|
|
|
// Initial render
|
|
renderTimeChart();
|
|
renderBarChart();
|
|
renderPointChart();
|
|
|
|
// Event listeners for controls
|
|
document.getElementById('scaleType').addEventListener('change', renderTimeChart);
|
|
document.getElementById('colorScheme').addEventListener('change', renderTimeChart);
|
|
</script>
|
|
|
|
<footer>
|
|
<h3>Iteration 2 - Web Learning Applied</h3>
|
|
<ul>
|
|
<li><strong>Web Source:</strong> https://d3js.org/d3-scale</li>
|
|
<li><strong>Topic:</strong> D3 Scale Functions for Data-to-Visual Mapping</li>
|
|
<li><strong>Techniques Learned:</strong>
|
|
<ol style="margin-top: 10px; margin-left: 20px;">
|
|
<li><strong>scaleTime()</strong> - Temporal data mapping with automatic date handling and tick formatting</li>
|
|
<li><strong>scaleSequential()</strong> - Continuous color encoding with smooth interpolation (RdYlBu color scheme)</li>
|
|
<li><strong>scaleBand()</strong> - Categorical positioning with automatic padding for bar charts</li>
|
|
<li><strong>scaleLinear()</strong> - Standard numeric domain-to-range mapping with inverted y-axis</li>
|
|
<li><strong>scaleThreshold()</strong> - Discrete binning with custom breakpoints (10°, 20°, 30°) for color categories</li>
|
|
<li><strong>scalePoint()</strong> - Precise categorical positioning without bandwidth (point charts)</li>
|
|
<li><strong>scaleLog()</strong> - Logarithmic transformation for wide-range variance data compression</li>
|
|
</ol>
|
|
</li>
|
|
<li><strong>What This Demonstrates:</strong> Scales are the foundation of D3 visualizations - they map abstract data domains (temperatures from -5°C to 32°C, dates from Jan-Dec, city names) to visual ranges (pixel positions, colors, sizes). Each scale type solves specific mapping challenges: time scales handle temporal data, band scales divide categorical space evenly, threshold scales create discrete bins, and log scales compress exponential ranges.</li>
|
|
<li><strong>Improvement over Iteration 1:</strong> While Iteration 1 focused on DOM selection and manipulation (the "how to change elements" foundation), Iteration 2 demonstrates the "how to map data" layer. We now transform raw temperature data through 8 different scale types to create meaningful visual encodings. The progression shows D3's architecture: selections manipulate the DOM, scales transform the data before it reaches the DOM.</li>
|
|
<li><strong>Scale Mapping Examples:</strong>
|
|
<ul style="margin-top: 8px; margin-left: 20px; list-style: circle;">
|
|
<li>Time: <code>new Date(2024, 6, 1)</code> → 600px (middle of chart)</li>
|
|
<li>Sequential: <code>30°C</code> → <code>#d73027</code> (red/hot)</li>
|
|
<li>Band: <code>"Miami"</code> → x position with automatic 20% padding</li>
|
|
<li>Threshold: <code>25°C</code> → <code>#f59e0b</code> (warm category)</li>
|
|
<li>Log: <code>variance=20</code> → compressed visual height vs linear display</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</footer>
|
|
</body>
|
|
</html> |