Tabares
Tabares

Reputation: 4335

How to avoid overlapping and stack long text legends in D3 graph?

I have D3 graph base on Multi-line graph 3 with v7: Legend, this sample contains few and shorts legends for the graph. In my sample I want to increase the length for legends and stack the data if is necessary, I want to avoid overlapping in the legends,

https://bl.ocks.org/d3noob/d4a9e3e45094e89808095a47da19808d

dataNest.forEach(function(d,i) { 

    svg.append("path")
        .attr("class", "line")
        .style("stroke", function() { // Add the colours dynamically
            return d.color = color(d.key); })
        .attr("d", priceline(d.value));

    // Add the Legend
    svg.append("text")
        .attr("x", (legendSpace/2)+i*legendSpace)  // space legend
        .attr("y", height + (margin.bottom/2)+ 5)
        .attr("class", "legend")    // style the legend
        .style("fill", function() { // Add the colours dynamically
            return d.color = color(d.key); })
        .text(d.key); 

});

My legends overlaped

Upvotes: 0

Views: 505

Answers (1)

deristnochda
deristnochda

Reputation: 585

There are two possible solutions I can think of, shown in this JSFiddle.

First, if it is acceptable that the legend is not part of the svg, then realize the legend with a simple unordered list in a container next to the svg. This is probably the best when there is a varying number of legend entries and there are no restrictions considering the styling via css. The browser takes care of varying lengths and does an automatic line break.

Second, if the legend has to be a part of the svg, one can use the getBBox()-method that determines the coordinates of the smallest rectangle around an object inside an svg.

In a first step select all the legend entries that have been rendered and get the bounding boxes:

const bbox = svg.selectAll(".legend")
  .nodes()
  .map(legend_entry => legend_entry.getBBox());

With this array and the width of the svg, we can calculate the positions for each legend entry:

  bbox.reduce((pos, box) => {
    let left, right, line;
    if (pos.length === 0) {
      left = 0;
      line = 1;
    } else {
      /* The legend entry starts where the last one ended. */
      left = pos[pos.length - 1].right;
      line = pos[pos.length - 1].line;
    }
    /* Cumulative width of legend entries. */
    right = left + box.width;
    /* If right end of legend entry is outside of svg, make a line break. */
    if (right > svg_width) {
        line = line + 1;
      left = 0;
      right = box.width;
    }
    pos.push({
      left: left,
      right: right,
      line: line,
    });
    return pos;
  }, []);

Margins and paddings have to be included manually in the calculation of the positions. Of course, one could obtain the maximum width of all legend entries and make them all the same width with center alignment as in the d3noob example.

In the JSFiddle, I realized this repositioning by first rendering a hidden legend and then an additional one that is visible. It is of course possible to use only one legend, but I would not to take any chances that the process of repositioning is visible in the rendered document.

Upvotes: 1

Related Questions