Подтвердить что ты не робот

D3.js временной ряд бесконечный свиток

Я работаю над линейной диаграммой временного ряда, которая позволяет пользователю прокручивать назад из настоящего. Я могу найти учебники по диаграммам d3.js в реальном времени, я могу найти учебники по масштабированию и панорамированию, и я могу найти учебники по использованию внешних источников данных. Мне сложно собрать все эти знания.

Вот поведение, которое я ищу:

  • График может перемещаться назад во времени (что означает, что линии, точки данных и оси перемещаются при перетаскивании мышью или пальцем).
  • Панорамирование должно влиять только на ось X, и никакого масштабирования не должно происходить.
  • Когда пользователь нажимает на диаграмму, загружается больше данных, что дает возможность бесконечной прокрутки
  • Я планирую буферизацию, по крайней мере, для одной дополнительной информации о "странице", которую пользователь может прокрутить (уже получил эту часть)
  • Я не думаю, что мне нужны переходы, потому что панорамирование диаграммы уже плавно переведет его.

Вот что я до сих пор работаю:

  // set up a zoom handler only for panning
  // by limiting the scaleExtent    
  var zoom = d3.behavior.zoom()
  .x(x)
  .y(y)
  .scaleExtent([1, 1])
  .on("zoom", pan);

  var loadedPage = 1; // begin with one page of data loaded
  var nextPage = 2; // next page will be page 2
  var panX = 0;

  function pan() 
  {
     if (d3.event) 
     {
        panX = d3.event ? d3.event.translate[0] : 0;

        // is there a better way to determine when
        // to load the next page?
        nextPage = panX / (width + margin.left + margin.right) + 2;
        nextPage = Math.floor(nextPage);

        // if we haven't loaded in the next page data
        // load it in so that the user can scroll into it
        if (nextPage > loadedPage) {

          console.log("Load a new page");
          loadedPage += 1;

          // load more data
          Chart.query( /*params will be here*/ ).then(
            function(response) {

              // append the new data onto the front of the array
              data = data.concat(response);
              console.log(data.length);

              // I need to add the new data into the line chart
              // but how do I make that work with the pan
              // logic from zoom?

         }
       );
     }
        // is this where I update the axes and scroll the chart?
        // What the best way to do that?

      }
    }

В этом коде я могу узнать, когда вытащить больше данных с сервера, но я не уверен, как вставлять данные в диаграмму таким образом, чтобы работать со смещением панорамирования. Я использую преобразование трансляции, или могу ли я обновить значение d пути моей линии?

Любые предложения приветствуются... также, если кто-нибудь знает о каких-либо демках, которые уже показывают бесконечно панорамирование данных временных рядов, это было бы очень оценено.

4b9b3361

Ответ 1

Как упоминалось в другом ответе, я знаю, что это очень старое сообщение, но, надеюсь, следующее поможет кому-то...

Я сделал pen, который, как мне кажется, затрагивает все упомянутые требования. Поскольку у меня не было реального API для использования, я создал некоторые данные, используя json-generator (отличный инструмент), включил его и отсортировал это в порядке убывания. Затем я использую встроенные методы slice и concat для принятия битов массива, data и добавления в переменную chart_data (подобно тому, как можно использовать api).

Важные разделы:

Как только вы создали свои масштабы, оси и точки (линии, бары и т.д.), вам необходимо создать поведение масштабирования, Как упоминалось в этом вопросе, сохранение ограничения масштаба на один и тот же номер с обеих сторон предотвращает масштабирование:

var pan = d3.behavior.zoom()
    .x(x_scale)
    .scale(scale)
    .size([width, height])
    .scaleExtent([scale, scale])
    .on('zoom', function(e) { ... });

Теперь, когда мы создали поведение, нам нужно его вызвать. Я также вычисляю, что будет делать перевод x на этот момент времени, now и программно панорамирование там:

// Apply the behavior
viz.call(pan);

// Now that we've scaled in, find the farthest point that
// we'll allow users to pan forward in time (to the right)
max_translate_x = width - x_scale(new Date(now));
viz.call(pan.translate([max_translate_x, 0]).event);

Оба способа предотвращения прокрутки пользователя в настоящее время и загрузка большего количества данных выполняются в обработчике события масштабирования:

...
.scaleExtent([scale, scale])
.on('zoom', function(e) {
     var current_domain = x_scale.domain(),
         current_max = current_domain[1].getTime();

     // If we go past the max (i.e. now), reset translate to the max
     if (current_max > now)
        pan.translate([max_translate_x, 0]); 

    // Update the data & points once user hits the point where current data ends
     if (pan.translate()[0] > min_translate_x) {
        updateData();
        addNewPoints();
     }

     // Redraw any components defined by the x axis
     x_axis.call(x_axis_generator);
     circles.attr('cx', function(d) { 
        return x_scale(new Date(d.registered));
     });
});

Другие функции довольно просты и могут быть найдены в нижней части пера. Я не знаю какой-либо встроенной функции D3, чтобы предотвратить прокрутку мимо настоящего, но я определенно открыт для обратной связи, если я пропустил более простой способ сделать что-то из этого.

Сообщите мне, если у вас возникли проблемы с просмотром pen или вам нужно уточнить что-то. Если у меня есть время, я обновлю это с помощью другой версии, демонстрирующей бесконечную линейку прокрутки.

P.S. В ручке я утешаю выбор и данные по мере их обновления. Я предлагаю открыть консоль, чтобы увидеть, что именно происходит.

Дополнительные примеры:

Добавление диаграммы строки - добавление анимации при загрузке новых данных может сделать ее более сексуальной

Ответ 2

Это слишком поздно, но ответ на тот случай, если кто-то нуждается снова. У меня была большая часть кода, готового для моей диаграммы рассеяния, поэтому загрузив это. Надеюсь, это поможет вам. Код создается как пробная версия, когда я изучал эти функции. Поэтому, пожалуйста, проверьте, прежде чем использовать.

Примечание: D3js панорамирование реализовано с поведением масштабирования, масштабирование отключено с помощью scaleExtent, Y панорамирование ограничено. Данные загружаются при достижении x крайностей. Проверьте ссылку Plunkr

// Code goes here

window.chartBuilder = {};
(function(ns) {

  function getMargin() {
    var margin = {
      top: 20,
      right: 15,
      bottom: 60,
      left: 60
    };
    var width = 960 - margin.left - margin.right;
    var height = 500 - margin.top - margin.bottom;
    return {
      margin: margin,
      width: width,
      height: height
    };
  }

  function getData() {
    var data = [
      [5, 3],
      [10, 17],
      [15, 4],
      [2, 8]
    ];
    return data;
  }

  //function defineScales(data, width, height) {
  //    var x = d3.scale.linear()
  //        .domain([0, d3.max(data, function (d) {
  //            return d[0];
  //        })])
  //        .range([0, width]);
  //
  //    var y = d3.scale.linear()
  //        .domain([0, d3.max(data, function (d) {
  //            return d[1];
  //        })])
  //        .range([height, 0]);
  //    return {x: x, y: y};
  //}
  function defineYScale(data, domain, range) {
    var domainArr = domain;
    if (!domain || domain.length == 0) {
      domainArr = [0, d3.max(data, function(d) {
        return d[1];
      })];
    }
    var y = d3.scale.linear()
      .domain(domainArr)
      .range(range);

    return y;
  }

  function defineXScale(data, domain, range) {
    var domainArr = domain;
    if (!domain || domain.length == 0) {
      domainArr = [d3.min(data, function(d) {
        return d[0];
      }), d3.max(data, function(d) {
        return d[0];
      })];
    }

    var x = d3.scale.linear()
      .domain(domainArr)
      .range(range);
    return x;
  }

  function getSvg(width, margin, height) {
    var chart = d3.select('body')
      .append('svg:svg')
      .attr('width', width + margin.right + margin.left)
      .attr('height', height + margin.top + margin.bottom)
      .attr('class', 'chart');
    return chart;
  }

  function getContainerGroup(chart, margin, width, height) {
    var main = chart.append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
      .attr('width', width)
      .attr('height', height)
      .attr('class', 'main');
    return main;
  }

  function renderXAxis(x, main, height) {
    var xAxis = d3.svg.axis()
      .scale(x)

    .orient('bottom');
    var xAxisElement = main.select('.x.axis');
    if (xAxisElement.empty()) {
      xAxisElement = main.append('g')
        .attr('transform', 'translate(0,' + height + ')')
        .attr('class', 'x axis')
    }
    xAxisElement.call(xAxis);

    return xAxis;
  }

  function renderYAxis(y, main) {
    var yAxis = d3.svg.axis()
      .scale(y)
      .orient('left');
    var yAxisElement = main.select('.y.axis');
    if (yAxisElement.empty()) {

      yAxisElement = main.append('g')
        .attr('transform', 'translate(0,0)')
        .attr('class', 'y axis');
    }
    yAxisElement.call(yAxis);
    return yAxis;
  }

  function renderScatterplot(main, data, scales) {
    var g = main.append("svg:g");
    var divTooltip = d3.select('.tooltip1');
    if (divTooltip.empty()) {
      divTooltip = d3.select('body').append('div')
        .attr('class', 'tooltip1')
        .style('opacity', 0);
    }

    g.selectAll("scatter-dots")
      .data(data, function(d, i) {
        return i;
      })
      .enter().append("svg:circle")
      .attr("cx", function(d, i) {
        return scales.x(d[0]);
      })
      .attr("cy", function(d) {
        return scales.y(d[1]);
      })
      .on('click', function(d) {

        // log(d.toString());


      })

    .attr("r", 8);
  }

  function addZoomRect(main, scales, zoom) {
    var zoomRect = main.append('rect')
      .attr('width', function() {
        return scales.x(d3.max(scales.x.domain()));
      })
      .attr('height', function() {
        return scales.y(d3.min(scales.y.domain()));
      })
      .attr('x', 0)
      .attr('y', 0)
      .attr('fill', 'transparent')
      .attr('stroke', 'red');
    if (zoom) {
      zoomRect.call(zoom);
    }
    return zoomRect;
  }

  function restrictYPanning(zoom) {
    var zoomTranslate = this.translate();
    this.translate([zoomTranslate[0], 0]);
  }

  function addXScrollEndEvent(scales, direction, data) {
    var zoomTranslate = this.translate();
    var condition;
    var currentDomainMax = d3.max(scales.x.domain());
    var dataMax = d3.max(data, function(d) {
      return d[0];
    });
    var currentDomainMin = d3.min(scales.x.domain());
    var dataMin =
      d3.min(data, function(d) {
        return d[0];
      });
    if (currentDomainMax > dataMax && direction === 'right') {
      //log('currentDomainMax ', currentDomainMax);
      //log('dataMax ', dataMax);
      //log('----------------');
      condition = true;
    }

    if (dataMin > currentDomainMin && direction === 'left') {
      //log('currentDomainMin ', currentDomainMin);
      //log('dataMin ', dataMin);
      //log('----------------');
      condition = true;
    }
    //var xRightLimit, xTranslate;
    //if (direction === 'right') {
    //    xRightLimit = scales.x(d3.max(scales.x.domain())) - (getMargin().width + 60);
    //
    //    xTranslate = 0 - zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
    //
    //    condition = xTranslate > xRightLimit;
    //} else {
    //    xRightLimit = scales.x(d3.min(scales.x.domain()));
    //
    //    xTranslate = zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
    //
    //    condition = xTranslate > xRightLimit;
    //}
    return condition;
  }

  function onZoom(zoom, main, xAxis, yAxis, scales, data) {
    //var xAxis = d3.svg.axis()
    //    .scale(scales.x)
    //    .orient('bottom');
    //var yAxis = d3.svg.axis()
    //    .scale(scales.y)
    //    .orient('left');
    //alert(data);
    var translate = zoom.translate();
    var direction = '';

    if (translate[0] < ns.lastTranslate[0]) {
      direction = 'right';
    } else {
      direction = 'left';
    }
    ns.lastTranslate = translate; //d3.transform(main.attr('transform')).translate  ;
    // log('zoom translate', ns.lastTranslate);
    // log('d3 Event translate', d3.event.translate);
    window.scales = scales;
    window.data = data;


    // ns.lastTranslate = translate;

    var divTooltip = d3.select('.tooltip1');
    if (divTooltip.empty()) {
      divTooltip = d3.select('body').append('div')
        .attr('class', 'tooltip1')
        .style('opacity', 0);
    }


    restrictYPanning.call(zoom);
    var xScrollEndCondition = addXScrollEndEvent.call(zoom, scales, direction, data);
    if (xScrollEndCondition) {
      if (zoom.onXScrollEnd) {

        zoom.onXScrollEnd.call(this, {
          'translate': translate,
          'direction': direction

        });
      }
    }


    main.select(".x.axis").call(xAxis);
    main.select(".y.axis").call(yAxis);
    var dataElements = main.selectAll("circle")
      .data(data, function(d, i) {
        return i;
      });

    dataElements.attr("cx", function(d, i) {
        return scales.x(d[0]);
      })
      .attr("cy", function(d) {
        return scales.y(d[1]);
      }).attr("r", 8);

    dataElements.enter().append("svg:circle")
      .attr("cx", function(d, i) {
        return scales.x(d[0]);
      })
      .attr("cy", function(d) {
        return scales.y(d[1]);
      }).on('click', function(d) {

        // log(d.toString());


      })

    .attr("r", 8);
    // log(direction);



  }

  //var xRangeMax;
  //var xRangeMin;
  ns.lastTranslate = [0, 0];

  /**
   * Created by Lenovo on 7/4/2015.
   */
  function log(titlee, msgg) {
    var msg = msgg;

    var title;
    if (titlee) {
      title = titlee + ':-->';
    }

    if (!msgg) {
      msg = titlee;
      title = '';
    } else {
      if (Array.isArray(msgg)) {
        msg = msgg.toString();
      }
      if ((typeof msg === "object") && (msg !== null)) {
        msg = JSON.stringify(msg);
      }
    }

    var tooltip = d3.select('.tooltip1');
    var earlierMsg = tooltip.html();
    var num = tooltip.attr('data-serial') || 0;
    num = parseInt(num) + 1;

    msg = '<div style="border-bottom:solid 1px green"><span style="color:white">' + num + ')</span><strong>' + title + '</strong> ' + decodeURIComponent(msg) + ' </div>';
    tooltip.html('<br>' + msg + '<br>' + earlierMsg).style({
        'color': 'lightGray',
        'background': 'darkGray',
        'font-family': 'courier',
        'opacity': 1,
        'max-height': '200px',
        'overflow': 'auto'
      })
      .attr('data-serial', num);
  }

  function addLoggerDiv() {
    var divTooltip = d3.select('.tooltip1');
    if (divTooltip.empty()) {
      divTooltip = d3.select('body').append('div')
        .attr('class', 'tooltip1')
        .style({
          'opacity': 0,
          'position': 'relative'
        });

      d3.select('body').append('div')
        .text('close')
        .style({
          'top': 0,
          'right': 0,
          'position': 'absolute',
          'background': 'red',
          'color': 'white',
          'cursor': 'pointer'
        })
        .on('click', function() {
          var thisItem = divTooltip;
          var txt = thisItem.text();
          var display = 'none';
          if (txt === 'close') {
            thisItem.text('open');
            display = 'none';
          } else {
            thisItem.text('close');
            display = 'block';
          }
          devTooltip.style('display', display);

        });

      d3.select('body').append('div')
        .text('clear')
        .style({
          'top': 0,
          'right': 20,
          'position': 'absolute',
          'background': 'red',
          'color': 'white',
          'cursor': 'pointer'
        })
        .on('click', function() {
          divTooltip.html('');
          divTooltip.attr('data-serial', '0');
        });
    }
  }



  $(document).ready(function() {
    var data = getData();
    var __ret = getMargin();
    var margin = __ret.margin;
    var width = __ret.width;
    var height = __ret.height;
    var scales = {};
    var xRangeMax = width;
    scales.x = defineXScale(data, [], [0, xRangeMax]);
    scales.y = defineYScale(data, [], [height, 0]);
    addLoggerDiv();
    var svg = getSvg(width, margin, height);
    var main = getContainerGroup(svg, margin, width, height);
    // draw the x axis
    var xAxis = renderXAxis(scales.x, main, height);
    // draw the y axis
    var yAxis = renderYAxis(scales.y, main);

    var thisobj = this;
    var zoom = d3.behavior.zoom().x(scales.x).y(scales.y).scaleExtent([1, 1]).on('zoom', function() {
      onZoom.call(null, zoom, main, xAxis, yAxis, scales, data);
    });
    zoom.onXScrollEnd = function(e) {
      var maxX = d3.max(data, function(d) {
        return d[0];
      });
      var minX = d3.min(data, function(d) {
        return d[0];
      });
      var incrementX = Math.floor((Math.random() * 3) + 1);
      var maxY = d3.max(data, function(d) {
        return d[1];
      })
      var minY = d3.min(data, function(d) {
        return d[1];
      })
      var incrementY = Math.floor((Math.random() * 1) + 16);
      var xRangeMin1, xRangeMax1, dataPoint;
      if (e.direction === 'left') {
        incrementX = incrementX * -1;
        dataPoint = minX + incrementX;
        // log('dataPoint ', dataPoint);

        //xRangeMin1 = d3.min(scales.x.range()) - Math.abs(scales.x(minX) - scales.x(dataPoint));
        xRangeMin1 = scales.x(dataPoint);
        xRangeMax1 = d3.max(scales.x.range());
      } else {
        dataPoint = maxX + incrementX;
        // log('dataPoint ', dataPoint);

        //xRangeMax1 = d3.max(scales.x.range()) + (scales.x(dataPoint) - scales.x(maxX));
        xRangeMax1 = d3.max(scales.x.range()) + 20; //scales.x(dataPoint);
        xRangeMin1 = d3.min(scales.x.range()) //e.translate[0];

      }
      data.push([dataPoint, incrementY]);

      //scales = defineScales(data, width + incrementX, height );
      //             scales.x = defineXScale(data, [], [xRangeMin1, xRangeMax1]);
      //             scales.y = defineYScale(data, [], [height, 0]);

      scales.x.domain(d3.extent(data, function(d) {
        return d[0];
      }));
      x = scales.x;
      y = scales.y;
      xAxis = renderXAxis(scales.x, main, height);
      // draw the y axis
      yAxis = renderYAxis(scales.y, main);
      zoom.x(scales.x).y(scales.y);


    }
    var zoomRect = addZoomRect(main, scales, zoom);


    renderScatterplot(main, data, scales);

  });
})(window.chartBuilder);
/* Styles go here */

.chart {
    font-family: Arial, sans-serif;
    font-size: 10px;
}

.axis path, .axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
}

.bar {
    fill: steelblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>