D3.js Синхронизированное увеличение на нескольких графиках - PullRequest
0 голосов
/ 19 февраля 2019

Я хочу создать линейную диаграмму для масштабирования / панорамы в синхронизации с несколькими веб-страницами.

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)
    }
  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);
    }
  1. Получено сообщение от сервера, который содержит новый деньвремя.
  2. установить новый домен,
  3. обновить линейные графики.

При этом возможно увеличение | панорамирование всех линейных графиков.

Однако это не работает должным образом.

Если я увеличиваю | панорамируюклиент A, клиент B и клиент C будут изменены.Это нормально.

Далее я масштабирую | панорамирование на клиенте C (оранжевая линия на рисунке выше). Все графики меняются на исходный масштаб и положение.Почему!?

Я предполагаю, что координаты мыши не отправляются клиентам, но как мне справиться с этим, когда я отправляю координаты позиции мыши?

Процесс Zoom | Panраздвоенный от блока mbostock: Brush & Zoom .Отправитель также меняет диапазон домена 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>

Если вы попробуете этот код, вы должны выполнить в нескольких окнах 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");
  });
});

1 Ответ

0 голосов
/ 20 февраля 2019

Если я понимаю проблему,

(первая) проблема заключается в том, что вы не обновляете (*) zoom.

Где используется d3.zoom, часто это простоотслеживает текущее состояние масштабирования, а не применяет преобразование непосредственно к контейнеру.В примере с кистью и масштабированием масштабирование применяется путем повторного масштабирования данных, а не путем применения SVG-преобразования к контейнеру.Используя этот пример, мы можем видеть, что когда мы чистим, мы также вызываем:

svg.select(".zoom").call(zoom.transform, someZoomTransform);

This:

  • обновляет состояние / идентичность масштабирования в соответствии с переменной zoom
  • испускает событие масштабирования, которое вызывает функцию масштабирования (которая в примере с кистью и масштабированием игнорируется, если ее запускает кисть)

Если мы удаляем эту строку, изменяется масштабсостояние, созданное кистью, не обновляет масштаб.Перейдите на очень маленький домен, затем увеличьте масштаб и увидите здесь .

Это тот случай, когда вы обновляете диаграмму с помощью функции 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 является ссылкой, исходное значение.Да, как отмечает Герардо, это также масштаб кисти в вашем примере, но, что более важно, он утверждает, что этот масштаб не меняется.Из-за этого 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.
}

Скореечем отправка события, я отправляю параметры преобразования (только) вместе с флагом, чтобы отметить, что событие масштабирования, вызываемое функцией пассивного масштабирования, не нужно снова передавать дальше.Это в принципе основано именно на приведенных выше фрагментах.

Нет изменений в сценарии на стороне сервера.Вот клиентская сторона , которую я использовал - она ​​более простая, чем ваш код, так как я удалил шкалы y, ось y, источник данных csv и т. Д.

...