每个鼠标轮事件

时间:2015-07-21 18:19:19

标签: javascript performance d3.js javascript-events

当我尝试放大和缩小图表时,我遇到了d3.js的问题。变焦很慢而且很迟钝。我正在尝试使用分析工具(Opera / Chrome)进行调试。我期待缩放回调功能成为限制因素,但事实证明每个鼠标滚轮事件之间有很多空闲时间。D3 is idling for 1s between each scrolls

Motus operandum:我开始分析,然后在鼠标滚轮上画一个大的清晰滚动(图表上的5秒)。图形滞后几秒钟(图表上从5秒到8.5秒)然后定期调用缩放回调(图表上从8.5秒到14秒)。我检查了堆栈调用,并且所有缩放回调都是按顺序同步执行的,这让我觉得在空闲时间执行完毕。我认为探查器不记录一些系统/浏览器调用并将其限定为空闲,所以我尝试使用中断(event.preventDefault()等...)以确保在zoomend上没有执行任何操作。它略微改善了性能,但仍然有很多空闲时间:

It starts off nicely but slows down

有人可以帮我弄清楚为什么有这么多空闲时间?

以下是我的相关代码:

  • 不间断

     d3Zoom = d3.behavior.zoom()
                    .x(element.self.xScale)
                    .y(element.self.yScale)
                    .scaleExtent([0.99, Infinity])
                    .on("zoom", semanticZoom)
                    .on("zoomend", updateSelection);
    
  • 中断

     var delayTimer=0;
     d3Zoom = d3.behavior.zoom()
                    .x(xScale)
                    .y(yScale)
                    .scaleExtent([0.99, Infinity])
                    .on("zoom", semanticZoom)
                    .on("zoomstart", function () {
                        //prevent recalculating heavyCalculations too often
                        window.clearTimeout(delayTimer);                            
                        var evt = e ? e : window.event;
                        return cancelDefaultAction(evt);
                    })
                    .on("zoomend", function () {
                        // only start heavy calculations if user hasn't zoomed for 0.75sec
                        delayTimer = window.setTimeout(updateSelection, 750);
                    });
    
      function cancelDefaultAction(e) {
                    var evt = e ? e : window.event;
                    if (evt.preventDefault) evt.preventDefault();
                    evt.returnValue = false;
                    return false;
                }`
    

编辑:以下是工作代码的示例。 semanticZoom和更新选择在我的项目中比在这个例子中更复杂,但它们涉及自定义AngularJS指令,d3画笔,扭曲几何,聚合等...我已经裁剪了semanticZoom来执行基于a的进入/退出/更新模式四叉树(在这个例子中它可能表现得很有趣,但它只是为了展示我做的那种操作)。 UpdateSelection将可见数据更新为angular指令以执行计算(各种统计等...)。我没有在这里填充它,但它实际上并不是非常密集。



var size = 100;

var dataset = d3.range(10).map(function(d, idx) {
  return {
    x: d3.random.normal(size / 2, size / 4)(),
    y: d3.random.normal(size / 2, size / 4)(),
    uuid: idx
  };
});

//
// Init Scales
//

var xScale = d3.scale.linear()
  .domain([0, size])
  .range([0, 100]);

var yScale = d3.scale.linear()
  .domain([0, size])
  .range([0, 100]);

//
// Init Axes
//

var xAxis = d3.svg.axis()
  .scale(xScale)
  .ticks(10)
  .orient("bottom")
  .tickSize(-size);

var yAxis = d3.svg.axis()
  .scale(yScale)
  .ticks(10)
  .orient("left")
  .tickSize(-size);

//
// Init Zoom
//

var d3Zoom = d3.behavior.zoom()
  .x(xScale)
  .y(yScale)
  .scaleExtent([0.99, Infinity])
  .on("zoom", semanticZoom)
  .on("zoomend", updateSelection);

var quadtree = d3.geom.quadtree(dataset);


//------------------------ Callbacks --------------------------------

function semanticZoom() {

  var s = 1;
  var t = [0, 0];
  if (d3.event) {
    s = (d3.event.scale) ? d3.event.scale : 1;
    t = (d3.event.translate) ? d3.event.translate : [0, 0];
  }

  // set zoom boundaries
  // center of the zoom in svg coordinates
  var center = [(size / 2 - t[0]) / s, (size / 2 - t[1]) / s];
  // half size of the window in svg coordinates
  var halfsize = size / (2 * s);
  // top left corner in svg coordinates
  var tl = [center[0] - halfsize, center[1] - halfsize];
  // bottom right corner in svg coordinates
  var br = [center[0] + halfsize, center[1] + halfsize];

  /*

  	//
  	// Constrain zoom
  	//

  	if (!(tl[0] > -10 &&
  		  tl[1] > -10 &&
  		  br[0] < size + 10 &&
  		  br[1] < size + 10)) {

  		// limit zoom-window corners
  		tl = [Math.max(0, tl[0]), Math.max(0, tl[1])];
  		br = [Math.min(size, br[0]), Math.min(size, br[1])];
  		// get restrained center
  		center = [(tl[0] + br[0]) / 2, (tl[1] + br[1]) / 2];
  		// scale center
  		t = [size / 2 - s * center[0], size / 2 - s * center[1]];

  		// update svg
  		svg.transition()
  			.duration(1)
  			.call( d3Zoom.translate(t).event );

  	}
  	*/

  //
  // Store zoom extent
  //

  d3Zoom.extent = [tl, br];
  d3Zoom.scaleFactor = s;
  d3Zoom.translation = t;

  //
  // Update some heavy duty stuff 
  // (create a quadtree, search that quadtree and update an attribute for the elements found)
  //

  // Prune non visible data
  var displayedData = search(quadtree,
    d3Zoom.extent[0][0], d3Zoom.extent[0][1],
    d3Zoom.extent[1][0], d3Zoom.extent[1][1]);

  redrawSubset(displayedData);

  //
  // Update axes
  //

  d3.select(".x.axis").call(xAxis);
  d3.select(".y.axis").call(yAxis);

}

function redrawSubset(subset) {

  //Attach new data

  var elements = d3.select(".data_container")
    .selectAll(".datum")
    .data(subset, function(d) {
      return d.uuid;
    });

  //enter

  elements.enter()
    .append("circle")
    .attr("class", "datum")
    .attr("r", 1)
    .style("fill", "black");

  //exit

  elements.exit().remove();

  //update

  elements.attr("transform", ScaleData);

}


function updateSelection() {
  // some not so heavy duty stuff

}

function ScaleData(d) {
  return "translate(" + [xScale(d.x), yScale(d.y)] + ")";
}

//
// search quadtree
//

function search(qt, x0, y0, x3, y3) {
  var pts = [];
  qt.visit(function(node, x1, y1, x2, y2) {
    var p = node.point;

    if ((p) && (p.x >= x0) && (p.x <= x3) && (p.y >= y0) && (p.y <= y3)) {
      pts.push(p);
    }

    return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
  });

  return pts;
}



//------------------------- DOM Manipulation -------------------------		

var svg = d3.select("body").append("svg")
  .attr("width", size)
  .attr("height", size)
  .append("g")
  .attr("class", "data_container")
  .call(d3Zoom);



svg.append("rect")
  .attr("class", "overlay")
  .attr("width", size)
  .attr("height", size)
  .style("fill", "none")
  .style("pointer-events", "all");



var circle = svg.selectAll("circle")
  .data(dataset, function(d) {
    return d.uuid;
  }).enter()
  .append("circle")
  .attr("r", 1)
  .attr("class", "datum")
  .attr("transform", ScaleData);
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
&#13;
&#13;
&#13;

SemanticZoom和UpdateSelection都经过了单元测试,运行的时间与上面的分析图(50-100ms)相当,适用于大型数据集。

1 个答案:

答案 0 :(得分:1)

如果在圆圈数中添加几个零并使svg足够大以使其有用,则缩放速度会减慢到您描述的内容。但这并不奇怪,因为它有很多工作要访问四叉树中的节点并写入DOM来管理svg组件。我不明白为什么你要改变个别圈子而不是将它们分组并改变g。如果您这样做,那么您可以让svg元素剪切图像并避免所有svg开销,这将释放75%的预算。如果四叉树的唯一目的是确定哪些节点可见,那么这也将被消除。

我猜的一个关键观察是这个配置文件与你发布的照片​​明显不同,从你的照片的轮廓来看,它们似乎都是关于四叉树的,其余的是空闲时间。在配置文件中看到你的cpu和gpu加载会很有趣。

enter image description here

您可以通过使用剪辑路径来消除删除和重写节点的需要,这样唯一的开销就是重写转换属性。
您的搜索也存在问题。有一种更简单的方法可以正常工作,即使用刻度的#linear.invert(y)方法。
这些都在下面的示例代码中解决......

  var size = 500;

  var margin = {top: 30, right: 40, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 200 - margin.top - margin.bottom;

  d3.select("#clipButton").on("click", (function() {
    var clipped = false, clipAttr = [null, "url(#clip)"],
      value = ["clip", "brush"];
    return function() {
      circles
        .attr("clip-path", clipAttr[(clipped = !clipped, +clipped)]);
      this.value = value[+clipped];
    }
  })());

  var dataset = d3.range(1000).map(function(d, idx) {
    return {
      x: d3.random.normal(100 / 2, 100 / 4)(),
      y: d3.random.normal(100 / 2, 100 / 4)(),
      uuid: idx
    };
  });

  //
  // Init Scales
  //

  var xScale = d3.scale.linear()
      .domain([0, 100])
      .range([0, width])
      .nice(10);

  var yScale = d3.scale.linear()
    .domain([0, 100])
    .range([height, 0])
    .nice(10);

  //
  // Init Axes
  //

  var xAxis = d3.svg.axis()
    .scale(xScale)
    .ticks(10)
    .orient("bottom")
    .tickSize(-height);

  var yAxis = d3.svg.axis()
    .scale(yScale)
    .ticks(10)
    .orient("left")
    .tickSize(-width);

  //
  // Init Zoom
  //

  var d3Zoom = d3.behavior.zoom()
    .x(xScale)
    .y(yScale)
    .scaleExtent([0.99, Infinity])
    .on("zoom", semanticZoom)
//    .on("zoomend", updateSelection);

  var Quadtree = d3.geom.quadtree()
    .x(function(d){return d.x})
    .y(function(d){return d.y});
    quadtree = Quadtree(dataset);

  //------------------------ Callbacks --------------------------------

  function semanticZoom() {

    var s = 1;
    var t = [0, 0];
    if (d3.event) {
      s = (d3.event.scale) ? d3.event.scale : 1;
      t = (d3.event.translate) ? d3.event.translate : [0, 0];
    }

    var tl = [xScale.invert(0), yScale.invert(height)];
    var br = [xScale.invert(width), yScale.invert(0)];

    //
    // Store zoom extent
    //

    d3Zoom.extent = [tl, br];
    d3Zoom.scaleFactor = s;
    d3Zoom.translation = t;

    //
    // Update some heavy duty stuff
    // (create a quadtree, search that quadtree and update an attribute for the elements found)
    //

    // Prune non visible data
    var displayedData = search(quadtree, d3Zoom.extent);

    markSubset(displayedData, circle);
    updateSelection(circle);
    //
    // Update axes
    //

    d3.select(".x.axis").call(xAxis);
    d3.select(".y.axis").call(yAxis);

  };
  function markSubset(data, nodes){
    var marked = nodes.data(data, function(d){return d.uuid;});
    marked.enter();
    marked.classed("visible", true);
    marked.exit().classed("visible", false);
  }
  function updateSelection(elements) {
    // some not so heavy duty stuff
    elements.attr("transform", ScaleData);

  }

  function ScaleData(d) {
    return "translate(" + [xScale(d.x), yScale(d.y)] + ")";
  }

  //
  // search quadtree
  //

  function search(qt, extent) {
    var pts = [],
        x0=extent[0][0], y0=extent[0][1],
      x3=extent[1][0], y3=extent[1][1];
    qt.visit(function(node, x1, y1, x2, y2) {
      var p = node.point;

      if ((p) && (p.x >= x0) && (p.x <= x3) && (p.y >= y0) && (p.y <= y3)) {
        pts.push(p);
      }

      return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
    });

    return pts;
  }

  //------------------------- DOM Manipulation -------------------------

  var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("class", "data_container")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(d3Zoom),

    plotSurface = svg.append("rect")
    .attr("class", "overlay")
    .attr("width", width)
    .attr("height", height)
    .style({"fill": "steelblue", opacity: 0.8})
    .style("pointer-events", "all"),

    gX = svg.append("g")            // Add the X Axis
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis),

    gY = svg.append("g")
    .attr("class", "y axis")
    .call(yAxis),

    clipRect = svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height),

    circles = svg.append("g")/*
      .attr("clip-path", "url(#clip)")*/,
    circle = circles.selectAll("circle")
      .data(dataset, function(d) {
        return d.uuid;
      });

  circle.enter()
      .append("circle")
      .attr("r", 3)
      .attr("class", "datum")
      .attr("transform", ScaleData);

  semanticZoom();
    svg {
      outline: 1px solid red;
      overflow: visible;
    }
    .axis path {
      stroke: #000;
    }

    .axis line {
      stroke: steelblue;
      stroke-opacity: .5;
    }
    .axis path {
      fill: none;
    }
    .axis text {
      font-size: 8px;
    }
    .datum {
      fill: #ccc;
    }
    .datum.visible {
      fill: black;
    }
    #clipButton {
      position: absolute;
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<input id="clipButton" type="button" value="clip">