<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天气数据可视化</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.chart-container {
margin-bottom: 40px;
padding: 20px;
border: 1px solid #eee;
border-radius: 5px;
background-color: white;
}
.chart-title {
margin-top: 0;
color: #444;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
}
select, button {
padding: 8px 15px;
border-radius: 4px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
}
button {
background-color: #4CAF50;
color: white;
border: none;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.link {
text-align: center;
margin-top: 20px;
color: #666;
}
.link a {
color: #4CAF50;
text-decoration: none;
}
.link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div id="app" class="container">
<h1>天气数据可视化</h1>
<div class="controls">
<div>
<label for="city">选择城市: </label>
<select id="city" v-model="selectedCity" @change="fetchWeatherData">
<option v-for="city in cities" :value="city">{{ city }}</option>
</select>
</div>
<div>
<label for="days">显示天数: </label>
<select id="days" v-model="selectedDays" @change="fetchWeatherData">
<option v-for="day in [3,5,7]" :value="day">{{ day }}天</option>
</select>
</div>
<button @click="fetchWeatherData">刷新数据</button>
</div>
<div class="chart-container">
<h2 class="chart-title">温度柱状图</h2>
<div id="bar-chart"></div>
</div>
<div class="chart-container">
<h2 class="chart-title">温度变化折线图</h2>
<div id="line-chart"></div>
</div>
<div class="chart-container">
<h2 class="chart-title">天气状况饼图</h2>
<div id="pie-chart"></div>
</div>
</div>
<script>
const { createApp, ref, onMounted } = Vue;
createApp({
setup() {
const selectedCity = ref('北京');
const selectedDays = ref(5);
const cities = ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉'];
const weatherData = ref([]);
// 模拟从FastAPI获取数据
const fetchWeatherData = async () => {
// 这里应该是实际的API调用,例如:
// const response = await fetch(`/api/weather?city=${selectedCity.value}&days=${selectedDays.value}`);
// weatherData.value = await response.json();
// 模拟API响应
const mockData = generateMockWeatherData(selectedCity.value, selectedDays.value);
weatherData.value = mockData;
// 更新图表
updateCharts();
};
// 生成模拟天气数据
const generateMockWeatherData = (city, days) => {
const conditions = ['晴天', '多云', '小雨', '阴天', '雷阵雨'];
const data = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
const condition = conditions[Math.floor(Math.random() * conditions.length)];
const maxTemp = Math.round(20 + Math.random() * 15);
const minTemp = Math.round(maxTemp - 5 - Math.random() * 5);
data.push({
date: date.toLocaleDateString(),
condition,
maxTemp,
minTemp,
precipitation: Math.round(Math.random() * 10),
humidity: Math.round(50 + Math.random() * 40)
});
}
return data;
};
// 更新所有图表
const updateCharts = () => {
if (weatherData.value.length === 0) return;
createBarChart();
createLineChart();
createPieChart();
};
// 创建柱状图
const createBarChart = () => {
d3.select('#bar-chart').selectAll('*').remove();
const margin = {top: 20, right: 30, bottom: 40, left: 40};
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#bar-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3.scaleBand()
.domain(weatherData.value.map(d => d.date))
.range([0, width])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(weatherData.value, d => d.maxTemp) + 5])
.range([height, 0]);
// 添加最高温度柱状图
svg.selectAll('.max-bar')
.data(weatherData.value)
.enter()
.append('rect')
.attr('class', 'max-bar')
.attr('x', d => x(d.date))
.attr('y', d => y(d.maxTemp))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d.maxTemp))
.attr('fill', '#ff7f0e');
// 添加最低温度柱状图
svg.selectAll('.min-bar')
.data(weatherData.value)
.enter()
.append('rect')
.attr('class', 'min-bar')
.attr('x', d => x(d.date))
.attr('y', d => y(d.minTemp))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d.minTemp))
.attr('fill', '#1f77b4');
// 添加X轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加Y轴
svg.append('g')
.call(d3.axisLeft(y));
// 添加图例
const legend = svg.append('g')
.attr('transform', `translate(${width - 100}, 0)`);
legend.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 15)
.attr('height', 15)
.attr('fill', '#ff7f0e');
legend.append('text')
.attr('x', 20)
.attr('y', 12)
.text('最高温度')
.style('font-size', '12px');
legend.append('rect')
.attr('x', 0)
.attr('y', 20)
.attr('width', 15)
.attr('height', 15)
.attr('fill', '#1f77b4');
legend.append('text')
.attr('x', 20)
.attr('y', 32)
.text('最低温度')
.style('font-size', '12px');
// 添加标题
svg.append('text')
.attr('x', width / 2)
.attr('y', -5)
.attr('text-anchor', 'middle')
.style('font-size', '14px')
.text(`${selectedCity.value}未来${selectedDays.value}天温度变化`);
};
// 创建折线图
const createLineChart = () => {
d3.select('#line-chart').selectAll('*').remove();
const margin = {top: 20, right: 30, bottom: 40, left: 40};
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3.scaleBand()
.domain(weatherData.value.map(d => d.date))
.range([0, width])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(weatherData.value, d => d.maxTemp) + 5])
.range([height, 0]);
// 定义折线生成器
const line = d3.line()
.x(d => x(d.date) + x.bandwidth() / 2)
.y(d => y(d.maxTemp));
// 添加最高温度折线
svg.append('path')
.datum(weatherData.value)
.attr('fill', 'none')
.attr('stroke', '#ff7f0e')
.attr('stroke-width', 2)
.attr('d', line);
// 添加最低温度折线
const minLine = d3.line()
.x(d => x(d.date) + x.bandwidth() / 2)
.y(d => y(d.minTemp));
svg.append('path')
.datum(weatherData.value)
.attr('fill', 'none')
.attr('stroke', '#1f77b4')
.attr('stroke-width', 2)
.attr('d', minLine);
// 添加X轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加Y轴
svg.append('g')
.call(d3.axisLeft(y));
// 添加图例
const legend = svg.append('g')
.attr('transform', `translate(${width - 100}, 0)`);
legend.append('line')
.attr('x1', 0)
.attr('y1', 5)
.attr('x2', 15)
.attr('y2', 5)
.attr('stroke', '#ff7f0e')
.attr('stroke-width', 2);
legend.append('text')
.attr('x', 20)
.attr('y', 8)
.text('最高温度')
.style('font-size', '12px');
legend.append('line')
.attr('x1', 0)
.attr('y1', 25)
.attr('x2', 15)
.attr('y2', 25)
.attr('stroke', '#1f77b4')
.attr('stroke-width', 2);
legend.append('text')
.attr('x', 20)
.attr('y', 28)
.text('最低温度')
.style('font-size', '12px');
// 添加标题
svg.append('text')
.attr('x', width / 2)
.attr('y', -5)
.attr('text-anchor', 'middle')
.style('font-size', '14px')
.text(`${selectedCity.value}未来${selectedDays.value}天温度趋势`);
};
// 创建饼图
const createPieChart = () => {
d3.select('#pie-chart').selectAll('*').remove();
const margin = {top: 20, right: 30, bottom: 40, left: 30};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const radius = Math.min(width, height) / 2;
const svg = d3.select('#pie-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${width / 2 + margin.left},${height / 2 + margin.top})`);
// 统计天气状况
const conditionCounts = {};
weatherData.value.forEach(day => {
conditionCounts[day.condition] = (conditionCounts[day.condition] || 0) + 1;
});
const data = Object.keys(conditionCounts).map(key => ({
condition: key,
count: conditionCounts[key]
}));
const color = d3.scaleOrdinal()
.domain(data.map(d => d.condition))
.range(d3.schemeCategory10);
const pie = d3.pie()
.value(d => d.count);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
const arcs = svg.selectAll('arc')
.data(pie(data))
.enter()
.append('g')
.attr('class', 'arc');
arcs.append('path')
.attr('d', arc)
.attr('fill', d => color(d.data.condition))
.attr('stroke', 'white')
.style('stroke-width', '2px');
// 添加标签
arcs.append('text')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('text-anchor', 'middle')
.text(d => d.data.condition)
.style('font-size', '12px');
// 添加图例
const legend = svg.append('g')
.attr('transform', `translate(${radius + 20}, -${radius / 2})`);
data.forEach((item, i) => {
const legendItem = legend.append('g')
.attr('transform', `translate(0, ${i * 20})`);
legendItem.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', color(item.condition));
legendItem.append('text')
.attr('x', 20)
.attr('y', 12)
.text(`${item.condition} (${item.count}天)`)
.style('font-size', '12px');
});
// 添加标题
svg.append('text')
.attr('x', 0)
.attr('y', -radius - 10)
.attr('text-anchor', 'middle')
.style('font-size', '14px')
.text(`${selectedCity.value}未来${selectedDays.value}天天气状况分布`);
};
// 初始化
onMounted(() => {
fetchWeatherData();
});
return {
selectedCity,
selectedDays,
cities,
weatherData,
fetchWeatherData
};
}
}).mount('#app');
</script>
</body>
</html>
1951

被折叠的 条评论
为什么被折叠?



