D3.JS时间序列图与实时数据,平移和缩放 [英] D3.JS time-series line chart with real-time data, panning and zooming

查看:321
本文介绍了D3.JS时间序列图与实时数据,平移和缩放的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

FIDDLE <<<



我试图在d3中创建一个实时(实时更新)时间序列图表,它可以也可以平移(在X)和缩放。理想情况下,我想要的功能是,如果线的最右边部分是用户可见的,那么当新数据添加到图形中时,它会自动横向平移以包括新数据(不更改轴刻度)。



我的d3.json()请求应该返回JSON数组,如下所示:

  [{timestamp:1399325270,value: -  0.0029460209892230222598710528},{timestamp:1399325271,value: -  0.0029460209892230222598710528},{timestamp:1399325279, value: -  0.0029460209892230222598710528},....] 

一个请求,并获得所有可用的日期到现在,并绘制图 - 容易。下面的代码做到这一点,它也允许平移(在X)和缩放。

  var globalData; 
var lastUpdateTime =0;
var dataIntervals = 1;

var margin = {top:20,right:20,bottom:30,left:50},
width = document.getElementById(chartArea)。offsetWidth - margin.left - margin.right,
height = document.getElementById(chartArea)。offsetHeight - margin.top - margin.bottom;

var x = d3.time.scale()
.range([0,width]);

var y = d3.scale.linear()
.range([height,0]);

var xAxis = d3.svg.axis()
.scale(x)
.orient(bottom)
.ticks(10)
.tickFormat(d3.time.format('%X'))
.tickSize(1);
//.tickPadding(8);

var yAxis = d3.svg.axis()
.scale(y)
.orient(left);

var valueline = d3.svg.line()
.x(function(d){return x(d.timestamp);})
.y ){return y(d.value);});

var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1,4])
.on(zoom,zoomed);

var svg = d3.select(#chartArea)
.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 +))
.call(zoom);

svg.append(rect)
.attr(width,width)
.attr(height,height)
.attr类,情节); // ????

var clip = svg.append(clipPath)
.attr(id,clip)
.append(rect)
。 attr(x,0)
.attr(y,0)
.attr(width,width)
.attr(height,height);

var chartBody = svg.append(g)
.attr(clip-path,url(#clip));

svg.append(g)//添加X轴
.attr(class,x axis)
.attr(transform translate(0,+ height +))
.call(xAxis);

svg.append(g)//添加Y轴
.attr(class,y axis)
.call(yAxis);

svg.append(text)
.attr(transform,rotate(-90))
.attr(y,0 - left)
.attr(x,(0 - (height / 2)))
.attr(dy,1em)
.style ,middle)
.text(Return(%));

//通过检索从时间0开始绘制原始数据
d3.json(/ performance / benchmark / date / 0 / interval /+ dataIntervals,function(error,data) {
data.forEach(function(d){

lastUpdateTime = String(d.timestamp); //这将被调用,直到最后一个元素,因此将具有最后一个值元素
d.timestamp = new Date(d.timestamp);
d.value = d.value * 100;

});

globalData = data;

x.domain(d3.extent(globalData,function(d){return d.timestamp;}));
y.domain(d3.extent函数(d){return d.value;});

chartBody.append(path)//添加valueline路径
.datum(globalData)
。 attr(class,line)
.attr(d,valueline);

var inter = setInterval(function(){
updateData b $ b},5000);

});

var panMeasure = 0;
var oldScale = 1;
function zoomed(){
//onsole.log(d3.event);
d3.event.translate [1] = 0;
svg.select(。x.axis)。call(xAxis);

if(Math.abs(oldScale - d3.event.scale)> 1e-5){
oldScale = d3.event.scale;
svg.select(。y.axis)。call(yAxis);
}

svg.select(path.line)。attr(transform,translate(+ d3.event.translate [0] +,0)scale + d3.event.scale +,1));
panMeasure = d3.event.translate [0];

}

在下面的代码块中,以获取所有新数据并将其添加到图表中。这工作正常。现在我只需要整理出新的数据的泛逻辑 - 我想象会在这里:

  var dx = 0; 
function updateData(){

var newData = [];

d3.json(/ performance / benchmark / date /+ lastUpdateTime +/ interval /+ dataIntervals,function(error,data){
data.forEach ){

lastUpdateTime = String(d.timestamp); //必须在转换为Date()之前调用
d.timestamp = new Date(d.timestamp);
d.value = d.value * 100;

globalData.push(d);
newData.push(d);

});

// panMeasure将是用户已经平移多少的一些度量(即,如果图的最右边部分对于用户仍然可见)。
if(panMeasure <= 0) {//添加新数据和pan

x1 = newData [0] .timestamp;
x2 = newData [newData.length - 1] .timestamp;
dx = dx +(x(x1)-x(x2)); // dx需要累积

d3.select(path)
.datum(globalData)
。 attr(class,line)
.attr(d,valueline(globalData))
.transition()
.ease(linear)
。 attr(transform,translate(+ String(dx)+));

}

else {//否则只是添加新数据
d3.select(path)
.datum(globalData)
.attr(class,line)
.attr(d,valueline(globalData));
}

svg.select(。x.axis)。call(xAxis);

});
}

我想要做的(我想这就是我应该做的)获取新数据的时间值范围(即newData []数组的第一个值和最后一个值之间的差值),将其转换为像素,然后使用此数字平移线。



这似乎是排序的工作,但只有第一次更新另一个问题是,如果我做任何平移/缩放使用鼠标,而数据正在尝试更新,该行消失,不一定会在下一次更新的时候回来,我真的很感谢一些反馈,你可以发现在代码和/或这应该如何做的潜在错误。



UPDATE 1:



好吧,我想出了自动平移的问题是什么,因此一旦我做了dx累积(dx = dx +(x(x2)-x(x1)); ,那么横向平移开始为新数据添加时工作。




UPDATE 2:

http://jsfiddle.net/armensg/8Agje/3/\">小调,这是接近实际的方式,我希望数据被检索和绘制。它似乎工作在某种程度上我想要的方式除了:


  1. X轴刻度线不随新数据

  2. 当我进行手动平移时,行为在第一个平板上有点奇怪(向后跳一点)

  3. 如果我正在平移/缩放当尝试添加新数据时,该行会消失(多线程问题?:S)


解决方案

代替绘制整个数据和剪切不必要的部分,您可以保留所有值的全局数组,只要您需要更新图表即可。
那么你可以更容易地重新计算你的x轴&值。



因此,您将有两个变量:



<$> p $ p> var globalOffset = 0;
var panOffset = 0;

globalOffset 新值和 panOffset
然后你只需在绘制之前切分你的数据:

  var offset = Math.max globalOffset + panOffset); 
var graphData = globalData.slice(offset,offset + 100);

请参阅 fiddle 完全解决。


FIDDLE <<<< this has more up to date code than in the question.

I am trying to create a real-time (live-updating) time-series chart in d3, that can also be panned (in X) and zoomed. Ideally the functionality that I want is if the the right-most part of the line is visible to the user, then when new data is added to the graph, it will pan sideways automatically to include the new data (without changing axis scales).

My d3.json() requests should return JSON arrays that look like this :

[{"timestamp":1399325270,"value":-0.0029460209892230222598710528},{"timestamp":1399325271,"value":-0.0029460209892230222598710528},{"timestamp":1399325279,"value":-0.0029460209892230222598710528},....]

When the page first loads, I make a request and get all the available date up to now, and draw the graph - easy. The following code does this, it also allows panning (in X) and zooming.

var globalData;
var lastUpdateTime = "0";
var dataIntervals = 1;

var margin = { top: 20, right: 20, bottom: 30, left: 50 },
    width = document.getElementById("chartArea").offsetWidth - margin.left - margin.right,
    height = document.getElementById("chartArea").offsetHeight - margin.top - margin.bottom;

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .ticks(10)
    .tickFormat(d3.time.format('%X'))
    .tickSize(1);
    //.tickPadding(8);

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var valueline = d3.svg.line()
    .x(function (d) { return x(d.timestamp); })
    .y(function (d) { return y(d.value); });

var zoom = d3.behavior.zoom()
    .x(x)
    .y(y)
    .scaleExtent([1, 4])
    .on("zoom", zoomed);

var svg = d3.select("#chartArea")
    .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 + ")")
    .call(zoom);

svg.append("rect")
    .attr("width", width)
    .attr("height", height)
    .attr("class", "plot"); // ????

var clip = svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", width)
    .attr("height", height);

var chartBody = svg.append("g")
    .attr("clip-path", "url(#clip)");

svg.append("g")         // Add the X Axis
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

svg.append("g")         // Add the Y Axis
    .attr("class", "y axis")
    .call(yAxis);

svg.append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 0 - margin.left)
    .attr("x", (0 - (height / 2)))
    .attr("dy", "1em")
    .style("text-anchor", "middle")
    .text("Return (%)");

// plot the original data by retrieving everything from time 0
d3.json("/performance/benchmark/date/0/interval/" + dataIntervals, function (error, data) {
    data.forEach(function (d) {

        lastUpdateTime = String(d.timestamp); // this will be called until the last element and so will have the value of the last element
        d.timestamp = new Date(d.timestamp);
        d.value = d.value * 100;

    });

    globalData = data;

    x.domain(d3.extent(globalData, function (d) { return d.timestamp; }));
    y.domain(d3.extent(globalData, function (d) { return d.value; }));

    chartBody.append("path")        // Add the valueline path
        .datum(globalData)
        .attr("class", "line")
        .attr("d", valueline);

    var inter = setInterval(function () {
        updateData();
    }, 5000);

});

var panMeasure = 0;
var oldScale = 1;
function zoomed() {
    //onsole.log(d3.event);
    d3.event.translate[1] = 0;
    svg.select(".x.axis").call(xAxis);

    if (Math.abs(oldScale - d3.event.scale) > 1e-5) {
        oldScale = d3.event.scale;
        svg.select(".y.axis").call(yAxis);
    }

    svg.select("path.line").attr("transform", "translate(" + d3.event.translate[0] + ",0)scale(" + d3.event.scale + ", 1)");
    panMeasure = d3.event.translate[0];

}

In the following block of code, I make a http request to get all the new data and add this to the chart. This works fine. Now I just need to sort out the pan logic for the new data - which I imagine would go in here:

var dx = 0;
function updateData() {

    var newData = [];

        d3.json("/performance/benchmark/date/" + lastUpdateTime + "/interval/" + dataIntervals, function (error, data) {
            data.forEach(function (d) {

                lastUpdateTime = String(d.timestamp); // must be called before its converted to Date()
                d.timestamp = new Date(d.timestamp);
                d.value = d.value * 100;

                globalData.push(d);
                newData.push(d);

            });

            // panMeasure would be some measure of how much the user has panned (ie if the right-most part of the graph is still visible to the user.
            if (panMeasure <= 0) { // add the new data and pan

                x1 = newData[0].timestamp;
                x2 = newData[newData.length - 1].timestamp;
                dx = dx + (x(x1) - x(x2)); // dx needs to be cummulative

                d3.select("path")
                    .datum(globalData)
                    .attr("class", "line")
                    .attr("d", valueline(globalData))
                .transition()
                    .ease("linear")
                    .attr("transform", "translate(" + String(dx) + ")");

            }

            else { // otherwise - just add the new data 
                d3.select("path")
                    .datum(globalData)
                    .attr("class", "line")
                    .attr("d", valueline(globalData));
            }

            svg.select(".x.axis").call(xAxis);

        });
}

What I'm trying to do (I guess that's what I should be doing) is get the range of the time values for the new data (ie the difference between the first value and the last value of the newData[] array, convert this to pixels and then pan the line using this number.

This seems to sort of work, but only on the first update. Another problem is if I do any panning/zooming using the mouse while the data is trying to be updated, the line disappears and doesn't necessarily come back on the next update. I would really appreciate some feedback on potential errors you can spot in the code and/or how this should be done. Thanks.

UPDATE 1:

Okay, so I have figured out what the problem with the automatic panning was. I realised that the translate vector needs to have a cumulative value from some origin, so once I made dx cumulative (dx = dx + (x(x2) - x(x1)); then the sideways panning started working for when new data was added.

UPDATE 2:

I have now included a fiddle that is close to the actual way I expect data to be retrieved and plotted. It seems to work to some extent the way I want it except for:

  1. X-axis tick marks don't pan with the new data
  2. When I do manual pan, the behaviour becomes a bit strange on the first pan (jumps back a bit)
  3. If I am panning/zooming when the new data is trying to be added - the line disappears ('multi-threading' issues? :S)

解决方案

Instead of plotting the whole data and clipping the unnecessary parts, you could keep a global array of all values that you just slice each time you need to update the graph. Then you can more easily recompute your x axis & values. Downside is you cannot easily have a transition.

So, you would have two variables:

var globalOffset = 0;
var panOffset = 0;

globalOffset would be updated each time you populate new values and panOffset each time you pan. Then you just slice your data before plotting:

var offset = Math.max(0, globalOffset + panOffset);
var graphData = globalData.slice(offset, offset + 100);

See fiddle for complete solution.

这篇关于D3.JS时间序列图与实时数据,平移和缩放的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆