javascript - D3.js 跨多个图同步缩放

标签 javascript node.js d3.js zooming interaction

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

enter image description here

这些客户端具有相同的 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 的块中 fork 出来的:Brush & Zoom .发件人还使用 t.rescalex (x2).domain() 更改 X2 域的范围.
    由于绘图中没有使用X2,我将X更改为x2,但只能放大。我不明白X2的含义。

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

    此代码适用于从 Simple line graph with v4 fork 的客户端.
    <!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>
    

    如果你尝试这段代码,你应该在几个浏览器窗口中执行,并运行这个 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 跟踪的缩放状态/身份变量
  • 发出缩放事件,该事件调用缩放函数(在画笔和缩放示例中,如果画笔触发它,则忽略该函数)

  • 如果我们删除这条线,刷亮引起的缩放状态变化不会更新缩放。刷到一个很小的域,然后放大看here .

    当您使用 zoomed 更新图表时,您的代码就是这种情况。功能和 d3.event.transform您没有更新缩放状态。您正在更新比例 - 但是 zoom没有更新。

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

    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。如果我们将形状的当前大小乘以新的比例,我们会得到 size=1 → size=2 → size=8,这是不正确的(并且在缩小到 k=2 时,我们将放大两倍在,而不是缩小)。变换已经是累积的,我们不想将它应用到应用了变换的值。

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

    所以,这给我带来了第二个问题,x2 . x2是引用,原始值。是的,正如 Gerardo 指出的,它也是您示例中画笔的比例,但更重要的是,他指出此比例不会改变。正因为如此,x2非常适合用作引用比例,我们可以使用它来转换 x给定缩放状态:
    x.domain(t.rescaleX(x2).domain()); 
    

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

    让我们用一个带有刻度而不是普通形状的基本图形/绘图来看看这个:

    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 数据源等。

    关于javascript - D3.js 跨多个图同步缩放,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54757298/

    相关文章:

    javascript - Redux combineReducers 没有将状态传递给 reducer

    javascript - IE 中拖动锯齿状滚动

    javascript - facebook FB.Event.subscribe ('auth.authResponseChange' ) 不工作

    javascript - D3.js 剪辑路径切断了我的图表的边缘

    javascript - D3js - 对文件中的多个数据类别重用行定义/函数

    javascript - 如何在 React 中通配符

    javascript - Node.js:在二进制数组上迭代哪个命令

    javascript - 如何仅停止当前页面进程,而不停止整个 NodeJS 服务器

    javascript - Node 应用程序中的 MVC Controller ,这些是 Controller 吗?

    javascript - Uncaught TypeError : this. y.rangeBand 不是迁移到 D3 版本 4 后的函数