d3.js时间序列无限滚动

37
我正在制作一个时间序列线图,用户可以从现在开始向后滚动。我能够找到有关实时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's 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's the best way to do that?

      }
    }

在这段代码中,我可以知道何时从服务器拉取更多数据,但我不确定如何以适合pan偏移量的方式将数据插入图表中。我是使用transform translate还是可以更新我的线路径的d值?欢迎任何建议......此外,如果有人知道任何已经显示无限遍历时间序列数据的演示,那将不胜感激。

1
使用哪种方法取决于您的要求。使用“transform”将使缩放和平移更容易(因为您只需要更新一个属性),但可能会成为内存问题。我不知道是否有无限平移演示,但您应该能够根据众多时间序列、平移等演示之一进行工作。 - Lars Kotthoff
@EmptyArray - 你解决了这个问题吗?我正在构建类似的东西,卡在同样的功能上。任何更新都会很感激。 - DeBraid
我已经得到了一个原型,尽管代码不太好看。在连接新数据后,我更新了域,更新了线路路径的“d”,并对线条和点进行了转换平移。主要问题是它只是无限地连接,因此您需要限制加载的数据点或页面数量,并“卸载”足够远离屏幕的数据。然后,您将跟踪用户所见左侧的minLoadedPage编号和右侧的maxLoadedPage编号。这有帮助吗? - EmptyArray
10
一个 jsfiddle 的示例会有所帮助。 - Ortomala Lokni
如果您正在寻求帮助,则应提供代码。@OrtomalaLokni 是正确的。 - Manvendra SK
2个回答

6

如其他答案所述,我知道这是一个非常古老的帖子,但希望以下内容能帮助某些人...

我制作了一个笔记本,我认为它符合所有提到的要求。由于我没有真正的API可用,我使用json-generator(很棒的工具)创建了一些数据,并将其包含在内,然后按降序排序。然后我使用内置的sliceconcat方法来取出数组data的一部分,并添加到chart_data变量中(类似于使用API的方式)。

重要部分:

一旦您创建了比例尺、轴和点(线条、条形图等),您需要创建缩放行为。如问题中所述,保持scaleExtent在两侧限制为相同的数字可以防止缩放:

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

现在我们已经创建了这个行为,需要调用它。同时我正在计算此刻的x轴位移,并通过编程进行平移:

// 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或需要澄清某些内容,请告诉我。如果我有时间,我会更新一个版本,演示无限滚动折线图。
附注:在笔中,我将选择和数据输出到控制台。建议打开控制台以查看发生了什么。

请问您能否更新您的折线图添加示例的链接?看起来链接已经失效了。我也在做类似的项目,所以想确认一下这是否是我需要的内容。 - Melvin Davis
@MelvinDavis 很抱歉。我已更新原始代码,从正确的时间点开始 (Date.now()很久以前了),并在每次加载新数据时添加了一个控制台语句。我之前清理了一下我的 codepen 账户,所以不幸的是第二个示例已经不存在了,但我认为第一个很好地回答了问题。 - Greg Venech

1
这可能有些晚了,但我还是回答一下以防有人需要。我已经准备好了大部分的散点图代码,上传到了网站上。希望对你有所帮助。这段代码是我学习这些特性时创建的试验版本,请在使用前进行检查。
注意: 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>

我已创建了zoom.onXScrollEnd函数以向数据中添加新点。
希望能有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接