44

I am having trouble adding a chart legend to my d3js chart. Here is my current approach:

var legend = svg.append("g")
  .attr("class", "legend")
  .attr("x", w - 65)
  .attr("y", 25)
  .attr("height", 100)
  .attr("width", 100);

legend.append("rect")
  .attr("x", w - 65)
  .attr("y", 25)
  .attr("width", 10)
  .attr("height", 10)
  .style("fill", function(d) { return color_hash[dataset.indexOf(d)][1] });

legend.append("text")
  .attr("x", w - 65)
  .attr("y", 25)
  .text(function(d) { return color_hash[dataset.indexOf(d)][0] + ": " + d; });

Then I am attempting to style the .legend class:

.legend {
            padding: 5px;
            font: 10px sans-serif;
            background: yellow;
            box-shadow: 2px 2px 1px #888;
        }

But I'm not having much luck.

Is anyone familiar with adding legends to charts able to provide the best way to do so? I am not finding many resources for this online.

Here is my entire graph: http://jsbin.com/ewiwag/2/edit

VividD
  • 10,456
  • 6
  • 64
  • 111
darko
  • 2,438
  • 8
  • 42
  • 54

2 Answers2

41

You need to bind data to the nodes (rectangles and text elements) that make up the legend.

Currently you get an error when trying to style rectangles:

Uncaught TypeError: Cannot read property '1' of undefined 

The reason: there's no bound data

legend.append("rect")
      /*...*/
      .style("fill", function(d) { 
         // d <---- is undefined
         return color_hash[dataset.indexOf(d)][1] 
      });

Notice that D3 focuses on data transformation and operates on selections. So, first select a set of nodes and then bind data

legend.selectAll('rect')
      .data(dataset)
      .enter()

Once you enter the selection with enter, you can add nodes and apply properties dynamically. Notice that to avoid creating rectangles on top of others, when setting the y property pass the i counter and multiply it by an integer.

  /*.....*/
      .append("rect")
      .attr("x", w - 65)
      .attr("y", function(d, i){ return i *  20;})
      .attr("width", 10)
      .attr("height", 10)
      .style("fill", function(d) { 
         var color = color_hash[dataset.indexOf(d)][1];
         return color;
      });

Here's the fixed example: http://jsbin.com/ubafur/3

jaime
  • 41,961
  • 10
  • 82
  • 52
  • Ah ha, that makes sense! One major issue yet: though the font is, the background and border styles of .legend are not applied. I am assuming the svg element can be styled in the same way any other div may be. Is this incorrect? – darko Nov 27 '12 at 15:16
  • @ddarko, that is incorrect. When using CSS (selectors) to style SVG elements, only [SVG attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute) can be used, not CSS property names. As suggested in [this book](http://chimera.labs.oreilly.com/books/1230000000345/ch03.html#_styling_svg_elements), to distinguish which rules in your stylesheet are SVG-specific, you may want to add `svg` to those selectors: `svg .legend { ... }` – Mark Rajcok Oct 25 '13 at 16:14
13

Ok, here's one way to do it: http://jsbin.com/isuris/1/edit

Sorry, had to make too many changes to be able to explain it all. See if you can figure it out. If you have questions, ask the in the comments and I'll modify the answer.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
    <style type="text/css">

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

      .axis text {
        font-family: sans-serif;
        font-size: 11px;
      }

      .y1 {
        fill: white;
        stroke: orange;
        stroke-width: 1.5px;
      }

      .y2 {
        fill: white;
        stroke: red;
        stroke-width: 1.5px;
      }

      .y3 {
        fill: white;
        stroke: steelblue;
        stroke-width: 1.5px;
      }

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

      div.tooltip {
              position: absolute;
              text-align: center;
              width: 50px;
              height: 10px;
              padding: 5px;
              font: 10px sans-serif;
              background: whiteSmoke;
              border: solid 1px #aaa;
              pointer-events: none;
              box-shadow: 2px 2px 1px #888;
            }

            .legend {
              padding: 5px;
              font: 10px sans-serif;
              background: yellow;
              box-shadow: 2px 2px 1px #888;
            }

            .title {
              font: 13px sans-serif;
            }

    </style>
  </head>
  <body>
    <script type="text/javascript">

    //Width and height
    var w = 500;
    var h = 300;
    var padding = 50;

    var now = d3.time.hour.utc(new Date);
    var dataset = [ [ ],[ ] ];
    dataset[0].push({x: d3.time.hour.utc.offset(now, -5), y: 0});
    dataset[0].push({x: d3.time.hour.utc.offset(now, -4), y: 0});
    dataset[0].push({x: d3.time.hour.utc.offset(now, -3), y: 2});
    dataset[0].push({x: d3.time.hour.utc.offset(now, -2), y: 0});
    dataset[0].push({x: d3.time.hour.utc.offset(now, -1), y: 0});
    dataset[0].push({x: now, y: 0});

    dataset[1].push({x: d3.time.hour.utc.offset(now, -5), y: 3});
    dataset[1].push({x: d3.time.hour.utc.offset(now, -4), y: 1});
    dataset[1].push({x: d3.time.hour.utc.offset(now, -3), y: 3});
    dataset[1].push({x: d3.time.hour.utc.offset(now, -2), y: 1});
    dataset[1].push({x: d3.time.hour.utc.offset(now, -1), y: 5});
    dataset[1].push({x: now, y: 1});

    var color_hash = {  0 : ["apple", "green"],
              1 : ["mango", "orange"],
              2 : ["cherry", "red"]
            }                      

    // Define axis ranges & scales        
    var yExtents = d3.extent(d3.merge(dataset), function (d) { return d.y; });
    var xExtents = d3.extent(d3.merge(dataset), function (d) { return d.x; });

  var xScale = d3.time.scale()
         .domain([xExtents[0], xExtents[1]])
         .range([padding, w - padding * 2]);

  var yScale = d3.scale.linear()
         .domain([0, yExtents[1]])
         .range([h - padding, padding]);


  // Create SVG element
  var svg = d3.select("body")
      .append("svg")
      .attr("width", w)
      .attr("height", h);


  // Define lines
  var line = d3.svg.line()
         .x(function(d) { return x(d.x); })
         .y(function(d) { return y(d.y1, d.y2, d.y3); });

  var pathContainers = svg.selectAll('g.line')
  .data(dataset);

  pathContainers.enter().append('g')
  .attr('class', 'line')
  .attr("style", function(d) {
    return "stroke: " + color_hash[dataset.indexOf(d)][1]; 
  });

  pathContainers.selectAll('path')
  .data(function (d) { return [d]; }) // continues the data from the pathContainer
  .enter().append('path')
    .attr('d', d3.svg.line()
      .x(function (d) { return xScale(d.x); })
      .y(function (d) { return yScale(d.y); })
    );

  // add circles
  pathContainers.selectAll('circle')
  .data(function (d) { return d; })
  .enter().append('circle')
  .attr('cx', function (d) { return xScale(d.x); })
  .attr('cy', function (d) { return yScale(d.y); })
  .attr('r', 3); 

    //Define X axis
  var xAxis = d3.svg.axis()
          .scale(xScale)
          .orient("bottom")
          .ticks(5);

  //Define Y axis
  var yAxis = d3.svg.axis()
          .scale(yScale)
          .orient("left")
          .ticks(5);

  //Add X axis
  svg.append("g")
  .attr("class", "axis")
  .attr("transform", "translate(0," + (h - padding) + ")")
  .call(xAxis);

  //Add Y axis
  svg.append("g")
  .attr("class", "axis")
  .attr("transform", "translate(" + padding + ",0)")
  .call(yAxis);

  // Add title    
  svg.append("svg:text")
       .attr("class", "title")
     .attr("x", 20)
     .attr("y", 20)
     .text("Fruit Sold Per Hour");


  // add legend   
  var legend = svg.append("g")
    .attr("class", "legend")
    .attr("x", w - 65)
    .attr("y", 25)
    .attr("height", 100)
    .attr("width", 100);

  legend.selectAll('g').data(dataset)
      .enter()
      .append('g')
      .each(function(d, i) {
        var g = d3.select(this);
        g.append("rect")
          .attr("x", w - 65)
          .attr("y", i*25)
          .attr("width", 10)
          .attr("height", 10)
          .style("fill", color_hash[String(i)][1]);

        g.append("text")
          .attr("x", w - 50)
          .attr("y", i * 25 + 8)
          .attr("height",30)
          .attr("width",100)
          .style("fill", color_hash[String(i)][1])
          .text(color_hash[String(i)][0]);

      });
    </script>
  </body>
</html>
meetamit
  • 24,727
  • 9
  • 57
  • 68
  • Nope, background and border aren't applicable CSS for SVG elements. The main styleable properties of SVG (off the top of my head) are stroke, fill, font-family and font-size. You'll have to either create another `svg:rect` for each legend entry, with the desired size, background and border. Or, you can make the whole legend with HTML, which IMO is the easier option. It may be possible to have the said HTML nested inside the SVG element (I've never tried that), but you may as well just make it a sibling node, overlaid on top of the SVG. – meetamit Nov 27 '12 at 17:58
  • A slightly edited version of your jsbin (points now added via `push()`) http://jsbin.com/isuris/437/edit – Adriano May 23 '14 at 10:06
  • @1252748 The link isn't dead; jsbin — the website — was temporarily down when you clicked it. Try again (and consider upvoting). – meetamit Oct 13 '17 at 14:17