0

I'm trying to make a legend for my graph using d3, and I figured I could use a table with the following HTML for each row:

<tr>
    <td class="swatch" style="background: red;"></td>
    <td>user</td>
    <td>123</td>
</tr>

The end result looks something like this.

I have my data neatly placed on each row in the following form:

{color: 'red', key: 'user', value: 123}

And I'm using this code to create the cells:

let cells = d3.selectAll('tr').selectAll('td')
    .data(function (d) {
        return [d.color, d.key, d.value];
    });
cells.enter().append('td').merge(cells)
    .text(function (d, i) {
        return i == 0 ? '' : d;
    })
    .style('background', function(d, i) {
        return i == 0 ? d : '';
    })
    .each(function(d, i) {
        if (i == 0) {
            this.classList.add('swatch');
        }
    });

The code works fine, but it looks really ugly with the repeated i == 0 checks to special-case the behavior for the first cell. Is there a cleaner approach I could be using?

It seems like maybe I should be setting everything using text and using CSS to style it into a swatch of color, but that isn't possible.

Altay_H
  • 489
  • 6
  • 14
  • I think what the others were saying is that it is not possible to set the text coloring using *just* plain CSS -- since you already have the color string defined in your dataset, you can just use that string in your rendering, either as a classname, inline style, or attribute value. Are you wanting to remove the color from your dataset, or just simplify the current d3 logic? – SteveR Jun 08 '18 at 19:35
  • I want to simplify the current d3 logic. The repeated conditional feels like a hack, and the fact that `color` needs a `td` of its own is the only reason it's in the same `enter`/`merge` as `key` and `value`. – Altay_H Jun 08 '18 at 20:07

2 Answers2

1

Since the different table cells operate quite differently, you can separate the elements by using multiple appends and classes. It'd look something like this:

  let cells = d3.select('tbody').selectAll('tr').data(bindData);
  let cellsEnter = cells.enter().append('tr');
  let swatches = cellsEnter.append('td').classed('swatch', true);
  d3.selectAll('table .swatch')
    .style('background', function (d) { return d.color });
  let otherCells = d3.selectAll('table tr').selectAll('.other')
    .data(function (d) {
      return [d.key, d.value];
    });
  otherCells.enter()
    .append('td').classed('other', true)
      .merge(otherCells).text(function (d) { return d; });

Here's a working snippet:

function updateData(bindData) {
  let cells = d3.select('tbody').selectAll('tr').data(bindData);
  let cellsEnter = cells.enter().append('tr');
  let newSwatches = cellsEnter.append('td').classed('swatch', true);

  d3.selectAll('table .swatch')
    .style('background', function (d) { return d.color });
  let otherCells = d3.selectAll('table tr').selectAll('.other').data(function (d) {
    return [d.key, d.value];
  });
  otherCells.enter()
    .append('td').classed('other', true)
      .merge(otherCells).text(function (d) { return d; });
}

var data = [
  { color: 'red', key: 'agnes', value: '1' },
  { color: 'green', key: 'bernard', value: '2' },
  { color: 'blue', key: 'connie', value: '3' }
];

updateData(data);
.swatch {
  display: inline-block;
  height: 10px;
  width: 10px;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div id="legend">
<table>
<tbody>
</tbody>
</table>
</div>
Steve
  • 10,435
  • 15
  • 21
  • Thanks. I didn't realize I could have different types of data on sibling elements based on a selection, or that I could select based on class. – Altay_H Jun 12 '18 at 12:43
  • A minor point: otherCells is never used so there's no need to save that selection. Alternatively, you could use it and do away with the subsequent selector by joining them via `.merge(otherCells)`. – Altay_H Jun 12 '18 at 12:46
  • Good points: I've updated my answer using `merge`. Just for educational purposes, I generally like to assign statements to variables if they are meaningful (like `swatches`) even if they aren't used elsewhere; there's no need to actually assign unused variables, of course. – Steve Jun 14 '18 at 08:57
1

Do you really need to build an html table for this? It may be more natural to map your data into a <ul> list, with the bullet color set by the incoming data:

var data = [
    { color: "red", user: "Bob", value: 123 },
    { color: "orange", user: "Carol", value: 234 },
    { color: "green", user: "Ted", value: 345 },
    { color: "blue", user: "Alice", value: 1234 }
];
var list = d3.select("#userList")
    .selectAll("li")
    .data(data);
var item = list.enter()
    .append("li")
        .attr("class", "swatch")
        .style("color", d => d.color);
var keys = item
    .append("span")
        .text(d => d.user);
var vals = item
    .append("pre")
        .text(d => d.value);
.swatch {
    list-style-type: square;
}
.swatch span {
    display: inline-block;
    width: 50px;
}
.swatch pre {
    display: inline-block;
    width: 50px;
    text-align: right;
    color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<ul id="userList">
</ul>
SteveR
  • 1,015
  • 8
  • 12
  • This is clever, but I want the swatches to be much larger relative to the text. I could change the font size for just the li, but that creates [alignment issues](https://stackoverflow.com/questions/14342788/vertically-align-smaller-bullets-with-larger-text). I'm also not a fan of the fixed widths for the alignment, since changing the contents or font size could cause the spans to overflow. – Altay_H Jun 12 '18 at 12:42