d3.js - 堆积条形图中的组2数据值

时间:2014-02-03 08:39:47

标签: d3.js

我有以下csv数据,

date,scanned,unscanned,compid,sbu
01/2014,10,90,101,f&r
02/2014,55,40,101,f&r
03/2014,45,23,101,f&r
04/2014,65,35,101,f&r
05/2014,100,20,101,f&r
06/2014,50,30,101,f&r
07/2014,10,90,101,f&r
08/2014,22,48,101,f&r
09/2014,0,100,101,f&r
10/2014,3,97,101,f&r
11/2014,22,60,101,f&r
12/2014,57,37,101,f&r
01/2014,30,100,101,ip
02/2014,130,10,101,ip

有没有办法我们可以将jan-2014的数据用于f& r和ip sbu值,并以堆积条显示值。例如,如果我选中一个复选框进行分组,我需要将扫描显示为30 + 10 = 40并且在x轴上为2014年1月的堆栈中未扫描为100 + 90 = 190。

构建堆栈栏的代码如下:

var w = 960,
    h = 500,
    p = [20, 50, 30, 20],

    x = d3.time.scale().range([1, 80]);
    y = d3.scale.linear().range([0, h - p[0] - p[2]]),
    z = d3.scale.ordinal().range(["#819FF7", "#CB491A"]),
    parse = d3.time.format("%m/%Y").parse,
    format = d3.time.format("%b-%y");

    var xAxis=d3.svg.axis()
          .scale(x)
          .orient("bottom")
          .ticks(d3.time.month, 1)
              //.ticks(12)



   xAxis.tickFormat(d3.time.format("%b-%y"));


    /*var yAxis = d3.svg.axis()
    .scale(y)
    .ticks(12)
    .orient("left");*/

var svg = d3.select("#container").append("svg:svg")
    .attr("width", w)
    .attr("height", h)
  .append("svg:g")
    .attr("transform", "translate(" + p[3] + "," + (h - p[2]) + ")");

d3.csv("scandata.csv", function(scan) {

  // Transpose the data into layers by cause.
  var scantypes = d3.layout.stack()(["scanned", "unscanned"].map(function(scans) {
    return scan.map(function(d) {
      return {x: parse(d.date), y: +d[scans],z:d.compid,typescan:scans};
    });
  }));




  // Compute the x-domain (by date) and y-domain (by top).
  x.domain(scantypes [0].map(function(d) { return d.x; }));
  y.domain([0, d3.max(scantypes[scantypes .length - 1], function(d) { return d.y0 + d.y; })]);

  // Add a group for each scan.
  var cause = svg.selectAll("g.scan")
      .data(scantypes)
    .enter().append("svg:g")
      .attr("class", "scan")
      .style("fill", function(d, i) { return z(i); })
      .style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); });

  // Add a rect for each date.
  var rect = cause.selectAll("rect")
      .data(Object)
    .enter().append("svg:rect")
      .attr("id", function(d,i) { return i + " comp " + d.z;  })
      .attr("x", function(d,i) { 
                        if (i ==0) 
                        { 
                            return x(d.x) ;
                        } 
                        else 
                        {
                            return x(d.x);
                        }} )
      .attr("y", function(d) { return -y(d.y0) - y(d.y); })
      .attr("height", function(d) { return y(d.y); })
      .attr("width", 30)//x.rangeBand()/2
    .on("mouseover", function(d){

                   return tooltip.style("visibility", "visible")
                                   .text((d.y))//d.typescan + " -  " + 
                                   .style("left", (d3.event.pageX) + "px") 
                                   .style("top", (d3.event.pageY - 20) + "px");      ;})
      .on("mousemove", function(d){

                      return tooltip.style("visibility", "visible")
                                   .text((d.y)) //d.typescan + " -  " + 
                                   .style("left", (d3.event.pageX) + "px") 
                                   .style("top", (d3.event.pageY - 20) + "px");      ;})

      .on("mouseout", function(d){return tooltip.style("visibility", "hidden");}) 
      .on("click", function(d){});



  var tooltip = d3.select("#container")
    .append("div")
    .style("position", "absolute")
    .style("z-index", "10")
    .style("visibility", "visible")
    .text("Scanned vs UnScanned")
    .style("font", "Arial")
      .style("color", "white")
    .style("font-size", "14px");

  //Add x-Axis
    svg.append("g")
    .attr("class", "x axis")
    //.attr("transform", function(d) { return "translate(0,80)"; })
    .call(xAxis)




  // Add a label per date.
  var label = svg.selectAll("text")
      .data(x.domain())
    .enter().append("svg:text")
      .attr("x", function(d) { return x(d.x); })//x.rangeBand() / 4
      .attr("y", 6)
      .attr("text-anchor", "middle")
      .attr("dy", ".71em")
      .text(format);

  // Add y-axis rules.
  var rule = svg.selectAll("g.rule")
      .data(y.ticks(5))
    .enter().append("svg:g")
      .attr("class", "rule")
      .attr("transform", function(d) { return "translate(0," + -y(d) + ")"; });

  rule.append("svg:line")
      .attr("x2", w - p[1] - p[3])
      .style("stroke", function(d) { return d ? "#fff" : "#000"; })
      .style("stroke-opacity", function(d) { return d ? .7 : null; });

  rule.append("svg:text")
      .attr("x", -15)
      .style("font-family","Arial 12px")
      .attr("dy", ".25em")
      .text(d3.format(",d"));

1 个答案:

答案 0 :(得分:19)

你似乎对SVG应该是什么样子感到困惑,因此不知道如何实现它。

SVG中的条形只是矩形。你需要告诉他们应该在哪里定位(这总是由酒吧的左上角定义)以及他们应该有多大。

要使条形图在堆叠图形中排列,您需要根据该堆栈的所有值确定它们的位置和大小。

我使用您的数据创建了一个非常简化的堆积条形图示例(只是已扫描/未扫描的数据,我没有通过sbu变量分离出来)。

这是工作example

以下是带注释的代码:

var width = 400;
    height = 500;

var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height);

var xScale = d3.scale.ordinal()
               .rangeRoundBands([0,width], 0.1);
var yScale = d3.scale.linear()
               .range([height, 0]);
        //note the inverted range, so that small values
        //scale to the bottom of the SVG

var data = d3.csv.parse( d3.select("pre#data").text() );
//this just grabs the text from the preformatted block
//and parses it as if it was a csv file
//in your real code, you would use d3.csv(filename, callbackFunction) 
//and the rest would be inside your callback function:

xScale.domain( data.map(function(d){return d.date;}) );
//the xScale domain is the list of all categorical values.
//The map function grabs all the date values from the data
//and returns them as a new array.
//I'm not worrying about parsing dates, since
//strings work fine with an ordinal scale
//(although you'd want to parse them if you wanted to reformat them).

yScale.domain( [0,
                d3.max(data,
                       function(d){
                           return +d.scanned + +d.unscanned;
                       })
                ]);
//The yScale domain starts at 0 (since it's a bar chart)
//and goes to the maximum *total* value for each date.
//The d3.max function finds the maximum for an array
//based on the result returned by the function for each
//element of the array.  This function just finds the sum
//of the scanned and unscanned values 
//(after converting them from strings to numbers with "+").


var dateGroups = svg.selectAll("g") 
        //create an empty selection of groups
   .data(data); //join to the data, each row will get a group

dateGroups.enter().append("g")
    //create the actual <g> elements for each row of data
    .attr("class", "dateGroup"); 
    //give them a meaningful class

//Now, within each group create a rectangle 
//for each category (scanned and unscanned).
//If you had lots of categories, you'd want to 
//use a nested selection and a second data join.
//However, to do that you'd need to do a lot of 
//data manipulation to create an array of 
//separate data objects for each category.
//
//With only two categories, it's easier to just
//do each one separately, and let them inherit
//the data from the parent <g> element.

//For the bottom of the stack:
var bottom = dateGroups.append("rect")
    .attr("class", "data scanned");

bottom.attr("y", function(d){
        return yScale(+d.scanned);
    } )
        //y is the TOP of the rectangle
        //i.e., the position of this data value
        //on the scale
    .attr("height", function(d){
        return Math.abs( yScale(+d.scanned) - yScale(0) );
        //The height of the rectangle is the difference between
        //its data value and the zero line.
        //Note that the yScale value of zero is 
        //bigger than the yScale value of the data
        //because of the inverted scale, so we use
        //absolute value to always get a positive height.
    } );

//For the top of the stack:
var top = dateGroups.append("rect")
    .attr("class", "data unscanned");

top.attr("y", function(d){
        return yScale(+d.unscanned + +d.scanned);
    } )
        //y is the TOP of the rectangle
        //i.e., the position on the scale of 
        //the *total* of the two data categories
    .attr("height", function(d){
        return Math.abs( yScale(+d.unscanned) - yScale(0) );
        //The height of this bar is just based on 
        //its value.  However, this could also be 
        //written as
        //Math.abs(+yScale(+d.scanned + +d.unscanned) 
        //              - yScale(+d.scanned) )
        //i.e., as the difference between the total
        //(top of the bar) and the other category's 
        //value (bottom of the bar)
    } );

//The x value and width are the same for both bars
//so we can re-select all the rectangles and 
//set these attributes at the same time:
dateGroups.selectAll("rect.data")
    .attr("x", function(d){
        return xScale(d.date);
    })
    .attr("width", xScale.rangeBand() );
    //don't need a function for width,
    //since it doesn't depend on the data

一旦您确定了解该程序的每一步发生了什么,那么您就可以开始添加轴或工具提示等额外功能。您也可以很好地调整代码以使用多个类别,但在这种情况下,您可能希望创建一个表示每个类别的数据的子数组,并使用嵌套选择来创建矩形。这是大多数堆积条形图示例使用的方法;在使用这个非常简化的版本后,他们希望更容易理解。

修改

如果您知道每个堆栈中只有两个值,并且数据表的同一行中的两个值都有数据,则上述解决方案有效。如果每个堆栈中可能有多个条,和/或它们来自数据表的多行,则需要使用nested selection将数据与各个条形匹配。

为了使用嵌套选择方法,首先必须对数据进行一些操作。您需要将其转换为嵌套数组格式。外部数组必须代表每个堆栈,每个堆栈数据对象必须包含一个代表每个 bar 的子数组。

如何制作嵌套数组取决于您的原始数据格式。当您要堆叠的值位于不同的行中时,d3.nest运算符可以将它们组合成子数组。当堆栈值与数据表的同一行中的数字不同时,您必须使用forEach()函数循环遍历数据的所有行,并从每个行构造一个数组。

在您的示例中,您希望同时执行这两项操作,因此我们将结合嵌套操作和forEach操作。同时,我们将计算堆栈的运行总计:为了正确定位每个柱,我们不仅要知道它自己的计数,还需要知道堆栈中它下面所有值的总数。

这是一个有效的fiddle

数据操作代码是

/*Nest data by date string */
var nestFunction = d3.nest().key( function(d){return d.date;} );
var nestedData = nestFunction.entries(data);

var maxTotal = 0; //maximum count per date, 
                  //for setting the y domain
nestedData.forEach(function(dateGroup) {
   //for each entry in the nested array,
   //each of which contains all the rows for a given date,
   //calculate the total count,
   //and the before-and-after counts for each category.  

    dateGroup.date = dateGroup.key;
    //just using the original strings here, but you could
    //parse the string date value to create a date object

    dateGroup.bars = [];
        //create an array to hold one value for each bar
        //(i.e., two values for each of the original rows)

    var total = 0; //total count per date


    dateGroup.values.forEach(function(row) {
        //the values array created by the nest function
        //contians all the original row data objects
        //that match this date (i.e., the nesting key)

        //create an object representing the bar for
        //the scanned count, and add to the bars array
        dateGroup.bars.push(
            {date:dateGroup.date,
             type: "scanned",
             count: +row.scanned,
             compid: row.compid,
             sbu: row.sbu,
             y0: total, //total value *before* this bar
             y1: (total = total + +row.scanned) //new total
            }
            );

        //create an object representing the bar for
        //the UNscanned count, and add to the bars array
        dateGroup.bars.push(
            {date:dateGroup.date,
             type: "unscanned",
             count: +row.unscanned,
             compid: row.compid,
             sbu: row.sbu,
             y0: total, //total value *before* this bar
             y1: (total = total + +row.unscanned) //new total
            }
            );
    });

    maxTotal = Math.max(maxTotal, total); //update max

});

如果你没有想要将某些类型的条形图堆叠在一起 - 例如,如果你想保持不同堆栈中不同compid的值 - 那么你将该参数包含为嵌套函数的第二个键。如果值与所有嵌套键匹配,则它们仅嵌套在一起。当然,那么你还必须修改你的x-scale以通过两个键分离堆栈。查找分组条形图的示例,了解如何执行此操作。

一旦拥有了正确嵌套的数据,就可以将外部数组(嵌套对象数组)加入到代表每个堆栈的<g>个元素中,然后在其中创建一个嵌套的矩形选择每个组并将内部数组(条形数据)加入其中:

var dateGroups = svg.selectAll("g") 
        //create an empty selection of groups
   .data(nestedData); //join to the data, 
    //each nested object (i.e., date) will get a group

dateGroups.enter().append("g")
    //create the actual <g> elements for each row of data
    .attr("class", "dateGroup"); 
    //give them a meaningful class

//Now, within each group create a rectangle 
//for each category from the "bars" array created earlier.
//This uses a nested selection, since we don't know
//how many bars there will be for a given date.


var bars = dateGroups.selectAll("rect")
    .data( function(d) {return d.bars;})
        //the 'd' value passed in is the data for each
        //dateGroup, each of which will now have a 
        //nested selection of bars

bars.enter().append("rect"); //create the rectangles

bars.attr("class", function(d){
        //assign classes for all the categorical values
        //(after stripping out non-word characters)
        //so they can be styled with CSS

        var specialchars = /\W+/g; 
             //regular expression to match all non-letter, non-digit characters

        return ["data",
                "type-" + d.type.replace(specialchars, ""), 
                "compid-" + d.compid.replace(specialchars, ""),
                "sbu-" + d.sbu.replace(specialchars, "")
               ].join(" "); //class list is space-separated

    })
    .attr("y", function(d){
        return yScale(d.y1);
        //y is the TOP of the rectangle
        //i.e., the position of the *total* value
        //for this bar and all others under it in the stack
    } )
    .attr("height", function(d){
        return Math.abs( yScale(d.y1) - yScale(d.y0) );
        //the height of the rectangle is the difference 
        //between the total *after* 
        //this value is added to the stack
        // (top of the bar, y1)
        //and the total *before* it is added 
        // (bottom of the bar, y0)

        //Since this is a linear scale, this could also 
        //be written as
        //Math.abs( yScale(d.count) - yScale(0) )
        //i.e., as the difference between
        //its data value and zero line.

        //Note the use of absolute value to
        //compensate for a possibly inverted scale.
    } )
    .attr("x", function(d){
        return xScale(d.date);
    })
    .attr("width", xScale.rangeBand() )
    //don't need a function for width,
    //since it doesn't depend on the data
    .append("title") //add a tooltip title
        .text(function(d) {
            return d.sbu + ", " +d.type +":" + d.count;
        });