Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
9 lars 1
<!DOCTYPE HTML>
2
<html>
3
	<head>
4
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
		<title>Highcharts Example</title>
6
 
7
		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
8
		<style type="text/css">
9
${demo.css}
10
		</style>
11
		<script type="text/javascript">
12
/**
13
 * This is a complex demo of how to set up a Highcharts chart, coupled to a
14
 * dynamic source and extended by drawing image sprites, wind arrow paths
15
 * and a second grid on top of the chart. The purpose of the demo is to inpire
16
 * developers to go beyond the basic chart types and show how the library can
17
 * be extended programmatically. This is what the demo does:
18
 *
19
 * - Loads weather forecast from www.yr.no in form of an XML service. The XML
20
 *   is translated on the Higcharts website into JSONP for the sake of the demo
21
 *   being shown on both our website and JSFiddle.
22
 * - When the data arrives async, a Meteogram instance is created. We have
23
 *   created the Meteogram prototype to provide an organized structure of the different
24
 *   methods and subroutines associated with the demo.
25
 * - The parseYrData method parses the data from www.yr.no into several parallel arrays. These
26
 *   arrays are used directly as the data option for temperature, precipitation
27
 *   and air pressure. As the temperature data gives only full degrees, we apply
28
 *   some smoothing on the graph, but keep the original data in the tooltip.
29
 * - After this, the options structure is build, and the chart generated with the
30
 *   parsed data.
31
 * - In the callback (on chart load), we weather icons on top of the temperature series.
32
 *   The icons are sprites from a single PNG image, placed inside a clipped 30x30
33
 *   SVG <g> element. VML interprets this as HTML images inside a clipped div.
34
 * - Lastly, the wind arrows are built and added below the plot area, and a grid is
35
 *   drawn around them. The wind arrows are basically drawn north-south, then rotated
36
 *   as per the wind direction.
37
 */
38
 
39
function Meteogram(xml, container) {
40
    // Parallel arrays for the chart data, these are populated as the XML/JSON file
41
    // is loaded
42
    this.symbols = [];
43
    this.symbolNames = [];
44
    this.precipitations = [];
45
    this.windDirections = [];
46
    this.windDirectionNames = [];
47
    this.windSpeeds = [];
48
    this.windSpeedNames = [];
49
    this.temperatures = [];
50
    this.pressures = [];
51
 
52
    // Initialize
53
    this.xml = xml;
54
    this.container = container;
55
 
56
    // Run
57
    this.parseYrData();
58
}
59
/**
60
 * Return weather symbol sprites as laid out at http://om.yr.no/forklaring/symbol/
61
 */
62
Meteogram.prototype.getSymbolSprites = function (symbolSize) {
63
    return {
64
        '01d': {
65
            x: 0,
66
            y: 0
67
        },
68
        '01n': {
69
            x: symbolSize,
70
            y: 0
71
        },
72
        '16': {
73
            x: 2 * symbolSize,
74
            y: 0
75
        },
76
        '02d': {
77
            x: 0,
78
            y: symbolSize
79
        },
80
        '02n': {
81
            x: symbolSize,
82
            y: symbolSize
83
        },
84
        '03d': {
85
            x: 0,
86
            y: 2 * symbolSize
87
        },
88
        '03n': {
89
            x: symbolSize,
90
            y: 2 * symbolSize
91
        },
92
        '17': {
93
            x: 2 * symbolSize,
94
            y: 2 * symbolSize
95
        },
96
        '04': {
97
            x: 0,
98
            y: 3 * symbolSize
99
        },
100
        '05d': {
101
            x: 0,
102
            y: 4 * symbolSize
103
        },
104
        '05n': {
105
            x: symbolSize,
106
            y: 4 * symbolSize
107
        },
108
        '18': {
109
            x: 2 * symbolSize,
110
            y: 4 * symbolSize
111
        },
112
        '06d': {
113
            x: 0,
114
            y: 5 * symbolSize
115
        },
116
        '06n': {
117
            x: symbolSize,
118
            y: 5 * symbolSize
119
        },
120
        '07d': {
121
            x: 0,
122
            y: 6 * symbolSize
123
        },
124
        '07n': {
125
            x: symbolSize,
126
            y: 6 * symbolSize
127
        },
128
        '08d': {
129
            x: 0,
130
            y: 7 * symbolSize
131
        },
132
        '08n': {
133
            x: symbolSize,
134
            y: 7 * symbolSize
135
        },
136
        '19': {
137
            x: 2 * symbolSize,
138
            y: 7 * symbolSize
139
        },
140
        '09': {
141
            x: 0,
142
            y: 8 * symbolSize
143
        },
144
        '10': {
145
            x: 0,
146
            y: 9 * symbolSize
147
        },
148
        '11': {
149
            x: 0,
150
            y: 10 * symbolSize
151
        },
152
        '12': {
153
            x: 0,
154
            y: 11 * symbolSize
155
        },
156
        '13': {
157
            x: 0,
158
            y: 12 * symbolSize
159
        },
160
        '14': {
161
            x: 0,
162
            y: 13 * symbolSize
163
        },
164
        '15': {
165
            x: 0,
166
            y: 14 * symbolSize
167
        },
168
        '20d': {
169
            x: 0,
170
            y: 15 * symbolSize
171
        },
172
        '20n': {
173
            x: symbolSize,
174
            y: 15 * symbolSize
175
        },
176
        '20m': {
177
            x: 2 * symbolSize,
178
            y: 15 * symbolSize
179
        },
180
        '21d': {
181
            x: 0,
182
            y: 16 * symbolSize
183
        },
184
        '21n': {
185
            x: symbolSize,
186
            y: 16 * symbolSize
187
        },
188
        '21m': {
189
            x: 2 * symbolSize,
190
            y: 16 * symbolSize
191
        },
192
        '22': {
193
            x: 0,
194
            y: 17 * symbolSize
195
        },
196
        '23': {
197
            x: 0,
198
            y: 18 * symbolSize
199
        }
200
    };
201
};
202
 
203
 
204
/**
205
 * Function to smooth the temperature line. The original data provides only whole degrees,
206
 * which makes the line graph look jagged. So we apply a running mean on it, but preserve
207
 * the unaltered value in the tooltip.
208
 */
209
Meteogram.prototype.smoothLine = function (data) {
210
    var i = data.length,
211
        sum,
212
        value;
213
 
214
    while (i--) {
215
        data[i].value = value = data[i].y; // preserve value for tooltip
216
 
217
        // Set the smoothed value to the average of the closest points, but don't allow
218
        // it to differ more than 0.5 degrees from the given value
219
        sum = (data[i - 1] || data[i]).y + value + (data[i + 1] || data[i]).y;
220
        data[i].y = Math.max(value - 0.5, Math.min(sum / 3, value + 0.5));
221
    }
222
};
223
 
224
/**
225
 * Callback function that is called from Highcharts on hovering each point and returns
226
 * HTML for the tooltip.
227
 */
228
Meteogram.prototype.tooltipFormatter = function (tooltip) {
229
 
230
    // Create the header with reference to the time interval
231
    var index = tooltip.points[0].point.index,
232
        ret = '<small>' + Highcharts.dateFormat('%A, %b %e, %H:%M', tooltip.x) + '-' +
233
            Highcharts.dateFormat('%H:%M', tooltip.points[0].point.to) + '</small><br>';
234
 
235
    // Symbol text
236
    ret += '<b>' + this.symbolNames[index] + '</b>';
237
 
238
    ret += '<table>';
239
 
240
    // Add all series
241
    Highcharts.each(tooltip.points, function (point) {
242
        var series = point.series;
243
        ret += '<tr><td><span style="color:' + series.color + '">\u25CF</span> ' + series.name +
244
            ': </td><td style="white-space:nowrap">' + Highcharts.pick(point.point.value, point.y) +
245
            series.options.tooltip.valueSuffix + '</td></tr>';
246
    });
247
 
248
    // Add wind
249
    ret += '<tr><td style="vertical-align: top">\u25CF Wind</td><td style="white-space:nowrap">' + this.windDirectionNames[index] +
250
        '<br>' + this.windSpeedNames[index] + ' (' +
251
        Highcharts.numberFormat(this.windSpeeds[index], 1) + ' m/s)</td></tr>';
252
 
253
    // Close
254
    ret += '</table>';
255
 
256
 
257
    return ret;
258
};
259
 
260
/**
261
 * Draw the weather symbols on top of the temperature series. The symbols are sprites of a single
262
 * file, defined in the getSymbolSprites function above.
263
 */
264
Meteogram.prototype.drawWeatherSymbols = function (chart) {
265
    var meteogram = this,
266
        symbolSprites = this.getSymbolSprites(30);
267
 
268
    $.each(chart.series[0].data, function (i, point) {
269
        var sprite,
270
            group;
271
 
272
        if (meteogram.resolution > 36e5 || i % 2 === 0) {
273
 
274
            sprite = symbolSprites[meteogram.symbols[i]];
275
            if (sprite) {
276
 
277
                // Create a group element that is positioned and clipped at 30 pixels width and height
278
                group = chart.renderer.g()
279
                    .attr({
280
                        translateX: point.plotX + chart.plotLeft - 15,
281
                        translateY: point.plotY + chart.plotTop - 30,
282
                        zIndex: 5
283
                    })
284
                    .clip(chart.renderer.clipRect(0, 0, 30, 30))
285
                    .add();
286
 
287
                // Position the image inside it at the sprite position
288
                chart.renderer.image(
289
                    'http://www.highcharts.com/samples/graphics/meteogram-symbols-30px.png',
290
                    -sprite.x,
291
                    -sprite.y,
292
                    90,
293
                    570
294
                )
295
                    .add(group);
296
            }
297
        }
298
    });
299
};
300
 
301
/**
302
 * Create wind speed symbols for the Beaufort wind scale. The symbols are rotated
303
 * around the zero centerpoint.
304
 */
305
Meteogram.prototype.windArrow = function (name) {
306
    var level,
307
        path;
308
 
309
    // The stem and the arrow head
310
    path = [
311
        'M', 0, 7, // base of arrow
312
        'L', -1.5, 7,
313
        0, 10,
314
        1.5, 7,
315
        0, 7,
316
        0, -10 // top
317
    ];
318
 
319
    level = $.inArray(name, ['Calm', 'Light air', 'Light breeze', 'Gentle breeze', 'Moderate breeze',
320
        'Fresh breeze', 'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm',
321
        'Violent storm', 'Hurricane']);
322
 
323
    if (level === 0) {
324
        path = [];
325
    }
326
 
327
    if (level === 2) {
328
        path.push('M', 0, -8, 'L', 4, -8); // short line
329
    } else if (level >= 3) {
330
        path.push(0, -10, 7, -10); // long line
331
    }
332
 
333
    if (level === 4) {
334
        path.push('M', 0, -7, 'L', 4, -7);
335
    } else if (level >= 5) {
336
        path.push('M', 0, -7, 'L', 7, -7);
337
    }
338
 
339
    if (level === 5) {
340
        path.push('M', 0, -4, 'L', 4, -4);
341
    } else if (level >= 6) {
342
        path.push('M', 0, -4, 'L', 7, -4);
343
    }
344
 
345
    if (level === 7) {
346
        path.push('M', 0, -1, 'L', 4, -1);
347
    } else if (level >= 8) {
348
        path.push('M', 0, -1, 'L', 7, -1);
349
    }
350
 
351
    return path;
352
};
353
 
354
/**
355
 * Draw the wind arrows. Each arrow path is generated by the windArrow function above.
356
 */
357
Meteogram.prototype.drawWindArrows = function (chart) {
358
    var meteogram = this;
359
 
360
    $.each(chart.series[0].data, function (i, point) {
361
        var sprite, arrow, x, y;
362
 
363
        if (meteogram.resolution > 36e5 || i % 2 === 0) {
364
 
365
            // Draw the wind arrows
366
            x = point.plotX + chart.plotLeft + 7;
367
            y = 255;
368
            if (meteogram.windSpeedNames[i] === 'Calm') {
369
                arrow = chart.renderer.circle(x, y, 10).attr({
370
                    fill: 'none'
371
                });
372
            } else {
373
                arrow = chart.renderer.path(
374
                    meteogram.windArrow(meteogram.windSpeedNames[i])
375
                ).attr({
376
                    rotation: parseInt(meteogram.windDirections[i], 10),
377
                    translateX: x, // rotation center
378
                    translateY: y // rotation center
379
                });
380
            }
381
            arrow.attr({
382
                stroke: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black',
383
                'stroke-width': 1.5,
384
                zIndex: 5
385
            })
386
            .add();
387
 
388
        }
389
    });
390
};
391
 
392
/**
393
 * Draw blocks around wind arrows, below the plot area
394
 */
395
Meteogram.prototype.drawBlocksForWindArrows = function (chart) {
396
    var xAxis = chart.xAxis[0],
397
        x,
398
        pos,
399
        max,
400
        isLong,
401
        isLast,
402
        i;
403
 
404
    for (pos = xAxis.min, max = xAxis.max, i = 0; pos <= max + 36e5; pos += 36e5, i += 1) {
405
 
406
        // Get the X position
407
        isLast = pos === max + 36e5;
408
        x = Math.round(xAxis.toPixels(pos)) + (isLast ? 0.5 : -0.5);
409
 
410
        // Draw the vertical dividers and ticks
411
        if (this.resolution > 36e5) {
412
            isLong = pos % this.resolution === 0;
413
        } else {
414
            isLong = i % 2 === 0;
415
        }
416
        chart.renderer.path(['M', x, chart.plotTop + chart.plotHeight + (isLong ? 0 : 28),
417
            'L', x, chart.plotTop + chart.plotHeight + 32, 'Z'])
418
            .attr({
419
                'stroke': chart.options.chart.plotBorderColor,
420
                'stroke-width': 1
421
            })
422
            .add();
423
    }
424
};
425
 
426
/**
427
 * Get the title based on the XML data
428
 */
429
Meteogram.prototype.getTitle = function () {
430
    return 'Meteogram for ' + this.xml.location.name + ', ' + this.xml.location.country;
431
};
432
 
433
/**
434
 * Build and return the Highcharts options structure
435
 */
436
Meteogram.prototype.getChartOptions = function () {
437
    var meteogram = this;
438
 
439
    return {
440
        chart: {
441
            renderTo: this.container,
442
            marginBottom: 70,
443
            marginRight: 40,
444
            marginTop: 50,
445
            plotBorderWidth: 1,
446
            width: 800,
447
            height: 310
448
        },
449
 
450
        title: {
451
            text: this.getTitle(),
452
            align: 'left'
453
        },
454
 
455
        credits: {
456
            text: 'Forecast from <a href="http://yr.no">yr.no</a>',
457
            href: this.xml.credit.link['@attributes'].url,
458
            position: {
459
                x: -40
460
            }
461
        },
462
 
463
        tooltip: {
464
            shared: true,
465
            useHTML: true,
466
            formatter: function () {
467
                return meteogram.tooltipFormatter(this);
468
            }
469
        },
470
 
471
        xAxis: [{ // Bottom X axis
472
            type: 'datetime',
473
            tickInterval: 2 * 36e5, // two hours
474
            minorTickInterval: 36e5, // one hour
475
            tickLength: 0,
476
            gridLineWidth: 1,
477
            gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0',
478
            startOnTick: false,
479
            endOnTick: false,
480
            minPadding: 0,
481
            maxPadding: 0,
482
            offset: 30,
483
            showLastLabel: true,
484
            labels: {
485
                format: '{value:%H}'
486
            }
487
        }, { // Top X axis
488
            linkedTo: 0,
489
            type: 'datetime',
490
            tickInterval: 24 * 3600 * 1000,
491
            labels: {
492
                format: '{value:<span style="font-size: 12px; font-weight: bold">%a</span> %b %e}',
493
                align: 'left',
494
                x: 3,
495
                y: -5
496
            },
497
            opposite: true,
498
            tickLength: 20,
499
            gridLineWidth: 1
500
        }],
501
 
502
        yAxis: [{ // temperature axis
503
            title: {
504
                text: null
505
            },
506
            labels: {
507
                format: '{value}°',
508
                style: {
509
                    fontSize: '10px'
510
                },
511
                x: -3
512
            },
513
            plotLines: [{ // zero plane
514
                value: 0,
515
                color: '#BBBBBB',
516
                width: 1,
517
                zIndex: 2
518
            }],
519
            // Custom positioner to provide even temperature ticks from top down
520
            tickPositioner: function () {
521
                var max = Math.ceil(this.max) + 1,
522
                    pos = max - 12, // start
523
                    ret;
524
 
525
                if (pos < this.min) {
526
                    ret = [];
527
                    while (pos <= max) {
528
                        ret.push(pos += 1);
529
                    }
530
                } // else return undefined and go auto
531
 
532
                return ret;
533
 
534
            },
535
            maxPadding: 0.3,
536
            tickInterval: 1,
537
            gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0'
538
 
539
        }, { // precipitation axis
540
            title: {
541
                text: null
542
            },
543
            labels: {
544
                enabled: false
545
            },
546
            gridLineWidth: 0,
547
            tickLength: 0
548
 
549
        }, { // Air pressure
550
            allowDecimals: false,
551
            title: { // Title on top of axis
552
                text: 'hPa',
553
                offset: 0,
554
                align: 'high',
555
                rotation: 0,
556
                style: {
557
                    fontSize: '10px',
558
                    color: Highcharts.getOptions().colors[2]
559
                },
560
                textAlign: 'left',
561
                x: 3
562
            },
563
            labels: {
564
                style: {
565
                    fontSize: '8px',
566
                    color: Highcharts.getOptions().colors[2]
567
                },
568
                y: 2,
569
                x: 3
570
            },
571
            gridLineWidth: 0,
572
            opposite: true,
573
            showLastLabel: false
574
        }],
575
 
576
        legend: {
577
            enabled: false
578
        },
579
 
580
        plotOptions: {
581
            series: {
582
                pointPlacement: 'between'
583
            }
584
        },
585
 
586
 
587
        series: [{
588
            name: 'Temperature',
589
            data: this.temperatures,
590
            type: 'spline',
591
            marker: {
592
                enabled: false,
593
                states: {
594
                    hover: {
595
                        enabled: true
596
                    }
597
                }
598
            },
599
            tooltip: {
600
                valueSuffix: '°C'
601
            },
602
            zIndex: 1,
603
            color: '#FF3333',
604
            negativeColor: '#48AFE8'
605
        }, {
606
            name: 'Precipitation',
607
            data: this.precipitations,
608
            type: 'column',
609
            color: '#68CFE8',
610
            yAxis: 1,
611
            groupPadding: 0,
612
            pointPadding: 0,
613
            borderWidth: 0,
614
            shadow: false,
615
            dataLabels: {
616
                enabled: true,
617
                formatter: function () {
618
                    if (this.y > 0) {
619
                        return this.y;
620
                    }
621
                },
622
                style: {
623
                    fontSize: '8px'
624
                }
625
            },
626
            tooltip: {
627
                valueSuffix: 'mm'
628
            }
629
        }, {
630
            name: 'Air pressure',
631
            color: Highcharts.getOptions().colors[2],
632
            data: this.pressures,
633
            marker: {
634
                enabled: false
635
            },
636
            shadow: false,
637
            tooltip: {
638
                valueSuffix: ' hPa'
639
            },
640
            dashStyle: 'shortdot',
641
            yAxis: 2
642
        }]
643
    }
644
};
645
 
646
/**
647
 * Post-process the chart from the callback function, the second argument to Highcharts.Chart.
648
 */
649
Meteogram.prototype.onChartLoad = function (chart) {
650
 
651
    this.drawWeatherSymbols(chart);
652
    this.drawWindArrows(chart);
653
    this.drawBlocksForWindArrows(chart);
654
 
655
};
656
 
657
/**
658
 * Create the chart. This function is called async when the data file is loaded and parsed.
659
 */
660
Meteogram.prototype.createChart = function () {
661
    var meteogram = this;
662
    this.chart = new Highcharts.Chart(this.getChartOptions(), function (chart) {
663
        meteogram.onChartLoad(chart);
664
    });
665
};
666
 
667
/**
668
 * Handle the data. This part of the code is not Highcharts specific, but deals with yr.no's
669
 * specific data format
670
 */
671
Meteogram.prototype.parseYrData = function () {
672
 
673
    var meteogram = this,
674
        xml = this.xml,
675
        pointStart;
676
 
677
    if (!xml || !xml.forecast) {
678
        $('#loading').html('<i class="fa fa-frown-o"></i> Failed loading data, please try again later');
679
        return;
680
    }
681
 
682
    // The returned xml variable is a JavaScript representation of the provided XML,
683
    // generated on the server by running PHP simple_load_xml and converting it to
684
    // JavaScript by json_encode.
685
    $.each(xml.forecast.tabular.time, function (i, time) {
686
        // Get the times - only Safari can't parse ISO8601 so we need to do some replacements
687
        var from = time['@attributes'].from + ' UTC',
688
            to = time['@attributes'].to + ' UTC';
689
 
690
        from = from.replace(/-/g, '/').replace('T', ' ');
691
        from = Date.parse(from);
692
        to = to.replace(/-/g, '/').replace('T', ' ');
693
        to = Date.parse(to);
694
 
695
        if (to > pointStart + 4 * 24 * 36e5) {
696
            return;
697
        }
698
 
699
        // If it is more than an hour between points, show all symbols
700
        if (i === 0) {
701
            meteogram.resolution = to - from;
702
        }
703
 
704
        // Populate the parallel arrays
705
        meteogram.symbols.push(time.symbol['@attributes']['var'].match(/[0-9]{2}[dnm]?/)[0]);
706
        meteogram.symbolNames.push(time.symbol['@attributes'].name);
707
 
708
        meteogram.temperatures.push({
709
            x: from,
710
            y: parseInt(time.temperature['@attributes'].value),
711
            // custom options used in the tooltip formatter
712
            to: to,
713
            index: i
714
        });
715
 
716
        meteogram.precipitations.push({
717
            x: from,
718
            y: parseFloat(time.precipitation['@attributes'].value)
719
        });
720
        meteogram.windDirections.push(parseFloat(time.windDirection['@attributes'].deg));
721
        meteogram.windDirectionNames.push(time.windDirection['@attributes'].name);
722
        meteogram.windSpeeds.push(parseFloat(time.windSpeed['@attributes'].mps));
723
        meteogram.windSpeedNames.push(time.windSpeed['@attributes'].name);
724
 
725
        meteogram.pressures.push({
726
            x: from,
727
            y: parseFloat(time.pressure['@attributes'].value)
728
        });
729
 
730
        if (i == 0) {
731
            pointStart = (from + to) / 2;
732
        }
733
    });
734
 
735
    // Smooth the line
736
    this.smoothLine(this.temperatures);
737
 
738
    // Create the chart when the data is loaded
739
    this.createChart();
740
};
741
// End of the Meteogram protype
742
 
743
 
744
 
745
$(function () { // On DOM ready...
746
 
747
    // Set the hash to the yr.no URL we want to parse
748
    if (!location.hash) {
749
        var place = 'United_Kingdom/England/London';
750
        //place = 'France/Rhône-Alpes/Val_d\'Isère~2971074';
751
        //place = 'Norway/Sogn_og_Fjordane/Vik/Målset';
752
        //place = 'United_States/California/San_Francisco';
753
        //place = 'United_States/Minnesota/Minneapolis';
754
        location.hash = 'http://www.yr.no/place/' + place + '/forecast_hour_by_hour.xml';
755
 
756
    }
757
 
758
    // Then get the XML file through Highcharts' jsonp provider, see
759
    // https://github.com/highslide-software/highcharts.com/blob/master/samples/data/jsonp.php
760
    // for source code.
761
    $.getJSON(
762
        'http://www.highcharts.com/samples/data/jsonp.php?url=' + location.hash.substr(1) + '&callback=?',
763
        function (xml) {
764
            var meteogram = new Meteogram(xml, 'container');
765
        }
766
    );
767
 
768
});
769
		</script>
770
	</head>
771
	<body>
772
 
773
<script src="../../js/highcharts.js"></script>
774
<script src="../../js/modules/exporting.js"></script>
775
<link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
776
 
777
<div id="container" style="width: 800px; height: 310px; margin: 0 auto">
778
	<div style="margin-top: 100px; text-align: center" id="loading">
779
		<i class="fa fa-spinner fa-spin"></i> Loading data from external source
780
	</div>
781
</div>
782
<!--
783
<div style="width: 800px; margin: 0 auto">
784
	<a href="#http://www.yr.no/place/United_Kingdom/England/London/forecast_hour_by_hour.xml">London</a>,
785
	<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>,
786
	<a href="#http://www.yr.no/place/United_States/California/San_Francisco/forecast_hour_by_hour.xml">San Francisco</a>,
787
	<a href="#http://www.yr.no/place/Norway/Vik/Vikafjell/forecast_hour_by_hour.xml">Vikjafjellet</a>
788
</div>
789
-->
790
	</body>
791
</html>