From 8df145734b9f73c2bf63b8da8a8b67bd83651f80 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Tue, 28 Feb 2017 01:43:18 +0300 Subject: [PATCH 01/13] Adding plugin helper functions --- NAMESPACE | 4 ++++ R/dependency.R | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 13fc721..ceb7e4f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ export("%>%") export(as.yearmon) export(as.yearqtr) export(dyAnnotation) +export(dyArrow) export(dyAxis) export(dyCSS) export(dyCallbacks) @@ -12,6 +13,8 @@ export(dyCrosshair) export(dyDataHandler) export(dyDependency) export(dyEvent) +export(dyGetPluginOptions) +export(dyHasPlugin) export(dyHighlight) export(dyLegend) export(dyLimit) @@ -24,6 +27,7 @@ export(dyRibbon) export(dyRoller) export(dySeries) export(dySeriesData) +export(dySetPluginOptions) export(dyShading) export(dyUnzoom) export(dygraph) diff --git a/R/dependency.R b/R/dependency.R index a2c34ae..0822111 100644 --- a/R/dependency.R +++ b/R/dependency.R @@ -62,6 +62,58 @@ dyPlugin <- function(dygraph, name, path, options = list(), version = "1.0") { dygraph } +#' Check if plugin was included +#' +#' @param dygraph Dygraph to check plugin existance in +#' @param name Name of plugin +#' +#' @return TRUE if plugin was included in dygraph, FALSE otherwise. +#' +#' @export +dyHasPlugin <- function(dygraph, name) { + name %in% names(dygraph$x$plugins) +} + +#' Get options of included plugin +#' +#' @param dygraph Dygraph with plugin to get options of +#' @param name Name of plugin +#' +#' @return A list with plugin options. +#' +#' @export +dyGetPluginOptions <- function(dygraph, name) { + if (!dyHasPlugin(dygraph, name)) { + stop(paste0("Can't get options of not loaded plugin: ", name)) + } + + # return options list + dygraph$x$plugins[[name]] +} + +#' Set options to included plugin +#' +#' @param dygraph Dygraph with plugin to set options to +#' @param name Name of plugin +#' @param options New options to set +#' +#' @return A dygraph with plugin with updated options. +#' +#' @export +dySetPluginOptions <- function(dygraph, name, options = list()) { + if (!dyHasPlugin(dygraph, name)) { + stop(paste0("Can't set options for not loaded plugin: ", name)) + } + + if (length(options) == 0) { + options <- JS("{}") + } + dygraph$x$plugins[[name]] <- options + + # return dygraph + dygraph +} + #' Include a dygraph plotter #' #' @param dygraph Dygraph to add plotter to From 903fb5b6c967600f638a54c86494e915cfd94291 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Tue, 28 Feb 2017 01:44:00 +0300 Subject: [PATCH 02/13] Initial commit --- R/arrow.R | 101 ++++++++++++++ docs/gallery-arrow.Rmd | 41 ++++++ inst/plugins/arrow.js | 283 ++++++++++++++++++++++++++++++++++++++ man/dyArrow.Rd | 49 +++++++ man/dyGetPluginOptions.Rd | 20 +++ man/dyHasPlugin.Rd | 20 +++ man/dySetPluginOptions.Rd | 22 +++ 7 files changed, 536 insertions(+) create mode 100644 R/arrow.R create mode 100644 docs/gallery-arrow.Rmd create mode 100644 inst/plugins/arrow.js create mode 100644 man/dyArrow.Rd create mode 100644 man/dyGetPluginOptions.Rd create mode 100644 man/dyHasPlugin.Rd create mode 100644 man/dySetPluginOptions.Rd diff --git a/R/arrow.R b/R/arrow.R new file mode 100644 index 0000000..1a44a70 --- /dev/null +++ b/R/arrow.R @@ -0,0 +1,101 @@ +#' dygraph trade arrow +#' +#' Add an arrow to a dygraph +#' +#' @param dygraph Dygraph to add arrow to +#' @param seriesName Name of series within data set. +#' @param x Either numeric or date/time for the event, depending on the format of the +#' x-axis of the dygraph. (For date/time must be a \code{POSIXct} object or another +#' object convertible to \code{POSIXct} via \code{as.POSIXct}) +#' @param label Label for event. Defaults to blank +#' @param direction Direction of arrow (up or down) +#' @param fillColor Fill color of arrow. This can be of the form "#AABBCC" or +#' "rgb(255,100,200)" or "yellow". Defaults to white. +#' @param strokeColor Stroke color of arrow. This can be of the form "#AABBCC" or +#' "rgb(255,100,200)" or "yellow". Defaults to black. +#' +#' @return A dygraph with the specified trade arrow. +#' +#' @examples +#' library(dygraphs) +#' dygraph(presidents, main = "Quarterly Presidential Approval Ratings") %>% +#' dyArrow("1950-6-30", "Korea", direction = "down", fillColor = "red") %>% +#' dyArrow("1965-2-09", "Vietnam", direction = "down", fillColor = "red") +#' +#' dygraph(presidents, main = "Quarterly Presidential Approval Ratings") %>% +#' dyArrow(c("1950-6-30", "1965-2-09"), +#' c("Korea", "Vietnam"), +#' direction = "down", +#' fillColor = "red") +#' +#' @export +dyArrow <- function(dygraph, + x, + text = NULL, + tooltip = NULL, + direction = c("up", "down", "left", "right", "ne", "se", "sw", "nw"), + fillColor = "white", + strokeColor = "black", + series = NULL) { + + # create arrows + if (!is.null(text) && length(x) != length(text)) { + stop("Length of 'x' and 'text' does not match") + } + if (!is.null(tooltip) && length(x) != length(tooltip)) { + stop("Length of 'x' and 'tooltip' does not match") + } + + # validate series if specified + if (!is.null(series) && ! series %in% dygraph$x$attrs$labels) { + stop("The specified series was not found. Valid series names are: ", + paste(dygraph$x$attrs$labels[-1], collapse = ", ")) + } + + # default the series if necessary + if (is.null(series)) + series <- dygraph$x$attrs$labels[length(dygraph$x$attrs$labels)] + + fixedtz <- ifelse(is.null(dygraph$x$fixedtz), FALSE, dygraph$x$fixedtz) + scale <- dygraph$x$scale + + direction <- match.arg(direction) + arrows <- lapply(seq_along(x), + function(i) { + if (is.null(text)) + arrowText <- NULL + else + arrowText <- text[i] + if (is.null(tooltip)) + arrowTooltip <- NULL + else + arrowTooltip <- tooltip[i] + list(xval = ifelse(dygraph$x$format == "date", + asISO8601Time(x[i]), x[i]), + text = arrowText, + tooltip = arrowTooltip, + direction = direction, + fillColor = fillColor, + strokeColor = strokeColor, + series = series, + fixedtz = fixedtz, + scale = scale) + }) + + pluginName <- "Arrow" + if (dyHasPlugin(dygraph, pluginName)) { + dygraph <- dySetPluginOptions(dygraph, + pluginName, + append(dyGetPluginOptions(dygraph, pluginName), + arrows)) + } else { + dygraph <- dyPlugin(dygraph = dygraph, + name = pluginName, + path = system.file("plugins/arrow.js", + package = "dygraphs"), + options = arrows) + } + + # return dygraph + dygraph +} diff --git a/docs/gallery-arrow.Rmd b/docs/gallery-arrow.Rmd new file mode 100644 index 0000000..4c5688b --- /dev/null +++ b/docs/gallery-arrow.Rmd @@ -0,0 +1,41 @@ +--- +title: (Trade) Arrows with Optional Annotations +--- + +```{r setup, include=FALSE} +library(dygraphs) +``` + +```{r} +dygraph(presidents, main = "Quarterly Presidential Approval Ratings") %>% + dyArrow("1950-7-1", + "Korea", + direction = "se", + fillColor = "#d9534f") %>% + dyArrow("1965-1-1", + "Vietnam", + direction = "sw", + fillColor = "#5cb85c") +``` + +```{r} +library(quantmod) + +getSymbols("SPY", from = "2016-12-01", auto.assign=TRUE) + +difference <- SPY[, "SPY.Open"] - SPY[, "SPY.Close"] +decreasing <- which(difference < 0) +increasing <- which(difference > 0) + +dyData <- SPY[, "SPY.Close"] + +dygraph(dyData) %>% + dyArrow(index(dyData[increasing]), + as.vector(dyData[increasing]), + direction = "up", + fillColor = "green") %>% + dyArrow(index(dyData[decreasing]), + as.vector(dyData[decreasing]), + direction = "down", + fillColor = "red") +``` diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js new file mode 100644 index 0000000..a5fac32 --- /dev/null +++ b/inst/plugins/arrow.js @@ -0,0 +1,283 @@ +/** + * (Trade) Arrows Dygraph Plugin. + */ +Dygraph.Plugins.Arrow = (function() { + 'use strict'; + + var arrow = function(data) { + this.arrowPoints = []; // Dygraph points with arrows + this.arrows = []; // Arrows DOM elements attached to chart + this.data = data; // Raw arrow data + this.dygraph = null; // Dygraph reference + }; + + var isNumeric = function(v) { + var obj = {}; + return (typeof v === 'number' || + obj.toString.call(v) === '[object Number]') && !isNaN(v); + }; + + var 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; + }; + + arrow.prototype.toString = function() { + return 'Arrow Plugin'; + }; + + arrow.prototype.activate = function(g) { + this.dygraph = g; + + return { + didDrawChart: this.didDrawChart, + clearChart: this.clearChart + }; + }; + + arrow.prototype.didDrawChart = function() { + // Early out in the (common) case of no data. + if (this.data.length === 0) return; + + this.attachArrowsToChart(); + + var canvasx; + for (var i = 0; i < this.data.length; i++) { + var item = this.data[i]; + if (isNumeric(item.xval)) { + canvasx = this.dygraph.toDomXCoord(item.xval); + } else { + canvasx = normalizeDateValue(item.scale, item.xval, item.fixedtz) + .getTime(); + canvasx = this.dygraph.toDomXCoord(canvasx); + } + console.log(canvasx); + } + }; + + arrow.prototype.clearChart = function() { + // Early out in the (common) case of zero arrows. + if (this.arrows.length === 0) return; + + this.detachArrows(); + }; + + arrow.prototype.attachArrowsToChart = function() { + this.evaluateArrows(); + + for (var i = 0; i < this.arrowPoints.length; i++) { + var point = this.arrowPoints[i]; + if (this.pointInvisible(point)) continue; + + this.attachArrow(point); + } + }; + + arrow.prototype.evaluateArrows = function() { + var arrows = {}; + + for (var i = 0; i < this.data.length; i++) { + var a = this.data[i]; + var xval = isNumeric(a.xval) ? + a.xval : normalizeDateValue(a.scale, a.xval, a.fixedtz).getTime(); + arrows[xval + ',' + a.series] = a; + } + console.log(arrows); + + this.arrowPoints = []; + + for (var i = 0; i < this.dygraph.layout_.points.length; i++) { + var points = this.dygraph.layout_.points[i]; + for (var j = 0; j < points.length; j++) { + var p = points[j]; + var k = p.xval + ',' + p.name; + console.log(k); + if (k in arrows) { + p.arrow = arrows[k]; + this.arrowPoints.push(p); + } + } + } + }; + + arrow.prototype.pointInvisible = function(point) { + var area = this.dygraph.plotter_.area; + + if (point.canvasx < area.x || point.canvasx > area.x + area.w || + point.canvasy < area.y || point.canvasy > area.y + area.h) { + return true; + } + return false; + }; + + arrow.prototype.attachArrow = function(point) { + var tickHeight = 10; + + var canvas = this.makeCanvas(point.arrow, tickHeight); + var position = + this.calcPosition( + canvas, + point.canvasx, + point.canvasy, + tickHeight); + this.setCanvasPosition(canvas, position); + + var div = this.makeDiv(canvas, 'test'); + this.setDivPosition(div, position); + + this.dygraph.graphDiv.appendChild(canvas); + this.dygraph.graphDiv.appendChild(div); + this.arrows.push(canvas); + this.arrows.push(div); + }; + + arrow.prototype.makeCanvas = function(arrow, tickHeight) { + var size = 11; + var width = (size + tickHeight) * 4; + var height = width; + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + canvas.width = width; + canvas.height = height; + canvas.style.width = width + 'px'; // for IE + canvas.style.height = height + 'px'; // for IE + canvas.style.position = 'absolute'; + canvas.style.pointerEvents = 'none'; + + var cx = width / 2; + var cy = width / 2; + this.rotateCanvas(ctx, arrow.direction, tickHeight); + ctx.translate(-cx, -cy + tickHeight); + + this.shape(ctx, size, arrow.strokeColor, arrow.fillColor); + + return canvas; + }; + + arrow.prototype.calcPosition = function(canvas, x, y, tickHeight) { + return { + 'left': Math.ceil(x - canvas.width / 2), + 'top': y - canvas.height / 2 + tickHeight + }; + }; + + arrow.prototype.setCanvasPosition = function(canvas, position) { + canvas.style.left = position.left + 'px'; + canvas.style.top = position.top + 'px'; + }; + + arrow.prototype.rotateCanvas = function(ctx, direction, tickHeight) { + var directions = { + 'up': 0, + 'down': 180, + 'left': 90, + 'right': 270, + 'ne': 45, + 'se': 135, + 'sw': 225, + 'nw': 315 + }; + var rotation = directions[direction] || 0; + var cx = ctx.canvas.width / 2; + var cy = cx; + ctx.translate(cx, cy - tickHeight); + ctx.rotate(Math.PI / 180 * rotation); + }; + + arrow.prototype.getBoundingBox = function(ctx, alpha) { + alpha = alpha || 15; + + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + + var w = ctx.canvas.width; + var h = ctx.canvas.height; + + var data = ctx.getImageData(0, 0, w, h).data; + + for (var x = 0; x < w; ++x) { + for (var y = 0; y < h; ++y) { + var a = data[(w * y + x) * 4 + 3]; + + if (a > alpha) { + if (x > maxX) maxX = x; + if (x < minX) minX = x; + if (y > maxY) maxY = y; + if (y < minY) minY = y; + } + } + } + return { + x: minX, + y: minY, + w: maxX - minX, + h: maxY - minY + }; + }; + + arrow.prototype.makeDiv = function(canvas, tooltip) { + var ctx = canvas.getContext('2d'); + var bbox = this.getBoundingBox(ctx); + var div = document.createElement('div'); + + div.style.position = 'absolute'; + div.style.width = bbox.w + 'px'; + div.style.height = bbox.h + 'px'; + div.style.left = bbox.x + 'px'; + div.style.top = bbox.y + 'px'; + div.title = tooltip; + + return div; + }; + + arrow.prototype.setDivPosition = function(div, position) { + var currentLeft = parseInt(div.style.left, 10); + var currentTop = parseInt(div.style.top, 10); + div.style.left = currentLeft + position.left + 'px'; + div.style.top = currentTop + position.top + 'px'; + }; + + arrow.prototype.shape = function(ctx, size, stroke, fill) { + var cx = ctx.canvas.width / 2; + var cy = cx; + + ctx.strokeStyle = stroke; + ctx.fillStyle = fill; + ctx.lineWidth = 0.6; + + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.lineTo(cx + size / 2, cy + size); + ctx.lineTo(cx + size / 6, cy + size * 0.8); + ctx.lineTo(cx + size / 6, cy + size * 2); + ctx.lineTo(cx - size / 6, cy + size * 2); + ctx.lineTo(cx - size / 6, cy + size * 0.8); + ctx.lineTo(cx - size / 2, cy + size); + ctx.lineTo(cx, cy); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + }; + + arrow.prototype.detachArrows = function() { + for (var i = 0; i < this.arrows.length; i++) { + var a = this.arrows[i]; + if (a.parentNode) { + a.parentNode.removeChild(a); + } + this.arrows[i] = null; + } + this.arrows = []; + }; + + return arrow; +})(); diff --git a/man/dyArrow.Rd b/man/dyArrow.Rd new file mode 100644 index 0000000..38a9fdf --- /dev/null +++ b/man/dyArrow.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/arrow.R +\name{dyArrow} +\alias{dyArrow} +\title{dygraph trade arrow} +\usage{ +dyArrow(dygraph, x, text = NULL, tooltip = NULL, direction = c("up", + "down", "left", "right", "ne", "se", "sw", "nw"), fillColor = "white", + strokeColor = "black", series = NULL) +} +\arguments{ +\item{dygraph}{Dygraph to add arrow to} + +\item{x}{Either numeric or date/time for the event, depending on the format of the +x-axis of the dygraph. (For date/time must be a \code{POSIXct} object or another +object convertible to \code{POSIXct} via \code{as.POSIXct})} + +\item{direction}{Direction of arrow (up or down)} + +\item{fillColor}{Fill color of arrow. This can be of the form "#AABBCC" or +"rgb(255,100,200)" or "yellow". Defaults to white.} + +\item{strokeColor}{Stroke color of arrow. This can be of the form "#AABBCC" or +"rgb(255,100,200)" or "yellow". Defaults to black.} + +\item{seriesName}{Name of series within data set.} + +\item{label}{Label for event. Defaults to blank} +} +\value{ +A dygraph with the specified trade arrow. +} +\description{ +Add an arrow to a dygraph +} +\examples{ +library(dygraphs) +dygraph(presidents, main = "Quarterly Presidential Approval Ratings") \%>\% + dyArrow("1950-6-30", "Korea", direction = "down", fillColor = "red") \%>\% + dyArrow("1965-2-09", "Vietnam", direction = "down", fillColor = "red") + +dygraph(presidents, main = "Quarterly Presidential Approval Ratings") \%>\% + dyArrow(c("1950-6-30", "1965-2-09"), + c("Korea", "Vietnam"), + direction = "down", + fillColor = "red") + +} + diff --git a/man/dyGetPluginOptions.Rd b/man/dyGetPluginOptions.Rd new file mode 100644 index 0000000..54910f5 --- /dev/null +++ b/man/dyGetPluginOptions.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dependency.R +\name{dyGetPluginOptions} +\alias{dyGetPluginOptions} +\title{Get options of included plugin} +\usage{ +dyGetPluginOptions(dygraph, name) +} +\arguments{ +\item{dygraph}{Dygraph with plugin to get options of} + +\item{name}{Name of plugin} +} +\value{ +A list with plugin options. +} +\description{ +Get options of included plugin +} + diff --git a/man/dyHasPlugin.Rd b/man/dyHasPlugin.Rd new file mode 100644 index 0000000..c2d839d --- /dev/null +++ b/man/dyHasPlugin.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dependency.R +\name{dyHasPlugin} +\alias{dyHasPlugin} +\title{Check if plugin was included} +\usage{ +dyHasPlugin(dygraph, name) +} +\arguments{ +\item{dygraph}{Dygraph to check plugin existance in} + +\item{name}{Name of plugin} +} +\value{ +TRUE if plugin was included in dygraph, FALSE otherwise. +} +\description{ +Check if plugin was included +} + diff --git a/man/dySetPluginOptions.Rd b/man/dySetPluginOptions.Rd new file mode 100644 index 0000000..d5e83f1 --- /dev/null +++ b/man/dySetPluginOptions.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dependency.R +\name{dySetPluginOptions} +\alias{dySetPluginOptions} +\title{Set options to included plugin} +\usage{ +dySetPluginOptions(dygraph, name, options = list()) +} +\arguments{ +\item{dygraph}{Dygraph with plugin to set options to} + +\item{name}{Name of plugin} + +\item{options}{New options to set} +} +\value{ +A dygraph with plugin with updated options. +} +\description{ +Set options to included plugin +} + From 5c0106f71c93135535a5cbebf39880f4ba116f52 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Mon, 29 May 2017 00:21:31 +0300 Subject: [PATCH 03/13] Remove console.log calls --- inst/plugins/arrow.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index a5fac32..b403fc5 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -58,7 +58,6 @@ Dygraph.Plugins.Arrow = (function() { .getTime(); canvasx = this.dygraph.toDomXCoord(canvasx); } - console.log(canvasx); } }; @@ -89,7 +88,6 @@ Dygraph.Plugins.Arrow = (function() { a.xval : normalizeDateValue(a.scale, a.xval, a.fixedtz).getTime(); arrows[xval + ',' + a.series] = a; } - console.log(arrows); this.arrowPoints = []; @@ -98,7 +96,6 @@ Dygraph.Plugins.Arrow = (function() { for (var j = 0; j < points.length; j++) { var p = points[j]; var k = p.xval + ',' + p.name; - console.log(k); if (k in arrows) { p.arrow = arrows[k]; this.arrowPoints.push(p); From 4aadd3b33383808ddd591f7a7879ad43e9029e2f Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Fri, 16 Feb 2018 01:20:10 +0300 Subject: [PATCH 04/13] Remove redundant tooltip property --- R/arrow.R | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/R/arrow.R b/R/arrow.R index 1a44a70..8f2fdf8 100644 --- a/R/arrow.R +++ b/R/arrow.R @@ -32,7 +32,6 @@ dyArrow <- function(dygraph, x, text = NULL, - tooltip = NULL, direction = c("up", "down", "left", "right", "ne", "se", "sw", "nw"), fillColor = "white", strokeColor = "black", @@ -42,9 +41,6 @@ dyArrow <- function(dygraph, if (!is.null(text) && length(x) != length(text)) { stop("Length of 'x' and 'text' does not match") } - if (!is.null(tooltip) && length(x) != length(tooltip)) { - stop("Length of 'x' and 'tooltip' does not match") - } # validate series if specified if (!is.null(series) && ! series %in% dygraph$x$attrs$labels) { @@ -62,18 +58,9 @@ dyArrow <- function(dygraph, direction <- match.arg(direction) arrows <- lapply(seq_along(x), function(i) { - if (is.null(text)) - arrowText <- NULL - else - arrowText <- text[i] - if (is.null(tooltip)) - arrowTooltip <- NULL - else - arrowTooltip <- tooltip[i] list(xval = ifelse(dygraph$x$format == "date", asISO8601Time(x[i]), x[i]), - text = arrowText, - tooltip = arrowTooltip, + text = ifelse(is.null(text), NULL, text[i]), direction = direction, fillColor = fillColor, strokeColor = strokeColor, From 007a19f9447c6d6e7ed24e5c14c4ebca9ff90336 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Fri, 16 Feb 2018 01:21:12 +0300 Subject: [PATCH 05/13] Load arrow popup styles from external CSS file --- R/arrow.R | 3 +++ inst/plugins/arrow.css | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 inst/plugins/arrow.css diff --git a/R/arrow.R b/R/arrow.R index 8f2fdf8..4792e32 100644 --- a/R/arrow.R +++ b/R/arrow.R @@ -83,6 +83,9 @@ dyArrow <- function(dygraph, options = arrows) } + # add necessary CSS styles + dygraph <- dyCSS(dygraph, system.file("plugins/arrow.css", package = "dygraphs")) + # return dygraph dygraph } diff --git a/inst/plugins/arrow.css b/inst/plugins/arrow.css new file mode 100644 index 0000000..794d72c --- /dev/null +++ b/inst/plugins/arrow.css @@ -0,0 +1,12 @@ +.dygraphs__arrow-popup { + position: absolute; + z-index: 10; + border: 1px solid black; + background: white; + pointer-events: none; +} + +.dygraphs__arrow-popup--hidden { + top: -9999px; + left: -9999px; +} \ No newline at end of file From 1e3a67f81ed77ec2c05599f4c4776c360954fffa Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Fri, 16 Feb 2018 01:28:16 +0300 Subject: [PATCH 06/13] Show popup on point with arrow select --- inst/plugins/arrow.js | 112 ++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 63 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index b403fc5..bbe791e 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -9,6 +9,7 @@ Dygraph.Plugins.Arrow = (function() { this.arrows = []; // Arrows DOM elements attached to chart this.data = data; // Raw arrow data this.dygraph = null; // Dygraph reference + this.popup = null; // Popup div }; var isNumeric = function(v) { @@ -36,9 +37,16 @@ Dygraph.Plugins.Arrow = (function() { arrow.prototype.activate = function(g) { this.dygraph = g; + var div = document.createElement('div'); + div.className = 'dygraphs__arrow-popup'; + g.graphDiv.appendChild(div); + this.popup = div; + return { didDrawChart: this.didDrawChart, - clearChart: this.clearChart + clearChart: this.clearChart, + select: this.select, + deselect: this.deselect, }; }; @@ -81,8 +89,9 @@ Dygraph.Plugins.Arrow = (function() { arrow.prototype.evaluateArrows = function() { var arrows = {}; + var i; - for (var i = 0; i < this.data.length; i++) { + for (i = 0; i < this.data.length; i++) { var a = this.data[i]; var xval = isNumeric(a.xval) ? a.xval : normalizeDateValue(a.scale, a.xval, a.fixedtz).getTime(); @@ -91,7 +100,7 @@ Dygraph.Plugins.Arrow = (function() { this.arrowPoints = []; - for (var i = 0; i < this.dygraph.layout_.points.length; i++) { + for (i = 0; i < this.dygraph.layout_.points.length; i++) { var points = this.dygraph.layout_.points[i]; for (var j = 0; j < points.length; j++) { var p = points[j]; @@ -126,13 +135,8 @@ Dygraph.Plugins.Arrow = (function() { tickHeight); this.setCanvasPosition(canvas, position); - var div = this.makeDiv(canvas, 'test'); - this.setDivPosition(div, position); - this.dygraph.graphDiv.appendChild(canvas); - this.dygraph.graphDiv.appendChild(div); this.arrows.push(canvas); - this.arrows.push(div); }; arrow.prototype.makeCanvas = function(arrow, tickHeight) { @@ -188,61 +192,6 @@ Dygraph.Plugins.Arrow = (function() { ctx.rotate(Math.PI / 180 * rotation); }; - arrow.prototype.getBoundingBox = function(ctx, alpha) { - alpha = alpha || 15; - - var minX = Infinity; - var minY = Infinity; - var maxX = -Infinity; - var maxY = -Infinity; - - var w = ctx.canvas.width; - var h = ctx.canvas.height; - - var data = ctx.getImageData(0, 0, w, h).data; - - for (var x = 0; x < w; ++x) { - for (var y = 0; y < h; ++y) { - var a = data[(w * y + x) * 4 + 3]; - - if (a > alpha) { - if (x > maxX) maxX = x; - if (x < minX) minX = x; - if (y > maxY) maxY = y; - if (y < minY) minY = y; - } - } - } - return { - x: minX, - y: minY, - w: maxX - minX, - h: maxY - minY - }; - }; - - arrow.prototype.makeDiv = function(canvas, tooltip) { - var ctx = canvas.getContext('2d'); - var bbox = this.getBoundingBox(ctx); - var div = document.createElement('div'); - - div.style.position = 'absolute'; - div.style.width = bbox.w + 'px'; - div.style.height = bbox.h + 'px'; - div.style.left = bbox.x + 'px'; - div.style.top = bbox.y + 'px'; - div.title = tooltip; - - return div; - }; - - arrow.prototype.setDivPosition = function(div, position) { - var currentLeft = parseInt(div.style.left, 10); - var currentTop = parseInt(div.style.top, 10); - div.style.left = currentLeft + position.left + 'px'; - div.style.top = currentTop + position.top + 'px'; - }; - arrow.prototype.shape = function(ctx, size, stroke, fill) { var cx = ctx.canvas.width / 2; var cy = cx; @@ -276,5 +225,42 @@ Dygraph.Plugins.Arrow = (function() { this.arrows = []; }; + arrow.prototype.select = function(e) { + for (var i = 0; i < e.selectedPoints.length; i++) { + var p = e.selectedPoints[i]; + if (!p.hasOwnProperty('arrow')) { + this.hidePopup(); + continue; + } + this.showPopup(p); + } + }; + + arrow.prototype.showPopup = function(p) { + this.popup.innerText = p.arrow.text; + var area = this.dygraph.plotter_.area; + var popupWidth = this.popup.offsetWidth; + var yAxisLabelWidth = this.dygraph.getOptionForAxis('axisLabelWidth', 'y'); + var offset = 10; + var leftPopup = p.canvasx + offset; + var topPopup = p.canvasy - offset; + if ((leftPopup + popupWidth + 1) > area.w) { + leftPopup = leftPopup - 2 * offset - popupWidth - (yAxisLabelWidth - area.x); + } + this.popup.classList.remove("dygraphs__arrow-popup--hidden"); + this.popup.style.left = leftPopup + "px"; + this.popup.style.top = topPopup + "px"; + }; + + arrow.prototype.hidePopup = function() { + this.popup.style.left = ""; + this.popup.style.top = ""; + this.popup.classList.add("dygraphs__arrow-popup--hidden"); + }; + + arrow.prototype.deselect = function(e) { + this.hidePopup(); + }; + return arrow; })(); From 7da1ba63f84bc30977d2ce32f757a6525f2214e9 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Fri, 16 Feb 2018 01:28:56 +0300 Subject: [PATCH 07/13] Better data sample --- docs/gallery-arrow.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gallery-arrow.Rmd b/docs/gallery-arrow.Rmd index 4c5688b..906078f 100644 --- a/docs/gallery-arrow.Rmd +++ b/docs/gallery-arrow.Rmd @@ -21,7 +21,7 @@ dygraph(presidents, main = "Quarterly Presidential Approval Ratings") %>% ```{r} library(quantmod) -getSymbols("SPY", from = "2016-12-01", auto.assign=TRUE) +getSymbols("SPY", from = "2018-01-01", to = "2018-02-01", auto.assign=TRUE) difference <- SPY[, "SPY.Open"] - SPY[, "SPY.Close"] decreasing <- which(difference < 0) From f93ba4c29eefd75a997de708ae80fe397604cfb1 Mon Sep 17 00:00:00 2001 From: pshevtsov Date: Thu, 15 Mar 2018 00:56:59 +0300 Subject: [PATCH 08/13] Change CSS classes names --- inst/plugins/arrow.css | 6 +++--- inst/plugins/arrow.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inst/plugins/arrow.css b/inst/plugins/arrow.css index 794d72c..d653af6 100644 --- a/inst/plugins/arrow.css +++ b/inst/plugins/arrow.css @@ -1,4 +1,4 @@ -.dygraphs__arrow-popup { +.arrow-popup { position: absolute; z-index: 10; border: 1px solid black; @@ -6,7 +6,7 @@ pointer-events: none; } -.dygraphs__arrow-popup--hidden { +.arrow-popup--hidden { top: -9999px; left: -9999px; -} \ No newline at end of file +} diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index bbe791e..38393e9 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -255,7 +255,7 @@ Dygraph.Plugins.Arrow = (function() { arrow.prototype.hidePopup = function() { this.popup.style.left = ""; this.popup.style.top = ""; - this.popup.classList.add("dygraphs__arrow-popup--hidden"); + this.popup.classList.add("arrow-popup--hidden"); }; arrow.prototype.deselect = function(e) { From 7745e652d2e815213d2e9b7fed92b1fe6290894a Mon Sep 17 00:00:00 2001 From: pshevtsov Date: Thu, 15 Mar 2018 00:58:34 +0300 Subject: [PATCH 09/13] Formatting and code style --- inst/plugins/arrow.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index 38393e9..4c94c53 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -20,9 +20,9 @@ Dygraph.Plugins.Arrow = (function() { var normalizeDateValue = function(scale, value, fixedtz) { var date = new Date(value); - if (scale != 'minute' && - scale != 'hourly' && - scale != 'seconds' && + if (scale !== 'minute' && + scale !== 'hourly' && + scale !== 'seconds' && !fixedtz) { var localAsUTC = date.getTime() + (date.getTimezoneOffset() * 60000); date = new Date(localAsUTC); @@ -128,8 +128,7 @@ Dygraph.Plugins.Arrow = (function() { var canvas = this.makeCanvas(point.arrow, tickHeight); var position = - this.calcPosition( - canvas, + this.calcPosition(canvas, point.canvasx, point.canvasy, tickHeight); From c226ec695ec87b85719c84bc4edad733ef2651de Mon Sep 17 00:00:00 2001 From: pshevtsov Date: Thu, 15 Mar 2018 00:59:27 +0300 Subject: [PATCH 10/13] Popup initialization refactoring --- inst/plugins/arrow.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index 4c94c53..b0f058d 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -37,10 +37,7 @@ Dygraph.Plugins.Arrow = (function() { arrow.prototype.activate = function(g) { this.dygraph = g; - var div = document.createElement('div'); - div.className = 'dygraphs__arrow-popup'; - g.graphDiv.appendChild(div); - this.popup = div; + this.popup = this.initPopup(); return { didDrawChart: this.didDrawChart, @@ -50,6 +47,13 @@ Dygraph.Plugins.Arrow = (function() { }; }; + arrow.prototype.initPopup = function() { + var div = document.createElement('div'); + div.className = 'arrow-popup arrow-popup--hidden'; + this.dygraph.graphDiv.appendChild(div); + return div; + }; + arrow.prototype.didDrawChart = function() { // Early out in the (common) case of no data. if (this.data.length === 0) return; From 165f0471ae65b018e354e0074c12d2d0f845f4d6 Mon Sep 17 00:00:00 2001 From: pshevtsov Date: Thu, 15 Mar 2018 01:00:08 +0300 Subject: [PATCH 11/13] Correct angles for directions --- inst/plugins/arrow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index b0f058d..bcd4d17 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -181,8 +181,8 @@ Dygraph.Plugins.Arrow = (function() { var directions = { 'up': 0, 'down': 180, - 'left': 90, - 'right': 270, + 'left': 270, + 'right': 90, 'ne': 45, 'se': 135, 'sw': 225, From 50fa46e580ae6bb60134c2f054fed782ea541cd7 Mon Sep 17 00:00:00 2001 From: pshevtsov Date: Thu, 15 Mar 2018 01:00:44 +0300 Subject: [PATCH 12/13] Popup positions depend on arrows directions and graph bounds --- inst/plugins/arrow.js | 69 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/inst/plugins/arrow.js b/inst/plugins/arrow.js index bcd4d17..2aabd5f 100644 --- a/inst/plugins/arrow.js +++ b/inst/plugins/arrow.js @@ -241,16 +241,71 @@ Dygraph.Plugins.Arrow = (function() { arrow.prototype.showPopup = function(p) { this.popup.innerText = p.arrow.text; - var area = this.dygraph.plotter_.area; + this.setPopupPosition(p); + + this.popup.classList.remove("arrow-popup--hidden"); + for (var i = 0; i < this.popup.classList.length; i++) { + var oldClass = this.popup.classList.item(i); + var newClass = "arrow-popup--direction-" + p.arrow.direction; + if (oldClass === newClass) { + break; + } + if (oldClass.startsWith("arrow-popup--direction-")) { + this.popup.classList.replace(oldClass, newClass); + break; + } + this.popup.classList.add(newClass); + } + }; + + arrow.prototype.setPopupPosition = function(p) { var popupWidth = this.popup.offsetWidth; - var yAxisLabelWidth = this.dygraph.getOptionForAxis('axisLabelWidth', 'y'); + var popupHeight = this.popup.offsetHeight; var offset = 10; - var leftPopup = p.canvasx + offset; - var topPopup = p.canvasy - offset; - if ((leftPopup + popupWidth + 1) > area.w) { - leftPopup = leftPopup - 2 * offset - popupWidth - (yAxisLabelWidth - area.x); + var leftPopup, topPopup; + + if (p.arrow.direction === "up") { + leftPopup = p.canvasx - popupWidth / 2; + topPopup = p.canvasy - popupHeight - offset; + } else if (p.arrow.direction === "down") { + leftPopup = p.canvasx - popupWidth / 2; + topPopup = p.canvasy + offset; + } else if (p.arrow.direction === "left") { + leftPopup = p.canvasx - popupWidth - offset; + topPopup = p.canvasy - popupHeight / 2; + } else if (p.arrow.direction === "right") { + leftPopup = p.canvasx + offset; + topPopup = p.canvasy - popupHeight / 2; + } else if (p.arrow.direction === "se") { + leftPopup = p.canvasx + offset; + topPopup = p.canvasy + offset; + } else if (p.arrow.direction === "sw") { + leftPopup = p.canvasx - popupWidth - offset; + topPopup = p.canvasy + offset; + } else if (p.arrow.direction === "ne") { + leftPopup = p.canvasx + offset; + topPopup = p.canvasy - popupHeight - offset; + } else if (p.arrow.direction === "nw") { + leftPopup = p.canvasx - popupWidth - offset; + topPopup = p.canvasy - popupHeight - offset; } - this.popup.classList.remove("dygraphs__arrow-popup--hidden"); + + // Strip redundant CSS classes + this.popup.classList.remove("arrow-popup--left-bound", "arrow-popup--right-bound"); + + var xLabelWidth = this.dygraph.getOptionForAxis('axisLabelWidth', 'y'); + if (leftPopup < xLabelWidth) { + leftPopup = xLabelWidth; + this.popup.classList.add("arrow-popup--left-bound"); + } + + var area = this.dygraph.plotter_.area; + var diff = (leftPopup + popupWidth) - area.w - xLabelWidth - offset; + if (diff > 0) { + leftPopup = leftPopup - diff; + this.popup.classList.add("arrow-popup--right-bound"); + } + this.popup.style.left = leftPopup + "px"; this.popup.style.top = topPopup + "px"; }; From 25b6549662f5eb9ada3222132e433040832775b0 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Mon, 18 Jun 2018 18:32:30 +0300 Subject: [PATCH 13/13] Better disableZoom attribute handling --- inst/htmlwidgets/dygraphs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/htmlwidgets/dygraphs.js b/inst/htmlwidgets/dygraphs.js index dcc4f23..403e8e2 100644 --- a/inst/htmlwidgets/dygraphs.js +++ b/inst/htmlwidgets/dygraphs.js @@ -50,7 +50,7 @@ HTMLWidgets.widget({ attrs.file = x.data; // disable zoom interaction except for clicks - if (attrs.disableZoom !== false) { + if (!!attrs.disableZoom) { attrs.interactionModel = Dygraph.Interaction.nonInteractiveModel_; }