【问题标题】:Not able to create stacked bar chart?无法创建堆积条形图?
【发布时间】:2019-08-27 06:21:45
【问题描述】:

我正在使用 d3 图表创建堆积条形图。我在前端有按钮。单击按钮图表应显示。单击按钮时出现以下错误。

错误:属性 y:预期长度,“NaN”。

错误:属性高度:预期长度,“NaN”。

错误:属性宽度:预期长度,“10,900”。

import * as d3 from 'd3';

export class stackedbarchart  {

    sbar(box_id){

      d3.select(box_id)
      .select("svg")
      .remove();

      var margin = { top: 20, right: 160, bottom: 35, left: 30 };

      var width = 1100 - margin.left - margin.right,
          height = 500 - margin.top - margin.bottom;

      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 + ")");

      var data = [
        { country:"India", sales:100, profit:100, loss:10 },
        { country:"US", sales:100, profit:80, loss:40 },
        { country:"aus", sales:100, profit:70, loss:30 }
      ];

      var stack = d3.stack()
      .keys(["sales","profit","loss"])
      .order(d3.stackOrderNone)
      .offset(d3.stackOffsetNone);

      var series = stack(data);

      var x = d3.scaleOrdinal()
        .domain(series[0].map(function(d) {
          console.log(d)
           return d.x; }))
        .range([10, width-10], 0.02);

      var y = d3.scaleLinear()
        .domain([0, d3.max(series, function(d) {   
          return d3.max(d, function(d) { 
            return d.y0 + d.y; });  })])
        .range([height, 0]);

      var colors = ["b33040", "#d25c4d", "#f2b447", "#d9d574"];

      var yAxis = d3.axisLeft(y)
        .ticks(5)
        .tickSize(-width, 0, 0)
        .tickFormat( function(d) { return d } );

      var xAxis = d3.axisBottom(x)
      console.log(xAxis)

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

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

      var groups = svg.selectAll("g.cost")
        .data(series)
        .enter().append("g")
        .attr("class", "cost")
        .style("fill", function(d, i) { return colors[i]; });

      var rect = groups.selectAll("rect")    
        .data(function(d) { return d; })
        .enter()
        .append("rect")
        .attr("x", function(d) { return x(d.x); })
        .attr("y", function(d) { return y(d.y0 + d.y); })
        .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
        .attr("width", x.range())
        .on("mouseover", function() { tooltip.style("display", null); })
        .on("mouseout", function() { tooltip.style("display", "none"); })
        .on("mousemove", function(d) {
          var xPosition = d3.mouse(this)[0] - 15;
          var yPosition = d3.mouse(this)[1] - 25;
          tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
          tooltip.select("text").text(d.y);
        });

      var legend = svg.selectAll(".legend")
        .data(colors)
        .enter().append("g")
        .attr("class", "legend")
        .attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });

      legend.append("rect")
        .attr("x", width - 18)
        .attr("width", 18)
        .attr("height", 18)
        .style("fill", function(d, i) {return colors.slice().reverse()[i];});

      legend.append("text")
        .attr("x", width + 5)
        .attr("y", 9)
        .attr("dy", ".35em")
        .style("text-anchor", "start")
        .text(function(d, i) { 
          switch (i) {
            case 0: return "sales";
            case 1: return "profit";
            case 2: return "loss"; 
          }
        });

      var tooltip = svg.append("g")
        .attr("class", "tooltip")
        .style("display", "none");

      tooltip.append("rect")
        .attr("width", 30)
        .attr("height", 20)
        .attr("fill", "white")
        .style("opacity", 0.5);

      tooltip.append("text")
        .attr("x", 15)
        .attr("dy", "1.2em")
        .style("text-anchor", "middle")
        .attr("font-size", "12px")
        .attr("font-weight", "bold") ;

    }
}

任何有关正确方向的帮助或提示将不胜感激。

【问题讨论】:

  • 你能添加一个 jsfiddle/codepen/.. 例子吗?

标签: javascript angular typescript d3.js


【解决方案1】:

D3v4 的变化

在 d3v4 中进行的更改会破坏 d3v3 图表。 d3v3 中的堆栈具有 x 访问器,并使用 y0y 来表示堆叠图表每个段的开始值和结束值。结果如下所示:

 .append("rect")
  .attr("x", function(d) { return x(d.x); })
  .attr("y", function(d) { return y(d.y0 + d.y); })
  .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })

这种方法不适用于 d3v4+,但它似乎是您正在使用的方法。乍一看,您的代码可能基于this v3 代码。

d3v4+ 中的堆栈

让我们看看 d3.stack 在 d3v4+ 中产生了什么。

我们有我们的输入数据:

var data = [
  { country:"India", sales:100, profit:100, loss:10 },
  { country:"US", sales:100, profit:80, loss:40 },
  { country:"aus", sales:100, profit:70, loss:30 }
];

还有一个堆栈生成器:

var stack = d3.stack()
  .keys(["sales","profit","loss"])
  .order(d3.stackOrderNone)     // default value, does not need to be specified.
  .offset(d3.stackOffsetNone)   // default value, does not need to be specified

当我们将数据传递给堆栈生成器时,我们得到:

[/*  India     US        AUS   */
 [[0,  100],[0,  100],[0,  100]],  // Sales
 [[100,200],[100,180],[100,170]],  // Profit
 [[200,210],[180,220],[170,200]]   // Loss 
]

如果将数组排列为表格,则列代表原始数据数组(代表国家的系列)中的每个项目,行代表每个键。两个元素数组中的每一个都代表一个堆叠的条形段。

包含给定键的所有值的数组(例如,[[0,100],[0,100],[0,100]] 用于销售)还包含一个属性“键”,该属性“键”包含该键名称。

每个表示单个段的数组(例如:[0,100])都有一个属性data,它是原始数据数组中的一项(例如:{"country": "India","sales": 100,"profit": 100,"loss": 10})。

栈数组中的项数(行数)不等于系列数(本例中为国家),而是等于键数。 p>

创建音阶

您遇到的第一个问题是如何指定比例:

var x = d3.scaleOrdinal()
  .domain(series[0].map(function(d) { return d.x; }))
  .range([10, width-10], 0.02);

var y = d3.scaleLinear()
  .domain([0, d3.max(series, function(d) { return d3.max(d, function(d) { return d.y0 + d.y; });  })])
  .range([height, 0]);

因为堆栈没有创建任何y0yx 属性,这些将不起作用。

x刻度应该是带刻度(条形图一般使用带刻度带d3),我们可以直接使用原始数据数组创建域:

var x = d3.scaleBand()  // band scale
   .domain(data.map(function(d) {
         return d.country; }))  // return the country.
   .range([10, width-10]); 

y尺度需要访问d[0]和d[1]而不是d.y0和d.y:

var y = d3.scaleLinear()
   .domain([0, d3.max(series, function(d) {   
       return d3.max(d, function(d) { 
            return d[0] + d[1]; });  })]) // access stack values (not d.y, d.y0)
   .range([height, 0]);  

附加矩形

现在让我们转到添加矩形的位置:

    .append("rect")
    .attr("x", function(d) { return x(d.x); })
    .attr("y", function(d) { return y(d.y0 + d.y); })
    .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
    .attr("width", x.range())

由于 d.x 未定义,我们可以使用x(d.data.country)。在出现d.y0d.y 的地方,我们需要替换d[0]d[1]。最后x.range()几乎是图形的宽度,我们可以用x.bandwidth()代替,也就是单条的宽度。

以下是包含这些更新的代码(从定义边距的位置开始):

var margin = { top: 20, right: 160, bottom: 35, left: 30 };

var width = 500 - margin.left - margin.right,
    height = 300 - margin.top - margin.bottom;

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 + ")");

var data = [
  { country:"India", sales:100, profit:100, loss:10 },
  { country:"US", sales:100, profit:80, loss:40 },
  { country:"Aus", sales:100, profit:70, loss:30 },
  { country:"NZ", sales:100, profit:70, loss:30 }
];

var stack = d3.stack()
   .keys(["sales","profit","loss"])
   .order(d3.stackOrderNone)
   .offset(d3.stackOffsetNone);

var series = stack(data);


var x = d3.scaleBand()  // band scale
   .domain(data.map(function(d) {
         return d.country; }))  // return the country.
   .range([10, width-10])
   .padding(0.1); // space between (value between 0 and 1).

var y = d3.scaleLinear()
   .domain([0, d3.max(series, function(d) {   
       return d3.max(d, function(d) { 
            return d[0] + d[1]; });  })]) // access stack values (not d.y, d.y0)
   .range([height, 0]);  

// color scale:
var colors = d3.scaleOrdinal()
  .range(["b33040", "#d25c4d", "#f2b447", "#d9d574"])
  .domain(series.map(function(d) { return d.key; }));

var yAxis = d3.axisLeft(y)
   .ticks(5)
   .tickSize(-width, 0, 0)
   .tickFormat( function(d) { return d } );

var xAxis = d3.axisBottom(x)
     
svg.append("g")
   .attr("class", "y axis")
   .call(yAxis);

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

var groups = svg.selectAll("g.cost")
  .data(series)
  .enter().append("g")
  .attr("class", "cost")
  .style("fill", function(d) { return colors(d); });


// More changes:
var rect = groups.selectAll("rect")    
 .data(function(d) { return d; })
 .enter()
 .append("rect")
 .attr("x", function(d) { return x(d.data.country); })  // Access country.
 .attr("y", function(d) { return y(d[0] + d[1]); })
 .attr("height", function(d) { return y(d[0]) - y(d[0] + d[1]); })
 .attr("width", x.bandwidth())

// Minor changes:
var legend = svg.selectAll(".legend")
 .data(series)
 .enter().append("g")
 .attr("class", "legend")
 .attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });

legend.append("rect")
  .attr("x", width - 18)
  .attr("width", 18)
  .attr("height", 18)
  .style("fill", function(d) {
      return colors(d.key);
  });

legend.append("text")
  .attr("x", width + 5)
  .attr("y", 9)
  .attr("dy", ".35em")
  .style("text-anchor", "start")
  .text(function(d) { return d.key; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

为了简化 sn-p 我删除了鼠标交互

创造传奇

这可以稍微简化一下,这不是问题的一部分,不过,我已经将它与色标一起包含在此处。

【讨论】: