D3.JS时间序列折线图,具有实时数据、平移和缩放功能

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

本文关键字:功能 缩放 数据 实时 时间序列 JS 折线图 D3      更新时间:2023-09-26

FIDDLE<lt<lt;这有比问题中更多的最新代码。

我试图在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(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];
}

在下面的代码块中,我发出http请求以获取所有新数据,并将其添加到图表中。这很好用。现在我只需要为新数据整理泛逻辑——我想它会放在这里:

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);
        });
}

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

这似乎是可行的,但只是在第一次更新时。另一个问题是,如果我在尝试更新数据时使用鼠标进行任何平移/缩放,则该行将消失,并且不一定在下一次更新时返回。如果您能在代码中发现潜在的错误和/或应该如何做到这一点,我将非常感谢您的反馈。谢谢

更新1:

好吧,所以我已经弄清楚了自动平移的问题是什么。我意识到平移矢量需要有一个来自某个原点的累积值,所以一旦我使dx累积(dx = dx + (x(x2) - x(x1));,当添加新数据时,横向平移就开始工作。

更新2:

我现在加入了一个小提琴,它接近于我期望的数据检索和绘制的实际方式。它似乎在某种程度上按照我想要的方式工作,除了:

  1. X轴刻度线不随新数据平移
  2. 当我进行手动平移时,第一次平移时的行为会变得有点奇怪(向后跳一点(
  3. 如果我在尝试添加新数据时进行平移/缩放,则该行将消失("多重读取"问题?:S(

您可以保留所有值的全局数组,而不是绘制整个数据并剪裁不必要的部分,每次需要更新图形时只需对其进行切片。然后你可以更容易地重新计算你的x轴&价值观不利的一面是你不能轻易地进行过渡。

因此,您将有两个变量:

var globalOffset = 0;
var panOffset = 0;

globalOffset将在每次填充新值时更新,panOffset将在每次平移时更新。然后,您只需在绘图前对数据进行切片:

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

完整的解决方案请参阅fiddle。