【问题标题】:d3js - How to plot a multi line chart chart for multiple groups of items that can be updated?d3js - 如何为可更新的多组项目绘制多折线图?
【发布时间】:2019-11-23 11:41:01
【问题描述】:

我是 d3 的新手,目前有一个简单的折线图,可以为单个组的某些数据显示两条线。我想做的是能够以灵活的方式为多个组创建多行。每个组将有几行,它们将为组父级进行颜色编码 - 在我的数据中,在这种情况下它是资产或股票代码

目前您可以在我的代码中看到我正在为每个路径手动创建一个数组,但我想如果我开始有 100 条路径,这可能会变得非常混乱。有没有更好的方法来做到这一点?

我尝试使用 d3.nest,但我一生都无法弄清楚数据应该如何,因此可以为每组行应用更新/进入/退出模式

这是我的 WIP 代码:


<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */

body { font: 12px Arial;}

path { 
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}

.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}

</style>
<body>
<div id="graphDiv"></div>

<!-- load the d3.js library -->    
<script src="https://d3js.org/d3.v5.min.js"></script>

<script>
    var data_set = [{
    'Date': '2009-03-23',
    'Raw': 25,
    'Raw2': 25,
    'Asset': 'A'
  },
  {
    'Date': '2009-03-24',
    'Raw': 28,
    'Raw2': 25.4,
    'Asset': 'A'
  },
  {
    'Date': '2009-03-25',
    'Raw': 26,
    'Raw2': 25.37,
    'Asset': 'B'
  },
  {
    'Date': '2009-03-26',
    'Raw': 22,
    'Raw2': 25.03,
    'Asset': 'B'
  },
  {
    'Date': '2009-03-27',
    'Raw': 19,
    'Raw2': 24.42,
    'Asset': 'C'
  },
  {
    'Date': '2009-03-28',
    'Raw': 23,
    'Raw2': 24.28,
    'Asset': 'D'
  }
]

var margin = {
  top: 30,
  right: 50,
  bottom: 30,
  left: 50
};
var svgWidth = 600;
var svgHeight = 1000;
var graphWidth = svgWidth - margin.left - margin.right;
var graphHeight = svgHeight - margin.top - margin.bottom;
// var parseDate = d3.timeParse("%d/%m/%Y");
var parseDate = d3.timeParse("%Y-%m-%d");

var x = d3.scaleTime().range([0, graphWidth]);
var y = d3.scaleLinear().range([graphHeight, 0]);
var z = d3.scaleOrdinal(d3.schemeCategory10); // for colours
var xAxis = d3.axisBottom().scale(x).ticks(10);
var yAxis = d3.axisLeft().scale(y).ticks(10);

// Need to create the lines manually for each bit of data
var line = d3.line()
  .x(function(d) {
    return x(d.date);
  })
  .y(function(d) {
    return y(d.y);
  });

// Creates the SVG area within the div on the dom 
// Just doing this once 
var svg = d3.select("#graphDiv")
  .append("svg")
  .attr("width", svgWidth)
  .attr("height", svgHeight)
var g = svg.append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")")
  .call(d3.zoom().on("zoom", function() {
    svg.attr("transform", d3.event.transform)
  }));

// Add the X Axis
g.append("g").attr("class", "x axis")
  .attr("transform", "translate(0," + graphHeight + ")")
  .call(xAxis);
// Text label for x axis 
g.append("text")
  .style("text-anchor", "middle")
  .text("timeseries dates")
  .attr("transform", "translate(" + (graphWidth / 2) + " ," + (graphHeight + margin.top) + ")");

// Add the Y Axis
g.append("g")
  .attr("class", "y axis")
  .call(yAxis);
// text label for the y axis
g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("y", 0 - margin.left)
  .attr("x", 0 - (graphHeight / 2))
  .attr("dy", "1em")
  .style("text-anchor", "middle")
  .text("price points");


function drawGraph(data_set) {

  let pathData = []
  //assume 2 paths
  pathData.push([])
  pathData.push([])

  // Pass in the data here 
  data_set.forEach(function(d) {

    let path0 = {}
    let path1 = {}

    path0.date = parseDate(d.Date)
    path1.date = parseDate(d.Date)

    path0.y = +d.Raw
    path1.y = +d.Raw2

    pathData[0].push(path0)
    pathData[1].push(path1)

  });

  x.domain(d3.extent(data_set, function(d) {
    return parseDate(d.Date);
  }));
  y.domain([
    d3.min(data_set, function(d) {
      return Math.min(d.Raw, d.Raw2)
    }),
    d3.max(data_set, function(d) {
      return Math.max(d.Raw, d.Raw2)
    })
  ]);

  var lines = g.selectAll(".path")
    .data(pathData)

  lines.exit().remove()

  var enter = lines.enter()
    .append("path")
    .attr("class", "path")
    .style("stroke", (d, i) => z(i))

  var merge = enter.merge(lines)
    .attr("d", line)

}

// display initial chart
window.onload = drawGraph(data_set)
// Push new data every 5 seconds for a specific date
var h = setInterval(function() {
  data_set.push({
    'Date': '2009-03-29',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  }, {
    'Date': '2009-03-30',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  }, {
    'Date': '2009-03-31',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  }, {
    'Date': '2009-04-01',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  }, {
    'Date': '2009-04-02',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  }, {
    'Date': '2009-04-03',
    'Raw': Math.floor(Math.random() * 50),
    'Raw2': Math.floor(Math.random() * 25),
    'Asset': 'A'
  });
  drawGraph(data_set);
}, 5000);
</script>
</body>```



【问题讨论】:

  • 所以如果我对每个日期的理解正确,你有两行由RawRaw2 键中的数字表示?并且您有一个 data_set 开始,但随后会有更多日期的新数据进入,您希望图表相应更新?
  • 嗨@Coola,非常感谢您抽出宝贵时间回复。是的,部分正确,我想要的是有两条线来表示 Raw 和 Raw2 值,但可能有几个组,即资产!因此,如果有资产 A 和资产 B,则将有 4 行,每个资产 2 行。理想情况下,它们也应该是颜色协调的!所以我正在寻找一种方法来绘制它并让它全部更新
  • 下面的代码解决了杂乱的问题。但是,您所说的更多是在数据绑定发生的性质上做的。目前您只为资产 A (Raw) 和资产 B (Raw2) 绘制 2 条线?您在问题中的数据结构没有 4 个单独行的数据。因此,您需要更新您的数据以占 4 行,以便我能够解决该问题。
  • 嗨@Coola,所有数据都已经存在于 data_set 中——你能帮忙解决这个问题还是我应该问另一个问题?非常感谢清理混乱的代码 :)
  • 也许我不理解用例?您将拥有 100 条 Raw 数据吗?或 100 个日期,您有 2 个 Raws 的数据?

标签: javascript html css d3.js svg


【解决方案1】:

您的代码中有几个问题:

  1. 你没有更新你的轴。因此,您的 x 轴显示的日期是 2000 年 1 月,而不是 2009 年 3 月/4 月的日期。要解决此问题,您必须将 drawGraph 函数中的轴更新为:

    svg.selectAll('.x.axis').call(xAxis);
    svg.selectAll('.y.axis').call(yAxis);
    
  2. 在循环的第一次迭代之后,您使用 setInterval 的测试会一遍又一遍地发送具有相同日期的数据。结果,您的data_set 将相同日期的数据传递到您的pathData。为了解决这个问题,最好将测试构建为:

    let newdata = [{
      'Date': '2009-03-29',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }, {
      'Date': '2009-03-30',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }, {
      'Date': '2009-03-31',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }, {
      'Date': '2009-04-01',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }, {
      'Date': '2009-04-02',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }, {
      'Date': '2009-04-03',
      'Raw': Math.floor(Math.random() * 50),
      'Raw2': Math.floor(Math.random() * 25),
      'Asset': 'A'
    }
    ]
    
    let counter = 0;
    // Push new data every 5 seconds for a specific date
    var h = setInterval(function () {
      if (counter < newdata.length) { // this limits the steps to only the length of the newdata
       data_set.push(newdata[counter]);
       counter++;
       drawGraph(data_set);
      }
    }, 3000);
    

Working block here.

但是,如果您预计新数据将在同一日期出现,则必须先更新您的 data_set,然后再使用以下命令在您的 setInterval 中调用 drawGraph

    // Push new data every 5 seconds for a specific date
    var h = setInterval(function () {
      //obtain the index from the data_set for the same date
      let index = data_set.findIndex((f) => f.Date === newdata[counter].Date);
      if (index === -1) { // if data with same date not found push the new data
        data_set.push(newdata[counter]);
      } else { //else if it is found replace the old data
        data_set[index] = newdata[counter];
      }
      counter++;
      drawGraph(data_set);
    }, 3000);

Working Block Here。请注意,在此示例中,newdata 在末尾还包含一些旧日期的更多数据。因此,您将看到线条根据新日期上下跳跃。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-21
    • 1970-01-01
    • 2019-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多