Allow shared brush IDs

This commit is contained in:
Joe Cheng
2015-09-08 11:08:48 -07:00
parent 67823556d2
commit 129714b044
9 changed files with 113 additions and 20 deletions

View File

@@ -927,7 +927,10 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' be able to draw a rectangle in the plotting area and drag it around. The
#' value will be a named list with \code{xmin}, \code{xmax}, \code{ymin}, and
#' \code{ymax} elements indicating the brush area. To control the brush
#' behavior, use \code{\link{brushOpts}}.
#' behavior, use \code{\link{brushOpts}}. Multiple
#' \code{imageOutput}/\code{plotOutput} calls may share the same \code{id}
#' value; brushing one image or plot will cause any other brushes with the
#' same \code{id} to disappear.
#' @inheritParams textOutput
#' @note The arguments \code{clickId} and \code{hoverId} only work for R base
#' graphics (see the \pkg{\link{graphics}} package). They do not work for

View File

@@ -91,7 +91,10 @@ hoverOpts <- function(id = NULL, delay = 300,
#' \code{\link{plotOutput}}.
#'
#' @param id Input value name. For example, if the value is \code{"plot_brush"},
#' then the coordinates will be available as \code{input$plot_brush}.
#' then the coordinates will be available as \code{input$plot_brush}. Multiple
#' \code{imageOutput}/\code{plotOutput} calls may share the same \code{id}
#' value; brushing one image or plot will cause any other brushes with the
#' same \code{id} to disappear.
#' @param fill Fill color of the brush.
#' @param stroke Outline color of the brush.
#' @param opacity Opacity of the brush

View File

@@ -1407,6 +1407,8 @@ $.extend(imageOutputBinding, {
// * Bind those event handlers to events.
// * Insert the new image.
var outputId = this.getId(el);
var $el = $(el);
// Load the image before emptying, to minimize flicker
var img = null;
@@ -1522,7 +1524,7 @@ $.extend(imageOutputBinding, {
$el.on('selectstart.image_output', function() { return false; });
var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts,
opts.coordmap);
opts.coordmap, outputId);
$el.on('mousedown.image_output', brushHandler.mousedown);
$el.on('mousemove.image_output', brushHandler.mousemove);
@@ -1986,7 +1988,7 @@ imageutils.createHoverHandler = function(inputId, delay, delayType, clip,
// Returns a brush handler object. This has three public functions:
// mousedown, mousemove, and onRemoveImg.
imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId) {
// Parameter: expand the area in which a brush can be started, by this
// many pixels in all directions. (This should probably be a brush option)
var expandPixels = 20;
@@ -1994,6 +1996,25 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
// Represents the state of the brush
var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);
// Brush IDs can span multiple image/plot outputs. When an output is brushed,
// if a brush with the same ID is active on a different image/plot, it must
// be dismissed (but without sending any data to the server). We implement
// this by sending the shiny-internal:brushed event to all plots, and letting
// each plot decide for itself what to do.
//
// The decision to have the event sent to each plot (as opposed to a single
// event triggered on, say, the document) was made to make cleanup easier;
// listening on an event on the document would prevent garbage collection
// of plot outputs that are removed from the document.
$el.on("shiny-internal:brushed.image_output", function(e, coords) {
// If the new brush shares our ID but not our output element ID, we
// need to clear our brush (if any).
if (coords.brushId === inputId && coords.outputId !== outputId) {
$el.data("mostRecentBrush", false);
brush.reset();
}
});
// Set cursor to one of 7 styles. We need to set the cursor on the whole
// el instead of the brush div, because the brush div has
// 'pointer-events:none' so that it won't intercept pointer events.
@@ -2010,6 +2031,10 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.onInputChange(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
});
return;
}
@@ -2028,8 +2053,14 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
coords.direction = opts.brushDirection;
coords.brushId = inputId;
coords.outputId = outputId;
// Send data to server
exports.onInputChange(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
}
var brushInfoSender;
@@ -2196,17 +2227,26 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
}
// Brush maintenance: When an image is re-rendered, the brush must either
// be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
// "mostRecentBrush" bit is to ensure that when multiple outputs share the
// same brush ID, inactive brushes don't send null values up to the server.
// This should be called when the img (not the el) is removed
function onRemoveImg() {
if (opts.brushResetOnNew) {
brush.reset();
brushInfoSender.immediateCall();
if ($el.data("mostRecentBrush")) {
brush.reset();
brushInfoSender.immediateCall();
}
}
}
if (!opts.brushResetOnNew) {
brush.importOldBrush();
brushInfoSender.immediateCall();
if ($el.data("mostRecentBrush")) {
brush.importOldBrush();
brushInfoSender.immediateCall();
}
}
return {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,10 @@ brushOpts(id = NULL, fill = "#9cf", stroke = "#036", opacity = 0.25,
}
\arguments{
\item{id}{Input value name. For example, if the value is \code{"plot_brush"},
then the coordinates will be available as \code{input$plot_brush}.}
then the coordinates will be available as \code{input$plot_brush}. Multiple
\code{imageOutput}/\code{plotOutput} calls may share the same \code{id}
value; brushing one image or plot will cause any other brushes with the
same \code{id} to disappear.}
\item{fill}{Fill color of the brush.}

View File

@@ -63,7 +63,10 @@ accessible via \code{input$plot_brush}. Brushing means that the user will
be able to draw a rectangle in the plotting area and drag it around. The
value will be a named list with \code{xmin}, \code{xmax}, \code{ymin}, and
\code{ymax} elements indicating the brush area. To control the brush
behavior, use \code{\link{brushOpts}}.}
behavior, use \code{\link{brushOpts}}. Multiple
\code{imageOutput}/\code{plotOutput} calls may share the same \code{id}
value; brushing one image or plot will cause any other brushes with the
same \code{id} to disappear.}
\item{clickId}{Deprecated; use \code{click} instead. Also see the
\code{\link{clickOpts}} function.}

View File

@@ -11,6 +11,8 @@ $.extend(imageOutputBinding, {
// * Bind those event handlers to events.
// * Insert the new image.
var outputId = this.getId(el);
var $el = $(el);
// Load the image before emptying, to minimize flicker
var img = null;
@@ -126,7 +128,7 @@ $.extend(imageOutputBinding, {
$el.on('selectstart.image_output', function() { return false; });
var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts,
opts.coordmap);
opts.coordmap, outputId);
$el.on('mousedown.image_output', brushHandler.mousedown);
$el.on('mousemove.image_output', brushHandler.mousemove);
@@ -590,7 +592,7 @@ imageutils.createHoverHandler = function(inputId, delay, delayType, clip,
// Returns a brush handler object. This has three public functions:
// mousedown, mousemove, and onRemoveImg.
imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId) {
// Parameter: expand the area in which a brush can be started, by this
// many pixels in all directions. (This should probably be a brush option)
var expandPixels = 20;
@@ -598,6 +600,25 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
// Represents the state of the brush
var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);
// Brush IDs can span multiple image/plot outputs. When an output is brushed,
// if a brush with the same ID is active on a different image/plot, it must
// be dismissed (but without sending any data to the server). We implement
// this by sending the shiny-internal:brushed event to all plots, and letting
// each plot decide for itself what to do.
//
// The decision to have the event sent to each plot (as opposed to a single
// event triggered on, say, the document) was made to make cleanup easier;
// listening on an event on the document would prevent garbage collection
// of plot outputs that are removed from the document.
$el.on("shiny-internal:brushed.image_output", function(e, coords) {
// If the new brush shares our ID but not our output element ID, we
// need to clear our brush (if any).
if (coords.brushId === inputId && coords.outputId !== outputId) {
$el.data("mostRecentBrush", false);
brush.reset();
}
});
// Set cursor to one of 7 styles. We need to set the cursor on the whole
// el instead of the brush div, because the brush div has
// 'pointer-events:none' so that it won't intercept pointer events.
@@ -614,6 +635,10 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.onInputChange(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
});
return;
}
@@ -632,8 +657,14 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
coords.direction = opts.brushDirection;
coords.brushId = inputId;
coords.outputId = outputId;
// Send data to server
exports.onInputChange(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
}
var brushInfoSender;
@@ -800,17 +831,26 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap) {
}
// Brush maintenance: When an image is re-rendered, the brush must either
// be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
// "mostRecentBrush" bit is to ensure that when multiple outputs share the
// same brush ID, inactive brushes don't send null values up to the server.
// This should be called when the img (not the el) is removed
function onRemoveImg() {
if (opts.brushResetOnNew) {
brush.reset();
brushInfoSender.immediateCall();
if ($el.data("mostRecentBrush")) {
brush.reset();
brushInfoSender.immediateCall();
}
}
}
if (!opts.brushResetOnNew) {
brush.importOldBrush();
brushInfoSender.immediateCall();
if ($el.data("mostRecentBrush")) {
brush.importOldBrush();
brushInfoSender.immediateCall();
}
}
return {