面向气象行业高级定制 Demo:类面向对象封装、远程气象 API 解析、自定义 SVG 图标绘制
本气象预报图是 Highcharts 高级扩展能力的标杆 Demo充分证明其不局限于基础折线柱状可深度行业定制复杂复合型可视化解决气象系统多指标、多刻度、自定义图形绘制的开发难点。关于案例这是一个如何设置Highcharts图表的复杂演示动态源并通过绘制图像精灵、风箭头路径进行扩展在图表顶部添加第二个网格。演示的目的是启发开发人员可以超越基本的图表类型并展示该库的功能以编程方式进行扩展。这是演示程序所做的-以JSON服务的形式从www.yr.no加载天气预报。-当数据异步到达时创建一个Meteogram实例。我们有 创建Meteogram原型以提供有组织的结构与演示相关的不同方法和子程序。- parseYrData方法将www.yr.no中的数据解析为几个并行的 数组。这些阵列直接用作温度的数据选项降水和气压。-在此之后将构建options结构并生成图表解析后的数据。-在图表加载时天气图标和风向箭头的框架是使用自定义逻辑渲染。Highcharts 图表 简要Highcharts 提供完整扩展体系支撑行业级气象复合图原生多独立 Y 轴、windbarb 风羽专用系列、chart.renderer 自由手绘 SVG 图形、自定义图案填充、面向对象封装灵活解耦支持远程 AJAX 时序数据接入可一站式整合气温、降水、气压、风力、天气图标多维度气象指标是气象时序可视化最优前端方案。图表定位Highcharts气象图 这是一套高度行业定制化气象预报时序图并非基础图表核心亮点面向气象 API 做面向对象封装Meteogram类统一管理数据解析、配置生成、后置渲染多独立 Y 轴共存气温、降水量、海平面气压三套独立刻度5 种系列同画布气温平滑曲线、降水柱状、误差填充柱、气压点虚线、风羽风向风力箭头双层 X 时间轴小时细刻度 上层日期星期利用chart.renderer自定义绘制天气 SVG 图标、风羽分隔刻度线、区间边框远程 AJAX 拉取挪威气象 API 实时预报数据自动解析多时段预报内置天气编码字典映射几十种晴 / 雨 / 雪 / 雷 / 雾图标与提示文案支持横向滚动长时序、图案填充降水误差区间、正负气温分色曲线。完整JS示例代码/** * This is a complex demo of how to set up a Highcharts chart, coupled to a * dynamic source and extended by drawing image sprites, wind arrow paths * and a second grid on top of the chart. The purpose of the demo is to inpire * developers to go beyond the basic chart types and show how the library can * be extended programmatically. This is what the demo does: * * - Loads weather forecast from www.yr.no in form of a JSON service. * - When the data arrives async, a Meteogram instance is created. We have * created the Meteogram prototype to provide an organized structure of the * different methods and subroutines associated with the demo. * - The parseYrData method parses the data from www.yr.no into several parallel * arrays. These arrays are used directly as the data option for temperature, * precipitation and air pressure. * - After this, the options structure is built, and the chart generated with * the parsed data. * - On chart load, weather icons and the frames for the wind arrows are * rendered using custom logic. */ function Meteogram(json, container) { // Parallel arrays for the chart data, these are populated as the JSON file // is loaded this.symbols []; this.precipitations []; this.precipitationsError []; // Only for some data sets this.winds []; this.temperatures []; this.pressures []; // Initialize this.json json; this.container container; // Run this.parseYrData(); } /** * Mapping of the symbol code in yr.nos API to the icons in their public * GitHub repo, as well as the text used in the tooltip. * * https://api.met.no/weatherapi/weathericon/2.0/documentation */ Meteogram.dictionary { clearsky: { symbol: 01, text: Clear sky }, fair: { symbol: 02, text: Fair }, partlycloudy: { symbol: 03, text: Partly cloudy }, cloudy: { symbol: 04, text: Cloudy }, lightrainshowers: { symbol: 40, text: Light rain showers }, rainshowers: { symbol: 05, text: Rain showers }, heavyrainshowers: { symbol: 41, text: Heavy rain showers }, lightrainshowersandthunder: { symbol: 24, text: Light rain showers and thunder }, rainshowersandthunder: { symbol: 06, text: Rain showers and thunder }, heavyrainshowersandthunder: { symbol: 25, text: Heavy rain showers and thunder }, lightsleetshowers: { symbol: 42, text: Light sleet showers }, sleetshowers: { symbol: 07, text: Sleet showers }, heavysleetshowers: { symbol: 43, text: Heavy sleet showers }, lightsleetshowersandthunder: { symbol: 26, text: Light sleet showers and thunder }, sleetshowersandthunder: { symbol: 20, text: Sleet showers and thunder }, heavysleetshowersandthunder: { symbol: 27, text: Heavy sleet showers and thunder }, lightsnowshowers: { symbol: 44, text: Light snow showers }, snowshowers: { symbol: 08, text: Snow showers }, heavysnowshowers: { symbol: 45, text: Heavy show showers }, lightsnowshowersandthunder: { symbol: 28, text: Light snow showers and thunder }, snowshowersandthunder: { symbol: 21, text: Snow showers and thunder }, heavysnowshowersandthunder: { symbol: 29, text: Heavy snow showers and thunder }, lightrain: { symbol: 46, text: Light rain }, rain: { symbol: 09, text: Rain }, heavyrain: { symbol: 10, text: Heavy rain }, lightrainandthunder: { symbol: 30, text: Light rain and thunder }, rainandthunder: { symbol: 22, text: Rain and thunder }, heavyrainandthunder: { symbol: 11, text: Heavy rain and thunder }, lightsleet: { symbol: 47, text: Light sleet }, sleet: { symbol: 12, text: Sleet }, heavysleet: { symbol: 48, text: Heavy sleet }, lightsleetandthunder: { symbol: 31, text: Light sleet and thunder }, sleetandthunder: { symbol: 23, text: Sleet and thunder }, heavysleetandthunder: { symbol: 32, text: Heavy sleet and thunder }, lightsnow: { symbol: 49, text: Light snow }, snow: { symbol: 13, text: Snow }, heavysnow: { symbol: 50, text: Heavy snow }, lightsnowandthunder: { symbol: 33, text: Light snow and thunder }, snowandthunder: { symbol: 14, text: Snow and thunder }, heavysnowandthunder: { symbol: 34, text: Heavy snow and thunder }, fog: { symbol: 15, text: Fog } }; /** * Draw the weather symbols on top of the temperature series. The symbols are * fetched from yr.nos MIT licensed weather symbol collection. * https://github.com/YR/weather-symbols */ Meteogram.prototype.drawWeatherSymbols function (chart) { chart.series[0].data.forEach((point, i) { if (this.resolution 36e5 || i % 2 0) { const [symbol, specifier] this.symbols[i].split(_), icon Meteogram.dictionary[symbol].symbol ({ day: d, night: n }[specifier] || ); if (Meteogram.dictionary[symbol]) { chart.renderer .image( https://cdn.jsdelivr.net/gh/nrkno/yr-weather-symbols 8.0.1/dist/svg/${icon}.svg, point.plotX chart.plotLeft - 8, point.plotY chart.plotTop - 30, 30, 30 ) .attr({ zIndex: 5 }) .add(); } else { console.log(symbol); } } }); }; /** * Draw blocks around wind arrows, below the plot area */ Meteogram.prototype.drawBlocksForWindArrows function (chart) { const xAxis chart.xAxis[0]; for ( let pos xAxis.min, max xAxis.max, i 0; pos max 36e5; pos 36e5, i 1 ) { // Get the X position const isLast pos max 36e5, x Math.round(xAxis.toPixels(pos)) (isLast ? 0.5 : -0.5); // Draw the vertical dividers and ticks const isLong this.resolution 36e5 ? pos % this.resolution 0 : i % 2 0; chart.renderer .path([ M, x, chart.plotTop chart.plotHeight (isLong ? 0 : 28), L, x, chart.plotTop chart.plotHeight 32, Z ]) .attr({ stroke: chart.options.chart.plotBorderColor, stroke-width: 1 }) .add(); } // Center items in block chart.get(windbarbs).markerGroup.attr({ translateX: chart.get(windbarbs).markerGroup.translateX 8 }); }; /** * Build and return the Highcharts options structure */ Meteogram.prototype.getChartOptions function () { return { chart: { renderTo: this.container, marginBottom: 70, marginRight: 40, marginTop: 50, plotBorderWidth: 1, height: 310, alignTicks: false, scrollablePlotArea: { minWidth: 720 } }, defs: { patterns: [{ id: precipitation-error, path: { d: [ M, 3.3, 0, L, -6.7, 10, M, 6.7, 0, L, -3.3, 10, M, 10, 0, L, 0, 10, M, 13.3, 0, L, 3.3, 10, M, 16.7, 0, L, 6.7, 10 ].join( ), stroke: #68CFE8, strokeWidth: 1 } }] }, title: { text: Meteogram for London, England, align: left, style: { whiteSpace: nowrap, textOverflow: ellipsis } }, credits: { text: Forecast from a hrefhttps://yr.noyr.no/a, href: https://yr.no, position: { x: -40 } }, tooltip: { shared: true, headerFormat: small{point.x:%A, %b %e, %H:%M} - {point.point.to:%H:%M}/smallbr b{point.point.symbolName}/bbr }, xAxis: [{ // Bottom X axis type: datetime, tickInterval: 2 * 36e5, // two hours minorTickInterval: 36e5, // one hour tickLength: 0, gridLineWidth: 1, gridLineColor: rgba(128, 128, 128, 0.1), startOnTick: false, endOnTick: false, minPadding: 0, maxPadding: 0, offset: 30, showLastLabel: true, labels: { format: {value:%H} }, crosshair: true }, { // Top X axis linkedTo: 0, type: datetime, tickInterval: 24 * 3600 * 1000, labels: { format: {value:span stylefont-size: 12px; font-weight: bold%a/span %b %e}, align: left, x: 3, y: 8 }, opposite: true, tickLength: 20, gridLineWidth: 1 }], yAxis: [{ // temperature axis title: { text: null }, labels: { format: {value}°, style: { fontSize: 10px }, x: -3 }, plotLines: [{ // zero plane value: 0, color: var(--highcharts-neutral-color-20, #bbb), width: 1, zIndex: 2 }], maxPadding: 0.3, minRange: 8, tickInterval: 1, gridLineColor: rgba(128, 128, 128, 0.1) }, { // precipitation axis title: { text: null }, labels: { enabled: false }, gridLineWidth: 0, tickLength: 0, minRange: 10, min: 0 }, { // Air pressure allowDecimals: false, title: { // Title on top of axis text: hPa, offset: 0, align: high, rotation: 0, style: { fontSize: 10px, color: Highcharts.getOptions().colors[2] }, textAlign: left, x: 3 }, labels: { style: { fontSize: 8px, color: Highcharts.getOptions().colors[2] }, y: 2, x: 3 }, gridLineWidth: 0, opposite: true, showLastLabel: false }], legend: { enabled: false }, plotOptions: { series: { pointPlacement: between } }, series: [{ name: Temperature, data: this.temperatures, type: spline, marker: { enabled: false, states: { hover: { enabled: true } } }, tooltip: { pointFormat: span stylecolor:{point.color}\u25CF/span {series.name}: b{point.y}°C/bbr/ }, zIndex: 1, color: #FF3333, negativeColor: #48AFE8 }, { name: Precipitation, data: this.precipitationsError, type: column, color: url(#precipitation-error), yAxis: 1, groupPadding: 0, pointPadding: 0, tooltip: { valueSuffix: mm, pointFormat: span stylecolor:{point.color}\u25CF/span {series.name}: b{point.minvalue} mm - {point.maxvalue} mm/bbr/ }, grouping: false, dataLabels: { enabled: this.hasPrecipitationError, filter: { operator: , property: maxValue, value: 0 }, style: { fontSize: 8px, color: var(--highcharts-neutral-color-60, gray) } } }, { name: Precipitation, data: this.precipitations, type: column, color: #68CFE8, yAxis: 1, groupPadding: 0, pointPadding: 0, grouping: false, dataLabels: { enabled: !this.hasPrecipitationError, filter: { operator: , property: y, value: 0 }, style: { fontSize: 8px, color: var(--highcharts-neutral-color-60, gray) } }, tooltip: { valueSuffix: mm } }, { name: Air pressure, color: Highcharts.getOptions().colors[2], data: this.pressures, marker: { enabled: false }, shadow: false, tooltip: { valueSuffix: hPa }, dashStyle: shortdot, yAxis: 2 }, { name: Wind, type: windbarb, id: windbarbs, color: Highcharts.getOptions().colors[1], lineWidth: 1.5, data: this.winds, vectorLength: 18, yOffset: -15, tooltip: { valueSuffix: m/s } }] }; }; /** * Post-process the chart from the callback function, the second argument * Highcharts.Chart. */ Meteogram.prototype.onChartLoad function (chart) { this.drawWeatherSymbols(chart); this.drawBlocksForWindArrows(chart); }; /** * Create the chart. This function is called async when the data file is loaded * and parsed. */ Meteogram.prototype.createChart function () { this.chart new Highcharts.Chart(this.getChartOptions(), chart { this.onChartLoad(chart); }); }; Meteogram.prototype.error function () { document.getElementById(loading).innerHTML i classfa fa-frown-o/i Failed loading data, please try again later; }; /** * Handle the data. This part of the code is not Highcharts specific, but deals * with yr.nos specific data format */ Meteogram.prototype.parseYrData function () { let pointStart; if (!this.json) { return this.error(); } // Loop over hourly (or 6-hourly) forecasts this.json.properties.timeseries.forEach((node, i) { const x Date.parse(node.time), nextHours node.data.next_1_hours || node.data.next_6_hours, symbolCode nextHours nextHours.summary.symbol_code, to node.data.next_1_hours ? x 36e5 : x 6 * 36e5; if (to pointStart 48 * 36e5) { return; } // Populate the parallel arrays this.symbols.push(nextHours.summary.symbol_code); this.temperatures.push({ x, y: node.data.instant.details.air_temperature, // custom options used in the tooltip formatter to, symbolName: Meteogram.dictionary[ symbolCode.replace(/_(day|night)$/, ) ].text }); this.precipitations.push({ x, y: nextHours.details.precipitation_amount }); if (i % 2 0) { this.winds.push({ x, value: node.data.instant.details.wind_speed, direction: node.data.instant.details.wind_from_direction }); } this.pressures.push({ x, y: node.data.instant.details.air_pressure_at_sea_level }); if (i 0) { pointStart (x to) / 2; } }); // Create the chart when the data is loaded this.createChart(); }; // End of the Meteogram protype // On DOM ready... // Set the hash to the yr.no URL we want to parse if (!location.hash) { location.hash https://api.met.no/weatherapi/locationforecast/2.0/compact?lat51.50853lon-0.12574altitude25; } const url location.hash.substr(1); Highcharts.ajax({ url, dataType: json, success: json { window.meteogram new Meteogram(json, container); }, error: Meteogram.prototype.error, headers: { // Override the Content-Type to avoid preflight problems with CORS // in the Highcharts demos Content-Type: text/plain } });核心代码分层拆解1Meteogram 封装类架构数据存储数组气温、降水、误差降水、风力、气压、天气编码静态字典dictionary气象编码→图标文件名 中文 / 英文描述核心方法parseYrData解析气象 API JSON批量组装时序点位数据getChartOptions动态生成完整 Highcharts 多轴配置drawWeatherSymbols渲染顶部天气 SVG 图标drawBlocksForWindArrows绘制底部风羽分隔竖线createChart实例化图表onChartLoad图表渲染完成后执行自定义绘图。2多轴设计yAxis [0] 气温红色高温 / 蓝色低温0 度基准参考线yAxis [1] 降水柱状独立 0 起刻度yAxis [2] 海平面气压右侧虚线曲线独立单位 hPa。35 大数据系列spline 气温曲线column 降水误差区间图案填充column 标准降水量蓝色柱shortdash 气压时序windbarb 风羽系列自带风向角度、风速数值。4自定义渲染扩展Highcharts 核心扩展能力体现renderer.image远程加载官方天气 SVG 图标悬浮 Z 轴置顶renderer.path手绘底部时间分隔竖线用于风羽区块对齐自定义 SVG 图案defs.patterns实现降水误差斜纹填充图表加载完成回调二次绘图不侵入原生系列逻辑。5数据层Highcharts.ajax跨域请求气象开放 API自动读取 URL 哈希切换城市经纬度预报逐小时 / 6 小时预报自动截断 48 小时短期预报点位携带自定义拓展字段时段结束时间、天气文字说明。图表类型多轴复合型气象时序预报图Meteogram融合 spline /column/windbarb / 自定义渲染多独立 Y 轴、双层时间轴、自定义矢量绘图属于行业深度定制高级复合图表。核心业务功能48 小时精细化逐时段气象预报自动截断数据只展示 2 天短期预报支持 1 小时 / 6 小时两种预报粒度。多气象指标一体化展示气温曲线红色高温、蓝色零下低温0℃基准线直观区分冷暖降水柱状蓝色柱代表标准降水量斜纹填充柱代表降水预估误差区间海平面气压右侧虚线时序单位百帕风羽标记底部风羽箭头自动匹配风向、风速数值带分隔刻度。可视化气象状态图标 顶部自动渲染晴 / 雨 / 雪 / 雷 / 雾 SVG 图标日夜两套图标自动匹配时段。双层时间标尺 上层日期 星期宏观刻度下层24 小时小时细刻度兼顾全局与精细查看。超长时序横向滚动scrollablePlotArea窄屏自动生成横向滚动条完整展示两天连续预报。统一悬浮提示框 鼠标悬浮同时展示时段、天气状况、气温、降水、气压、风速多维度信息。适用行业场景气象监测平台、气象预报 Web 端、气象大屏风电、光伏场站气象工况预报看板景区、交通物流气象预警系统农业种植气象时序分析城市环境监测综合预报页面。