【问题标题】:Having trouble with updating D3.js multiline chart更新 D3.js 多线图时遇到问题
【发布时间】:2016-01-29 17:02:17
【问题描述】:

我对 D3 还很陌生,遇到了一个问题。

我创建了一个简单的示例来说明我想要实现的目标。

首先,我有一个包含数据的 CSV 文件。在此示例中,它包含 2 家商店的某些流行手机几个月的手机销售数据。数据如下:

Store,Product,Month,Sold
London,iPhone,0,5
London,iPhone,1,4
London,iPhone,2,3
London,iPhone,3,5
London,iPhone,4,6
London,iPhone,5,7
London,Android Phone,0,3
London,Android Phone,1,4
London,Android Phone,2,5
London,Android Phone,3,7
London,Android Phone,4,8
London,Android Phone,5,9
London,Windows Phone,0,1
London,Windows Phone,1,2
London,Windows Phone,2,6
London,Windows Phone,3,7
London,Windows Phone,4,8
London,Windows Phone,5,5
Glasgow,iPhone,0,3
Glasgow,iPhone,1,4
Glasgow,iPhone,2,5
Glasgow,iPhone,3,2
Glasgow,iPhone,4,1
Glasgow,iPhone,5,3
Glasgow,Android Phone,0,4
Glasgow,Android Phone,1,3
Glasgow,Android Phone,2,7
Glasgow,Android Phone,3,4
Glasgow,Android Phone,4,3
Glasgow,Android Phone,5,6
Glasgow,Windows Phone,0,3
Glasgow,Windows Phone,1,6
Glasgow,Windows Phone,2,7
Glasgow,Windows Phone,3,5
Glasgow,Windows Phone,4,3
Glasgow,Windows Phone,5,4

我在JS/D3.js中写了如下代码:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    svg {
      font: 10px sans-serif;
    }

    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }

    .x.axis path {
      fill:none;
      stroke:#000;
      shape-rendering: crispEdges;
    }

    .line {
      fill: none;
      stroke-width: 1.5px;
    }

</style>
<body>
    <p id="menu"><b>Test</b>
    <br>Select Store: 
    <select>
        <option value="0">London</option>
        <option value="1">Glasgow</option>
   </select>
   </p>
<script src="http://d3js.org/d3.v3.js"></script>
<script>


var margin = {top: 20, right: 80, bottom: 30, left: 50},
    width = 900 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

// construct a linear scale for x axis
var x = d3.scale.linear()
    .range([0,width]);

// construct a linear scale for y axis
var y = d3.scale.linear()
    .range([height,0]);

// use the default line colours (see http://stackoverflow.com/questions/21208031/how-to-customize-the-color-scale-in-a-d3-line-chart for info on setting colours per line)
var color = d3.scale.category10();

// create the x axis and orient of ticks and labels at the bottom
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

// create the y axis and orient of ticks and labels on the left
var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

// line generator function
var line = d3.svg.line()
    //.interpolate("basis")
    .x(function(d) { return x(d.Month); })
    .y(function(d) { return y(d.Sold); });

var svg = d3.select("body").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 + ")");

    d3.csv("sampleData.csv", function(error, data) {


       color.domain(d3.keys(data[0]).filter(function(key) { return key == "Product"; }));

    // first we need to corerce the data into the right formats
        // map the data from the CSV file
      data = data.map( function (d) { 
        return { 
          Store: d.Store,
          Product: d.Product,
          Month: +d.Month,
          Sold: +d.Sold }; 
    });   


    // nest the data by regime and then CI
    var salesDataByStoreProduct = d3.nest()
          .key(function(d) { return d.Store; })
          .key(function(d) { return d.Product; })         
          .entries(data);

    // get the first regime's nest
    var salesDataForLondon;
    salesDataForLondon = salesDataByStoreProduct[0].values;

    console.log(salesDataForLondon);

    x.domain([d3.min(salesDataForLondon, function(d) { return d3.min(d.values, function (d) { return d.Month; }); }),
             d3.max(salesDataForLondon, function(d) { return d3.max(d.values, function (d) { return d.Month; }); })]);
    y.domain([0, d3.max(salesDataForLondon, function(d) { return d3.max(d.values, function (d) { return d.Sold; }); })]);


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

      svg.append("g")
          .attr("class", "y axis")
          .call(yAxis);


      var Products = svg.selectAll(".Product")
          .data(salesDataForLondon, function(d) { return d.key; })
        .enter().append("g")
          .attr("class", "Product");

      Products.append("path")
          .attr("class", "line")
          .attr("d", function(d) { return line(d.values); })
          .style("stroke", function(d) { return color(d.key); });

    function redraw()
    {
        var salesDataByStoreProduct = d3.nest()
          .key(function(d) { return d.Store; })
          .key(function(d) { return d.Product; })         
          .entries(data);

        var salesDataForGlasgow;
        salesDataForGlasgow = salesDataByStoreProduct[1].values;

        console.log(salesDataForGlasgow);

        x.domain([d3.min(salesDataForGlasgow, function(d) { return d3.min(d.values, function (d) { return d.Product; }); }),
                 d3.max(salesDataForGlasgow, function(d) { return d3.max(d.values, function (d) { return d.Product; }); })]);
        y.domain([0, d3.max(salesDataForGlasgow, function(d) { return d3.max(d.values, function (d) { return d.Sales; }); })]);


          svg.select("g")
              .call(xAxis);

          svg.select("g")
              .call(yAxis);


          var Products = svg.selectAll(".Product")
              .data(salesDataForGlasgow, function(d) { return d.key; })
            .enter().select("g")
              .attr("class", "Product");

          Products.select("path")
              .attr("d", function(d) { return line(d.values); })
              .style("stroke", function(d) { return color(d.key); });

    }

    /******************************************************/
 var menu = d3.select("#menu select")
    .on("change", change);   


function change() 
{
    clearTimeout(timeout);
    d3.transition()
    .duration(altKey ? 7500 : 1500);
    redraw();
}
var timeout = setTimeout(function() {
  menu.property("value", "ENEUSE").node().focus();
  change();
}, 7000);

var altKey;
d3.select(window)
    .on("keydown", function() { altKey = d3.event.altKey; })
    .on("keyup", function() { altKey = false; });

/******************************************************/

});                    

</script>
</body>
</html>

我在 CSV 文件中读取的位置,然后使用 D3 嵌套创建了如下所示的层次结构:

商店->产品->月份->销售

我希望图表按月显示伦敦的每个产品的销售数据,然后如果更改选择,则按月显示格拉斯哥的销售数据。

但是,虽然显示的是伦敦数据,但当我选择格拉斯哥时,图表并未更新。

为了排除任何过于明显的情况,我对每个商店的数组索引进行了硬编码。

我还添加了 console.log,可以看到正在使用正确的数据,但在调用 redraw() 时未在图表中呈现。

如果我怀疑与以下代码有关的问题原因的任何建议,我将不胜感激:

var Products = svg.selectAll(".Product")
.data(salesDataForGlasgow, function(d) { return d.key; })
.enter().select("g")
.attr("class", "Product");

Products.select("path")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.key); });

非常感谢您对改进或简化代码的任何其他建议。

【问题讨论】:

    标签: javascript d3.js


    【解决方案1】:

    正如您所怀疑的,问题确实出在以下两个陈述中:

          var Products = svg.selectAll(".Product")
              .data(salesDataForGlasgow, function(d) { return d.key; })
            .enter().select("g")
              .attr("class", "Product");
    
          Products.select("path")
              .attr("d", function(d) { return line(d.values); })
              .style("stroke", function(d) { return color(d.key); });
    

    Products 派生自 .enter() 选择。对于每个未连接到 DOM 中现有元素的数据项,这包含一个元素。更改图表以显示 Glasgow 数据时,没有要添加的新元素(London 数据与 Glasgow 数据一样具有三个产品),因此 .enter() 选择为空。

    相反,您需要从.Product 重新开始选择。将两个语句中的第二个更改为以下内容:

          svg.selectAll(".Product")
              .select("path")
              .attr("d", function(d) { return line(d.values); })
              .style("stroke", function(d) { return color(d.key); });
    

    我在您的代码中发现了一些其他问题。首先,设置x.domain()y.domain() 的三行最后使用了错误的属性名称。这会导致各种NaNs 出现在xy 比例尺的范围内,因为D3 尝试将产品名称或undefined 转换为数字。在这三行的末尾,将d.Product 替换为d.Month,将d.Sales 替换为d.Sold,以便它们与为伦敦设置xy 刻度范围的行一致销售数据。

    最后,您需要调整重置轴的方式。目前您正在使用以下代码:

          svg.select("g")
              .call(xAxis);
    
          svg.select("g")
              .call(yAxis);
    

    这将结束在所有 g 元素上调用 xAxisyAxis 函数,包括两个轴、所有轴刻度和三个图形线,因此图形看起来有点混乱。您已将 X 轴的 class 设置为 x axis,但由于类名中不能包含空格,因此您实际上已为轴指定了类 xaxis。 y 轴也会发生类似的情况。

    您需要做的是在调用xAxisyAxis 之前使用您分配给它们的类单独选择轴。将上面的行替换为以下内容:

          svg.select("g.x.axis")
              .call(xAxis);
    
          svg.select("g.y.axis")
              .call(yAxis);
    

    在您完成所有这些更改后,图表应该可以满足您的需求。

    【讨论】:

    • 非常感谢卢克。结合下面马克的代码,我已经能够解决这个问题。我将重新发布工作代码。
    【解决方案2】:

    虽然这不是一个代码审查网站,但您的代码可能需要进行大量重构。首先,您没有正确处理输入、更新、退出模式。其次,一旦你正确处理了模式,你就不需要单独的 redraw 函数。只需一个函数即可处理创建和更新。

    这是一个快速重构:

    <!DOCTYPE html>
    <meta charset="utf-8">
    <style>
      svg {
        font: 10px sans-serif;
      }
    
      .axis path,
      .axis line {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
      }
    
      .x.axis path {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
      }
    
      .line {
        fill: none;
        stroke-width: 1.5px;
      }
    </style>
    
    <body>
      <p id="menu"><b>Test</b>
        <br>Select Store:
        <select>
          <option value="London">London</option>
          <option value="Glasgow">Glasgow</option>
        </select>
      </p>
      <script src="http://d3js.org/d3.v3.js"></script>
      <script>
        var margin = {
            top: 20,
            right: 80,
            bottom: 30,
            left: 50
          },
          width = 900 - margin.left - margin.right,
          height = 500 - margin.top - margin.bottom;
    
        // construct a linear scale for x axis
        var x = d3.scale.linear()
          .range([0, width]);
    
        // construct a linear scale for y axis
        var y = d3.scale.linear()
          .range([height, 0]);
    
        // use the default line colours (see http://stackoverflow.com/questions/21208031/how-to-customize-the-color-scale-in-a-d3-line-chart for info on setting colours per line)
        var color = d3.scale.category10();
    
        // create the x axis and orient of ticks and labels at the bottom
        var xAxis = d3.svg.axis()
          .scale(x)
          .orient("bottom");
    
        // create the y axis and orient of ticks and labels on the left
        var yAxis = d3.svg.axis()
          .scale(y)
          .orient("left");
    
        // line generator function
        var line = d3.svg.line()
          //.interpolate("basis")
          .x(function(d) {
            return x(d.Month);
          })
          .y(function(d) {
            return y(d.Sold);
          });
    
        var svg = d3.select("body").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 + ")");
    
        // create gs for axis but don't add yet
        svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")");
    
        svg.append("g")
          .attr("class", "y axis");
    
        var salesDataByStoreProduct = null;
        d3.csv("data.csv", function(error, data) {
    
          color.domain(d3.keys(data[0]).filter(function(key) {
            return key == "Product";
          }));
    
          // first we need to corerce the data into the right formats
          // map the data from the CSV file
          data = data.map(function(d) {
            return {
              Store: d.Store,
              Product: d.Product,
              Month: +d.Month,
              Sold: +d.Sold
            };
          });
    
          // nest the data by regime and then CI
          salesDataByStoreProduct = d3.nest()
            .key(function(d) {
              return d.Store;
            })
            .key(function(d) {
              return d.Product;
            })
            .entries(data);
    
          draw("London");
    
        });
    
        function draw(which) {
    
          // get the first regime's nest
          var salesData = null;
          salesDataByStoreProduct.forEach(function(d) {
            if (d.key === which) {
              salesData = d.values;
            }
          });
    
          // set domains
          x.domain([d3.min(salesData, function(d) {
              return d3.min(d.values, function(d) {
                return d.Month;
              });
            }),
            d3.max(salesData, function(d) {
              return d3.max(d.values, function(d) {
                return d.Month;
              });
            })
          ]);
          y.domain([0, d3.max(salesData, function(d) {
            return d3.max(d.values, function(d) {
              return d.Sold;
            });
          })]);
    
          // draw axis
          svg.select(".x.axis").call(xAxis);    
          svg.select(".y.axis").call(yAxis);
    
          // this is the update selection
          var Products = svg.selectAll(".Product")
            .data(salesData, function(d) {
              return d.key;
            });
    
          // this is the enter selection
          Products
            .enter().append("g")
            .attr("class", "Product")
            .append("path");
    
          // now do update
          Products.selectAll("path")
            .attr("class", "line")
            .attr("d", function(d) {
              return line(d.values);
            })
            .style("stroke", function(d) {
              return color(d.key);
            });
        }
    
        var menu = d3.select("#menu select")
          .on("change", change);
    
        function change() {
          draw(this.options[this.selectedIndex].value);
        }
      </script>
    </body>
    
    </html>
    

    运行代码here.

    【讨论】:

    • 谢谢马克。尽管乍一看,您的代码似乎正在运行,但我认为它不是因为它呈现相同的数据而是重新调整轴。我正在尝试确定原因
    • 我通过更改解决了您建议的代码的问题: Products.selectAll("path") .attr("class", "line") .attr("d", function(d) { return line(d.values); }) .style("stroke", function(d) { return color(d.key); }); to: svg.selectAll(".Product") .select("path") .attr("class", "line") .attr("d", function(d) { return line(d.values); } ) .style("stroke", function(d) { return color(d.key); });这似乎解决了这个问题。再次感谢示例代码。
    • @JamesSaunders,我很抱歉。它的意思是输入Products.select("path").attr("d", function(d) ... 。请注意,这是select 而不是selectAll。请参阅here 了解我为什么搞砸的详细信息!有时回答问题太快了...在此处更正了 plunker:plnkr.co/edit/MGjzEiGOQi7sUeC6nUuP?p=preview
    猜你喜欢
    • 1970-01-01
    • 2019-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-24
    • 2020-11-29
    • 2020-11-10
    • 1970-01-01
    相关资源
    最近更新 更多