Skip to content

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.

To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line’s color.

My Problem

-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that’s it.

I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I’m doing wrong?

JS Code

const margin = { top: 20, right: 30, bottom: 30, left: 0 },
  width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;

// maybe a translate line

// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);

const svg = d3
  .select("svg")
  .attr("width", width + margin.left + margin.right) // viewport size
  .attr("height", height + margin.top + margin.bottom) // viewport size
  .append("g")
  .attr("transform", "translate(40, 20)"); // center g in svg

// load csv

d3.csv("breitbartData.csv").then((data) => {
  // convert Count column values to numbers
  data.forEach((d) => {
    d.Count = +d.Count;
    d.Date = new Date(d.Date);
  });
  // group the data with the word as the key
  const words = d3
    .nest()
    .key(function (d) {
      return d.Word;
    })
    .entries(data);

  // create x scale

  const x = d3
    .scaleTime() // creaters linear scale for time
    .domain(
      d3.extent(
        data,

        // d3.extent returns [min, max]
        (d) => d.Date
      )
    )
    .range([margin.left - -30, width - margin.right]);

  // x axis

  svg
    .append("g")
    .attr("class", "x-axis")
    .style("transform", `translate(-3px, 522px)`)
    .call(d3.axisBottom(x))
    .append("text")
    .attr("class", "axis-label-x")
    .attr("x", "55%")
    .attr("dy", "4em")
    // .attr("dy", "20%")
    .style("fill", "black")
    .text("Months");

  // create y scale

  const y = d3
    .scaleLinear()
    .domain([0, d3.max(data, (d) => d.Count)])
    .range([height - margin.bottom, margin.top]);

  // y axis

  svg
    .append("g")
    .attr("class", "y-axis")
    .style("transform", `translate(27px, 0px)`)
    .call(d3.axisLeft(y));

  // line colors

  const line_colors = words.map(function (d) {
    return d.key; // list of words
  });

  const color = d3
    .scaleOrdinal()
    .domain(line_colors)
    .range([
      "#e41a1c",
      "#377eb8",
      "#4daf4a",
      "#984ea3",
      "#ff7f00",
      "#ffff33",
      "#a65628",
      "#f781bf",
      "#999999",
      "#872ff8",
    ]); //https://observablehq.com/@d3/d3-scaleordinal

  // craete legend variable
  const legend = svg
    .append("g")
    .attr("class", "legend")
    .attr("height", 100)
    .attr("width", 100)
    .attr("transform", "translate(-20, 50)");

  // create legend shapes and locations

  legend
    .selectAll("rect")
    .data(words)
    .enter()
    .append("rect")
    .attr("x", width + 65)
    .attr("y", function (d, i) {
      return i * 20;
    })
    .attr("width", 10)
    .attr("height", 10)
    .style("fill", function (d) {
      return color(d.key);
    });

  // create legend labels

  legend
    .append("text")
    .attr("x", width + 85)
    .attr("y", function (d, i) {
      return i * 20 + 9;
    })
    // .attr("dy", "0.32em")
    .text(
      words.map(function (d, i) {
        return d.key; // list of words
      })
    );
  // returning an array as text
  // });

  svg
    .selectAll(".line")
    .data(words)
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("stroke", function (d) {
      return color(d.key);
    })
    .attr("stroke-width", 1.5)
    .attr("d", function (d) {
      return d3
        .line()
        .x(function (d) {
          return x(d.Date);
        })
        .y(function (d) {
          return y(d.Count);
        })(d.values);
    });
});

Image of the problem: enter image description here

P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.

My Temporary Solution

  function leg_labels() {
    let the_word = "";
    let num = 0;

    for (i = 0; i < words.length; i++) {
      the_word = words[i].key;
      num += 50;

      d3.selectAll(".legend")
        .append("text")
        .attr("x", width + 85)
        .attr("y", function (d, i) {
          return i + num;
        })
        // .attr("dy", "0.32em")
        .text(the_word);
    }
  }

  leg_labels();

Answer

Problem

Your problem has to do with this code

legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
  return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
  words.map(function (d, i) {
    return d.key; // list of words
  })
);

You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.

Solution

Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.

You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.

I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.

Here is my example:

let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
  return `translate(${width + 65}px, ${i * 20}px)`;
});

legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
  return color(d.key);
});


legendGroups 
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });

This should work as expected. Please note the use of groups for easier positioning.