2013-07-09 3 views
33

Я работаю над линейной диаграммой временных рядов, которая позволяет пользователю прокручивать назад от настоящего. Я могу найти учебники по диаграммам 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'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? 

     } 
    } 

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

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

+0

Какой метод вы используете, зависит от ваших требований. Использование 'transform' упростит масштабирование и панорамирование (потому что вам нужно только обновить один атрибут), но может стать проблемой памяти. Я не знаю ни о каких бесконечно панорамирующих демках, но вы должны иметь возможность работать с одной из многочисленных демоверсий для временных рядов, панорамирования и т. Д. –

+0

@ EmptyArray - вы получили эту работу? Я строил нечто похожее и застрял на одной и той же функции. Любые обновления оцениваются. – DeBraid

+0

У меня есть прототип, хотя код был некрасивым. После конкатенации новых данных я обновил домен, обновил «d» пути линии и сделал трансовый перевод строки и точек. Основная проблема заключалась в том, что он просто конкатенировался навсегда, поэтому вам нужно было бы ограничить количество точек данных или загруженных страниц, а также «выгрузить» данные, которые достаточно далеко от экрана. Затем вы будете отслеживать номер minLoadedPage слева от того, что видит пользователь, а maxLoadedPage - вправо. Это помогает? – EmptyArray

ответ

1

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

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

// 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 для добавления новых точек к данным.

Надеюсь, это поможет.

3

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

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

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

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

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

Теперь, когда мы создали поведение, мы должны назвать его. Я также вычисляя, что х перевод будет на данный момент времени, 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. В ручке я утешаю выбор и данные по мере их обновления. Я предлагаю открыть консоль, чтобы увидеть, что именно происходит.

Другие примеры:

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

+0

Не могли бы вы обновить ссылку для примера добавления линейной диаграммы. Похоже, что ссылка истекло. Я также работал над подобным проектом, поэтому просто хотел проверить, действительно ли это то, что мне нужно. –

Смежные вопросы