Skip to content

Commit

Permalink
begin implementing proxy and methods for use and control in Shiny; #11
Browse files Browse the repository at this point in the history
  • Loading branch information
timelyportfolio committed Oct 6, 2018
1 parent 0b55c5e commit 5433868
Show file tree
Hide file tree
Showing 12 changed files with 9,037 additions and 1,250 deletions.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: parcoords
Title: htmlwidget for d3.js parallel coordinates chart
Version: 0.5.0
Date: 2017-06-25
Version: 0.6.0
Date: 2018-09-08
Authors@R: c(
person("Mike", "Bostock", role = c("aut", "cph"), comment =
"d3.js library in htmlwidgets/lib, http://d3js.org"),
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

export(parcoords)
export(parcoordsOutput)
export(parcoordsProxy)
export(pcFilter)
export(renderParcoords)
import(htmlwidgets)
106 changes: 106 additions & 0 deletions R/proxy.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# this code is very strongly based off of RStudio's leaflet package
# https://github.com/rstudio/leaflet/blob/master/R/utils.R in
# an attempt to establish some consistency around the htmlwidget
# proxy mechanism for use with Shiny

# Given a remote operation and a parcoords proxy, execute. If code was
# not provided for the appropriate mode, an error will be raised.
invokeRemote <- function(pc, method, args = list()) {
if (!inherits(pc, "parcoords_proxy"))
stop("Invalid pc parameter; parcoords proxy object was expected")

msg <- list(
id = pc$id,
calls = list(
list(
dependencies = lapply(pc$dependencies, shiny::createWebDependency),
method = method,
args = args
)
)
)

sess <- pc$session
if (pc$deferUntilFlush) {
sess$onFlushed(function() {
sess$sendCustomMessage("parcoords-calls", msg)
}, once = TRUE) # nolint
} else {
sess$sendCustomMessage("parcoords-calls", msg)
}
pc
}




#' Send commands to a Proxy instance in a Shiny app
#'
#' Creates a parcoords-like object that can be used to customize and control a parcoords
#' that has already been rendered. For use in Shiny apps and Shiny docs only.
#'
#' Normally, you create a parcoords chart using the \code{\link{parcoords}} function.
#' This creates an in-memory representation of a parcoords that you can customize.
#' Such a parcoords can be printed at the R console, included in an R Markdown
#' document, or rendered as a Shiny output.
#'
#' In the case of Shiny, you may want to further customize a parcoords, even after it
#' is rendered to an output. At this point, the in-memory representation of the
#' parcoords is long gone, and the user's web browser has already realized the
#' parcoords instance.
#'
#' This is where \code{parcoordsProxy} comes in. It returns an object that can
#' stand in for the usual parcoords object. The usual parcoords functions
#' can be called, and instead of customizing an in-memory representation,
#' these commands will execute on the live parcoords instance.
#'
#' @param parcoordsId single-element character vector indicating the output ID of the
#' parcoords to modify (if invoked from a Shiny module, the namespace will be added
#' automatically)
#' @param session the Shiny session object to which the map belongs; usually the
#' default value will suffice
#' @param deferUntilFlush indicates whether actions performed against this
#' instance should be carried out right away, or whether they should be held
#' until after the next time all of the outputs are updated; defaults to
#' \code{TRUE}
#'
#' @export
parcoordsProxy <- function(parcoordsId, session = shiny::getDefaultReactiveDomain(),
deferUntilFlush = TRUE) {

if (is.null(session)) {
stop("parcoordsProxy must be called from the server function of a Shiny app")
}

# If this is a new enough version of Shiny that it supports modules, and
# we're in a module (nzchar(session$ns(NULL))), and the parcoordsId doesn't begin
# with the current namespace, then add the namespace.
#
# We could also have unconditionally done `parcoordsId <- session$ns(parcoordsId)`, but
# older versions of Parcoords would have broken unless the user did session$ns
# themselves, and we hate to break their code unnecessarily.
#
# This won't be necessary in future versions of Shiny, as session$ns (and
# other forms of ns()) will be smart enough to only namespace un-namespaced
# IDs.
if (
!is.null(session$ns) &&
nzchar(session$ns(NULL)) &&
substring(parcoordsId, 1, nchar(session$ns(""))) != session$ns("")
) {
parcoordsId <- session$ns(parcoordsId)
}

structure(
list(
session = session,
id = parcoordsId,
x = structure(
list()
),
deferUntilFlush = deferUntilFlush,
dependencies = NULL
),
class = "parcoords_proxy"
)
}
22 changes: 22 additions & 0 deletions R/proxy_methods.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

#' Filter \code{parcoords} through \code{parcoordsProxy}
#'
#' @param pc \code{parcoordsProxy}
#' @param filters \code{list} of filters to apply to the parcoords proxy. Please see
#' \href{https://github.com/deitch/searchjs}{search.js} for example queries as filters.
#'
#' @return \code{parcoords_proxy}
#' @rdname parcoords_methods
#' @export
#'
pcFilter <- function(pc=NULL, filters = NULL) {
if(!inherits(pc, "parcoords_proxy")) {
stop(
paste0("expecting pc argument to be parcoordsProxy but got ", class(pc)),
call. = FALSE
)
}

invokeRemote(pc, "filter", list(filters))
pc
}
21 changes: 21 additions & 0 deletions inst/examples/examples_shiny.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
library(parcoords)
library(shiny)

ui <- parcoordsOutput("pc")

server <- function(input, output, session) {
output$pc <- renderParcoords(
parcoords(mtcars)
)

pcp <<- parcoordsProxy("pc")
pcFilter(
parcoordsProxy("pc"),
list(
cyl = c(6,8),
hp = list(gt = 200)
)
)
}

shinyApp(ui = ui, server = server)
1,846 changes: 959 additions & 887 deletions inst/htmlwidgets/parcoords.js

Large diffs are not rendered by default.

41 changes: 38 additions & 3 deletions javascript/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import '@babel/polyfill';
import {select} from 'd3-selection';
import ParCoords from 'parcoord-es';
import methods from './src/methods';

var HTMLWidgets = window.HTMLWidgets;
// don't like adding in global/window so adding to HTMLWidgets instead
// try to think of a better way of accomplishing this at some point in the future
window.HTMLWidgets.parcoordsWidget = {methods: methods};

HTMLWidgets.widget({

Expand All @@ -20,7 +24,7 @@ HTMLWidgets.widget({

//ugly but currently have to clear out
// each time to get proper render
// delete all children of el
// devare all children of el
// possibly revisit to see if we should be a little more delicate
select( el ).selectAll("*").remove();

Expand Down Expand Up @@ -207,12 +211,12 @@ HTMLWidgets.widget({
select("#" + el.id + " .dimension .axis > text").remove();
}

// sloppy but for now let's force text smaller
// sloppy but for now var's force text smaller
// ?? how best to provide parameter in R
select("#" + el.id).selectAll("svg text")
.style("font-size","10px");

// set up a container for tasks to perform after completion
// set up a container for tasks to perform after compvarion
// one example would be add callbacks for event handling
// styling
if (!(typeof x.tasks === "undefined" || x.tasks === null) ){
Expand Down Expand Up @@ -313,3 +317,34 @@ HTMLWidgets.widget({
};
}
});

// receive and handle parcoords proxy messages with Shiny
if (HTMLWidgets.shinyMode) {
Shiny.addCustomMessageHandler("parcoords-calls", function(data) {
var id = data.id;
var el = document.getElementById(id);
var pcw = el ? HTMLWidgets.find("#" + id) : null;
var methods = HTMLWidgets.parcoordsWidget.methods;

if (!pcw) {
console.log("Couldn't find parcoords with id " + id);
return;
}

var pc = pcw.instance.parcoords;
if(!pc) {
console.log("Founds parcoords with " + id + " but no parcoords attached");
}

for (var i = 0; i < data.calls.length; i++) {
var call = data.calls[i];
if (call.dependencies) {
Shiny.renderDependencies(call.dependencies);
}
if (methods[call.method])
methods[call.method].apply(pc, call.args);
else
console.log("Unknown method " + call.method);
}
});
}
Loading

0 comments on commit 5433868

Please sign in to comment.