使用 ajax 的带有子行的闪亮数据表

Posted

技术标签:

【中文标题】使用 ajax 的带有子行的闪亮数据表【英文标题】:Shiny datatable with child rows using ajax 【发布时间】:2014-07-01 09:08:43 【问题描述】:

我正在尝试使用数据表库进行更多自定义。

这是我正在尝试制作的示例。 https://datatables.net/examples/api/row_details.html 请注意,我在不同的 data.frame R 变量中有详细信息。像这样

A= data.frame(Name = c("Airi Satou", "Angelica Ramos","Paul Byrd")
               , Position = c("Accountant","Accountant", "CEO")
               , Office   = c("Tokyo", "Tokyo", "New York"))
A.detail= data.frame(Name = c("Airi Satou", "Angelica Ramos")
               , Extension= c("5407c", "8422")
               , salary   = c(16000, 20000))

我不喜欢合并两个 data.frame 变量,如果可以不合并的话,因为计算时间。显然,有些行可能没有任何细节。

我可以在数据表中选择一行,并通过将其绑定为输入将行信息发送到 R(感谢https://github.com/toXXIc/datatables.Selectable/) 然后我可以从第二个 data.frame 变量中找到与 R 中所选行相关的详细信息。 但我不知道如何将其发回以显示在 html 上(在所选行下)。我已经将第一个表绑定为闪亮输出,所以我不确定是否可以传递另一个数据来再次更改此输出。

也许我应该在单击详细信息按钮时使用 ajax 来请求更多数据,但我不知道如何在闪亮中进行 ajax 请求。

【问题讨论】:

您可以将其作为输出对象(df 或列表)从服务器发送回来,然后在输出变量更改或设置标志时显示详细信息。如果您将现有代码放在 github 上,我可能会尝试这样做并提出拉取请求。 @Mahdi Jadaliha:我也有兴趣让它发挥作用。你有一个可行的例子吗? 很遗憾,我还没有找到答案。 【参考方案1】:

在回答您的问题之前,我想指出,CRAN 上的 Shiny (0.10.1) 当前版本使用旧版本的 DataTables.js (1.0.9),而您提到的示例使用 DataTables。 js 1.10。 DataTables 1.10 中有相当一部分 API 与 1.0.9 版本不兼容。

你可以在 Github 上查看这个 pull request:https://github.com/rstudio/shiny/pull/558,它提供 DataTables.js 1.10 支持。


首先,让我们离题一点,了解一下在 Shiny 中数据表是如何呈现的。

该示例使用 AJAX 请求从服务器 URL“拉取”数据,然后将数据绑定到表格模板。这就是所谓的服务器端数据渲染

Shiny 还使用服务器端数据渲染。但是,您提供的示例与 Shiny 之间的主要区别在于,在 Shiny 中传输的数据对您来说是透明的。

从技术上讲,Shiny 通过调用 shiny:::registerDataObj() 为 AJAX 请求创建了一个 JSON API。您可以在此处找到构建自定义 AJAX 请求的示例:http://shiny.rstudio.com/gallery/selectize-rendering-methods.html。

示例和 Shiny 之间的另一个区别(稍后将在 R 代码中体现)是它们如何将表格内容编码为 JSON blob。 该示例使用普通对象对每一行进行编码。比如第一行编码为:

"name": "Tiger Nixon", "position": "System Architect", "salary": "$320,800", "start_date": "2011\/04\/25", "office": "Edinburgh", "extn": "5421" ,

而 Shiny 将 data.frame 的每一行编码为 数组,例如,

["Tiger Nixon", "System Architect", "$320,800", "2011\/04\/25", "Edinburgh", "5421"]

原始 JSON 数据的差异会影响我们稍后实现format() 函数的方式。

最后,该示例使用固定的 HTML <table> 模板来呈现数据表。您可能已经注意到模板中只包含可见列(例如,分机号列不在<table> 模板中);而 Shiny 为您创建模板,您无法决定如何执行数据绑定(例如 "data": "name" ,)。


注意:下面的 R 代码使用了 Shiny 的开发分支,您可以在上面的拉取请求链接中找到它。

虽然我们无法决定将哪些列绑定到哪些数据,但我们可以通过在调用DataTable() 函数时指定columnDefs 选项来选择隐藏哪些列。您可以通过将 https://datatables.net/reference/option/ 中定义的任何选项包装在 R 中的 list 中来传递它们。

使用您的示例数据的 Shiny 应用示例如下:

ui.R

library(shiny)

format.func <- "
<script type='text/javascript'>
function format ( d ) 
    return '<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" style=\"padding-left:50px;\">'+
        '<tr>'+
            '<td>Full name:</td>'+
            '<td>'+d[1]+'</td>'+
        '</tr>'+
        '<tr>'+
            '<td>Extension number:</td>'+
            '<td>'+d[4]+'</td>'+
        '</tr>'+
    '</table>';

</script>
"

shinyUI(
    fluidPage(
        h5("Data table"),
        dataTableOutput("dt"),
        tags$head(HTML(format.func))
    ) 
)

这里没有什么特别之处,只是我相应地更改了format() 函数,因为如前所述,Shiny 将数据作为行数组而不是对象发送。

服务器.R

library(shiny)
library(dplyr)

shinyServer(function(input, output, session) 
    A <- data.frame(Name = c("Airi Satou", "Angelica Ramos","Paul Byrd"),
                  Position = c("Accountant","Accountant", "CEO"),
                  Office   = c("Tokyo", "Tokyo", "New York"))

    A.detail <- data.frame(Name = c("Airi Satou", "Angelica Ramos"),
                          Extension = c("5407c", "8422"),
                          Salary    = c(16000, 20000))

    # You don't necessarily need to use left_join. You can simply put every column,
    # including the columns you would by default to hide, in a data.frame.
    # Then later choose which to hide.
    # Here an empty column is appended to the left to mimic the "click to expand"
    # function you've seen in the example.
    A.joined <- cbind("", left_join(A, A.detail, by="Name"))

    columns.to.hide <- c("Extension", "Salary")
    # Javascript uses 0-based index
    columns.idx.hidden <- which(names(A.joined) %in% columns.to.hide) - 1

    # Everytime a table is redrawn (can be triggered by sorting, searching and 
    # pagination), rebind the click event.

    draw.callback <- "
function(settings) 
    var api = this.api();
    var callback = (function($api) 
        return function() 
            var tr = $(this).parent();
            var row = $api.row(tr);
            if (row.child.isShown()) 
                row.child.hide();
                tr.removeClass('shown');
            
            else 
                row.child(format(row.data())).show();
                tr.addClass('shown');
            
        
    )(api);

    $(this).on('click', 'td.details-control', callback);
"
    # wrap all options you would like to specify in options=list(),
    # which will be converted into corresponding JSON object.
    output$dt <- renderDataTable(A.joined,
        options=list(
            searching=F,
            columnDefs=list(
                            list(targets=0,
                                 title="", class="details-control"),
                            list(targets=columns.idx.hidden,
                                 visible=F)
                         ),
            drawCallback=I(draw.callback)
    ))
)

现在,如果您单击数据表的第一(空)列(因为我没有编写任何 CSS),您应该能够看到扩展区域中显示的额外信息。


编辑:延迟加载“更多详细信息”信息

上述解决方案涉及将所有信息发送到客户端,尽管在大多数用例中,用户可能不会费心查看隐藏的信息。本质上,我们最终会向客户端发送大量冗余数据。

更好的解决方案是在 Shiny 中实现一个 AJAX 请求处理程序,它只在需要时返回信息(即当用户请求时)。

要实现 AJAX 请求处理程序,可以使用session$registerDataObj。此函数在 唯一 URL 处注册请求处理程序,并返回此 URL。

为了调用这个注册的请求处理程序,你需要先把这个 AJAX URL 发送给客户端。

下面我破解了一个快速的解决方案:基本上你在网页上创建一个隐藏的&lt;input&gt; 元素,你可以在其上绑定一个change 事件监听器。 Shiny 服务器通过函数调用session$sendInputMessage 向客户端发送消息来更新此&lt;input&gt; 元素的值。收到消息后,它会更改 &lt;input&gt; 元素的值,从而触发事件侦听器。然后我们可以正确设置 AJAX 请求 URL

之后,您可以发起任何正常的 AJAX 请求来获取您需要的数据。

ui.R

library(shiny)

format.func <- "
<script type='text/javascript'>
var _ajax_url = null;

function format ( d ) 
    // `d` is the original data object for the row
    return '<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" style=\"padding-left:50px;\">'+
        '<tr>'+
            '<td>Full name:</td>'+
            '<td>'+d.Name+'</td>'+
        '</tr>'+
        '<tr>'+
            '<td>Extension number:</td>'+
            '<td>'+d.Extension+'</td>'+
        '</tr>'+
    '</table>';


$(document).ready(function() 
  $('#ajax_req_url').on('change', function()  _ajax_url = $(this).val());
)
</script>
"

shinyUI(
    fluidPage(
        # create a hidden input element to receive AJAX request URL
        tags$input(id="ajax_req_url", type="text", value="", class="shiny-bound-input", style="display:none;"),

        h5("Data table"),
        dataTableOutput("dt"),
        tags$head(HTML(format.func))
    ) 
)

服务器.R

library(shiny)
library(dplyr)

shinyServer(function(input, output, session) 
    # extra more.details dummy column
    A <- data.frame(more.details="", Name = c("Airi Satou", "Angelica Ramos","Paul Byrd"),
                  Position = c("Accountant","Accountant", "CEO"),
                  Office   = c("Tokyo", "Tokyo", "New York"))

    A.detail <- data.frame(Name = c("Airi Satou", "Angelica Ramos"),
                          Extension = c("5407c", "8422"),
                          Salary    = c(16000, 20000))

    draw.callback <- "
function(settings) 
    var api = this.api();
    var callback = (function($api) 
        return function() 
            var tr = $(this).parent();
            var row = $api.row(tr);
            if (row.child.isShown()) 
                row.child.hide();
                tr.removeClass('shown');
            
            else 
                // we can use the unique ajax request URL to get the extra information.
                $.ajax(_ajax_url, 
                  data: name: row.data()[1],
                  success: function(res)  
                      row.child(format(res)).show(); 
                      tr.addClass('shown');
                  
                );
            
        
    )(api);

    $(this).on('click', 'td.details-control', callback);
"

    ajax_url <- session$registerDataObj(
      name = "detail_ajax_handler", # an arbitrary name for the AJAX request handler
      data = A.detail,  # binds your data
      filter = function(data, req) 
        query <- parseQueryString(req$QUERY_STRING)
        name <- query$name

        # pack data into JSON and send.
        shiny:::httpResponse(
          200, "application/json", 
          # use as.list to convert a single row into a JSON Plain Object, easier to parse at client side
          RJSONIO:::toJSON(as.list(data[data$Name == name, ]))
        )        
      
    )

    # send this UNIQUE ajax request URL to client
    session$sendInputMessage("ajax_req_url", list(value=ajax_url))

    output$dt <- renderDataTable(A,
        options=list(
            searching=F,
            columnDefs=list(
                            list(targets=0,
                                 title="", class="details-control")
                         ),
            drawCallback=I(draw.callback)
    ))
)

【讨论】:

感谢 killkeeper,它正在工作。这是否可以避免“left_join(A, A.detail, by="Name")”?我的表很大,很少有用户有兴趣查看更多细节。我在想如何避免合并表。一种解决方案是通过对另一个表的新查询或类似的方式来请求详细信息。 您可以在 Shiny 中构造一个 AJAX 请求处理程序,但这不是很简单。每个请求处理程序一旦创建,就与绑定到当前会话的唯一 URL 相关联。不知何故,您需要将此 URL 发送到客户端。然后您可以发起一个常规的 AJAX 请求来获取“更多详细信息”数据。 嗨,Mahdi,我已经更新了答案,以演示如何创建一个 ajax 处理程序以仅在需要时加载额外信息。 哇哦!这正是我一直在寻找的。 :D 非常感谢 killkeeper。 我最近阅读了更多关于 Shiny 的源代码。您实际上可以使用Shiny.addCustomMessageHandler 直接注册某种特定类型的消息处理程序,以避免&lt;input&gt; hack。基本上,您创建一个专用管道来发送 AJAX 请求 URL 有效负载,而不是多路复用现有管道。

以上是关于使用 ajax 的带有子行的闪亮数据表的主要内容,如果未能解决你的问题,请参考以下文章

数据表中的多个子行,来自asp.net核心中sql server的数据

R闪亮包中的父/子行

带有子行的 T-SQL SELECT 存在优化

SQL从每个父行的子行返回最大值

使用 AJAX 的 DataTables 中的子行

闪亮的数据表:在新窗口中弹出有关所选行的数据