D3.js 跨多个图形同步缩放
D3.js Synchronized Zoom Across Multiple Graphs
我想制作一个与多个网页同步的折线图zoom/pan。
这些客户端具有相同的 Javascript 和 HTML 来源。
用户在客户端 A 上缩放或平移,消息是数据域的日期时间被发送到另一个和发送者(上图中的蓝线),并且接收到的客户端的图形将同时更改。当然,其他客户端也可以这样做。
它类似于聊天应用程序。
缩放功能为:
function zoomed() {
let msg = [];
let t = d3.event.transform; //1)
msg[0] = t.rescaleX(x2).domain()[0].toString(); //2)
msg[1] = t.rescaleX(x2).domain()[1].toString(); //2)
sendMessage(msg); //3)
}
- d3.event.transform 捕获鼠标事件。
- 转换为日期时间和字符串。
- 将新的比例域发送到服务器。
服务器将收到的数据发送给所有客户端:
function passiveZoom(rcv){
let leftend;
let rightend;
leftend = new Date(rcv[0]);
rightend = new Date(rcv[1]);
x.domain([leftend, rightend]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
- 从服务器收到包含新日期时间的消息。
- 设置新域,
- 更新折线图。
使用它可以缩放|平移所有折线图。
但是,它没有按要求工作。
如果我缩放|平移客户端 A,客户端 B 和客户端 C 将发生变化。没关系。
接下来,我缩放|平移客户端C(上图中的橙色线),所有图形都更改为初始比例和位置。为什么!?
假设鼠标坐标没有发送给客户端,但是发送鼠标位置坐标时应该如何处理?
Zoom|Pan 进程是从 mbostock 的块中分叉出来的:Brush & Zoom。发件人还使用 t.rescalex (x2).domain()
更改 X2 域的范围。
由于图中没有用到X2,所以我把X改成了x2,但是只能放大,看不懂X2的意思
你能告诉我如何同步所有客户端吗?
什么是 x2?
此代码适用于从 Simple line graph with v4 派生的客户端。
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="socket.io.js"></script>
<script>
//--- Network----
let rcvT;
let socket = io.connect('http://localhost:3000');
//Recive event from server
socket.on("connect", function() {});
socket.on("disconnect", function(client) {});
socket.on("S_to_C_message", function(data) {
rcvT = data.value;
passiveZoom(rcvT);
});
socket.on("S_to_C_broadcast", function(data) {
console.log("Rcv broadcast " + data.value);
rcvT = data.value;
passiveZoom(rcvT);
});
function sendMessage(msg) {
socket.emit("C_to_S_message", { value: msg }); //send to server
}
function sendBroadcast(msg) {
socket.emit("C_to_S_broadcast", { value: msg }); // send to server
}
// --------------------
// Set the dimensions of the canvas / graph
var margin = { top: 30, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.timeParse("%d-%b-%y");
// Set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleTime().range([height, 0]);
var x2 = d3.scaleTime().range([0, width]);
xAxis = d3.axisBottom(x)
.tickFormat(d3.timeFormat('%d-%b-%y'))
.ticks(5);
// var yAxis = d3.svg.axis().scale(y)
// .orient("left").ticks(5);
yAxis = d3.axisLeft(y);
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
x2.domain(x.domain());
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
//follow is zoom method------------------
zoom = d3.zoom()
.scaleExtent([1, 45])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
let msg = [];
let t = d3.event.transform;
msg[0] = t.rescaleX(x2).domain()[0].toString();
msg[1] = t.rescaleX(x2).domain()[1].toString();
sendMessage(msg);
}
function passiveZoom(rcv){
let start;
let end;
start = new Date(rcv[0]);
end = new Date(rcv[1]);
x.domain([start, end]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
</script>
</body>
如果你尝试这段代码,你应该在几个 bowser windows 和 运行 这个 node.js 脚本中执行。
var http = require("http");
var socketio = require("socket.io");
var fs = require("fs");
console.log("reflector start");
var server = http.createServer(function(req, res) {
res.writeHead(200, {"Content-Type":"text/html"});
var output = fs.readFileSync("./index.html", "utf-8");
res.end(output);
}).listen(process.env.VMC_APP_PORT || 3000);
var io = socketio.listen(server);
io.sockets.on("connection", function (socket) {
// send message to all
socket.on("C_to_S_message", function (data) {
io.sockets.emit("S_to_C_message", {value:data.value});
console.log("MSG "+data.value);
});
// boradcast send to all without sender
socket.on("C_to_S_broadcast", function (data) {
socket.broadcast.emit("S_to_C_broadcast", {value:data.value});
});
// disconnection
socket.on("disconnect", function () {
console.log("disconnect");
});
});
假设我理解了这个问题,
(第一个)问题是您没有更新 zoom
本身。
在使用 d3.zoom
的地方,它通常只是跟踪当前缩放状态,而不是直接在容器上应用变换。在画笔和缩放示例中,缩放是通过重新缩放数据来应用的——而不是通过对容器应用 SVG 转换。使用那个例子,我们可以看到,当我们刷的时候,我们也调用:
svg.select(".zoom").call(zoom.transform, someZoomTransform);
这个:
- 更新由
zoom
变量跟踪的缩放 state/identity
- 发出缩放事件,调用缩放函数(在画笔和缩放示例中,如果画笔触发它,该函数将被忽略)
如果我们删除此行,则通过刷亮进行的缩放状态更改不会更新缩放。刷到一个很小的域,然后放大看here.
您的代码就是这种情况,当您使用 zoomed
函数和 d3.event.transform
更新图表时,您没有更新缩放状态。您正在更新比例 - 但 zoom
未更新。
下面我将演示使用一个缩放更新另一个缩放。 注意:如果每个缩放函数都调用其他函数,我们将进入无限循环。有了brush和zoom我们可以看看trigger是不是brush来查看是否需要zoomed函数,下面我用d3.event.sourceEvent.target看看其他zoomed函数是否需要传播zoom:
var svg = d3.select("svg");
var size = 100;
var zoom1 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed1);
var zoom2 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed2);
var rect1 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 10)
.attr("y", 10)
.call(zoom1);
var rect2 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 300)
.attr("y", 10)
.call(zoom2);
function zoomed1() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect1.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect2.call(zoom2.transform,t);
}
}
function zoomed2() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect2.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect1.call(zoom2.transform,t);
}
}
rect {
cursor: pointer;
stroke: #ccc;
stroke-width: 10;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one rectangle to update the other.
<svg width="600" height="300"></svg>
您可能想知道为什么我硬编码大小,为什么我不只修改当前大小,而不是原始大小。答案是缩放变换比例是相对于原始状态的比例——而不是最后一个状态。例如,如果每次缩放比例加倍,并且我们放大 2 倍,则比例从:k=1 → k=2 → k=4。如果我们将形状的当前大小乘以新的比例,我们得到大小=1 → 大小=2 → 大小=8,这是不正确的(并且在缩小到 k=2 时,我们会将缩放量加倍内,而不是缩小)。变换是累积的已经,我们不想将它应用于应用了变换的值。
将变换应用于变换后的值而不是原始值,即使在缩小时也会导致缩放增加 - 这可能就是您在缩小时遇到问题的原因
所以,这让我想到了第二个问题,x2
。 x2
是参考,原值。是的,正如 Gerardo 指出的那样,它也是您示例中画笔的比例,但更重要的是,他指出该比例不会改变。因此,x2
非常适合用作参考比例,在给定缩放状态的情况下,我们可以使用它来转换 x
:
x.domain(t.rescaleX(x2).domain());
这里发生了什么? transform.rescaleX(x2)
不修改 x2
,它 "returns a copy of the continuous scale x whose domain is transformed [given a zoom transformation].(docs)"。我们获取副本的域并将其分配给 x
比例(范围当然保持不变),通过这样做,将变换应用到 x
比例。 这与我上面的 square/rectangles 片段基本相同,我在其中保留了形状初始大小的参考值并将变换应用到该值。
让我们用带有鳞片而不是普通形状的基本 graph/plot 来实际操作一下:
var svg = d3.select("svg");
var data = [[0,300],[1,20],[2,300]];
// Area generators:
var leftArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return leftX(d[0]); })
var rightArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return rightX(d[0]); })
// Scales
var leftX = d3.scaleLinear().domain([0,2]).range([0,250]);
var rightX = d3.scaleLinear().domain([0,2]).range([300,550]);
var leftX2 = leftX.copy();
var rightX2 = rightX.copy();
// Zooms
var leftZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", leftZoomed);
var rightZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", rightZoomed);
// Graphs
var leftGraph = svg.append("path")
.attr("d", leftArea(data))
.call(leftZoom);
var rightGraph = svg.append("path")
.attr("d", rightArea(data))
.call(rightZoom);
function leftZoomed() {
var t = d3.event.transform;
leftX.domain(t.rescaleX(leftX2).domain());
leftGraph.attr("d",leftArea(data));
if(d3.event.sourceEvent.target == this) {
rightGraph.call(rightZoom.transform,t);
}
}
function rightZoomed() {
var t = d3.event.transform;
rightX.domain(t.rescaleX(rightX2).domain());
rightGraph.attr("d",rightArea(data));
if(d3.event.sourceEvent.target == this) {
leftGraph.call(leftZoom.transform,t);
}
}
path {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one plot to update the other (zoom on the path area itself)
<svg width="600" height="300"></svg>
简单地说,要在一个页面或跨客户端同步多个可缩放比例图,您应该:
- 使用
selection.call(zoom.transform,transform)
更新每个缩放
- 使用当前变换和参考比例重新缩放每个比例。
我还没有深入研究过对多个客户端和套接字进行尝试。但是,以上内容应该有助于解释如何解决这个问题。但是,对于多个客户端,您可能需要修改我停止缩放事件无限循环的方式,在变换对象中使用或设置 属性 可能是最简单的方法。此外,正如 rioV8 指出的那样,您可能应该传递缩放参数(或者更好的是,d3.event 本身),而不是域,尽管仅域选项是可能的。
使用套接字时,我在发送对象时确实遇到了一些问题 - 我不熟悉 socket.io,也没有花大量时间寻找,但我得到了它来处理缩放和被动缩放功能如下:
function zoomed() {
let t = d3.event.transform;
// 1. update the scale, same as in brush and zoom:
x.domain(t.rescaleX(x2).domain());
// 2. redraw the graph and axis, same as in brush and zoom:
path.attr("d", area); // where path is the graph
svg.select(".xaxis").call(xAxis);
// 3. Send the transform, if needed:
if(t.alreadySent == undefined) {
t.alreadySent = true; // custom property.
sendMessage([t.k,t.x,t.y,t.alreadySent]);
}
}
function passiveZoom(rcv){
// build a transform object (since I was unable to successfully transmit the transform)
var t = d3.zoomIdentity;
t.k = rcv[0];
t.x = rcv[1];
t.y = rcv[2];
t.alreadySent = rcv[3];
//trigger a zoom event (invoke zoomed function with new transform data).
rect.call(zoom.transform,t); // where rect is the selection that zoom is called on.
}
我没有发送事件,而是发送了转换参数(仅)和一个标志,以注意被动缩放功能触发的缩放事件不需要再次向前传递。这原则上完全基于上述片段。
服务器端脚本没有修改。这是我使用的 client side - 它比您的代码更基本,因为我去掉了 y 刻度、y 轴、csv 数据源等。
我想制作一个与多个网页同步的折线图zoom/pan。
这些客户端具有相同的 Javascript 和 HTML 来源。 用户在客户端 A 上缩放或平移,消息是数据域的日期时间被发送到另一个和发送者(上图中的蓝线),并且接收到的客户端的图形将同时更改。当然,其他客户端也可以这样做。 它类似于聊天应用程序。
缩放功能为:
function zoomed() {
let msg = [];
let t = d3.event.transform; //1)
msg[0] = t.rescaleX(x2).domain()[0].toString(); //2)
msg[1] = t.rescaleX(x2).domain()[1].toString(); //2)
sendMessage(msg); //3)
}
- d3.event.transform 捕获鼠标事件。
- 转换为日期时间和字符串。
- 将新的比例域发送到服务器。
服务器将收到的数据发送给所有客户端:
function passiveZoom(rcv){
let leftend;
let rightend;
leftend = new Date(rcv[0]);
rightend = new Date(rcv[1]);
x.domain([leftend, rightend]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
- 从服务器收到包含新日期时间的消息。
- 设置新域,
- 更新折线图。
使用它可以缩放|平移所有折线图。
但是,它没有按要求工作。
如果我缩放|平移客户端 A,客户端 B 和客户端 C 将发生变化。没关系。
接下来,我缩放|平移客户端C(上图中的橙色线),所有图形都更改为初始比例和位置。为什么!?
假设鼠标坐标没有发送给客户端,但是发送鼠标位置坐标时应该如何处理?
Zoom|Pan 进程是从 mbostock 的块中分叉出来的:Brush & Zoom。发件人还使用 t.rescalex (x2).domain()
更改 X2 域的范围。
由于图中没有用到X2,所以我把X改成了x2,但是只能放大,看不懂X2的意思
你能告诉我如何同步所有客户端吗? 什么是 x2?
此代码适用于从 Simple line graph with v4 派生的客户端。
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="socket.io.js"></script>
<script>
//--- Network----
let rcvT;
let socket = io.connect('http://localhost:3000');
//Recive event from server
socket.on("connect", function() {});
socket.on("disconnect", function(client) {});
socket.on("S_to_C_message", function(data) {
rcvT = data.value;
passiveZoom(rcvT);
});
socket.on("S_to_C_broadcast", function(data) {
console.log("Rcv broadcast " + data.value);
rcvT = data.value;
passiveZoom(rcvT);
});
function sendMessage(msg) {
socket.emit("C_to_S_message", { value: msg }); //send to server
}
function sendBroadcast(msg) {
socket.emit("C_to_S_broadcast", { value: msg }); // send to server
}
// --------------------
// Set the dimensions of the canvas / graph
var margin = { top: 30, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.timeParse("%d-%b-%y");
// Set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleTime().range([height, 0]);
var x2 = d3.scaleTime().range([0, width]);
xAxis = d3.axisBottom(x)
.tickFormat(d3.timeFormat('%d-%b-%y'))
.ticks(5);
// var yAxis = d3.svg.axis().scale(y)
// .orient("left").ticks(5);
yAxis = d3.axisLeft(y);
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
x2.domain(x.domain());
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
//follow is zoom method------------------
zoom = d3.zoom()
.scaleExtent([1, 45])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
let msg = [];
let t = d3.event.transform;
msg[0] = t.rescaleX(x2).domain()[0].toString();
msg[1] = t.rescaleX(x2).domain()[1].toString();
sendMessage(msg);
}
function passiveZoom(rcv){
let start;
let end;
start = new Date(rcv[0]);
end = new Date(rcv[1]);
x.domain([start, end]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
</script>
</body>
如果你尝试这段代码,你应该在几个 bowser windows 和 运行 这个 node.js 脚本中执行。
var http = require("http");
var socketio = require("socket.io");
var fs = require("fs");
console.log("reflector start");
var server = http.createServer(function(req, res) {
res.writeHead(200, {"Content-Type":"text/html"});
var output = fs.readFileSync("./index.html", "utf-8");
res.end(output);
}).listen(process.env.VMC_APP_PORT || 3000);
var io = socketio.listen(server);
io.sockets.on("connection", function (socket) {
// send message to all
socket.on("C_to_S_message", function (data) {
io.sockets.emit("S_to_C_message", {value:data.value});
console.log("MSG "+data.value);
});
// boradcast send to all without sender
socket.on("C_to_S_broadcast", function (data) {
socket.broadcast.emit("S_to_C_broadcast", {value:data.value});
});
// disconnection
socket.on("disconnect", function () {
console.log("disconnect");
});
});
假设我理解了这个问题,
(第一个)问题是您没有更新 zoom
本身。
在使用 d3.zoom
的地方,它通常只是跟踪当前缩放状态,而不是直接在容器上应用变换。在画笔和缩放示例中,缩放是通过重新缩放数据来应用的——而不是通过对容器应用 SVG 转换。使用那个例子,我们可以看到,当我们刷的时候,我们也调用:
svg.select(".zoom").call(zoom.transform, someZoomTransform);
这个:
- 更新由
zoom
变量跟踪的缩放 state/identity - 发出缩放事件,调用缩放函数(在画笔和缩放示例中,如果画笔触发它,该函数将被忽略)
如果我们删除此行,则通过刷亮进行的缩放状态更改不会更新缩放。刷到一个很小的域,然后放大看here.
您的代码就是这种情况,当您使用 zoomed
函数和 d3.event.transform
更新图表时,您没有更新缩放状态。您正在更新比例 - 但 zoom
未更新。
下面我将演示使用一个缩放更新另一个缩放。 注意:如果每个缩放函数都调用其他函数,我们将进入无限循环。有了brush和zoom我们可以看看trigger是不是brush来查看是否需要zoomed函数,下面我用d3.event.sourceEvent.target看看其他zoomed函数是否需要传播zoom:
var svg = d3.select("svg");
var size = 100;
var zoom1 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed1);
var zoom2 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed2);
var rect1 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 10)
.attr("y", 10)
.call(zoom1);
var rect2 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 300)
.attr("y", 10)
.call(zoom2);
function zoomed1() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect1.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect2.call(zoom2.transform,t);
}
}
function zoomed2() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect2.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect1.call(zoom2.transform,t);
}
}
rect {
cursor: pointer;
stroke: #ccc;
stroke-width: 10;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one rectangle to update the other.
<svg width="600" height="300"></svg>
您可能想知道为什么我硬编码大小,为什么我不只修改当前大小,而不是原始大小。答案是缩放变换比例是相对于原始状态的比例——而不是最后一个状态。例如,如果每次缩放比例加倍,并且我们放大 2 倍,则比例从:k=1 → k=2 → k=4。如果我们将形状的当前大小乘以新的比例,我们得到大小=1 → 大小=2 → 大小=8,这是不正确的(并且在缩小到 k=2 时,我们会将缩放量加倍内,而不是缩小)。变换是累积的已经,我们不想将它应用于应用了变换的值。
将变换应用于变换后的值而不是原始值,即使在缩小时也会导致缩放增加 - 这可能就是您在缩小时遇到问题的原因
所以,这让我想到了第二个问题,x2
。 x2
是参考,原值。是的,正如 Gerardo 指出的那样,它也是您示例中画笔的比例,但更重要的是,他指出该比例不会改变。因此,x2
非常适合用作参考比例,在给定缩放状态的情况下,我们可以使用它来转换 x
:
x.domain(t.rescaleX(x2).domain());
这里发生了什么? transform.rescaleX(x2)
不修改 x2
,它 "returns a copy of the continuous scale x whose domain is transformed [given a zoom transformation].(docs)"。我们获取副本的域并将其分配给 x
比例(范围当然保持不变),通过这样做,将变换应用到 x
比例。 这与我上面的 square/rectangles 片段基本相同,我在其中保留了形状初始大小的参考值并将变换应用到该值。
让我们用带有鳞片而不是普通形状的基本 graph/plot 来实际操作一下:
var svg = d3.select("svg");
var data = [[0,300],[1,20],[2,300]];
// Area generators:
var leftArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return leftX(d[0]); })
var rightArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return rightX(d[0]); })
// Scales
var leftX = d3.scaleLinear().domain([0,2]).range([0,250]);
var rightX = d3.scaleLinear().domain([0,2]).range([300,550]);
var leftX2 = leftX.copy();
var rightX2 = rightX.copy();
// Zooms
var leftZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", leftZoomed);
var rightZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", rightZoomed);
// Graphs
var leftGraph = svg.append("path")
.attr("d", leftArea(data))
.call(leftZoom);
var rightGraph = svg.append("path")
.attr("d", rightArea(data))
.call(rightZoom);
function leftZoomed() {
var t = d3.event.transform;
leftX.domain(t.rescaleX(leftX2).domain());
leftGraph.attr("d",leftArea(data));
if(d3.event.sourceEvent.target == this) {
rightGraph.call(rightZoom.transform,t);
}
}
function rightZoomed() {
var t = d3.event.transform;
rightX.domain(t.rescaleX(rightX2).domain());
rightGraph.attr("d",rightArea(data));
if(d3.event.sourceEvent.target == this) {
leftGraph.call(leftZoom.transform,t);
}
}
path {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one plot to update the other (zoom on the path area itself)
<svg width="600" height="300"></svg>
简单地说,要在一个页面或跨客户端同步多个可缩放比例图,您应该:
- 使用
selection.call(zoom.transform,transform)
更新每个缩放
- 使用当前变换和参考比例重新缩放每个比例。
我还没有深入研究过对多个客户端和套接字进行尝试。但是,以上内容应该有助于解释如何解决这个问题。但是,对于多个客户端,您可能需要修改我停止缩放事件无限循环的方式,在变换对象中使用或设置 属性 可能是最简单的方法。此外,正如 rioV8 指出的那样,您可能应该传递缩放参数(或者更好的是,d3.event 本身),而不是域,尽管仅域选项是可能的。
使用套接字时,我在发送对象时确实遇到了一些问题 - 我不熟悉 socket.io,也没有花大量时间寻找,但我得到了它来处理缩放和被动缩放功能如下:
function zoomed() {
let t = d3.event.transform;
// 1. update the scale, same as in brush and zoom:
x.domain(t.rescaleX(x2).domain());
// 2. redraw the graph and axis, same as in brush and zoom:
path.attr("d", area); // where path is the graph
svg.select(".xaxis").call(xAxis);
// 3. Send the transform, if needed:
if(t.alreadySent == undefined) {
t.alreadySent = true; // custom property.
sendMessage([t.k,t.x,t.y,t.alreadySent]);
}
}
function passiveZoom(rcv){
// build a transform object (since I was unable to successfully transmit the transform)
var t = d3.zoomIdentity;
t.k = rcv[0];
t.x = rcv[1];
t.y = rcv[2];
t.alreadySent = rcv[3];
//trigger a zoom event (invoke zoomed function with new transform data).
rect.call(zoom.transform,t); // where rect is the selection that zoom is called on.
}
我没有发送事件,而是发送了转换参数(仅)和一个标志,以注意被动缩放功能触发的缩放事件不需要再次向前传递。这原则上完全基于上述片段。
服务器端脚本没有修改。这是我使用的 client side - 它比您的代码更基本,因为我去掉了 y 刻度、y 轴、csv 数据源等。