Эффективный способ изменить выбранные точки в onRender() htmlWidgets

Я работаю над интерактивной графикой, на которой отображаются черные точки для двух количественных переменных (A и B). Пользователь может выбрать порог. После этого на график наносятся только точки, которые превышают пороговое значение, а также отображается серый прямоугольник, обозначающий область, в которой пороговое значение не превышено. Кажется, все это работает нормально.

Однако сейчас я пытаюсь добиться того, чтобы пользователь мог выбирать определенные точки. При этом выбранные точки должны изменить цвет с черного на красный. Когда пользователь выбирает новое подмножество точек, цвет ранее выбранных точек меняется с красного на черный. Другими словами, единственными точками, которые отображаются красным цветом на графике, должны быть те, которые были выбраны последними.

К сожалению, эта часть не работает. Проблемы прямо сейчас:

1) Выбранные точки постоянно остаются красными. 2) Если выбраны уже красные точки, иногда невыбранные черные точки становятся красными. Вы можете увидеть пример этой ошибки (на картинках) внизу поста.

Я считаю, что это происходит, потому что я просто рисую совершенно новую трассировку красных точек, которые соответствуют выбранным значениям x и y (это ниже в коде, прокомментированном как «// Добавить выбранные пользователем точки красным»). Следовательно, красные точки становятся самостоятельными объектами.

Мой вопрос: каков эффективный способ решения этой проблемы (чтобы выбранные точки становились красными до тех пор, пока не будет выбрано другое подмножество?) Я использую термин «эффективный», потому что я работаю с большими наборами данных (100-1000 точек), и поэтому Я ищу способы сохранить вычислительную скорость. Может быть, более эффективно просто изменить выбранные черные точки на красные, чем полностью перерисовывать наложенные красные точки? Однако я не уверен, как этого добиться, используя синтаксис трассировки Plotly.

Любая помощь будет оценена по достоинству.

Ниже приведена упрощенная версия моего скрипта.

library(plotly)
library(GGally)
library(htmlwidgets)

ui <- shinyUI(fluidPage(
  sliderInput("thresh", "Threshold:", min = 0, max = 3, value=1, step=1),
  plotlyOutput("myPlot"),
  textOutput("selectedValues")
))

server <- shinyServer(function(input, output) {
  thresh <- reactive(input$thresh)

  set.seed(1)
  dat <- data.frame(Row = paste0("Row",sample(c(1:20),20)), A=4*rnorm(20), B=4*rnorm(20))
  dat$Row <- as.character(dat$Row)

  minVal = min(dat[,-1])
  maxVal = max(dat[,-1])

  gg <- ggplot(data = dat, aes(x=A, y=B)) + coord_cartesian(xlim = c(minVal, maxVal), ylim = c(minVal, maxVal))

  ggY <- ggplotly(gg)

  output$myPlot <- renderPlotly(ggY %>% onRender("
    function(el, x, data) {
    var Points = [];
    var Traces = [];
    var selRows = [];

    data.dat.forEach(function(row){
    if(Math.abs(row['B']) > data.thresh) selRows.push(row);});

    var xArr = [];
    var yArr = [];
    var keepIndex = [];
    for (a=0; a<selRows.length; a++){
      xArr.push(selRows[a]['A'])
      yArr.push(selRows[a]['B'])
      keepIndex.push(selRows[a]['Row'])
    }
    Points.push(keepIndex);

    // Add points above the threshold in black
    var tracePoints = {
      x: xArr,
      y: yArr,
      hoverinfo: 'none',
      mode: 'markers',
      marker: {
        color: 'black',
        size: 4
      }
    };

    // Add upper horizontal line of gray box
    var hiLine = {
      x: [-15,15],
      y: [data.thresh,data.thresh],
      mode: 'lines',
      line: {
        color: 'gray',
        width: 1
      },
      opacity: 0.25,
      hoverinfo: 'none'
    };

    // Add lower horizontal line of gray box
    var lowLine = {
      x: [-15,15],
      y: [-1*data.thresh,-1*data.thresh],
      mode: 'lines',
      fill: 'tonexty',
      line: {
        color: 'gray',
        width: 1
      },
      opacity: 0.25,
      hoverinfo: 'none'
    };

    Traces.push(tracePoints);
    Traces.push(hiLine);
    Traces.push(lowLine);
    Plotly.addTraces(el.id, Traces);

    var idRows = []
    for (a=0; a<data.dat.length; a++){
      idRows.push(data.dat[a]['Row'])
    }

    el.on('plotly_selected', function(e) {
      numSel = e.points.length
      cN = e.points[0].curveNumber;

      var pointNumbers = [];
      var selData = [];
      for (a=0; a<numSel; a++){
        pointNumbers.push(e.points[a].pointNumber)
        selData.push(data.dat[idRows.indexOf(Points[0][pointNumbers[a]])])
      }
      Shiny.onInputChange('selData', selData);
      var Traces = [];
      var xArr = [];
      var yArr = [];
      for (a=0; a<selData.length; a++){
        xArr.push(selData[a]['A'])
        yArr.push(selData[a]['B'])
      }

      // Add user-selected points in red
      var traceRed = {
        x: xArr,
        y: yArr,
        mode: 'markers',
        marker: {
          color: 'red',
          size: 4
        },
        hoverinfo: 'none'
      };
      Traces.push(traceRed);

      Plotly.addTraces(el.id, Traces);
    })

    }", data = list(dat=dat, thresh=thresh())))

  selData <- reactive(input$selData)
  output$selectedValues <- renderPrint({selData()})

})

shinyApp(ui, server)

Ниже приведен пример ошибки:

Скажем, я открываю приложение со всеми его значениями по умолчанию и просто Plotly-выбираю четыре верхние правые точки. Результат выглядит хорошо, потому что только эти четыре точки окрашены в красный цвет, как показано ниже:

Изображение 1

Теперь предположим, что я повторно выбираю эти четыре верхние точки. Результат больше не выглядит хорошим, потому что хотя эти четыре точки окрашены в красный цвет, две дополнительные точки также ошибочно окрашены в красный цвет:

Изображение 2


person Community    schedule 16.04.2017    source источник


Ответы (1)


Самый простой способ справиться с этим — просто отслеживать, выделили ли вы что-то, и удалить предыдущую трассировку, содержащую выбранные точки, если она была выбрана ранее. Для этого были внесены следующие изменения:

  • добавлен счетчик nseltrace, чтобы отслеживать, как часто мы выбирали вещи
  • добавлен оператор Plotly.deleteTraces для удаления последней трассировки (которая может быть указана с индексом -1.
  • это действие удаления защищено оператором if (nseltrace>0).
  • переформатировал ваши точки выбора, чтобы поместить их во фрейм данных (они объединяются вместе как вектор символов)
  • использовал verbatumTextOutput, чтобы распечатать их, так как это более чистый формат.

Обновлять:

  • добавлен охранник, чтобы он выбирал только точки, принадлежащие thecurveNumber 1, то есть исходные данные, а не выбранные последующие трассы. Это приводило к тому, что уже выбранные точки дважды вводились в вновь выбранный список.

Что касается того, является ли это наиболее эффективным способом, очевидно, зависит. Вы рисуете выбранные точки по сравнению с предыдущими невыбранными представлениями, в худшем случае будет коэффициент два, но рисование точек происходит очень быстро, и типичное замедление для небольшого процента точек, вероятно, даже не поддается измерению. Я бы не оптимизировал это, если позже это не окажется проблемой, а затем я бы использовал последнюю версию сюжета, поскольку в настоящее время в базе кода сюжета происходит довольно много оптимизации (и удаления ошибок).

Вот код:

library(plotly)
library(GGally)
library(htmlwidgets)
library(shiny)

ui <- shinyUI(fluidPage(
  sliderInput("thresh", "Threshold:", min = 0, max = 3, value=1, step=1),
  plotlyOutput("myPlot"),
  verbatimTextOutput("selectedValues")
))

server <- shinyServer(function(input, output) {
  thresh <- reactive(input$thresh)

  set.seed(1)
  dat <- data.frame(Row = paste0("Row",sample(c(1:20),20)), A=4*rnorm(20), B=4*rnorm(20))
  dat$Row <- as.character(dat$Row)

  minVal = min(dat[,-1])
  maxVal = max(dat[,-1])

  gg <- ggplot(data = dat, aes(x=A, y=B)) + coord_cartesian(xlim = c(minVal, maxVal), ylim = c(minVal, maxVal))

  ggY <- ggplotly(gg)

  output$myPlot <- renderPlotly(ggY %>% onRender("
                                                 function(el, x, data) {
                                                 var Points = [];
                                                 var Traces = [];
                                                 var selRows = [];

                                                 data.dat.forEach(function(row){
                                                 if(Math.abs(row['B']) > data.thresh) selRows.push(row);});

                                                 var xArr = [];
                                                 var yArr = [];
                                                 var keepIndex = [];
                                                 for (a=0; a<selRows.length; a++){
                                                 xArr.push(selRows[a]['A'])
                                                 yArr.push(selRows[a]['B'])
                                                 keepIndex.push(selRows[a]['Row'])
                                                 }
                                                 Points.push(keepIndex);

                                                 // Add points above the threshold in black
                                                 var tracePoints = {
                                                 x: xArr,
                                                 y: yArr,
                                                 hoverinfo: 'none',
                                                 mode: 'markers',
                                                 marker: {
                                                 color: 'black',
                                                 size: 4
                                                 }
                                                 };

                                                 // Add upper horizontal line of gray box
                                                 var hiLine = {
                                                 x: [-15,15],
                                                 y: [data.thresh,data.thresh],
                                                 mode: 'lines',
                                                 line: {
                                                 color: 'gray',
                                                 width: 1
                                                 },
                                                 opacity: 0.25,
                                                 hoverinfo: 'none'
                                                 };

                                                 // Add lower horizontal line of gray box
                                                 var lowLine = {
                                                 x: [-15,15],
                                                 y: [-1*data.thresh,-1*data.thresh],
                                                 mode: 'lines',
                                                 fill: 'tonexty',
                                                 line: {
                                                 color: 'gray',
                                                 width: 1
                                                 },
                                                 opacity: 0.25,
                                                 hoverinfo: 'none'
                                                 };

                                                 Traces.push(tracePoints);
                                                 Traces.push(hiLine);
                                                 Traces.push(lowLine);
                                                 Plotly.addTraces(el.id, Traces);

                                                 var idRows = []
                                                 for (a=0; a<data.dat.length; a++){
                                                 idRows.push(data.dat[a]['Row'])
                                                 }

                                                 var nseltrace = 0;
                                                 el.on('plotly_selected', function(e) {
                                                 console.log(e.points)
                                                 numSel = e.points.length

                                                 var pointNumbers = [];
                                                 var selData = [];
                                                 for (a=0; a<numSel; a++){
                                                   if (e.points[a].curveNumber==1){  // restrict to points in the original curve
                                                     pointNumbers.push(e.points[a].pointNumber)
                                                     selData.push(data.dat[idRows.indexOf(Points[0][pointNumbers[a]])])
                                                   }
                                                 }
                                                 Shiny.onInputChange('selData', selData);
                                                 var Traces = [];
                                                 var xArr = [];
                                                 var yArr = [];
                                                 for (a=0; a<selData.length; a++){
                                                 xArr.push(selData[a]['A'])
                                                 yArr.push(selData[a]['B'])
                                                 }

                                                 // Add user-selected points in red
                                                 var traceRed = {
                                                 x: xArr,
                                                 y: yArr,
                                                 mode: 'markers',
                                                 marker: {
                                                 color: 'red',
                                                 size: 4
                                                 },
                                                 hoverinfo: 'none'
                                                 };

                                                 if (nseltrace>0){
                                                 Plotly.deleteTraces(el.id,-1)
                                                 }
                                                 Traces.push(traceRed);
                                                 nseltrace = nseltrace+1

                                                 Plotly.addTraces(el.id, Traces);
                                                 })

                                                 }", data = list(dat=dat, thresh=thresh())))

  selData <- reactive({
    req(input$selData)
    rawc <- input$selData
    df <- data.frame(t(matrix(rawc,nrow=3)))
    names(df) <- names(rawc)[1:3]
    df
  })
  output$selectedValues <- renderPrint({selData()})

})

shinyApp(ui, server)

А вот снимок экрана:

введите здесь описание изображения

person Mike Wise    schedule 17.04.2017
comment
Я очень ценю полезные идеи, которые вы предлагаете. К сожалению, я думаю, что ошибка, возникающая, когда пользовательский выбор содержит какое-либо подмножество уже красных точек, остается. Я добавил пример (с изображениями) внизу моего поста. Спасибо еще раз. - person ; 17.04.2017
comment
Когда я использую код, который я вставил сюда, это не то, что я вижу. Каждый раз, когда я выбираю, предыдущие точки исчезают. Вы использовали мой код, как опубликовано? - person Mike Wise; 17.04.2017
comment
О, хорошо, теперь я вижу эту проблему. Он другой, чем раньше, перевыбор какой-то другой. Позвольте мне взглянуть на это. - person Mike Wise; 17.04.2017
comment
Спасибо. Ошибка довольно коварная, и она есть как в вашем, так и в моем подходе. - person ; 17.04.2017
comment
Да, ваш опубликованный код действительно решает первые проблемы, которые у меня были! - person ; 17.04.2017