score:2

Accepted answer

Here's an example that gets the width of each group and uses that to set the positions.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
    <div id="chart"></div>

    <script>
      // standard margin convention set up

      const margin = { top: 5, bottom: 5, left: 5, right: 5 };

      const width = 500 - margin.left - margin.right;
      const height = 500 - margin.top - margin.bottom;

      const svg = d3.select('#chart')
        .append('svg')
          .attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom)
          .attr('font-family', 'sans-serif');

      const g = svg.append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);

      // color scale

      const color = d3.scaleOrdinal()
          .domain(['Label 1', 'Much much much longer label 2', 'Label 3'])
          .range(d3.schemeCategory10);

      // color legend
      
      const legend = g.append('g')
          .attr('font-family', 'sans-serif');

      // create one g for each entry in the color scale
      const cell = legend.selectAll('g')
        .data(color.domain())
        .join('g');

      const squareSize = 14;

      // add the colored square for each entry
      cell.append('rect')
          .attr('fill', d => color(d))
          .attr('width', squareSize)
          .attr('height', squareSize)

      // add the text label for each entry
      cell.append('text')
          .attr('dominant-baseline', 'middle')
          .attr('x', squareSize * 1.5)
          .attr('y', squareSize / 2)
          .text(d => d);

      // position the cells
      let xPosition = 0;

      cell.each(function(d, i) {
        d3.select(this)
            .attr('transform', `translate(${xPosition})`);
        
        xPosition += (this.getBBox().width + squareSize);
      });
    </script>
</body>
</html>

Alternatively, you could also use HTML to create the legend. Then you can take advantage of flexbox to position the legend entries.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
    <div id="chart"></div>

    <script>
      // color scale

      const color = d3.scaleOrdinal()
          .domain(['Label 1', 'Much much much longer label 2', 'Label 3'])
          .range(d3.schemeCategory10);

      // color legend

      // create div for the legend to go in
      const legend = d3.select('#chart')
        .append('div')
          .style('display', 'flex')
          .style('font-family', 'sans-serif');

      // create one div for each entry in the color scale
      const cell = legend.selectAll('div')
        .data(color.domain())
        .join('div')
          .style('margin-right', '1em')
          .style('display', 'flex')
          .style('align-items', 'center');

      // add the colored square for each entry
      cell.append('div')
          .style('background', d => color(d))
          .style('min-width', '14px')
          .style('min-height', '14px')
          .style('margin-right', '0.5em');

      // add the text label for each entry
      cell.append('div')
          .text(d => d);
    </script>
</body>
</html>

Or, if the legend needs to be in the SVG element, then you could put the HTML inside a <foreignObject>.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
    <div id="chart"></div>

    <script>
      // standard margin convention set up

      const margin = { top: 5, bottom: 5, left: 5, right: 5 };

      const width = 500 - margin.left - margin.right;
      const height = 500 - margin.top - margin.bottom;

      const svg = d3.select('#chart')
        .append('svg')
          .attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom)
          .attr('font-family', 'sans-serif');

      const g = svg.append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);

      // color scale

      const color = d3.scaleOrdinal()
          .domain(['Label 1', 'Much much much longer label 2', 'Label 3'])
          .range(d3.schemeCategory10);

      // color legend
      
      const legend = g.append('g')
        .append('foreignObject')
          .attr('x', 0)
          .attr('y', 0)
          .attr('width', width)
          .attr('height', 20)
        .append('xhtml:div')
          .style('display', 'flex')
          .style('font-family', 'sans-serif');

      // create one div for each entry in the color scale
      const cell = legend.selectAll('div')
        .data(color.domain())
        .join('div')
          .style('margin-right', '1em')
          .style('display', 'flex')
          .style('align-items', 'center');

      // add the colored square for each entry
      cell.append('div')
          .style('background', d => color(d))
          .style('min-width', '14px')
          .style('min-height', '14px')
          .style('margin-right', '0.5em');

      // add the text label for each entry
      cell.append('div')
          .text(d => d);
    </script>
</body>
</html>


Related Query

More Query from same tag