你如何在 r2d3 中使用 d3 动画轴转换?

How do you animate axis transitions with d3 in r2d3?

我正在尝试使用 D3 创建直方图,它对条形图和轴都有很好的动画过渡。让酒吧工作很简单,但我正在努力了解如何用轴做同样的事情。在下面的示例中,转换看起来好像正在发生,但实际上每次都添加一个新轴而不删除旧轴。

我的最终目标是使用 R2D3 开发这样的小部件,然后将 javascript 交给其他人在 Java 的应用程序中实现,所以我需要确保它是可转让的,并且不使用 javascript 文件中的 R/shiny/R2D3 特定内容。

这是hist.js脚本

// !preview r2d3 data=data.frame(density = c(10,20,5), from = c(0, 1, 3), to = c(1, 3, 4))
//
// r2d3: https://rstudio.github.io/r2d3
//

var margin = {left:40, right:30, top:10, bottom:30, axis_offset:10};

var min_from = d3.min(data, function(d) {return d.from;});
var max_to = d3.max(data, function(d) {return d.to;});
var max_density = d3.max(data, function(d) { return d.density;});

svg.append('g')
  .attr('transform', 'translate('+(margin.left - margin.axis_offset)+', 0)')
  .attr("class", "y_axis");

svg.append('g')
  .attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
  .attr("class", "x_axis");

var x = d3.scaleLinear()
  .domain([min_from, max_to])
  .range([margin.left, width-margin.right])
  .nice();

var y = d3.scaleLinear()
  .domain([0, max_density])
  .range([height-margin.bottom, margin.top])
  .nice();

svg.selectAll('.y_axis')
  .transition()
  .duration(500)
  .call(d3.axisLeft(y));

svg.selectAll('.x_axis')
  .transition()
  .duration(500)
  .call(d3.axisBottom(x));

var bars = svg.selectAll('rect').data(data);

bars.enter().append('rect')
  .attr('x', function(d) { return x(d.from); })
  .attr('width', function(d) { return x(d.to) - x(d.from)-1;})
  .attr('y', function(d) { return y(d.density); })
  .attr('height', function(d) { return y(0) - y(d.density); })
  .attr('fill', 'steelblue');

bars.exit().remove();

bars.transition()
  .duration(500)
  .attr('x', function(d) { return x(d.from); })
  .attr('width', function(d) { return x(d.to) - x(d.from)-1;})
  .attr('y', function(d) { return y(d.density); })
  .attr('height', function(d) { return y(0) - y(d.density); });

这是我运行它的闪亮应用程序

library(shiny)
library(r2d3)
library(data.table)
library(jsonlite)

get_hist <- function(x) {
  buckets <- seq(0, mean(x)+3*sd(x), length.out = 21)
  h <- hist(x, breaks = c(buckets, Inf), plot = FALSE)
  y <- data.table(count = h$counts, from = head(h$breaks, -1), to = head(shift(h$breaks, -1), -1))[-.N]
  y[, density := count/(to-from)]
  y[]
}

new_data <- function() {
  sh <- 1
  rgamma(10, sh, 1/sh)
}

ui <- fluidPage(
  actionButton("add_data", "Add more data"),
  d3Output('d3_hist')
)

server <- function(input, output, session) {
  samp <- reactiveVal(new_data())
  observeEvent(input$add_data, {
    samp(c(samp(), new_data()))
  })
  output$d3_hist <- renderD3({
    y <- get_hist(samp())
    r2d3(data = toJSON(y), script = 'hist.js')
  })
}

shinyApp(ui, server)

每次更新数据时,都会为每个轴创建一个新的 g 元素。这将创建多个具有 class x_axis / y_axisg 元素,您将所有这些元素都称为轴生成器:

 svg.append('g') // append a new g every update for x axis
    .attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
    .attr("class", "x_axis");    

svg.selectAll('.x_axis')  // call axis generator for every x axis g.
 .transition()
 .duration(500)
 .call(d3.axisBottom(x));

r2d3 的设置有点不同,通常您会为每个轴创建一个 g,然后使用更新函数更新数据和轴。这里每次都是整个 javascript 脚本 运行s,所以我们需要避免在每个轴上附加一个 g

例如:

  if(svg.select(".y_axis").empty()) {
    svg.append('g')
       .attr('transform', 'translate('+(margin.left - margin.axis_offset)+', 0)')
       .attr("class", "y_axis");
  }
  if(svg.select(".x_axis").empty()) {
     svg.append('g')
        .attr('transform', 'translate(0, '+(height - margin.bottom + margin.axis_offset)+')')
        .attr("class", "x_axis");    
  }

javascript 中可以采用不同的方法,但这可能是最直接的方法。理想情况下,r2d3 更容易让您 运行 更新函数而不是每次更新整个脚本。