当我尝试放大和缩小图表时,我遇到了d3.js的问题。变焦很慢而且很迟钝。我正在尝试使用分析工具(Opera / Chrome)进行调试。我期待缩放回调功能成为限制因素,但事实证明每个鼠标滚轮事件之间有很多空闲时间。
Motus operandum:我开始分析,然后在鼠标滚轮上画一个大的清晰滚动(图表上的5秒)。图形滞后几秒钟(图表上从5秒到8.5秒)然后定期调用缩放回调(图表上从8.5秒到14秒)。我检查了堆栈调用,并且所有缩放回调都是按顺序同步执行的,这让我觉得在空闲时间执行完毕。我认为探查器不记录一些系统/浏览器调用并将其限定为空闲,所以我尝试使用中断(event.preventDefault()等...)以确保在zoomend上没有执行任何操作。它略微改善了性能,但仍然有很多空闲时间:
有人可以帮我弄清楚为什么有这么多空闲时间?
以下是我的相关代码:
不间断
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;
SemanticZoom和UpdateSelection都经过了单元测试,运行的时间与上面的分析图(50-100ms)相当,适用于大型数据集。
答案 0 :(得分:1)
如果在圆圈数中添加几个零并使svg足够大以使其有用,则缩放速度会减慢到您描述的内容。但这并不奇怪,因为它有很多工作要访问四叉树中的节点并写入DOM来管理svg组件。我不明白为什么你要改变个别圈子而不是将它们分组并改变g。如果您这样做,那么您可以让svg元素剪切图像并避免所有svg开销,这将释放75%的预算。如果四叉树的唯一目的是确定哪些节点可见,那么这也将被消除。
我猜的一个关键观察是这个配置文件与你发布的照片明显不同,从你的照片的轮廓来看,它们似乎都是关于四叉树的,其余的是空闲时间。在配置文件中看到你的cpu和gpu加载会很有趣。
您可以通过使用剪辑路径来消除删除和重写节点的需要,这样唯一的开销就是重写转换属性。
您的搜索也存在问题。有一种更简单的方法可以正常工作,即使用刻度的#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">