344 lines
10 KiB
JavaScript
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;
|