You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

791 lines
23 KiB

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Highcharts Example</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<style type="text/css">
${demo.css}
</style>
<script type="text/javascript">
/**
* 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 an XML service. The XML
* is translated on the Higcharts website into JSONP for the sake of the demo
* being shown on both our website and JSFiddle.
* - 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. As the temperature data gives only full degrees, we apply
* some smoothing on the graph, but keep the original data in the tooltip.
* - After this, the options structure is build, and the chart generated with the
* parsed data.
* - In the callback (on chart load), we weather icons on top of the temperature series.
* The icons are sprites from a single PNG image, placed inside a clipped 30x30
* SVG <g> element. VML interprets this as HTML images inside a clipped div.
* - Lastly, the wind arrows are built and added below the plot area, and a grid is
* drawn around them. The wind arrows are basically drawn north-south, then rotated
* as per the wind direction.
*/
function Meteogram(xml, container) {
// Parallel arrays for the chart data, these are populated as the XML/JSON file
// is loaded
this.symbols = [];
this.symbolNames = [];
this.precipitations = [];
this.windDirections = [];
this.windDirectionNames = [];
this.windSpeeds = [];
this.windSpeedNames = [];
this.temperatures = [];
this.pressures = [];
// Initialize
this.xml = xml;
this.container = container;
// Run
this.parseYrData();
}
/**
* Return weather symbol sprites as laid out at http://om.yr.no/forklaring/symbol/
*/
Meteogram.prototype.getSymbolSprites = function (symbolSize) {
return {
'01d': {
x: 0,
y: 0
},
'01n': {
x: symbolSize,
y: 0
},
'16': {
x: 2 * symbolSize,
y: 0
},
'02d': {
x: 0,
y: symbolSize
},
'02n': {
x: symbolSize,
y: symbolSize
},
'03d': {
x: 0,
y: 2 * symbolSize
},
'03n': {
x: symbolSize,
y: 2 * symbolSize
},
'17': {
x: 2 * symbolSize,
y: 2 * symbolSize
},
'04': {
x: 0,
y: 3 * symbolSize
},
'05d': {
x: 0,
y: 4 * symbolSize
},
'05n': {
x: symbolSize,
y: 4 * symbolSize
},
'18': {
x: 2 * symbolSize,
y: 4 * symbolSize
},
'06d': {
x: 0,
y: 5 * symbolSize
},
'06n': {
x: symbolSize,
y: 5 * symbolSize
},
'07d': {
x: 0,
y: 6 * symbolSize
},
'07n': {
x: symbolSize,
y: 6 * symbolSize
},
'08d': {
x: 0,
y: 7 * symbolSize
},
'08n': {
x: symbolSize,
y: 7 * symbolSize
},
'19': {
x: 2 * symbolSize,
y: 7 * symbolSize
},
'09': {
x: 0,
y: 8 * symbolSize
},
'10': {
x: 0,
y: 9 * symbolSize
},
'11': {
x: 0,
y: 10 * symbolSize
},
'12': {
x: 0,
y: 11 * symbolSize
},
'13': {
x: 0,
y: 12 * symbolSize
},
'14': {
x: 0,
y: 13 * symbolSize
},
'15': {
x: 0,
y: 14 * symbolSize
},
'20d': {
x: 0,
y: 15 * symbolSize
},
'20n': {
x: symbolSize,
y: 15 * symbolSize
},
'20m': {
x: 2 * symbolSize,
y: 15 * symbolSize
},
'21d': {
x: 0,
y: 16 * symbolSize
},
'21n': {
x: symbolSize,
y: 16 * symbolSize
},
'21m': {
x: 2 * symbolSize,
y: 16 * symbolSize
},
'22': {
x: 0,
y: 17 * symbolSize
},
'23': {
x: 0,
y: 18 * symbolSize
}
};
};
/**
* Function to smooth the temperature line. The original data provides only whole degrees,
* which makes the line graph look jagged. So we apply a running mean on it, but preserve
* the unaltered value in the tooltip.
*/
Meteogram.prototype.smoothLine = function (data) {
var i = data.length,
sum,
value;
while (i--) {
data[i].value = value = data[i].y; // preserve value for tooltip
// Set the smoothed value to the average of the closest points, but don't allow
// it to differ more than 0.5 degrees from the given value
sum = (data[i - 1] || data[i]).y + value + (data[i + 1] || data[i]).y;
data[i].y = Math.max(value - 0.5, Math.min(sum / 3, value + 0.5));
}
};
/**
* Callback function that is called from Highcharts on hovering each point and returns
* HTML for the tooltip.
*/
Meteogram.prototype.tooltipFormatter = function (tooltip) {
// Create the header with reference to the time interval
var index = tooltip.points[0].point.index,
ret = '<small>' + Highcharts.dateFormat('%A, %b %e, %H:%M', tooltip.x) + '-' +
Highcharts.dateFormat('%H:%M', tooltip.points[0].point.to) + '</small><br>';
// Symbol text
ret += '<b>' + this.symbolNames[index] + '</b>';
ret += '<table>';
// Add all series
Highcharts.each(tooltip.points, function (point) {
var series = point.series;
ret += '<tr><td><span style="color:' + series.color + '">\u25CF</span> ' + series.name +
': </td><td style="white-space:nowrap">' + Highcharts.pick(point.point.value, point.y) +
series.options.tooltip.valueSuffix + '</td></tr>';
});
// Add wind
ret += '<tr><td style="vertical-align: top">\u25CF Wind</td><td style="white-space:nowrap">' + this.windDirectionNames[index] +
'<br>' + this.windSpeedNames[index] + ' (' +
Highcharts.numberFormat(this.windSpeeds[index], 1) + ' m/s)</td></tr>';
// Close
ret += '</table>';
return ret;
};
/**
* Draw the weather symbols on top of the temperature series. The symbols are sprites of a single
* file, defined in the getSymbolSprites function above.
*/
Meteogram.prototype.drawWeatherSymbols = function (chart) {
var meteogram = this,
symbolSprites = this.getSymbolSprites(30);
$.each(chart.series[0].data, function (i, point) {
var sprite,
group;
if (meteogram.resolution > 36e5 || i % 2 === 0) {
sprite = symbolSprites[meteogram.symbols[i]];
if (sprite) {
// Create a group element that is positioned and clipped at 30 pixels width and height
group = chart.renderer.g()
.attr({
translateX: point.plotX + chart.plotLeft - 15,
translateY: point.plotY + chart.plotTop - 30,
zIndex: 5
})
.clip(chart.renderer.clipRect(0, 0, 30, 30))
.add();
// Position the image inside it at the sprite position
chart.renderer.image(
'http://www.highcharts.com/samples/graphics/meteogram-symbols-30px.png',
-sprite.x,
-sprite.y,
90,
570
)
.add(group);
}
}
});
};
/**
* Create wind speed symbols for the Beaufort wind scale. The symbols are rotated
* around the zero centerpoint.
*/
Meteogram.prototype.windArrow = function (name) {
var level,
path;
// The stem and the arrow head
path = [
'M', 0, 7, // base of arrow
'L', -1.5, 7,
0, 10,
1.5, 7,
0, 7,
0, -10 // top
];
level = $.inArray(name, ['Calm', 'Light air', 'Light breeze', 'Gentle breeze', 'Moderate breeze',
'Fresh breeze', 'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm',
'Violent storm', 'Hurricane']);
if (level === 0) {
path = [];
}
if (level === 2) {
path.push('M', 0, -8, 'L', 4, -8); // short line
} else if (level >= 3) {
path.push(0, -10, 7, -10); // long line
}
if (level === 4) {
path.push('M', 0, -7, 'L', 4, -7);
} else if (level >= 5) {
path.push('M', 0, -7, 'L', 7, -7);
}
if (level === 5) {
path.push('M', 0, -4, 'L', 4, -4);
} else if (level >= 6) {
path.push('M', 0, -4, 'L', 7, -4);
}
if (level === 7) {
path.push('M', 0, -1, 'L', 4, -1);
} else if (level >= 8) {
path.push('M', 0, -1, 'L', 7, -1);
}
return path;
};
/**
* Draw the wind arrows. Each arrow path is generated by the windArrow function above.
*/
Meteogram.prototype.drawWindArrows = function (chart) {
var meteogram = this;
$.each(chart.series[0].data, function (i, point) {
var sprite, arrow, x, y;
if (meteogram.resolution > 36e5 || i % 2 === 0) {
// Draw the wind arrows
x = point.plotX + chart.plotLeft + 7;
y = 255;
if (meteogram.windSpeedNames[i] === 'Calm') {
arrow = chart.renderer.circle(x, y, 10).attr({
fill: 'none'
});
} else {
arrow = chart.renderer.path(
meteogram.windArrow(meteogram.windSpeedNames[i])
).attr({
rotation: parseInt(meteogram.windDirections[i], 10),
translateX: x, // rotation center
translateY: y // rotation center
});
}
arrow.attr({
stroke: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black',
'stroke-width': 1.5,
zIndex: 5
})
.add();
}
});
};
/**
* Draw blocks around wind arrows, below the plot area
*/
Meteogram.prototype.drawBlocksForWindArrows = function (chart) {
var xAxis = chart.xAxis[0],
x,
pos,
max,
isLong,
isLast,
i;
for (pos = xAxis.min, max = xAxis.max, i = 0; pos <= max + 36e5; pos += 36e5, i += 1) {
// Get the X position
isLast = pos === max + 36e5;
x = Math.round(xAxis.toPixels(pos)) + (isLast ? 0.5 : -0.5);
// Draw the vertical dividers and ticks
if (this.resolution > 36e5) {
isLong = pos % this.resolution === 0;
} else {
isLong = 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();
}
};
/**
* Get the title based on the XML data
*/
Meteogram.prototype.getTitle = function () {
return 'Meteogram for ' + this.xml.location.name + ', ' + this.xml.location.country;
};
/**
* Build and return the Highcharts options structure
*/
Meteogram.prototype.getChartOptions = function () {
var meteogram = this;
return {
chart: {
renderTo: this.container,
marginBottom: 70,
marginRight: 40,
marginTop: 50,
plotBorderWidth: 1,
width: 800,
height: 310
},
title: {
text: this.getTitle(),
align: 'left'
},
credits: {
text: 'Forecast from <a href="http://yr.no">yr.no</a>',
href: this.xml.credit.link['@attributes'].url,
position: {
x: -40
}
},
tooltip: {
shared: true,
useHTML: true,
formatter: function () {
return meteogram.tooltipFormatter(this);
}
},
xAxis: [{ // Bottom X axis
type: 'datetime',
tickInterval: 2 * 36e5, // two hours
minorTickInterval: 36e5, // one hour
tickLength: 0,
gridLineWidth: 1,
gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0',
startOnTick: false,
endOnTick: false,
minPadding: 0,
maxPadding: 0,
offset: 30,
showLastLabel: true,
labels: {
format: '{value:%H}'
}
}, { // Top X axis
linkedTo: 0,
type: 'datetime',
tickInterval: 24 * 3600 * 1000,
labels: {
format: '{value:<span style="font-size: 12px; font-weight: bold">%a</span> %b %e}',
align: 'left',
x: 3,
y: -5
},
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: '#BBBBBB',
width: 1,
zIndex: 2
}],
// Custom positioner to provide even temperature ticks from top down
tickPositioner: function () {
var max = Math.ceil(this.max) + 1,
pos = max - 12, // start
ret;
if (pos < this.min) {
ret = [];
while (pos <= max) {
ret.push(pos += 1);
}
} // else return undefined and go auto
return ret;
},
maxPadding: 0.3,
tickInterval: 1,
gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0'
}, { // precipitation axis
title: {
text: null
},
labels: {
enabled: false
},
gridLineWidth: 0,
tickLength: 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: {
valueSuffix: '°C'
},
zIndex: 1,
color: '#FF3333',
negativeColor: '#48AFE8'
}, {
name: 'Precipitation',
data: this.precipitations,
type: 'column',
color: '#68CFE8',
yAxis: 1,
groupPadding: 0,
pointPadding: 0,
borderWidth: 0,
shadow: false,
dataLabels: {
enabled: true,
formatter: function () {
if (this.y > 0) {
return this.y;
}
},
style: {
fontSize: '8px'
}
},
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
}]
}
};
/**
* Post-process the chart from the callback function, the second argument to Highcharts.Chart.
*/
Meteogram.prototype.onChartLoad = function (chart) {
this.drawWeatherSymbols(chart);
this.drawWindArrows(chart);
this.drawBlocksForWindArrows(chart);
};
/**
* Create the chart. This function is called async when the data file is loaded and parsed.
*/
Meteogram.prototype.createChart = function () {
var meteogram = this;
this.chart = new Highcharts.Chart(this.getChartOptions(), function (chart) {
meteogram.onChartLoad(chart);
});
};
/**
* Handle the data. This part of the code is not Highcharts specific, but deals with yr.no's
* specific data format
*/
Meteogram.prototype.parseYrData = function () {
var meteogram = this,
xml = this.xml,
pointStart;
if (!xml || !xml.forecast) {
$('#loading').html('<i class="fa fa-frown-o"></i> Failed loading data, please try again later');
return;
}
// The returned xml variable is a JavaScript representation of the provided XML,
// generated on the server by running PHP simple_load_xml and converting it to
// JavaScript by json_encode.
$.each(xml.forecast.tabular.time, function (i, time) {
// Get the times - only Safari can't parse ISO8601 so we need to do some replacements
var from = time['@attributes'].from + ' UTC',
to = time['@attributes'].to + ' UTC';
from = from.replace(/-/g, '/').replace('T', ' ');
from = Date.parse(from);
to = to.replace(/-/g, '/').replace('T', ' ');
to = Date.parse(to);
if (to > pointStart + 4 * 24 * 36e5) {
return;
}
// If it is more than an hour between points, show all symbols
if (i === 0) {
meteogram.resolution = to - from;
}
// Populate the parallel arrays
meteogram.symbols.push(time.symbol['@attributes']['var'].match(/[0-9]{2}[dnm]?/)[0]);
meteogram.symbolNames.push(time.symbol['@attributes'].name);
meteogram.temperatures.push({
x: from,
y: parseInt(time.temperature['@attributes'].value),
// custom options used in the tooltip formatter
to: to,
index: i
});
meteogram.precipitations.push({
x: from,
y: parseFloat(time.precipitation['@attributes'].value)
});
meteogram.windDirections.push(parseFloat(time.windDirection['@attributes'].deg));
meteogram.windDirectionNames.push(time.windDirection['@attributes'].name);
meteogram.windSpeeds.push(parseFloat(time.windSpeed['@attributes'].mps));
meteogram.windSpeedNames.push(time.windSpeed['@attributes'].name);
meteogram.pressures.push({
x: from,
y: parseFloat(time.pressure['@attributes'].value)
});
if (i == 0) {
pointStart = (from + to) / 2;
}
});
// Smooth the line
this.smoothLine(this.temperatures);
// Create the chart when the data is loaded
this.createChart();
};
// End of the Meteogram protype
$(function () { // On DOM ready...
// Set the hash to the yr.no URL we want to parse
if (!location.hash) {
var place = 'United_Kingdom/England/London';
//place = 'France/Rhône-Alpes/Val_d\'Isère~2971074';
//place = 'Norway/Sogn_og_Fjordane/Vik/Målset';
//place = 'United_States/California/San_Francisco';
//place = 'United_States/Minnesota/Minneapolis';
location.hash = 'http://www.yr.no/place/' + place + '/forecast_hour_by_hour.xml';
}
// Then get the XML file through Highcharts' jsonp provider, see
// https://github.com/highslide-software/highcharts.com/blob/master/samples/data/jsonp.php
// for source code.
$.getJSON(
'http://www.highcharts.com/samples/data/jsonp.php?url=' + location.hash.substr(1) + '&callback=?',
function (xml) {
var meteogram = new Meteogram(xml, 'container');
}
);
});
</script>
</head>
<body>
<script src="../../js/highcharts.js"></script>
<script src="../../js/modules/exporting.js"></script>
<link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<div id="container" style="width: 800px; height: 310px; margin: 0 auto">
<div style="margin-top: 100px; text-align: center" id="loading">
<i class="fa fa-spinner fa-spin"></i> Loading data from external source
</div>
</div>
<!--
<div style="width: 800px; margin: 0 auto">
<a href="#http://www.yr.no/place/United_Kingdom/England/London/forecast_hour_by_hour.xml">London</a>,
<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>,
<a href="#http://www.yr.no/place/United_States/California/San_Francisco/forecast_hour_by_hour.xml">San Francisco</a>,
<a href="#http://www.yr.no/place/Norway/Vik/Vikafjell/forecast_hour_by_hour.xml">Vikjafjellet</a>
</div>
-->
</body>
</html>