From 55576fb7b83e94badc7d797d92a5af3b36b3ccec Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 13 Apr 2016 14:57:00 +1000 Subject: [PATCH 1/4] rough, but working transitions --- inst/examples/tourr/app.R | 33 ++++++++++++++++++ inst/htmlwidgets/plotly.js | 69 +++++++++++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 inst/examples/tourr/app.R diff --git a/inst/examples/tourr/app.R b/inst/examples/tourr/app.R new file mode 100644 index 0000000000..9ec0360192 --- /dev/null +++ b/inst/examples/tourr/app.R @@ -0,0 +1,33 @@ +# modified from https://github.com/rstudio/ggvis/blob/master/demo/tourr.r + +library(tourr) +library(plotly) +library(shiny) + +aps <- 2 +fps <- 30 + +mat <- rescale(as.matrix(flea[1:6])) +tour <- new_tour(mat, grand_tour(), NULL) +start <- tour(0) + +ui <- fluidPage( + plotlyOutput("p") +) + +server <- function(input, output) { + + proj_data <- reactive({ + invalidateLater(1000 / fps, NULL); + step <- tour(aps / fps) + data.frame(center(mat %*% step$proj), species = flea$species) + }) + + output$p <- renderPlotly({ + proj_data() %>% + plot_ly(x = X1, y = X2, color = species, mode = "markers") %>% + layout(xaxis = list(range = c(-1, 1)), yaxis = list(range = c(-1, 1))) + }) +} + +shinyApp(ui, server) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index e9a3f90eda..ac53231720 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -14,7 +14,6 @@ HTMLWidgets.widget({ }, renderValue: function(el, x, instance) { - var shinyMode; if (typeof(window) !== "undefined") { // make sure plots don't get created outside the network @@ -22,16 +21,78 @@ HTMLWidgets.widget({ window.PLOTLYENV.BASE_URL = x.base_url; shinyMode = !!window.Shiny; } - var graphDiv = document.getElementById(el.id); - // if no plot exists yet, create one with a particular configuration if (!instance.plotly) { Plotly.plot(graphDiv, x.data, x.layout, x.config); instance.plotly = true; instance.autosize = x.layout.autosize; } else { - Plotly.newPlot(graphDiv, x.data, x.layout); + // Can we do smooth transitions of x/y locations of points? + var doTransition = x.data.length === graphDiv._fullData.length; + for (i = 0; i < x.data.length; i++) { + doTransition = doTransition && + x.data[i].x.length === graphDiv._fullData[i].x.length && + x.data[i].y.length === graphDiv._fullData[i].y.length && + //x.data[i].type || "scatter" === "scatter"; + x.data[i].mode === "markers"; + } + // if transitioning x/y, construct scales from graphDiv + if (doTransition) { + var lay = graphDiv._fullLayout; + var xDom = lay.xaxis.range; + var yDom = lay.yaxis.range; + var xRng = [0, lay.width - lay.margin.l - lay.margin.r]; + var yRng = [lay.height - lay.margin.t - lay.margin.b, 0]; + // TODO: does this generalize to non-linear scales? + var xaxis = Plotly.d3.scale.linear().domain(xDom).range(xRng); + var yaxis = Plotly.d3.scale.linear().domain(yDom).range(yRng); + // store new x/y positions as an array of objects (for D3 bind) + x.transitionDat = []; + for (i = 0; i < x.data.length; i++) { + var d = x.data[i]; + for (j = 0; j < d.x.length; j++) { + x.transitionDat.push({x: d.x[j], y: d.y[j]}); + } + } + // we call newPlot() with old x/y locations and transition to new ones + for (i = 0; i < x.data.length; i++) { + x.data[i].xNew = x.data[i].x; + x.data[i].yNew = x.data[i].y; + x.data[i].x = graphDiv._fullData[i].x; + x.data[i].y = graphDiv._fullData[i].y; + } + } + + /* create a new plot with old x/y locations first + (this seems too slow to be useful) + setTimeout(function() { + Plotly.newPlot(graphDiv, x.data, x.layout); + }, 16); + */ + + // attempt to transition when appropriate + window.requestAnimationFrame(function() { + var pts = Plotly.d3.selectAll('.scatterlayer .point'); + if (doTransition && pts[0].length > 0) { + // Transition the transform -- https://gist.github.com/mbostock/1642874 + pts + .data(x.transitionDat) + .transition() + // TODO: how to provide arguments to these options? + .duration(1000) + .ease('linear') + .attr('transform', function(d) { + return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; + }); + // update the graphDiv data + for (i = 0; i < x.data.length; i++) { + graphDiv._fullData[i].x = x.data[i].xNew; + graphDiv._fullData[i].y = x.data[i].yNew; + } + } + }); + } sendEventData = function(eventType) { From 376b6704addfd3adc4cfc1ad7fe78e2b70d8f0ba Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 13 Apr 2016 15:53:08 +1000 Subject: [PATCH 2/4] set duration to 0 --- inst/htmlwidgets/plotly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index ac53231720..288fd6e8c2 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -80,7 +80,7 @@ HTMLWidgets.widget({ .data(x.transitionDat) .transition() // TODO: how to provide arguments to these options? - .duration(1000) + .duration(0) .ease('linear') .attr('transform', function(d) { return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; From 63b835d88a06458226a9d624a4ce53520aa031c8 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 13 Apr 2016 17:07:34 +1000 Subject: [PATCH 3/4] call newPlot if we can't transition --- inst/htmlwidgets/plotly.js | 58 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 288fd6e8c2..2253e5de66 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -37,8 +37,11 @@ HTMLWidgets.widget({ //x.data[i].type || "scatter" === "scatter"; x.data[i].mode === "markers"; } - // if transitioning x/y, construct scales from graphDiv - if (doTransition) { + + if (!doTransition) { + Plotly.newPlot(graphDiv, x.data, x.layout); + } else { + // construct x/y scales from graphDiv var lay = graphDiv._fullLayout; var xDom = lay.xaxis.range; var yDom = lay.yaxis.range; @@ -62,36 +65,29 @@ HTMLWidgets.widget({ x.data[i].x = graphDiv._fullData[i].x; x.data[i].y = graphDiv._fullData[i].y; } - } - - /* create a new plot with old x/y locations first - (this seems too slow to be useful) - setTimeout(function() { - Plotly.newPlot(graphDiv, x.data, x.layout); - }, 16); - */ - - // attempt to transition when appropriate - window.requestAnimationFrame(function() { - var pts = Plotly.d3.selectAll('.scatterlayer .point'); - if (doTransition && pts[0].length > 0) { - // Transition the transform -- https://gist.github.com/mbostock/1642874 - pts - .data(x.transitionDat) - .transition() - // TODO: how to provide arguments to these options? - .duration(0) - .ease('linear') - .attr('transform', function(d) { - return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; - }); - // update the graphDiv data - for (i = 0; i < x.data.length; i++) { - graphDiv._fullData[i].x = x.data[i].xNew; - graphDiv._fullData[i].y = x.data[i].yNew; + // attempt to transition when appropriate + window.requestAnimationFrame(function() { + var pts = Plotly.d3.selectAll('.scatterlayer .point'); + if (doTransition && pts[0].length > 0) { + // Transition the transform -- + // https://gist.github.com/mbostock/1642874 + pts + .data(x.transitionDat) + .transition() + // TODO: how to provide arguments to these options? + .duration(0) + .ease('linear') + .attr('transform', function(d) { + return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; + }); + // update the graphDiv data + for (i = 0; i < x.data.length; i++) { + graphDiv._fullData[i].x = x.data[i].xNew; + graphDiv._fullData[i].y = x.data[i].yNew; + } } - } - }); + }); + } } From e846d26b2598048a3e609a624599e1e226f8e682 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 13 Apr 2016 17:41:50 +1000 Subject: [PATCH 4/4] better condition checking; calling newPlot before transition seems feasible --- inst/htmlwidgets/plotly.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 2253e5de66..197a4fc617 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -31,13 +31,12 @@ HTMLWidgets.widget({ // Can we do smooth transitions of x/y locations of points? var doTransition = x.data.length === graphDiv._fullData.length; for (i = 0; i < x.data.length; i++) { + var type = x.data[i].type || "scatter"; doTransition = doTransition && x.data[i].x.length === graphDiv._fullData[i].x.length && x.data[i].y.length === graphDiv._fullData[i].y.length && - //x.data[i].type || "scatter" === "scatter"; - x.data[i].mode === "markers"; + type === "scatter" && x.data[i].mode === "markers"; } - if (!doTransition) { Plotly.newPlot(graphDiv, x.data, x.layout); } else { @@ -65,22 +64,23 @@ HTMLWidgets.widget({ x.data[i].x = graphDiv._fullData[i].x; x.data[i].y = graphDiv._fullData[i].y; } + Plotly.newPlot(graphDiv, x.data, x.layout); // attempt to transition when appropriate window.requestAnimationFrame(function() { var pts = Plotly.d3.selectAll('.scatterlayer .point'); - if (doTransition && pts[0].length > 0) { + if (pts[0].length > 0) { // Transition the transform -- // https://gist.github.com/mbostock/1642874 pts .data(x.transitionDat) .transition() - // TODO: how to provide arguments to these options? + // TODO: provide arguments to these options!! .duration(0) .ease('linear') .attr('transform', function(d) { return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; }); - // update the graphDiv data + // update graphDiv data for (i = 0; i < x.data.length; i++) { graphDiv._fullData[i].x = x.data[i].xNew; graphDiv._fullData[i].y = x.data[i].yNew;