如何用JavaScript和SVG实现Python版的气压等值线及高低压中心标注图?
用JavaScript + SVG复现MSLP与1000-500hPa厚度高低压符号图
以下是完整实现方案,包含等压线绘制、厚度场可视化、局部极值识别与标注三个核心部分:
1. 基础SVG画布搭建
先创建适配气象图展示的SVG容器:
<svg id="weatherChart" width="800" height="600" viewBox="0 0 800 600"></svg>
2. 模拟气象数据(实际场景替换为真实数据)
这里用网格数据模拟海平面气压(MSLP)和1000-500hPa厚度场,实际使用时可替换为API接口或本地文件导出的真实气象数据:
// 网格参数:30x30覆盖整个SVG画布 const gridSize = 30; const width = 800; const height = 600; const xStep = width / (gridSize - 1); const yStep = height / (gridSize - 1); // 生成模拟MSLP数据(单位hPa) function generateMSLPData() { const data = []; for (let y = 0; y < gridSize; y++) { const row = []; for (let x = 0; x < gridSize; x++) { // 正弦函数模拟高低压系统+随机噪声 const base = 1010 + Math.sin(x/5) * 15 + Math.cos(y/7) * 10; const noise = (Math.random() - 0.5) * 2; row.push(base + noise); } data.push(row); } return data; } // 生成模拟厚度数据(单位dam) function generateThicknessData() { const data = []; for (let y = 0; y < gridSize; y++) { const row = []; for (let x = 0; x < gridSize; x++) { const base = 550 + Math.sin(x/6 + y/8) * 20; const noise = (Math.random() - 0.5) * 1.5; row.push(base + noise); } data.push(row); } return data; } const mslpData = generateMSLPData(); const thicknessData = generateThicknessData();
3. 绘制等值线(等压线+厚度线)
采用气象领域常用的移动正方形法生成等值线,实现原生JS的等值线绘制:
// 移动正方形法计算等值线交点 function getContourPoints(data, targetValue) { const points = []; for (let y = 0; y < gridSize - 1; y++) { for (let x = 0; x < gridSize - 1; x++) { const vals = [ data[y][x], data[y][x+1], data[y+1][x+1], data[y+1][x] ]; const squareIndex = vals.reduce((idx, val, i) => idx + (val >= targetValue ? (1 << i) : 0), 0); const x0 = x * xStep; const y0 = y * yStep; const x1 = (x+1) * xStep; const y1 = (y+1) * yStep; // 简化版等值线交点计算(覆盖常见情况) switch(squareIndex) { case 1: case 14: points.push([x0, interpolateY(vals[3], vals[0], targetValue, y0, y1)]); points.push([interpolateX(vals[3], vals[2], targetValue, x0, x1), y1]); break; case 2: case 13: points.push([interpolateX(vals[0], vals[1], targetValue, x0, x1), y0]); points.push([x1, interpolateY(vals[1], vals[2], targetValue, y0, y1)]); break; case 3: case 12: points.push([x0, interpolateY(vals[3], vals[0], targetValue, y0, y1)]); points.push([x1, interpolateY(vals[1], vals[2], targetValue, y0, y1)]); break; case 4: case 11: points.push([interpolateX(vals[2], vals[3], targetValue, x0, x1), y1]); points.push([x1, interpolateY(vals[1], vals[2], targetValue, y0, y1)]); break; case 5: points.push([x0, interpolateY(vals[3], vals[0], targetValue, y0, y1)]); points.push([x0, y1]); points.push([x1, interpolateY(vals[1], vals[2], targetValue, y0, y1)]); break; case 6: case 9: points.push([interpolateX(vals[0], vals[1], targetValue, x0, x1), y0]); points.push([interpolateX(vals[2], vals[3], targetValue, x0, x1), y1]); break; case 7: case 8: points.push([x0, interpolateY(vals[3], vals[0], targetValue, y0, y1)]); points.push([interpolateX(vals[0], vals[1], targetValue, x0, x1), y0]); break; } } } return points; } // 线性插值计算X坐标 function interpolateX(v1, v2, target, x0, x1) { return x0 + (x1 - x0) * (target - v1) / (v2 - v1); } // 线性插值计算Y坐标 function interpolateY(v1, v2, target, y0, y1) { return y0 + (y1 - y0) * (target - v1) / (v2 - v1); } // 绘制等值线及数值标签 function drawContour(svg, points, color, width, labelValue) { if (points.length < 2) return; let pathStr = `M ${points[0][0]},${points[0][1]}`; for (let i = 1; i < points.length; i++) { pathStr += ` L ${points[i][0]},${points[i][1]}`; } const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", pathStr); path.setAttribute("stroke", color); path.setAttribute("stroke-width", width); path.setAttribute("fill", "none"); svg.appendChild(path); // 等值线中间位置添加数值标签 const midIdx = Math.floor(points.length / 2); const midPoint = points[midIdx]; const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); text.setAttribute("x", midPoint[0]); text.setAttribute("y", midPoint[1] - 5); text.setAttribute("fill", color); text.setAttribute("font-size", "12"); text.textContent = labelValue; svg.appendChild(text); } // 绘制MSLP等压线(990-1030hPa,间隔5hPa) const svg = document.getElementById("weatherChart"); for (let val = 990; val <= 1030; val += 5) { const points = getContourPoints(mslpData, val); drawContour(svg, points, "#2c3e50", 1.5, val); } // 绘制厚度线(530-570dam,间隔10dam) for (let val = 530; val <= 570; val += 10) { const points = getContourPoints(thicknessData, val); drawContour(svg, points, "#e74c3c", 1, val); }
4. 识别高低压中心并标注
通过对比网格点与8邻域点的数值,判断局部极值并添加可视化标注:
// 识别MSLP场的局部高低压极值 function findExtremes(data) { const extremes = []; const offset = [-1, 0, 1]; for (let y = 1; y < gridSize - 1; y++) { for (let x = 1; x < gridSize - 1; x++) { const current = data[y][x]; let isHigh = true; let isLow = true; // 遍历8邻域验证极值 for (const dy of offset) { for (const dx of offset) { if (dx === 0 && dy === 0) continue; const neighbor = data[y+dy][x+dx]; if (neighbor > current) isHigh = false; if (neighbor < current) isLow = false; } } if (isHigh) { extremes.push({ type: "high", x: x*xStep, y: y*yStep, value: current }); } else if (isLow) { extremes.push({ type: "low", x: x*xStep, y: y*yStep, value: current }); } } } return extremes; } // 绘制高低压符号及数值标注 function drawExtremes(svg, extremes) { extremes.forEach(extreme => { const group = document.createElementNS("http://www.w3.org/2000/svg", "g"); // 圆形背景 const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", extreme.x); circle.setAttribute("cy", extreme.y); circle.setAttribute("r", 15); circle.setAttribute("fill", extreme.type === "high" ? "#f39c12" : "#3498db"); circle.setAttribute("opacity", 0.8); group.appendChild(circle); // H/L符号 const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); text.setAttribute("x", extreme.x); text.setAttribute("y", extreme.y + 5); text.setAttribute("text-anchor", "middle"); text.setAttribute("fill", "white"); text.setAttribute("font-size", "18"); text.setAttribute("font-weight", "bold"); text.textContent = extreme.type === "high" ? "H" : "L"; group.appendChild(text); // 气压数值标注 const valueText = document.createElementNS("http://www.w3.org/2000/svg", "text"); valueText.setAttribute("x", extreme.x); valueText.setAttribute("y", extreme.y + 25); valueText.setAttribute("text-anchor", "middle"); valueText.setAttribute("fill", "#2c3e50"); valueText.setAttribute("font-size", "10"); valueText.textContent = extreme.value.toFixed(1); group.appendChild(valueText); svg.appendChild(group); }); } // 执行极值识别与标注 const extremes = findExtremes(mslpData); drawExtremes(svg, extremes);
关键注意事项
- 真实场景中需替换模拟数据为标准化气象数据(如NetCDF转JSON、气象API返回数据)
- 移动正方形法为简化实现,如需更精准的等值线,可引入
d3-contour等专业库 - 极值识别的邻域范围可根据数据分辨率调整,避免识别过多伪极值
内容的提问来源于stack exchange,提问作者ATMYDN
相关产品推荐
相关产品推荐

