简体   繁体   English

D3.JS 带实时数据、平移和缩放的时间序列折线图

[英]D3.JS time-series line chart with real-time data, panning and zooming

FIDDLE <<<< this has more up to date code than in the question. FIDDLE <<<< 这有比问题中更多的最新代码。

I am trying to create a real-time (live-updating) time-series chart in d3, that can also be panned (in X) and zoomed.我正在尝试在 d3 中创建一个实时(实时更新)时间序列图表,它也可以平移(在 X 中)和缩放。 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 :我的 d3.json() 请求应该返回如下所示的 JSON 数组:

[{"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.以下代码执行此操作,它还允许平移(在 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(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.在下面的代码块中,我发出一个 http 请求以获取所有新数据并将其添加到图表中。 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.我想要做的(我想这就是我应该做的)是获取新数据的时间值范围(即 newData[] 数组的第一个值和最后一个值之间的差异,将其转换为到像素,然后使用这个数字平移线。

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:更新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.我意识到平移向量需要有一个来自某个来源的累积值,所以一旦我使 dx 累积(dx = dx + (x(x2) - x(x1));然后横向平移开始工作时新数据添加。

UPDATE 2:更新 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 X 轴刻度线不随新数据平移
  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)如果我在尝试添加新数据时平移/缩放 - 该行消失(“多线程”问题?: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.然后您可以更轻松地重新计算 x 轴和值。 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. globalOffset将在每次填充新值,并及时更新panOffset每次平移的时间。 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.请参阅小提琴以获取完整的解决方案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM