wha*_*ext 18 svg zoom d3.js force-layout
已经通过SVG几何变焦显示了用于力导向图几何缩放的许多情况.
在几何缩放中,我只需要在缩放功能中添加变换属性.但是,在语义缩放中,如果我只在节点中添加转换属性,则链接将不会连接到节点.所以,我想知道在d3中是否存在用于力导向图的几何缩放的解决方案.
这是我的例子与下面前一种情况的几何变焦.我有两个问题:
Run Code Online (Sandbox Code Playgroud)function zoom() { vis.attr("transform", transform); } function transform(d){ return "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"; }
这只更新了一个svg元素的"transform"属性.但是如何使功能改变节点位置?
但我想要做的是语义缩放.我试图修改缩放和转换功能,但不确定正确的方法.这是我尝试的.我改变的功能:
Run Code Online (Sandbox Code Playgroud)function zoom() { node.call(transform); // update link position update(); } function transform(d){ // change node x, y position, not sure what function to put here. }
Ame*_*aBR 70
I tried to find a good tutorial to link to, but couldn't find anything that really covered all the issues, so I'm going to write it out step-by-step myself.
First, you need to clearly understand what you're trying to accomplish. This is different for the two types of zooming. I don't really like the terminology Mike Bostock has introduced, (it's not entirely consistent with non-d3 uses of the terms) but we might as well stick with it to be consistent with other d3 examples.
In "geometric zooming" you are zooming the entire image. Circles and lines get bigger as well as farther apart. SVG has an easy way to accomplish this through the "transform" attribute. When you set transform="scale(2)" on an SVG element, it is drawn as if everything was twice as big. For a circle, it's radius gets drawn twice a big, and it's cx and cy positions get plotted twice the distance from the (0,0) point. The entire coordinate system changes, so one unit is now equal to two pixels on screen, not one.
Likewise, transform="translate(-50,100)" changes the entire coordinate system, so that the (0,0) point of the coordinate system gets moved 50 units to the left and 100 units down from the top-left corner (which is the default origin point).
如果您同时翻译和缩放SVG元素,则顺序很重要.如果翻译是在缩放之前,则翻译是在原始单位中.如果翻译是在比例之后,则翻译是在缩放单位中.
该d3.zoom.behavior()方法创建一个侦听鼠标滚轮和拖动事件的函数,以及与缩放相关的触摸屏事件.它将这些用户事件转换为自定义"缩放"事件.
The zoom event is given a scale factor (a single number) and a translate factor (an array of two numbers), which the behaviour object calculates from the user's movements. What you do with these numbers is up to you; they don't change anything directly. (With the exception of when you attach a scale to the zoom behaviour function, as described later.)
For geometric zooming, what you usually do is set a scale and translate transform attribute on a <g> element that contains the content you want to zoom. This example implements that geometric zooming method on a simple SVG consisting of evenly placed gridlines:
http://jsfiddle.net/LYuta/2/
The zoom code is simply:
function zoom() {
console.log("zoom", d3.event.translate, d3.event.scale);
vis.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")"
);
}
Run Code Online (Sandbox Code Playgroud)
The zoom is accomplished by setting the transform attribute on "vis", which is a d3 selection containing a <g> element which itself contains all the content we want to zoom. The translate and scale factors come directly from the zoom event that the d3 behaviour created.
The result is that everything gets bigger or smaller -- the width of the gridlines as well as the spacing between them. The lines still have stroke-width:1.5; but the definition of what 1.5 equals on the screen has changed for them and anything else within the transformed <g> element.
For every zoom event, the translate and scale factors are also logged to the console. Looking at that, you'll notice that if you're zoomed out the scale will be between 0 and 1; if you're zoomed in it will be greater than 1. If you pan (drag to move) the graph, the scale won't change at all. The translate numbers, however, change on both pan and zoom. That's because the translate represents the position of the (0,0) point in the graph relative to the position of the top-left-corner of the SVG. When you zoom, the distance between (0,0) and any other point on the graph changes. So in order to keep the content under the mouse or finger-touch in the same position on the screen, the position of the (0,0) point has to move.
在该示例中,您还应注意其他一些事项:
我用.scaleExtent([min,max])方法修改了缩放行为对象.无论用户旋转多少轮,这都会对行为在缩放事件中使用的比例值设置限制.
变换是在<g>元素上,而不是在<svg>自身上.这是因为SVG元素作为一个整体被视为HTML元素,并具有不同的转换语法和属性.
The zoom behaviour is attached to a different <g> element, that contains the main <g> and a background rectangle. The background rectangle is there so that mouse and touch events can be observed even if the mouse or touch isn't right on a line. The <g> element itself doesn't have any height or width and so can't respond to user events directly, it only receives events from its children. I've left the rectangle black so you can tell where it is, but you can set it's style to fill:none; so long as you also set it to pointer-events:all;. The rectangle can't be inside the <g> that gets transformed, because then the area that responds to zoom events would also shrink when you zoom out, and possibly go out of sight off the edge of the SVG.
您可以<g>通过将缩放行为直接附加到SVG对象来跳过矩形和第二个元素,就像在此版本的小提琴中一样.但是,您通常不希望整个 SVG区域上的事件触发缩放,因此最好知道如何以及为何使用背景矩形选项.
这是相同的几何缩放方法,适用于您的力布局的简化版本:http:
//jsfiddle.net/cSn6w/5/
我减少了节点和链接的数量,并取消了节点拖动行为和节点扩展/折叠行为,因此您可以专注于缩放.我还改变了"摩擦"参数,这样图形停止移动需要更长的时间; 当它仍在移动时缩放它,你会发现一切都会像以前一样继续前进.
图像的"几何缩放"非常简单,只需很少的代码即可实现,并且可以通过浏览器实现快速,平滑的更改.但是,通常您想要放大图形的原因是因为数据点太靠近并且重叠.在这种情况下,只是让一切都变大无济于事.您希望在更大的空间内拉伸元素,同时保持各个点的大小相同.这就是"语义缩放"到位的地方.
在Mike Bostock使用该术语的意义上,图形的"语义缩放"是缩放图形的布局而不缩放单个元素. (注意,对于其他上下文,还有其他对"语义缩放"的解释.)
这是通过改变计算元素位置的方式,以及连接对象的任何线或路径的长度来完成的,而无需更改定义像素大小的基础坐标系,以便设置线宽或者形状或文字的大小.
您可以自己进行这些计算,使用translate和scale值根据这些公式定位对象:
zoomedPositionX = d3.event.translate[0] + d3.event.scale * dataPositionX
zoomedPositionY = d3.event.translate[1] + d3.event.scale * dataPositionY
Run Code Online (Sandbox Code Playgroud)
我已经使用这种方法在这个版本的网格线示例中实现语义缩放:http:
//jsfiddle.net/LYuta/4/
对于垂直线,它们最初定位如下
vLines.attr("x1", function(d){return d;})
.attr("y1", 0)
.attr("x2", function(d){return d;})
.attr("y2", h);
Run Code Online (Sandbox Code Playgroud)
在缩放功能中,变为
vLines.attr("x1", function(d){
return d3.event.translate[0] + d*d3.event.scale;
})
.attr("y1", d3.event.translate[1])
.attr("x2", function(d){
return d3.event.translate[0] + d*d3.event.scale;
})
.attr("y2", d3.event.translate[1] + h*d3.event.scale);
Run Code Online (Sandbox Code Playgroud)
水平线的变化类似.结果?线条的位置和长度在缩放时发生变化,线条不会变粗或变薄.
当我们尝试对力布局执行相同操作时,它会变得有点复杂.这是因为力布局图中的对象也在每次"tick"事件后重新定位.为了使它们位于正确的缩放位置,刻度定位方法必须使用缩放位置公式.意思就是:
The default scale will be 1, and the default translation will be [0,0], representing normal scale and no translation.
Here's what it looks like with semantic zooming on the simplified force layout:
http://jsfiddle.net/cSn6w/6/
The zoom function is now
function zoom() {
console.log("zoom", d3.event.translate, d3.event.scale);
scaleFactor = d3.event.scale;
translation = d3.event.translate;
tick(); //update positions
}
Run Code Online (Sandbox Code Playgroud)
It sets the scaleFactor and translation variables, then calls the tick function. The tick function does all the positioning: at initialization, after force-layout tick events, and after zoom events. It looks like
function tick() {
linkLines.attr("x1", function (d) {
return translation[0] + scaleFactor*d.source.x;
})
.attr("y1", function (d) {
return translation[1] + scaleFactor*d.source.y;
})
.attr("x2", function (d) {
return translation[0] + scaleFactor*d.target.x;
})
.attr("y2", function (d) {
return translation[1] + scaleFactor*d.target.y;
});
nodeCircles.attr("cx", function (d) {
return translation[0] + scaleFactor*d.x;
})
.attr("cy", function (d) {
return translation[1] + scaleFactor*d.y;
});
}
Run Code Online (Sandbox Code Playgroud)
Every position value for the circles and the links is adjusted by the translation and the scale factor. If this makes sense to you, this should be sufficient for your project and you don't need to use scales. Just make sure that you always use this formula to convert between the data coordinates (d.x and d.y) and the display coordinates (cx, cy, x1, x2, etc.) used to position the objects.
Where this gets complicated is if you need to do the reverse conversion from display coordinates to data coordinates. You need to do this if you want the user to be able to drag individual nodes -- you need to set the data coordinate based on the screen position of the dragged node. (Note that this wasn't working properly in either of your examples).
For geometric zoom, converting between screen position and data position can be down with d3.mouse(). Using d3.mouse(SVGElement) calculates the position of the mouse in the coordinate system used by that SVGElement. So if we pass in the element representing the transformed visualization, it returns coordinates that can be used directly to set the position of the objects.
The draggable geometric-zoom force-layout looks like this:
http://jsfiddle.net/cSn6w/7/
The drag function is:
function dragged(d){
if (d.fixed) return; //root is fixed
//get mouse coordinates relative to the visualization
//coordinate system:
var mouse = d3.mouse(vis.node());
d.x = mouse[0];
d.y = mouse[1];
tick();//re-position this node and any links
}
Run Code Online (Sandbox Code Playgroud)
For semantic zoom, however, the SVG coordinates returned by d3.mouse() no longer directly correspond to the data coordinates. You have to factor in the scale and translation. You do this by re-arranging the formulas given above:
zoomedPositionX = d3.event.translate[0] + d3.event.scale * dataPositionX
zoomedPositionY = d3.event.translate[1] + d3.event.scale * dataPositionY
Run Code Online (Sandbox Code Playgroud)
becomes
dataPositionX = (zoomedPositionX - d3.event.translate[0]) / d3.event.scale
dataPositionY = (zoomedPositionY - d3.event.translate[1]) / d3.event.scale
Run Code Online (Sandbox Code Playgroud)
The drag function for the semantic zoom example is therefore
function dragged(d){
if (d.fixed) return; //root is fixed
//get mouse coordinates relative to the visualization
//coordinate system:
var mouse = d3.mouse(vis.node());
d.x = (mouse[0] - translation[0])/scaleFactor;
d.y = (mouse[1] - translation[1])/scaleFactor;
tick();//re-position this node and any links
}
Run Code Online (Sandbox Code Playgroud)
This draggable semantic-zoom force-layout is implemented here:
http://jsfiddle.net/cSn6w/8/
That should be enough to get you back on track. I'll come back later and add an explanation of scales and how they make all these calculations easier.
...and I'm back:
Looking at all the data-to-display conversion functions above, doesn't it make you think "wouldn't it be easier to have a function to do this each time?" That's what the the d3 scales are for: to convert data values to display values.
You don't often see scales in the force-layout examples because the force layout object allows you to set a width and height directly, and then creates d.x and d.y data values within that range. Set the layout width and height to your visualization width and height, and you can use the data values directly for positioning objects in the display.
However, when you zoom in on the graph, you switch from having the entire extent of the data visible to only having a portion visible. So the data values no longer directly correspond to positioning values, and we need to convert between them. And a scale function would make that a lot easier.
In D3 terminology, the expected data values are the domain and the desired output/display values are the range. The initial domain of the scale will therefore by the expected maximum and minimum values from the layout, while the initial range will be the maximum and minimum coordinates on the visualization.
When you zoom, the relationship between domain and range changes, so one of those values will have to change on the scale. Luckily, we don't have to figure out the formulas ourselves, because the D3 zoom behaviour calculates it for us -- if we attach the scale objects to the zoom behaviour object using its .x() and .y() methods.
As a result, if we change the drawing methods to use the scales, then all we have to do in the zoom method is call the drawing function.
Here's the semantic zoom of the grid example implemented using scales:
http://jsfiddle.net/LYuta/5/
Key code:
/*** Configure zoom behaviour ***/
var zoomer = d3.behavior.zoom()
.scaleExtent([0.1,10])
//allow 10 times zoom in or out
.on("zoom", zoom)
//define the event handler function
.x(xScale)
.y(yScale);
//attach the scales so their domains
//will be updated automatically
function zoom() {
console.log("zoom", d3.event.translate, d3.event.scale);
//the zoom behaviour has already changed
//the domain of the x and y scales
//so we just have to redraw using them
drawLines();
}
function drawLines() {
//put positioning in a separate function
//that can be called at initialization as well
vLines.attr("x1", function(d){
return xScale(d);
})
.attr("y1", yScale(0) )
.attr("x2", function(d){
return xScale(d);
})
/* etc. */
Run Code Online (Sandbox Code Playgroud)
The d3 zoom behaviour object modifies the scales by changing their domain. You could get a similar effect by changing the scale range, since the important part is changing the relationship between domain and range. However, the range has another important meaning: representing the maximum and minimum values used in the display. By only changing the domain side of the scale with the zoom behaviour, the range still represents the valid display values. Which allows us to implement a different type of zoom, for when the user re-sizes the display. By letting the SVG change size according to the window size, and then setting the range of the scale based on the SVG size, the graph can be responsive to different window/device sizes.
Here's the semantic zoom grid example, made responsive with scales:
http://jsfiddle.net/LYuta/9/
I've given the SVG percentage-based height and width properties in CSS, which will over-ride the attribute height and width values. In the script, I've moved all the lines which relate to the display height and width into a function that checks the actual svg element for it's current height and width. Finally, I've added a window resize listener to call this method (which also triggers a re-draw).
Key code:
/* Set the display size based on the SVG size and re-draw */
function setSize() {
var svgStyles = window.getComputedStyle(svg.node());
var svgW = parseInt(svgStyles["width"]);
var svgH = parseInt(svgStyles["height"]);
//Set the output range of the scales
xScale.range([0, svgW]);
yScale.range([0, svgH]);
//re-attach the scales to the zoom behaviour
zoomer.x(xScale)
.y(yScale);
//resize the background
rect.attr("width", svgW)
.attr("height", svgH);
//console.log(xScale.range(), yScale.range());
drawLines();
}
//adapt size to window changes:
window.addEventListener("resize", setSize, false)
setSize(); //initialize width and height
Run Code Online (Sandbox Code Playgroud)
The same ideas -- using scales to layout the graph, with a changing domain from the zoom and a changing range from window resize events -- can of course be applied to the force-layout. However, we still have to deal with the complication discussed above: how to reverse the conversion from data values to display values when dealing with node-drag events. The d3 linear scale has a convenient method for that, too: scale.invert(). If w = scale(x) then x = scale.invert(w).
In the node-drag event, the code using scales is therefore:
function dragged(d){
if (d.fixed) return; //root is fixed
//get mouse coordinates relative to the visualization
//coordinate system:
var mouse = d3.mouse(vis.node());
d.x = xScale.invert(mouse[0]);
d.y = yScale.invert(mouse[1]);
tick();//re-position this node and any links
}
Run Code Online (Sandbox Code Playgroud)
The rest of the semantic zoom force-layout example, made responsive with scales is here:
http://jsfiddle.net/cSn6w/10/
I'm sure that was a lot longer a discussion than you were expecting, but I hope it helps you understand not only what you need to do, but also why you need to do it. I get really frustrated when I see code that has obviously been cut-and-pasted together from multiple examples by someone who doesn't actually understand what the code does. If you understand the code, it's a lot easier to adapt it to your needs. And hopefully, this will serve as a good reference for other people trying to figure out how to do similar tasks.
| 归档时间: |
|
| 查看次数: |
7555 次 |
| 最近记录: |