index.htm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Highcharts Example</title>
  7. <style type="text/css">
  8. </style>
  9. </head>
  10. <body>
  11. <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
  12. <script src="../../code/highcharts.js"></script>
  13. <script src="../../code/modules/windbarb.js"></script>
  14. <script src="../../code/modules/exporting.js"></script>
  15. <script src="https://highcharts.github.io/pattern-fill/pattern-fill-v2.js"></script>
  16. <link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
  17. <div id="container" style="max-width: 800px; min-width: 380px; height: 310px; margin: 0 auto">
  18. <div style="margin-top: 100px; text-align: center" id="loading">
  19. <i class="fa fa-spinner fa-spin"></i> Loading data from external source
  20. </div>
  21. </div>
  22. <!--
  23. <div style="width: 800px; margin: 0 auto">
  24. <a href="#http://www.yr.no/place/United_Kingdom/England/London/forecast_hour_by_hour.xml">London</a>,
  25. <a href="#http://www.yr.no/place/France/Rhône-Alpes/Val_d\'Isère~2971074/forecast_hour_by_hour.xml">Val d'Isère</a>,
  26. <a href="#http://www.yr.no/place/United_States/California/San_Francisco/forecast_hour_by_hour.xml">San Francisco</a>,
  27. <a href="#http://www.yr.no/place/Norway/Vik/Vikafjell/forecast_hour_by_hour.xml">Vikjafjellet</a>
  28. </div>
  29. -->
  30. <script type="text/javascript">
  31. /**
  32. * This is a complex demo of how to set up a Highcharts chart, coupled to a
  33. * dynamic source and extended by drawing image sprites, wind arrow paths
  34. * and a second grid on top of the chart. The purpose of the demo is to inpire
  35. * developers to go beyond the basic chart types and show how the library can
  36. * be extended programmatically. This is what the demo does:
  37. *
  38. * - Loads weather forecast from www.yr.no in form of an XML service. The XML
  39. * is translated on the Higcharts website into JSONP for the sake of the demo
  40. * being shown on both our website and JSFiddle.
  41. * - When the data arrives async, a Meteogram instance is created. We have
  42. * created the Meteogram prototype to provide an organized structure of the different
  43. * methods and subroutines associated with the demo.
  44. * - The parseYrData method parses the data from www.yr.no into several parallel arrays. These
  45. * arrays are used directly as the data option for temperature, precipitation
  46. * and air pressure. As the temperature data gives only full degrees, we apply
  47. * some smoothing on the graph, but keep the original data in the tooltip.
  48. * - After this, the options structure is build, and the chart generated with the
  49. * parsed data.
  50. * - In the callback (on chart load), we weather icons on top of the temperature series.
  51. * The icons are sprites from a single PNG image, placed inside a clipped 30x30
  52. * SVG <g> element. VML interprets this as HTML images inside a clipped div.
  53. * - Lastly, the wind arrows are built and added below the plot area, and a grid is
  54. * drawn around them. The wind arrows are basically drawn north-south, then rotated
  55. * as per the wind direction.
  56. */
  57. function Meteogram(xml, container) {
  58. // Parallel arrays for the chart data, these are populated as the XML/JSON file
  59. // is loaded
  60. this.symbols = [];
  61. this.precipitations = [];
  62. this.precipitationsError = []; // Only for some data sets
  63. this.winds = [];
  64. this.temperatures = [];
  65. this.pressures = [];
  66. // Initialize
  67. this.xml = xml;
  68. this.container = container;
  69. // Run
  70. this.parseYrData();
  71. }
  72. /**
  73. * Function to smooth the temperature line. The original data provides only whole degrees,
  74. * which makes the line graph look jagged. So we apply a running mean on it, but preserve
  75. * the unaltered value in the tooltip.
  76. */
  77. Meteogram.prototype.smoothLine = function (data) {
  78. var i = data.length,
  79. sum,
  80. value;
  81. while (i--) {
  82. data[i].value = value = data[i].y; // preserve value for tooltip
  83. // Set the smoothed value to the average of the closest points, but don't allow
  84. // it to differ more than 0.5 degrees from the given value
  85. sum = (data[i - 1] || data[i]).y + value + (data[i + 1] || data[i]).y;
  86. data[i].y = Math.max(value - 0.5, Math.min(sum / 3, value + 0.5));
  87. }
  88. };
  89. /**
  90. * Draw the weather symbols on top of the temperature series. The symbols are
  91. * fetched from yr.no's MIT licensed weather symbol collection.
  92. * https://github.com/YR/weather-symbols
  93. */
  94. Meteogram.prototype.drawWeatherSymbols = function (chart) {
  95. var meteogram = this;
  96. $.each(chart.series[0].data, function (i, point) {
  97. if (meteogram.resolution > 36e5 || i % 2 === 0) {
  98. chart.renderer
  99. .image(
  100. 'https://cdn.rawgit.com/YR/weather-symbols/6.0.2/dist/svg/' +
  101. meteogram.symbols[i] + '.svg',
  102. point.plotX + chart.plotLeft - 8,
  103. point.plotY + chart.plotTop - 30,
  104. 30,
  105. 30
  106. )
  107. .attr({
  108. zIndex: 5
  109. })
  110. .add();
  111. }
  112. });
  113. };
  114. /**
  115. * Draw blocks around wind arrows, below the plot area
  116. */
  117. Meteogram.prototype.drawBlocksForWindArrows = function (chart) {
  118. var xAxis = chart.xAxis[0],
  119. x,
  120. pos,
  121. max,
  122. isLong,
  123. isLast,
  124. i;
  125. for (pos = xAxis.min, max = xAxis.max, i = 0; pos <= max + 36e5; pos += 36e5, i += 1) {
  126. // Get the X position
  127. isLast = pos === max + 36e5;
  128. x = Math.round(xAxis.toPixels(pos)) + (isLast ? 0.5 : -0.5);
  129. // Draw the vertical dividers and ticks
  130. if (this.resolution > 36e5) {
  131. isLong = pos % this.resolution === 0;
  132. } else {
  133. isLong = i % 2 === 0;
  134. }
  135. chart.renderer.path(['M', x, chart.plotTop + chart.plotHeight + (isLong ? 0 : 28),
  136. 'L', x, chart.plotTop + chart.plotHeight + 32, 'Z'])
  137. .attr({
  138. 'stroke': chart.options.chart.plotBorderColor,
  139. 'stroke-width': 1
  140. })
  141. .add();
  142. }
  143. // Center items in block
  144. chart.get('windbarbs').markerGroup.attr({
  145. translateX: chart.get('windbarbs').markerGroup.translateX + 8
  146. });
  147. };
  148. /**
  149. * Get the title based on the XML data
  150. */
  151. Meteogram.prototype.getTitle = function () {
  152. return 'Meteogram for ' + this.xml.querySelector('location name').textContent +
  153. ', ' + this.xml.querySelector('location country').textContent;
  154. };
  155. /**
  156. * Build and return the Highcharts options structure
  157. */
  158. Meteogram.prototype.getChartOptions = function () {
  159. var meteogram = this;
  160. return {
  161. chart: {
  162. renderTo: this.container,
  163. marginBottom: 70,
  164. marginRight: 40,
  165. marginTop: 50,
  166. plotBorderWidth: 1,
  167. height: 310,
  168. alignTicks: false,
  169. scrollablePlotArea: {
  170. minWidth: 720
  171. }
  172. },
  173. defs: {
  174. patterns: [{
  175. 'id': 'precipitation-error',
  176. 'path': {
  177. d: [
  178. 'M', 3.3, 0, 'L', -6.7, 10,
  179. 'M', 6.7, 0, 'L', -3.3, 10,
  180. 'M', 10, 0, 'L', 0, 10,
  181. 'M', 13.3, 0, 'L', 3.3, 10,
  182. 'M', 16.7, 0, 'L', 6.7, 10
  183. ].join(' '),
  184. stroke: '#68CFE8',
  185. strokeWidth: 1
  186. }
  187. }]
  188. },
  189. title: {
  190. text: this.getTitle(),
  191. align: 'left',
  192. style: {
  193. whiteSpace: 'nowrap',
  194. textOverflow: 'ellipsis'
  195. }
  196. },
  197. credits: {
  198. text: 'Forecast from <a href="http://yr.no">yr.no</a>',
  199. href: this.xml.querySelector('credit link').getAttribute('url'),
  200. position: {
  201. x: -40
  202. }
  203. },
  204. tooltip: {
  205. shared: true,
  206. useHTML: true,
  207. headerFormat:
  208. '<small>{point.x:%A, %b %e, %H:%M} - {point.point.to:%H:%M}</small><br>' +
  209. '<b>{point.point.symbolName}</b><br>'
  210. },
  211. xAxis: [{ // Bottom X axis
  212. type: 'datetime',
  213. tickInterval: 2 * 36e5, // two hours
  214. minorTickInterval: 36e5, // one hour
  215. tickLength: 0,
  216. gridLineWidth: 1,
  217. gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0',
  218. startOnTick: false,
  219. endOnTick: false,
  220. minPadding: 0,
  221. maxPadding: 0,
  222. offset: 30,
  223. showLastLabel: true,
  224. labels: {
  225. format: '{value:%H}'
  226. },
  227. crosshair: true
  228. }, { // Top X axis
  229. linkedTo: 0,
  230. type: 'datetime',
  231. tickInterval: 24 * 3600 * 1000,
  232. labels: {
  233. format: '{value:<span style="font-size: 12px; font-weight: bold">%a</span> %b %e}',
  234. align: 'left',
  235. x: 3,
  236. y: -5
  237. },
  238. opposite: true,
  239. tickLength: 20,
  240. gridLineWidth: 1
  241. }],
  242. yAxis: [{ // temperature axis
  243. title: {
  244. text: null
  245. },
  246. labels: {
  247. format: '{value}°',
  248. style: {
  249. fontSize: '10px'
  250. },
  251. x: -3
  252. },
  253. plotLines: [{ // zero plane
  254. value: 0,
  255. color: '#BBBBBB',
  256. width: 1,
  257. zIndex: 2
  258. }],
  259. maxPadding: 0.3,
  260. minRange: 8,
  261. tickInterval: 1,
  262. gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0'
  263. }, { // precipitation axis
  264. title: {
  265. text: null
  266. },
  267. labels: {
  268. enabled: false
  269. },
  270. gridLineWidth: 0,
  271. tickLength: 0,
  272. minRange: 10,
  273. min: 0
  274. }, { // Air pressure
  275. allowDecimals: false,
  276. title: { // Title on top of axis
  277. text: 'hPa',
  278. offset: 0,
  279. align: 'high',
  280. rotation: 0,
  281. style: {
  282. fontSize: '10px',
  283. color: Highcharts.getOptions().colors[2]
  284. },
  285. textAlign: 'left',
  286. x: 3
  287. },
  288. labels: {
  289. style: {
  290. fontSize: '8px',
  291. color: Highcharts.getOptions().colors[2]
  292. },
  293. y: 2,
  294. x: 3
  295. },
  296. gridLineWidth: 0,
  297. opposite: true,
  298. showLastLabel: false
  299. }],
  300. legend: {
  301. enabled: false
  302. },
  303. plotOptions: {
  304. series: {
  305. pointPlacement: 'between'
  306. }
  307. },
  308. series: [{
  309. name: 'Temperature',
  310. data: this.temperatures,
  311. type: 'spline',
  312. marker: {
  313. enabled: false,
  314. states: {
  315. hover: {
  316. enabled: true
  317. }
  318. }
  319. },
  320. tooltip: {
  321. pointFormat: '<span style="color:{point.color}">\u25CF</span> ' +
  322. '{series.name}: <b>{point.value}°C</b><br/>'
  323. },
  324. zIndex: 1,
  325. color: '#FF3333',
  326. negativeColor: '#48AFE8'
  327. }, {
  328. name: 'Precipitation',
  329. data: this.precipitationsError,
  330. type: 'column',
  331. color: 'url(#precipitation-error)',
  332. yAxis: 1,
  333. groupPadding: 0,
  334. pointPadding: 0,
  335. tooltip: {
  336. valueSuffix: ' mm',
  337. pointFormat: '<span style="color:{point.color}">\u25CF</span> ' +
  338. '{series.name}: <b>{point.minvalue} mm - {point.maxvalue} mm</b><br/>'
  339. },
  340. grouping: false,
  341. dataLabels: {
  342. enabled: meteogram.hasPrecipitationError,
  343. formatter: function () {
  344. if (this.point.maxvalue > 0) {
  345. return this.point.maxvalue;
  346. }
  347. },
  348. style: {
  349. fontSize: '8px',
  350. color: 'gray'
  351. }
  352. }
  353. }, {
  354. name: 'Precipitation',
  355. data: this.precipitations,
  356. type: 'column',
  357. color: '#68CFE8',
  358. yAxis: 1,
  359. groupPadding: 0,
  360. pointPadding: 0,
  361. grouping: false,
  362. dataLabels: {
  363. enabled: !meteogram.hasPrecipitationError,
  364. formatter: function () {
  365. if (this.y > 0) {
  366. return this.y;
  367. }
  368. },
  369. style: {
  370. fontSize: '8px',
  371. color: 'gray'
  372. }
  373. },
  374. tooltip: {
  375. valueSuffix: ' mm'
  376. }
  377. }, {
  378. name: 'Air pressure',
  379. color: Highcharts.getOptions().colors[2],
  380. data: this.pressures,
  381. marker: {
  382. enabled: false
  383. },
  384. shadow: false,
  385. tooltip: {
  386. valueSuffix: ' hPa'
  387. },
  388. dashStyle: 'shortdot',
  389. yAxis: 2
  390. }, {
  391. name: 'Wind',
  392. type: 'windbarb',
  393. id: 'windbarbs',
  394. color: Highcharts.getOptions().colors[1],
  395. lineWidth: 1.5,
  396. data: this.winds,
  397. vectorLength: 18,
  398. yOffset: -15,
  399. tooltip: {
  400. valueSuffix: ' m/s'
  401. }
  402. }]
  403. };
  404. };
  405. /**
  406. * Post-process the chart from the callback function, the second argument to Highcharts.Chart.
  407. */
  408. Meteogram.prototype.onChartLoad = function (chart) {
  409. this.drawWeatherSymbols(chart);
  410. this.drawBlocksForWindArrows(chart);
  411. };
  412. /**
  413. * Create the chart. This function is called async when the data file is loaded and parsed.
  414. */
  415. Meteogram.prototype.createChart = function () {
  416. var meteogram = this;
  417. this.chart = new Highcharts.Chart(this.getChartOptions(), function (chart) {
  418. meteogram.onChartLoad(chart);
  419. });
  420. };
  421. Meteogram.prototype.error = function () {
  422. $('#loading').html('<i class="fa fa-frown-o"></i> Failed loading data, please try again later');
  423. };
  424. /**
  425. * Handle the data. This part of the code is not Highcharts specific, but deals with yr.no's
  426. * specific data format
  427. */
  428. Meteogram.prototype.parseYrData = function () {
  429. var meteogram = this,
  430. xml = this.xml,
  431. pointStart,
  432. forecast = xml && xml.querySelector('forecast');
  433. if (!forecast) {
  434. return this.error();
  435. }
  436. // The returned xml variable is a JavaScript representation of the provided
  437. // XML, generated on the server by running PHP simple_load_xml and
  438. // converting it to JavaScript by json_encode.
  439. Highcharts.each(
  440. forecast.querySelectorAll('tabular time'),
  441. function (time, i) {
  442. // Get the times - only Safari can't parse ISO8601 so we need to do
  443. // some replacements
  444. var from = time.getAttribute('from') + ' UTC',
  445. to = time.getAttribute('to') + ' UTC';
  446. from = from.replace(/-/g, '/').replace('T', ' ');
  447. from = Date.parse(from);
  448. to = to.replace(/-/g, '/').replace('T', ' ');
  449. to = Date.parse(to);
  450. if (to > pointStart + 4 * 24 * 36e5) {
  451. return;
  452. }
  453. // If it is more than an hour between points, show all symbols
  454. if (i === 0) {
  455. meteogram.resolution = to - from;
  456. }
  457. // Populate the parallel arrays
  458. meteogram.symbols.push(
  459. time.querySelector('symbol').getAttribute('var')
  460. .match(/[0-9]{2}[dnm]?/)[0]
  461. );
  462. meteogram.temperatures.push({
  463. x: from,
  464. y: parseInt(
  465. time.querySelector('temperature').getAttribute('value'),
  466. 10
  467. ),
  468. // custom options used in the tooltip formatter
  469. to: to,
  470. symbolName: time.querySelector('symbol').getAttribute('name')
  471. });
  472. var precipitation = time.querySelector('precipitation');
  473. meteogram.precipitations.push({
  474. x: from,
  475. y: parseFloat(
  476. Highcharts.pick(
  477. precipitation.getAttribute('minvalue'),
  478. precipitation.getAttribute('value')
  479. )
  480. )
  481. });
  482. if (precipitation.getAttribute('maxvalue')) {
  483. meteogram.hasPrecipitationError = true;
  484. meteogram.precipitationsError.push({
  485. x: from,
  486. y: parseFloat(precipitation.getAttribute('maxvalue')),
  487. minvalue: parseFloat(precipitation.getAttribute('minvalue')),
  488. maxvalue: parseFloat(precipitation.getAttribute('maxvalue')),
  489. value: parseFloat(precipitation.getAttribute('value'))
  490. });
  491. }
  492. if (i % 2 === 0) {
  493. meteogram.winds.push({
  494. x: from,
  495. value: parseFloat(time.querySelector('windSpeed')
  496. .getAttribute('mps')),
  497. direction: parseFloat(time.querySelector('windDirection')
  498. .getAttribute('deg'))
  499. });
  500. }
  501. meteogram.pressures.push({
  502. x: from,
  503. y: parseFloat(time.querySelector('pressure').getAttribute('value'))
  504. });
  505. if (i === 0) {
  506. pointStart = (from + to) / 2;
  507. }
  508. }
  509. );
  510. // Smooth the line
  511. this.smoothLine(this.temperatures);
  512. // Create the chart when the data is loaded
  513. this.createChart();
  514. };
  515. // End of the Meteogram protype
  516. // On DOM ready...
  517. // Set the hash to the yr.no URL we want to parse
  518. var place,
  519. url;
  520. if (!location.hash) {
  521. place = 'United_Kingdom/England/London';
  522. //place = 'France/Rhône-Alpes/Val_d\'Isère~2971074';
  523. //place = 'Norway/Sogn_og_Fjordane/Vik/Målset';
  524. //place = 'United_States/California/San_Francisco';
  525. //place = 'United_States/Minnesota/Minneapolis';
  526. location.hash = 'https://www.yr.no/place/' + place + '/forecast_hour_by_hour.xml';
  527. }
  528. // Then get the XML file through Highcharts' CORS proxy. Our proxy is limited to
  529. // this specific location. Useing the third party, rate limited cors.io service
  530. // for experimenting with other locations.
  531. url = location.hash.substr(1);
  532. $.ajax({
  533. dataType: 'xml',
  534. url: url === 'https://www.yr.no/place/United_Kingdom/England/London/forecast_hour_by_hour.xml' ?
  535. 'https://www.highcharts.com/samples/data/cors.php?url=' + url :
  536. 'https://cors.io/?' + url,
  537. success: function (xml) {
  538. window.meteogram = new Meteogram(xml, 'container');
  539. },
  540. error: Meteogram.prototype.error
  541. });
  542. </script>
  543. </body>
  544. </html>