D3.js跨多个图同步缩放

pro*_*et5 5 javascript interaction zoom node.js d3.js

我想制作折线图,以便与多个网页同步缩放/平移.

在此输入图像描述

这些客户端具有相同的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)
    }
Run Code Online (Sandbox Code Playgroud)
  1. d3.event.transform捕获鼠标事件.
  2. 转换为日期时间和字符串.
  3. 将新的扩展域发送到服务器.

服务器将收到的数据发送给所有客户

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);
    }
Run Code Online (Sandbox Code Playgroud)
  1. 收到来自服务器的消息,其中包含新的一天时间.
  2. 设置新域名,
  3. 更新折线图.

通过这种方式,可以缩放所有折线图.

但是,它不能按要求工作.

如果我在客户端A中进行缩放,则客户端B和客户端C将被更改.那没问题.

接下来,我在客户端C上放大(上图中的橙色线),所有图形都变为初始比例和位置.为什么!?

我假设鼠标坐标没有发送到客户端,但是当我发送鼠标的位置坐标时应该如何处理?

缩放|平移过程是从mbostock的块中分叉的:画笔和缩放.发件人还会更改X2域的范围t.rescalex (x2).domain().由于X2未在图纸中使用,我将X更改为x2,但我只能放大.我不明白X2的含义.

您能告诉我如何同步所有客户吗?什么是x2?

此代码适用于使用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>
Run Code Online (Sandbox Code Playgroud)

如果您尝试此代码,您应该执行几个bowser窗口,并运行此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");
  });
});
Run Code Online (Sandbox Code Playgroud)

And*_*eid 5

假设我理解了这个问题,

\n\n

(第一)问题是您没有更新zoom本身。

\n\n

在使用的地方d3.zoom,它通常只是跟踪当前的缩放状态,而不是直接在容器上应用变换。在画笔和缩放示例中,缩放是通过重新缩放数据来应用的,而不是通过对容器应用 SVG 转换。使用该示例,我们可以看到,当我们刷牙时,我们还会调用:

\n\n
svg.select(".zoom").call(zoom.transform, someZoomTransform);\n
Run Code Online (Sandbox Code Playgroud)\n\n

这:

\n\n
    \n
  • zoom更新变量跟踪的缩放状态/标识
  • \n
  • 发出缩放事件,该事件调用缩放函数(在画笔和缩放示例中,如果画笔触发它,则忽略该函数)
  • \n
\n\n

如果我们删除这条线,则通过刷涂对缩放状态所做的更改不会更新缩放。刷到一个很小的域,然后放大看这里

\n\n

当您使用函数更新图表zoomed并且d3.event.transform不更新缩放状态时,代码中就是这种情况。您正在更新秤 - 但zoom并未更新。

\n\n

下面我将演示如何使用一种缩放来更新另一种缩放。注意:如果每个缩放函数都调用其他函数,我们将进入无限循环。通过画笔和缩放,我们可以查看触发器是否为画笔,以查看是否需要缩放功能,下面我使用 d3.event.sourceEvent.target 来查看其他缩放功能是否需要传播缩放

\n\n

\r\n
\r\n
svg.select(".zoom").call(zoom.transform, someZoomTransform);\n
Run Code Online (Sandbox Code Playgroud)\r\n
var svg = d3.select("svg");\r\nvar size = 100;\r\nvar zoom1 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed1);\r\nvar zoom2 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed2);\r\n\r\nvar rect1 = svg.append("rect")\r\n  .attr("width", size)\r\n  .attr("height", size)\r\n  .attr("x", 10)\r\n  .attr("y", 10)\r\n  .call(zoom1);\r\nvar rect2 = svg.append("rect")\r\n  .attr("width", size)\r\n  .attr("height", size)\r\n  .attr("x", 300)\r\n  .attr("y", 10)\r\n  .call(zoom2);\r\n\r\nfunction zoomed1() {\r\n  var t = d3.event.transform;\r\n  var k = Math.sqrt(t.k);\r\n  rect1.attr("width",size/k).attr("height",size*k);\r\n  \r\n  if(d3.event.sourceEvent.target == this) {\r\n    rect2.call(zoom2.transform,t); \r\n  }\r\n}\r\nfunction zoomed2() {\r\n  var t = d3.event.transform;\r\n  var k = Math.sqrt(t.k);\r\n  rect2.attr("width",size/k).attr("height",size*k);\r\n   \r\n  if(d3.event.sourceEvent.target == this) {\r\n    rect1.call(zoom2.transform,t); \r\n  }\r\n}
Run Code Online (Sandbox Code Playgroud)\r\n
rect {\r\n    cursor: pointer;\r\n\tstroke: #ccc;\r\n\tstroke-width: 10;\r\n  }
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n\n

您可能想知道为什么我硬编码大小,为什么不直接修改当前大小,而不是原始大小。答案是缩放变换比例是相对于原始状态的比例——而不是最后状态。例如,如果每次缩放比例加倍,并且我们放大 2 倍,则比例为:k=1 \xe2\x86\x92 k=2 \xe2\x86\x92 k=4。如果我们将形状的当前大小乘以新的比例,我们得到 size=1 \xe2\x86\x92 size=2 \xe2\x86\x92 size=8,这是不正确的(并且在缩小到 k= 2,我们将放大的量加倍,而不是缩小)。变换已经是累积的,我们不想将其应用于已应用变换的值。

\n\n

将变换应用于变换后的值而不是原始值,即使在缩小时也会导致缩放增加 - 这可能就是您在缩小时遇到问题的原因

\n\n

所以,这让我想到了第二个问题x2x2是参考值,原始值。是的,正如 Gerardo 指出的那样,它也是示例中画笔的比例,但更重要的是,他指出该比例不会改变。因此,x2非常适合用作参考比例,我们可以使用它来转换x给定的缩放状态:

\n\n
x.domain(t.rescaleX(x2).domain()); \n
Run Code Online (Sandbox Code Playgroud)\n\n

这里会发生什么?transform.rescaleX(x2)不修改x2,它“返回连续比例 x 的副本,其域被变换[给定缩放变换]。(文档)”。我们获取副本的域并将其分配给x比例(范围当然保持不变),然后将变换应用于比例x这基本上与上面的正方形/矩形片段相同,其中我保留形状初始大小的参考值并将变换应用于该值。

\n\n

让我们用带有比例而不是简单形状的基本图形/图来看看它的实际效果:

\n\n

\r\n
\r\n
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>\r\nZoom on one rectangle to update the other.\r\n<svg width="600" height="300"></svg>
Run Code Online (Sandbox Code Playgroud)\r\n
x.domain(t.rescaleX(x2).domain()); \n
Run Code Online (Sandbox Code Playgroud)\r\n
var svg = d3.select("svg");\r\nvar data = [[0,300],[1,20],[2,300]];\r\n\r\n// Area generators:\r\nvar leftArea = d3.area().curve(d3.curveBasis)\r\n  .x(function(d) { return leftX(d[0]); })\r\n  \r\nvar rightArea = d3.area().curve(d3.curveBasis)\r\n  .x(function(d) { return rightX(d[0]); })\r\n\r\n// Scales\r\nvar leftX = d3.scaleLinear().domain([0,2]).range([0,250]);\r\nvar rightX = d3.scaleLinear().domain([0,2]).range([300,550]);\r\n\r\nvar leftX2 = leftX.copy();\r\nvar rightX2 = rightX.copy();\r\n\r\n// Zooms\r\nvar leftZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", leftZoomed);\r\nvar rightZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", rightZoomed);\r\n\r\n// Graphs\r\nvar leftGraph = svg.append("path")\r\n  .attr("d", leftArea(data))\r\n  .call(leftZoom);\r\n  \r\nvar rightGraph = svg.append("path")\r\n  .attr("d", rightArea(data))\r\n  .call(rightZoom);\r\n  \r\nfunction leftZoomed() {\r\n  var t = d3.event.transform;\r\n  leftX.domain(t.rescaleX(leftX2).domain());\r\n  leftGraph.attr("d",leftArea(data));\r\n        \r\n  if(d3.event.sourceEvent.target == this) {\r\n    rightGraph.call(rightZoom.transform,t); \r\n  }\r\n}\r\nfunction rightZoomed() {\r\n  var t = d3.event.transform;\r\n  rightX.domain(t.rescaleX(rightX2).domain());\r\n  rightGraph.attr("d",rightArea(data));\r\n        \r\n  if(d3.event.sourceEvent.target == this) {\r\n    leftGraph.call(leftZoom.transform,t); \r\n  }\r\n}
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n\n

简而言之,要在一个页面或跨客户端同步多个可缩放比例的图表,您应该:

\n\n
    \n
  • 更新每个缩放selection.call(zoom.transform,transform)
  • \n
  • 使用当前变换和参考比例重新调整每个比例。
  • \n
\n\n

我还没有深入尝试使用多个客户端和套接字。但是,上述内容应该有助于解释如何解决该问题。但是,对于多个客户端,您可能需要修改我停止缩放事件无限循环的方式,在变换对象中使用或设置属性可能是最简单的。另外,正如 rioV8 所指出的,您可能应该传递缩放参数(或者更好的是,d3.event 本身),而不是域,尽管仅域选项是可能的。

\n\n

对于套接字,我在发送对象时确实遇到了一些麻烦 - 我不熟悉 socket.io,也没有花大量时间寻找,但我让它与 Zoomed 和 PassiveZoom 函数一起使用,如下所示:

\n\n
function zoomed() {\n    let t = d3.event.transform;\n\n    // 1. update the scale, same as in brush and zoom:\n    x.domain(t.rescaleX(x2).domain());\n\n    // 2. redraw the graph and axis, same as in brush and zoom:\n    path.attr("d", area);  // where path is the graph\n    svg.select(".xaxis").call(xAxis);\n\n    // 3. Send the transform, if needed:\n    if(t.alreadySent == undefined) {\n      t.alreadySent = true; // custom property.\n      sendMessage([t.k,t.x,t.y,t.alreadySent]);\n    }\n}\n\nfunction passiveZoom(rcv){\n    // build a transform object (since I was unable to successfully transmit the transform)\n    var t = d3.zoomIdentity;\n    t.k = rcv[0];\n    t.x = rcv[1];\n    t.y = rcv[2];\n    t.alreadySent = rcv[3];\n    //trigger a zoom event (invoke zoomed function with new transform data).\n    rect.call(zoom.transform,t);  // where rect is the selection that zoom is called on.\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我没有发送事件,而是(仅)发送变换参数以及一个标志,以注意被动缩放功能触发的缩放事件不需要再次向前传递。这原则上完全基于上述片段。

\n\n

无需修改服务器端脚本。这是我使用的客户端- 它比您的代码更基本,因为我删除了 y 刻度、y 轴、csv 数据源等。

\n