如何在 r2d3 中使用 d3 为轴过渡设置动画?

问题描述

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

我的最终目标是使用 R2D3 开发这样的小部件,然后将 javascript 交给其他人以在 Java 应用程序中实现,所以我需要确保它是可转移的并且不使用 R/ javascript 文件中的闪亮/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 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))[-.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 元素。这将创建多个 g 元素,其类为 x_axis / y_axis,您可以在所有这些元素上调用轴生成器:

 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 脚本,因此我们需要避免在每个轴上附加 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','+(height - margin.bottom + margin.axis_offset)+')')
        .attr("class","x_axis");    
  }

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