blob: 3cd03913fc603301b41c3271de6dc58a6d6b3421 [file] [log] [blame]
// polyfill indexOf for IE8
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(elt /*, from*/) {
var len = this.length >>> 0;
var from = Number(arguments[1]) || 0;
from = (from < 0)
? Math.ceil(from)
: Math.floor(from);
if (from < 0)
from += len;
for (; from < len; from++) {
if (from in this &&
this[from] === elt)
return from;
}
return -1;
};
}
HTMLWidgets.widget({
name: "dygraphs",
type: "output",
factory: function(el, width, height) {
// reference to dygraph
var dygraph = null;
// reference to widget global groups
var groups = this.groups;
// add qt style if we are running under Qt
if (window.navigator.userAgent.indexOf(" Qt/") > 0)
el.className += " qt";
return {
renderValue: function(x) {
// reference to this for closures
var thiz = this;
// get dygraph attrs and populate file field
var attrs = x.attrs;
attrs.file = x.data;
// disable zoom interaction except for clicks
if (attrs.disableZoom) {
attrs.interactionModel = Dygraph.Interaction.nonInteractiveModel_;
}
// convert non-arrays to arrays
for (var index = 0; index < attrs.file.length; index++) {
if (!$.isArray(attrs.file[index]))
attrs.file[index] = [].concat(attrs.file[index]);
}
// resolve "auto" legend behavior
if (x.attrs.legend == "auto") {
if (x.data.length <= 2)
x.attrs.legend = "onmouseover";
else
x.attrs.legend = "always";
}
if (x.format == "date") {
// set appropriated function in case of fixed tz
if ((attrs.axes.x.axisLabelFormatter === undefined) && x.fixedtz)
attrs.axes.x.axisLabelFormatter = this.xAxisLabelFormatterFixedTZ(x.tzone);
if ((attrs.axes.x.valueFormatter === undefined) && x.fixedtz)
attrs.axes.x.valueFormatter = this.xValueFormatterFixedTZ(x.scale, x.tzone);
if ((attrs.axes.x.ticker === undefined) && x.fixedtz)
attrs.axes.x.ticker = this.customDateTickerFixedTZ(x.tzone);
// provide an automatic x value formatter if none is already specified
if ((attrs.axes.x.valueFormatter === undefined) && (x.fixedtz != true))
attrs.axes.x.valueFormatter = this.xValueFormatter(x.scale);
// convert time to js time
attrs.file[0] = attrs.file[0].map(function(value) {
return thiz.normalizeDateValue(x.scale, value, x.fixedtz);
});
if (attrs.dateWindow != null) {
attrs.dateWindow = attrs.dateWindow.map(function(value) {
var date = thiz.normalizeDateValue(x.scale, value, x.fixedtz);
return date.getTime();
});
}
}
// transpose array
attrs.file = HTMLWidgets.transposeArray2D(attrs.file);
// add drawCallback for group
if (x.group != null)
this.addGroupDrawCallback(x);
// add shading and event callback if necessary
this.addShadingCallback(x);
this.addEventCallback(x);
this.addZoomCallback(x);
// disable y-axis touch events on mobile phones
if (attrs.mobileDisableYTouch !== false && this.isMobilePhone()) {
// create default interaction model if necessary
if (!attrs.interactionModel)
attrs.interactionModel = Dygraph.Interaction.defaultModel;
// disable y touch direction
attrs.interactionModel.touchstart = function(event, dygraph, context) {
Dygraph.defaultInteractionModel.touchstart(event, dygraph, context);
context.touchDirections = { x: true, y: false };
};
}
// create plugins
if (x.plugins) {
attrs.plugins = [];
for (var plugin in x.plugins) {
if (x.plugins.hasOwnProperty(plugin)) {
// get plugin options
var options = x.plugins[plugin];
// create plugin and add to dygraph
var p = new Dygraph.Plugins[plugin](options);
attrs.plugins.push(p);
}
}
}
// custom plotter
if (x.plotter) {
attrs.plotter = Dygraph.Plotters[x.plotter];
}
// custom data handler
if (x.dataHandler) {
attrs.dataHandler = Dygraph.DataHandlers[x.dataHandler];
}
// custom circles
if (x.pointShape) {
if (typeof x.pointShape === 'string') {
attrs.drawPointCallback = Dygraph.Circles[x.pointShape.toUpperCase()];
attrs.drawHighlightPointCallback = Dygraph.Circles[x.pointShape.toUpperCase()];
} else {
for (var s in x.pointShape) {
if (x.pointShape.hasOwnProperty(s)) {
attrs.series[s].drawPointCallback = Dygraph.Circles[x.pointShape[s].toUpperCase()];
attrs.series[s].drawHighlightPointCallback = Dygraph.Circles[x.pointShape[s].toUpperCase()];
}
}
}
}
// if there is no existing dygraph perform initialization
if (!dygraph) {
// subscribe to custom shown event (fired by ioslides to trigger
// shiny reactivity but we can use it as well). this is necessary
// because if a dygraph starts out as display:none it has height
// and width == 0 and this doesn't change when it becomes visible
$(el).closest('slide').on('shown', function() {
if (dygraph)
dygraph.resize();
});
// do the same for reveal.js
$(el).closest('section.slide').on('shown', function() {
if (dygraph)
dygraph.resize();
});
// redraw on R Markdown {.tabset} tab visibility changed
var tab = $(el).closest('div.tab-pane');
if (tab !== null) {
var tabID = tab.attr('id');
var tabAnchor = $('a[data-toggle="tab"][href="#' + tabID + '"]');
if (tabAnchor !== null) {
tabAnchor.on('shown.bs.tab', function() {
if (dygraph)
dygraph.resize();
});
}
}
// add default font for viewer mode
if (this.queryVar("viewer_pane") === "1")
document.body.style.fontFamily = "Arial, sans-serif";
// inject css if necessary
if (x.css != null) {
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet)
style.styleSheet.cssText = x.css;
else
style.appendChild(document.createTextNode(x.css));
document.getElementsByTagName("head")[0].appendChild(style);
}
} else {
// retain the userDateWindow if requested
if (dygraph.userDateWindow != null
&& attrs.retainDateWindow == true) {
attrs.dateWindow = dygraph.xAxisRange();
}
// remove it from groups if it's there
if (x.group != null && groups[x.group] != null) {
var index = groups[x.group].indexOf(dygraph);
if (index != -1)
groups[x.group].splice(index, 1);
}
// destroy the existing dygraph
dygraph.destroy();
dygraph = null;
}
// create the dygraph and add it to it's group (if any)
dygraph = thiz.dygraph = new Dygraph(el, attrs.file, attrs);
dygraph.userDateWindow = attrs.dateWindow;
if (x.group != null)
groups[x.group].push(dygraph);
// add shiny inputs for date window and click
if (HTMLWidgets.shinyMode) {
var isDate = x.format == "date";
this.addClickShinyInput(el.id, isDate);
this.addDateWindowShinyInput(el.id, isDate);
}
// set annotations
if (x.annotations != null) {
dygraph.ready(function() {
if (x.format == "date") {
x.annotations.map(function(annotation) {
var date = thiz.normalizeDateValue(x.scale, annotation.x, x.fixedtz);
annotation.x = date.getTime();
});
}
dygraph.setAnnotations(x.annotations);
});
}
},
customDateTickerFixedTZ : function(tz){
return function(t,e,a,i,r) {
var a=Dygraph.pickDateTickGranularity(t,e,a,i);
if(a >= 0){
var n=i("axisLabelFormatter"),
o=i("labelsUTC"),
s=o?Dygraph.DateAccessorsUTC:Dygraph.DateAccessorsLocal;
l=Dygraph.TICK_PLACEMENT[a].datefield;
h=Dygraph.TICK_PLACEMENT[a].step;
p=Dygraph.TICK_PLACEMENT[a].spacing;
var y = [];
var d = moment(t);
d.tz(tz);
d.millisecond(0);
if(l > Dygraph.DATEFIELD_M){
var x;
if (l === Dygraph.DATEFIELD_SS) { // seconds
x = d.second();
d.second(x - x % h);
} else if(l === Dygraph.DATEFIELD_MM){
d.second(0)
x = d.minute();
d.minute(x - x % h);
} else if(l === Dygraph.DATEFIELD_HH){
d.second(0);
d.minute(0);
x = d.hour();
d.hour(x - x % h);
} else if(l === Dygraph.DATEFIELD_D){
d.second(0);
d.minute(0);
d.hour(0);
if (h == 7) { // one week
d.startOf('week');
}
}
v = d.valueOf();
_=moment(v).tz(tz);
// For spacings coarser than two-hourly, we want to ignore daylight
// savings transitions to get consistent ticks. For finer-grained ticks,
// it's essential to show the DST transition in all its messiness.
var start_offset_min = moment(v).tz(tz).zone();
var check_dst = (p >= Dygraph.TICK_PLACEMENT[Dygraph.TWO_HOURLY].spacing);
if(a<=Dygraph.HOURLY){
for(t>v&&(v+=p,_=moment(v).tz(tz));e>=v;){
y.push({v:v,label:n(_,a,i,r)});
v+=p;
_=moment(v).tz(tz);
}
}else{
for(t>v&&(v+=p,_=moment(v).tz(tz));e>=v;){
// This ensures that we stay on the same hourly "rhythm" across
// daylight savings transitions. Without this, the ticks could get off
// by an hour. See tests/daylight-savings.html or issue 147.
if (check_dst && _.zone() != start_offset_min) {
var delta_min = _.zone() - start_offset_min;
v += delta_min * 60 * 1000;
_= moment(v).tz(tz);
start_offset_min = _.zone();
// Check whether we've backed into the previous timezone again.
// This can happen during a "spring forward" transition. In this case,
// it's best to skip this tick altogether (we may be shooting for a
// non-existent time like the 2AM that's skipped) and go to the next
// one.
if (moment(v + p).tz(tz).zone() != start_offset_min) {
v += p;
_= moment(v).tz(tz);
start_offset_min = _.zone();
}
}
(a>=Dygraph.DAILY||_.get('hour')%h===0)&&y.push({v:v,label:n(_,a,i,r)});
v+=p;
_=moment(v).tz(tz);
}
}
}else{
var start_year = moment(t).tz(tz).year();
var end_year = moment(e).tz(tz).year();
var start_month = moment(t).tz(tz).month();
if(l === Dygraph.DATEFIELD_M){
var step_month = h;
for (var ii = start_year; ii <= end_year; ii++) {
for (var j = 0; j < 12;) {
var dt = moment(new Date(ii, j, 1)).tz(tz);
// fix some tz bug
dt.year(ii);
dt.month(j);
dt.date(1);
dt.hour(0);
v = dt.valueOf();
y.push({v:v,label:n(moment(v).tz(tz),a,i,r)});
j+=step_month;
}
}
}else{
var step_year = h;
for (var ii = start_year; ii <= end_year;) {
var dt = moment(new Date(ii, 1, 1)).tz(tz);
// fix some tz bug
dt.year(ii);
dt.month(j);
dt.date(1);
dt.hour(0);
v = dt.valueOf();
y.push({v:v,label:n(moment(v).tz(tz),a,i,r)});
ii+=step_year;
}
}
}
return y;
}else{
return [];
}
};
},
xAxisLabelFormatterFixedTZ : function(tz){
return function dateAxisFormatter(date, granularity){
var mmnt = moment(date).tz(tz);
if (granularity >= Dygraph.DECADAL){
return mmnt.format('YYYY');
}else{
if(granularity >= Dygraph.MONTHLY){
return mmnt.format('MMM YYYY');
}else{
var frac = mmnt.hour() * 3600 + mmnt.minute() * 60 + mmnt.second() + mmnt.millisecond();
if (frac === 0 || granularity >= Dygraph.DAILY) {
return mmnt.format('DD MMM');
} else {
if (mmnt.second()) {
return mmnt.format('HH:mm:ss');
} else {
return mmnt.format('HH:mm');
}
}
}
}
}
},
xValueFormatterFixedTZ: function(scale, tz) {
return function(millis) {
var mmnt = moment(millis).tz(tz);
if (scale == "yearly")
return mmnt.format('YYYY') + ' (' + mmnt.zoneAbbr() + ')';
else if (scale == "quarterly")
return mmnt.fquarter(1) + ' (' + mmnt.zoneAbbr() + ')';
else if (scale == "monthly")
return mmnt.format('MMM, YYYY')+ ' (' + mmnt.zoneAbbr() + ')';
else if (scale == "daily" || scale == "weekly")
return mmnt.format('MMM, DD, YYYY')+ ' (' + mmnt.zoneAbbr() + ')';
else
return mmnt.format('dddd, MMMM DD, YYYY HH:mm:ss')+ ' (' + mmnt.zoneAbbr() + ')';
}
},
xValueFormatter: function(scale) {
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return function(millis) {
var date = new Date(millis);
if (scale == "yearly")
return date.getFullYear();
else if (scale == "quarterly")
return moment(millis).fquarter(1);
else if (scale == "monthly")
return monthNames[date.getMonth()] + ', ' + date.getFullYear();
else if (scale == "daily" || scale == "weekly")
return monthNames[date.getMonth()] + ', ' +
date.getDate() + ', ' +
date.getFullYear();
else
return date.toLocaleString();
}
},
addZoomCallback: function(x) {
// alias this
var thiz = this;
// get attrs
var attrs = x.attrs;
// check for an existing zoomCallback
var prevZoomCallback = attrs["zoomCallback"];
attrs.zoomCallback = function(minDate, maxDate, yRanges) {
// call existing
if (prevZoomCallback)
prevZoomCallback(minDate, maxDate, yRanges);
// record user date window (or lack thereof)
if (dygraph.xAxisExtremes()[0] != minDate ||
dygraph.xAxisExtremes()[1] != maxDate) {
dygraph.userDateWindow = [minDate, maxDate];
} else {
dygraph.userDateWindow = null;
}
// record in group if necessary
if (x.group != null && groups[x.group] != null) {
var group = groups[x.group];
for(var i = 0; i<group.length; i++)
group[i].userDateWindow = dygraph.userDateWindow;
}
};
},
addGroupDrawCallback: function(x) {
// get attrs
var attrs = x.attrs;
// check for an existing drawCallback
var prevDrawCallback = attrs["drawCallback"];
groups[x.group] = groups[x.group] || [];
var group = groups[x.group];
var blockRedraw = false;
attrs.drawCallback = function(me, initial) {
// call existing
if (prevDrawCallback)
prevDrawCallback(me, initial);
// sync peers in group
if (blockRedraw || initial) return;
blockRedraw = true;
var range = dygraph.xAxisRange();
for (var j = 0; j < group.length; j++) {
if (group[j] == me) continue;
// update group range only if it's different (prevents
// infinite recursion in updateOptions)
var peerRange = group[j].xAxisRange();
if (peerRange[0] != range[0] || peerRange[1] != range[1]) {
group[j].updateOptions({
dateWindow: range
});
}
}
blockRedraw = false;
};
},
addShadingCallback: function(x) {
// bail if no shadings
if (x.shadings.length == 0)
return;
// alias this
var thiz = this;
// get attrs
var attrs = x.attrs;
// check for an existing underlayCallback
var prevUnderlayCallback = attrs["underlayCallback"];
// install callback
attrs.underlayCallback = function(canvas, area, g) {
// call existing
if (prevUnderlayCallback)
prevUnderlayCallback(canvas, area, g);
for (var i = 0; i < x.shadings.length; i++) {
var shading = x.shadings[i];
canvas.save();
canvas.fillStyle = shading.color;
if (shading.axis == "x") {
var x1 = shading.from;
var x2 = shading.to;
if (x.format == "date") {
x1 = thiz.normalizeDateValue(x.scale, x1, x.fixedtz).getTime();
x2 = thiz.normalizeDateValue(x.scale, x2, x.fixedtz).getTime();
}
var left = g.toDomXCoord(x1);
var right = g.toDomXCoord(x2);
canvas.fillRect(left, area.y, right - left, area.h);
} else if (shading.axis == "y") {
var bottom = g.toDomYCoord(shading.from);
var top = g.toDomYCoord(shading.to);
canvas.fillRect(area.x, bottom, area.w, top - bottom);
}
canvas.restore();
}
};
},
addEventCallback: function(x) {
// bail if no evets
if (x.events.length == 0)
return;
// alias this
var thiz = this;
// get attrs
var attrs = x.attrs;
// check for an existing underlayCallback
var prevUnderlayCallback = attrs["underlayCallback"];
// install callback
attrs.underlayCallback = function(canvas, area, g) {
// call existing
if (prevUnderlayCallback)
prevUnderlayCallback(canvas, area, g);
for (var i = 0; i < x.events.length; i++) {
// get event and x-coordinate
var event = x.events[i];
// draw line
canvas.save();
canvas.strokeStyle = event.color;
if (event.axis == "x") {
var xPos;
if (jQuery.isNumeric(event.pos)) {
xPos = g.toDomXCoord(event.pos);
} else {
xPos = thiz.normalizeDateValue(x.scale, event.pos, x.fixedtz).getTime();
xPos = g.toDomXCoord(xPos);
}
// draw line
thiz.dashedLine(canvas,
xPos,
area.y,
xPos,
area.y + area.h,
event.strokePattern);
} else if (event.axis == "y") {
yPos = g.toDomYCoord(event.pos);
thiz.dashedLine(canvas,
area.x,
yPos,
area.x + area.w,
yPos,
event.strokePattern);
}
canvas.restore();
// draw label
if (event.label != null) {
canvas.save();
thiz.setFontSize(canvas, 12);
var size = canvas.measureText(event.label);
if (event.axis == "x") {
var tx = xPos - 4;
var ty;
if (event.labelLoc == "top")
ty = area.y + size.width + 10;
else
ty = area.y + area.h - 10;
canvas.translate(tx, ty);
canvas.rotate(3 * Math.PI / 2);
canvas.translate(-tx,-ty);
} else if (event.axis == "y") {
var ty = yPos - 4;
var tx;
if (event.labelLoc == "right")
tx = area.x + area.w - size.width - 10;
else
tx = area.x + 10;
}
canvas.fillStyle = event.color;
canvas.fillText(event.label, tx, ty);
canvas.restore();
}
}
};
},
addDateWindowShinyInput: function(id, isDate) {
// check for an existing drawCallback
var prevDrawCallback = dygraph.getOption("drawCallback");
// install the callback
dygraph.updateOptions({
drawCallback: function(me, initial) {
// call existing
if (prevDrawCallback)
prevDrawCallback(me, initial);
// fire input change
var range = dygraph.xAxisRange();
if (isDate)
range = [new Date(range[0]), new Date(range[1])];
if (Shiny.onInputChange) // may note be ready yet in case of static render
Shiny.onInputChange(id + "_date_window", range);
}
});
},
addClickShinyInput: function(id, isDate) {
var prevClickCallback = dygraph.getOption("clickCallback")
dygraph.updateOptions({
clickCallback: function(e, x, points) {
// call existing
if (prevClickCallback)
prevClickCallback(e, x, points);
// fire input change
if (Shiny.onInputChange) { // may note be ready yet in case of static render
Shiny.onInputChange(el.id + "_click", {
x: isDate ? new Date(x) : x,
x_closest_point: isDate ? new Date(points[0].xval) : points[0].xval,
y_closest_point: points[0].yval,
series_name: points[0].name,
'.nonce': Math.random() // Force reactivity if click hasn't changed
});
}
}
});
},
// Add dashed line support to canvas rendering context
// See: http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
dashedLine: function(canvas, x, y, x2, y2, dashArray) {
canvas.beginPath();
if (!dashArray) dashArray=[10,5];
if (dashLength==0) dashLength = 0.001; // Hack for Safari
var dashCount = dashArray.length;
canvas.moveTo(x, y);
var dx = (x2-x), dy = (y2-y);
var slope = dx ? dy/dx : 1e15;
var distRemaining = Math.sqrt( dx*dx + dy*dy );
var dashIndex=0, draw=true;
while (distRemaining>=0.1){
var dashLength = dashArray[dashIndex++%dashCount];
if (dashLength > distRemaining) dashLength = distRemaining;
var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
if (dx<0) xStep = -xStep;
x += xStep
y += slope*xStep;
canvas[draw ? 'lineTo' : 'moveTo'](x,y);
distRemaining -= dashLength;
draw = !draw;
}
canvas.stroke();
},
setFontSize: function(canvas, size) {
var cFont = canvas.font;
var parts = cFont.split(' ');
if (parts.length === 2)
canvas.font = size + 'px ' + parts[1];
else if (parts.length === 3)
canvas.font = parts[0] + ' ' + size + 'px ' + parts[2];
},
// Returns the value of a GET variable
queryVar: function(name) {
return decodeURI(window.location.search.replace(
new RegExp("^(?:.*[&\\?]" +
encodeURI(name).replace(/[\.\+\*]/g, "\\$&") +
"(?:\\=([^&]*))?)?.*$", "i"),
"$1"));
},
// We deal exclusively in UTC dates within R, however dygraphs deals
// exclusively in the local time zone. Therefore, in order to plot date
// labels that make sense to the user when we are dealing with days,
// months or years we need to convert the UTC date value to a local time
// value that "looks like" the equivilant UTC value. To do this we add the
// timezone offset to the UTC date.
// Don't use in case of fixedtz
normalizeDateValue: function(scale, value, fixedtz) {
var date = new Date(value);
if (scale != "minute" && scale != "hourly" && scale != "seconds" && !fixedtz) {
var localAsUTC = date.getTime() + (date.getTimezoneOffset() * 60000);
date = new Date(localAsUTC);
}
return date;
},
// safely detect rendering on a mobile phone
isMobilePhone: function() {
try
{
return ! window.matchMedia("only screen and (min-width: 768px)").matches;
}
catch(e) {
return false;
}
},
resize: function(width, height) {
if (dygraph)
dygraph.resize();
},
// export dygraph so other code can get a hold of it
dygraph: null
};
},
// track groups globally
groups: {}
});