当我的分组大小不同时如何制作分组条形图?

4

我正在尝试使用一些分组数据制作条形图。这是虚拟数据,但结构基本相同。数据:选举结果包括一堆候选人,按他们参选的地区组织,并列出总票数:

district,candidate,votes
Dist 1,Leticia Putte,3580
Dist 2,David Barron,1620
Dist 2,John Higginson,339
Dist 2,Walter Bannister,2866
[...]

我想创建一个条形或柱状图(两种都可以,但我的最终目标是水平的),按地区分组候选人。
Mike Bostock有一个出色的演示,但我很难为自己的目的进行智能翻译。我开始在https://jsfiddle.net/97ur6cwt/6/上进行解释,但我的数据组织方式有些不同——与其按组、行排列不同,我有一列设置类别。而且可能只有一个候选人,也可能有几个候选人。
如果组的大小不同,我可以分组吗?

在玩弄你的jsfiddle之前,只是想确认一下:你希望每个条形图簇根据“区域”进行分组,然后在每个簇内(每个区域内),你希望为每个候选人设置一个条形图,对吗?因此,“第1区”将只有1个条形图,“第2区”将有3个条形图,依此类推...这就是你想要的吗? - Gerardo Furtado
是的!那正是我想要的。刚刚编辑了问题以使其更加清晰明确。 - Amanda
好的,我稍后再试。这里最大的问题不是嵌套数据(这是必要的一步),而是创建具有不同宽度的<g>元素... - Gerardo Furtado
这很有道理。因此似乎类似于 bar_width = (width - (num_dist * group_pad)) / (num_candidates + num_dists) - cand_pad 然后对于每个组... group_width = (bar_width + cand_pad) * num_candidates - Amanda
问题在于<g>元素没有宽度和高度属性,它们会自动调整大小以适应其包含的内容。如果有这些属性,那就简单多了...我在答案中提供了一种解决方法。 - Gerardo Furtado
2个回答

8

我的答案与 @GerardoFurtado 类似,但我使用 d3.nest 来构建每个区域的域。这样可以避免硬编码数值,并使代码更加清晰:

y0.domain(data.map(function(d) { return d.district; }));

var districtD = d3.nest()
  .key(function(d) { return d.district; })
  .rollup(function(d){
    return d3.scale.ordinal()
        .domain(d.map(function(c){return c.candidate}))
      .rangeRoundBands([0, y0.rangeBand()], pad);
  }).map(data);

districtD 成为你在放置矩形时用于y轴的域名映射:

  svg.selectAll("bar")
      .data(data)
      .enter().append("rect")
      .style("fill", function(d,i) {
          return color(d.district);
      })
      .attr("x", 0)
      .attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
      .attr("height", function(d){
        return districtD[d.district].rangeBand();
      })
      .attr("width", function(d) {
        return x(d.votes);
      });

我要去开会了,下一步是清理轴并将候选人的名字放上去。


完整可运行代码:

var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {top: 10, right: 10, bottom: 50, left: 110},
  w = 800 - m.left - m.right,
  h = 500 - m.top - m.bottom,
  pad = .1;

var x = d3.scale.linear().range([0, w]);
y0 = d3.scale.ordinal().rangeRoundBands([0, h], pad);

var color = d3.scale.category20c();

var yAxis = d3.svg.axis()
    .scale(y0)
    .orient("left");

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .ticks(5)
    .tickFormat(d3.format("$,.0f"));


var svg = d3.select("#chart").append("svg")
  .attr("width", w + m.right + m.left + 100)
  .attr("height", h + m.top + m.bottom)
  .append("g")
  .attr("transform",
        "translate(" + m.left + "," + m.top + ")");

        // This moves the SVG over by m.left(110)
        // and down by m.top (10)


  d3.csv(url, function(error, data) {

    data.forEach(function(d) {
      d.votes = +d.votes;
    });
    
    y0.domain(data.map(function(d) { return d.district; }));
    districtD = d3.nest()
     .key(function(d) { return d.district; })
      .rollup(function(d){
       console.log(d);
        return d3.scale.ordinal()
         .domain(d.map(function(c){return c.candidate}))
          .rangeRoundBands([0, y0.rangeBand()], pad);
      })
      .map(data);    
  
    x.domain([0, d3.max(data, function(d) {
        return d.votes;
      })]);

      svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + h + ")")
          .call(xAxis)
          .selectAll("text")
          .style("text-anchor", "middle");

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

      svg.selectAll("bar")
          .data(data)
          .enter().append("rect")
          .style("fill", function(d,i) {
              return color(d.district);
          })
          .attr("x", 0)
          .attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
          .attr("height", function(d){
           return districtD[d.district].rangeBand();
          })
          .attr("width", function(d) {
            return x(d.votes);
            });

      svg.selectAll(".label")
        .data(data)
        .enter().append("text")
        .text(function(d) {
             return (d.votes);
             })
        .attr("text-anchor", "start")
           .attr("x", function(d) { return x(d.votes)})
           .attr("y", function(d) { return y0(d.district) +  districtD[d.district](d.candidate) + districtD[d.district].rangeBand()/2;})
        .attr("class", "axis");

  });
    .axis {
      font: 10px sans-serif;
    }
    .axis path, .axis line {
      fill: none;
      stroke: black;
      shape-rendering: crispEdges;
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>

另一种版本将所有柱的大小相同,并根据外部范围进行适当缩放:

<!DOCTYPE html>
<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
  <style>
    .label {
      font: 10px sans-serif;
    }
    
    .axis {
      font: 11px sans-serif;
      font-weight: bold;
    }
    
    .axis path,
    .axis line {
      fill: none;
      stroke: black;
      shape-rendering: crispEdges;
    }
  </style>
</head>

<body>
  <div id="chart"></div>
  <script>
    var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
    var m = {
        top: 10,
        right: 10,
        bottom: 50,
        left: 110
      },
      w = 800 - m.left - m.right,
      h = 500 - m.top - m.bottom,
      pad = .1, padPixel = 5;

    var x = d3.scale.linear().range([0, w]);
    var y0 = d3.scale.ordinal();

    var color = d3.scale.category20c();

    var yAxis = d3.svg.axis()
      .scale(y0)
      .orient("left");

    var xAxis = d3.svg.axis()
      .scale(x)
      .orient("bottom")
      .ticks(5)
      .tickFormat(d3.format("$,.0f"));


    var svg = d3.select("#chart").append("svg")
      .attr("width", w + m.right + m.left + 100)
      .attr("height", h + m.top + m.bottom)
      .append("g")
      .attr("transform",
        "translate(" + m.left + "," + m.top + ")");

    // This moves the SVG over by m.left(110)
    // and down by m.top (10)


    d3.csv(url, function(error, data) {

      data.forEach(function(d) {
        d.votes = +d.votes;
      });

      var barHeight = h / data.length;

      y0.domain(data.map(function(d) {
        return d.district;
      }));
      
      var y0Range = [0];
      districtD = d3.nest()
        .key(function(d) {
          return d.district;
        })
        .rollup(function(d) {
          var barSpace = (barHeight * d.length);
          y0Range.push(y0Range[y0Range.length - 1] + barSpace);
          return d3.scale.ordinal()
            .domain(d.map(function(c) {
              return c.candidate
            }))
            .rangeRoundBands([0, barSpace], pad);
        })
        .map(data);
      
      y0.range(y0Range);
      
      x.domain([0, d3.max(data, function(d) {
        return d.votes;
      })]);

      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis)
        .selectAll("text")
        .style("text-anchor", "middle");

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

      svg.selectAll("bar")
        .data(data)
        .enter().append("rect")
        .style("fill", function(d, i) {
          return color(d.district);
        })
        .attr("x", 0)
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate);
        })
        .attr("height", function(d) {
          return districtD[d.district].rangeBand();
        })
        .attr("width", function(d) {
          return x(d.votes);
        });

      var ls = svg.selectAll(".labels")
        .data(data)
        .enter().append("g");
        
      ls.append("text")
        .text(function(d) {
          return (d.votes);
        })
        .attr("text-anchor", "start")
        .attr("x", function(d) {
          return x(d.votes)
        })
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
        })
        .attr("class", "label");

      ls.append("text")
        .text(function(d) {
          return (d.candidate);
        })
        .attr("text-anchor", "end")
        .attr("x", -2)
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
        })
        .style("alignment-baseline", "middle")
        .attr("class", "label");

    });
  </script>
</body>

</html>


你的解决方案比我的好多了,唯一缺少的是为条形图设置一个固定高度。 - Gerardo Furtado
1
@GerardoFurtado,谢谢,你的方案也很不错。我考虑过固定的条形高度,但不确定OP想要什么。我认为最好的解决方案是单一的条形高度,并根据条形数量使y0范围不规则。如果我今天有几分钟时间,我会更新我的方案,并添加一个候选人姓名的第二个y轴。 - Mark

2
这是一个部分解决方案:https://jsfiddle.net/hb13oe4v/ 主要问题在于为每个具有可变域的组创建比例尺。与Bostock的示例不同,您的每个组(区域)中没有相同数量的条形图(候选人)。
因此,我必须使用一个解决方法。首先,我以最简单的方式嵌套了数据:
var nested = d3.nest()
    .key(function(d) { return d.district; })
    .entries(data);

然后根据需求创建相应的群组:
var district = svg.selectAll(".district")
    .data(nested)
    .enter()
    .append("g")
    .attr("transform", function(d) { return "translate(0," + y(d.key) + ")"; });

由于我无法创建 y1 轴(Bostock 的示例中是 x1 轴),因此我不得不硬编码条形图的高度(这本质上是不好的)。此外,为了使每个分组中的条形居中,我创造了这个疯狂的数学公式,将一根条形放在中心位置,下一根在它下方,再下一根在它上方,接着下一根在它下方,以此类推:

.attr("y", function(d, i) {
      if( i % 2 == 0){ return (y.rangeBand()/2 - 10) + (i/2 + 0.5) * 10}
      else { return (y.rangeBand()/2 - 10) - (i/2) * 10}
      })

当然,如果我们能为每个组设置一个可变比例,所有这些都可以避免并以更加优雅的方式编码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接