weather-station/static/js/weather-chart.js
2025-09-01 18:16:07 +08:00

344 lines
10 KiB
JavaScript

const WeatherChart = {
chart: null,
display(historyData = [], forecastData = []) {
historyData = Array.isArray(historyData) ? historyData : [];
forecastData = Array.isArray(forecastData) ? forecastData : [];
if (historyData.length === 0 && forecastData.length === 0) {
return;
}
const allLabels = [...new Set([
...historyData.map(item => item.date_time),
...forecastData.map(item => item.date_time)
])].sort();
const historyTemperatures = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.temperature : null;
});
const historyHumidities = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.humidity : null;
});
const historyPressures = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.pressure : null;
});
const historyWindSpeeds = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.wind_speed : null;
});
const historyRainfalls = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.rainfall : null;
});
const historyRainTotals = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item && item.rain_total !== undefined ? item.rain_total : null;
});
const byTime = new Map();
forecastData.forEach(fp => {
if (!byTime.has(fp.date_time)) byTime.set(fp.date_time, {});
const bucket = byTime.get(fp.date_time);
const h = typeof fp.lead_hours === 'number' ? fp.lead_hours : null;
if (h !== null && h >= 0 && h <= 3) {
bucket[h] = fp;
}
});
const getRainAtLead = (label, lead) => {
const b = byTime.get(label);
if (!b || !b[lead]) return null;
return b[lead].rainfall != null ? b[lead].rainfall : null;
};
const getTempAtLead0 = (label) => {
const b = byTime.get(label);
if (!b || !b[0]) return null;
return b[0].temperature != null ? b[0].temperature : null;
};
const forecastRainfallsH0 = allLabels.map(label => getRainAtLead(label, 0));
const forecastRainfallsH1 = allLabels.map(label => getRainAtLead(label, 1));
const forecastRainfallsH2 = allLabels.map(label => getRainAtLead(label, 2));
const forecastRainfallsH3 = allLabels.map(label => getRainAtLead(label, 3));
const pickNearest = (label, field) => {
const b = byTime.get(label);
if (!b) return null;
if (b[0] && b[0][field] != null) return b[0][field];
if (b[1] && b[1][field] != null) return b[1][field];
if (b[2] && b[2][field] != null) return b[2][field];
if (b[3] && b[3][field] != null) return b[3][field];
return null;
};
const forecastTemperaturesNearest = allLabels.map(label => pickNearest(label, 'temperature'));
const forecastHumiditiesNearest = allLabels.map(label => pickNearest(label, 'humidity'));
const forecastPressuresNearest = allLabels.map(label => pickNearest(label, 'pressure'));
const forecastWindSpeedsNearest = allLabels.map(label => pickNearest(label, 'wind_speed'));
if (this.chart) this.chart.destroy();
const datasets = [
{
label: '温度 (°C) - 实测',
seriesKey: 'temp_actual',
data: historyTemperatures,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
yAxisID: 'y-temperature',
tension: 0.4,
spanGaps: false
},
{
label: '湿度 (%) - 实测',
seriesKey: 'hum_actual',
data: historyHumidities,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
yAxisID: 'y-humidity',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '大气压 (hPa) - 实测',
seriesKey: 'pressure_actual',
data: historyPressures,
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
yAxisID: 'y-pressure',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '风速 (m/s) - 实测',
seriesKey: 'wind_actual',
data: historyWindSpeeds,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
yAxisID: 'y-wind',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '雨量 (mm) - 实测',
seriesKey: 'rain_actual',
data: historyRainfalls,
type: 'bar',
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgb(54, 162, 235)',
yAxisID: 'y-rainfall'
},
{
label: '累计雨量 (mm) - 实测',
seriesKey: 'rain_total',
data: historyRainTotals,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
yAxisID: 'y-rainfall',
tension: 0.2,
spanGaps: false,
pointRadius: 0,
hidden: true
}
];
if (forecastData.length > 0) {
datasets.push(
{ label: '雨量 (mm) - 预报 (+1h)', seriesKey: 'rain_fcst_h1', data: forecastRainfallsH1, type: 'bar', backgroundColor: 'rgba(255, 99, 71, 0.55)', borderColor: 'rgb(255, 99, 71)', yAxisID: 'y-rainfall' },
{ label: '雨量 (mm) - 预报 (+2h)', seriesKey: 'rain_fcst_h2', data: forecastRainfallsH2, type: 'bar', backgroundColor: 'rgba(255, 205, 86, 0.55)', borderColor: 'rgb(255, 205, 86)', yAxisID: 'y-rainfall' },
{ label: '雨量 (mm) - 预报 (+3h)', seriesKey: 'rain_fcst_h3', data: forecastRainfallsH3, type: 'bar', backgroundColor: 'rgba(76, 175, 80, 0.55)', borderColor: 'rgb(76, 175, 80)', yAxisID: 'y-rainfall' }
);
datasets.push(
{
label: '温度 (°C) - 预报',
seriesKey: 'temp_fcst',
data: forecastTemperaturesNearest,
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
borderDash: [5, 5],
yAxisID: 'y-temperature',
tension: 0.4,
spanGaps: false,
hidden: true
},
{
label: '湿度 (%) - 预报',
seriesKey: 'hum_fcst',
data: forecastHumiditiesNearest,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderDash: [5, 5],
yAxisID: 'y-humidity',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '大气压 (hPa) - 预报',
seriesKey: 'pressure_fcst',
data: forecastPressuresNearest,
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
yAxisID: 'y-pressure',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '风速 (m/s) - 预报',
seriesKey: 'wind_fcst',
data: forecastWindSpeedsNearest,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
borderDash: [5, 5],
yAxisID: 'y-wind',
tension: 0.4,
hidden: true,
spanGaps: false
}
);
}
const ctx = document.getElementById('combinedChart').getContext('2d');
const chartConfig = {
type: 'line',
data: {
labels: allLabels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
layout: { padding: { top: 12, right: 16, bottom: 12, left: 16 } },
plugins: {
legend: {
display: true,
position: 'top',
align: 'center',
labels: {
padding: 16
}
}
},
scales: {
'y-temperature': {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: '温度 (°C)' }
},
'y-humidity': {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: '湿度 (%)' },
grid: { drawOnChartArea: false },
min: 0,
max: 100
},
'y-pressure': {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: '大气压 (hPa)' },
grid: { drawOnChartArea: false }
},
'y-wind': {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: '风速 (m/s)' },
grid: { drawOnChartArea: false },
beginAtZero: true
},
'y-rainfall': {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: '雨量 (mm)' },
grid: { drawOnChartArea: false },
beginAtZero: true
}
}
}
};
this.chart = new Chart(ctx, chartConfig);
const mode = document.getElementById('legendMode')?.value || 'combo_standard';
this.applyLegendMode(mode);
}
};
WeatherChart.applyLegendMode = function(mode) {
if (!this.chart) return;
// 设置数据集可见性
const map = new Map();
this.chart.data.datasets.forEach(ds => {
if (ds.seriesKey) map.set(ds.seriesKey, ds);
});
const setVisible = (keys) => {
const allKeys = ['temp_actual','hum_actual','rain_actual','rain_total','temp_fcst','hum_fcst','pressure_actual','pressure_fcst','wind_actual','wind_fcst','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3'];
allKeys.forEach(k => { if (map.has(k)) map.get(k).hidden = true; });
keys.forEach(k => { if (map.has(k)) map.get(k).hidden = false; });
};
switch (mode) {
case 'combo_compact':
setVisible(['temp_actual','hum_actual','rain_actual','rain_fcst_h1']);
break;
case 'verify_all':
setVisible(['temp_actual','temp_fcst','hum_actual','hum_fcst','pressure_actual','pressure_fcst','wind_actual','wind_fcst','rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
break;
case 'temp_compare':
setVisible(['temp_actual','temp_fcst']);
break;
case 'hum_compare':
setVisible(['hum_actual','hum_fcst']);
break;
case 'rain_all':
setVisible(['rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
break;
case 'pressure_compare':
setVisible(['pressure_actual','pressure_fcst']);
break;
case 'wind_compare':
setVisible(['wind_actual','wind_fcst']);
break;
case 'combo_standard':
default:
setVisible(['temp_actual','hum_actual','rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
break;
}
this.chart.update();
};
WeatherChart.updateAxesVisibility = function() {
if (!this.chart) return;
// 检查每个轴是否有可见的数据集
const hasVisibleDatasets = (axisId) => {
return this.chart.data.datasets.some(ds => !ds.hidden && ds.yAxisID === axisId);
};
// 更新每个轴的显示状态
Object.entries(this.chart.options.scales).forEach(([scaleId, scale]) => {
const shouldDisplay = hasVisibleDatasets(scaleId);
if (scale.display !== shouldDisplay) {
scale.display = shouldDisplay;
}
});
};
window.WeatherChart = WeatherChart;