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..197a4fc617 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,74 @@ 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++) { + 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 && + type === "scatter" && x.data[i].mode === "markers"; + } + 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; + 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; + } + Plotly.newPlot(graphDiv, x.data, x.layout); + // attempt to transition when appropriate + window.requestAnimationFrame(function() { + var pts = Plotly.d3.selectAll('.scatterlayer .point'); + if (pts[0].length > 0) { + // Transition the transform -- + // https://gist.github.com/mbostock/1642874 + pts + .data(x.transitionDat) + .transition() + // TODO: provide arguments to these options!! + .duration(0) + .ease('linear') + .attr('transform', function(d) { + return 'translate(' + xaxis(d.x) + ',' + yaxis(d.y) + ')'; + }); + // 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; + } + } + }); + } + } sendEventData = function(eventType) {