Reputation: 763
Firstly, I'm brand new to D3, but searching has yielded no results that work for my case.
I'm using latest D3 v5.
I'm trying to get d3 to fill in a table with a single column. I'm able to accomplish this in a manner that violates DRY:
var TRS = d3.select("#queueTable tbody")
.selectAll("tr")
.data(data);
TRS.exit().remove();
TRS.enter()
.append("tr")
.selectAll("td")
.data(function(row){return [row];})
.enter()
.append("td")
.text(function fillTD(d){return d.name;});
TRS
.selectAll("td")
.data(function(row){return [row];})
.text(function fillTD(d){
return d.name;
});
As you can see, I'm copy-pasting the function "fillTD".
Googling led me to the merge function, then down a rabbit hole of trying to get it to work:
var TRS = d3.select("#queueTable tbody")
.selectAll("tr")
.data(data);
TRS.exit().remove();
var TDS = TRS.selectAll("td")
.data(function(row){return [row];});
var RowsEntered = TRS.enter()
.append("tr")
var TDsEntered = RowsEntered.selectAll("td")
.data(function(row){return [row];})
.enter()
.append("td");
var Merged = TDS.merge(TDsEntered);
Merged
.text(function fillTD(d){return d.name;});
In this, the TRs and TDs are generated just fine. The battle is solely over the functions after merge is called.
The values of the variable passed as an argument to merge never show. That is, when I have TDsEntered.merge(TDS)
, only TDS's selections are passed, as if the merge never occurred and only new items are created. This behavior persists with TDS.merge(TDsEntered), though now only updates occur.
If I remove the .data line from here:
var TDS = TRS.selectAll("td")
.data(function(row){return [row];});
It works just the same.
If I swap RowsEntered's use for TDsEntered:
var RowsEntered = TRS.enter()
.append("tr")
var TDsEntered = TRS.selectAll("td")
.data(function(row){return [row];})
.enter()
.append("td");
I get the same behavior, but only after a second pass through the fromEvent that fires this. Which makes sense.
When inspected in the debugger, TDS and TDsEntered have variable numbers of "groups" and "parents", depending on if they're being used. E.g., TDsEntered will have 4 groups & parents that correspond to 4 new entries. If the are no changes to any existing entries, TDS will, in turn, have 0 groups & parents.
This is not the case if I attempt to merge TRS and RowsEntered. Both of them have equal number of parents & groups, and the merged result has the same value.
So, I thought I could use TRS and RowsEntered:
var Merged = TDS.merge(TDsEntered);
var Merge2 = TRS.merge(RowsEntered);
Merge2
.selectAll('td')
.text(function fillTD(d){return d.name;});
But now only new entries work. I flipped TRS and RowsEntered's places, but no change: still only new entries.
Okay.
So I violate DRY again by copying that same data selector used when TDsEntered was made:
var TRS = d3.select("#queueTable tbody")
.selectAll("tr")
.data(data);
TRS.exit().remove();
var TDS = TRS.selectAll("td");
var RowsEntered = TRS.enter()
.append("tr")
var TDsEntered = RowsEntered.selectAll("td")
.data(function(row){return [row];})
.enter()
.append("td");
var Merged = TDS.merge(TDsEntered);
var Merge2 = TRS.merge(RowsEntered);
Merge2
.selectAll('td')
.data(function(row){return [row];})
.text(function fillTD(d){return d.name;});
And this WORKS.
But I'm violating DRY again, just less.
So I try making a "TDSReselected" with that formula to prevent DRY:
var TDSReselected = RowsEntered.selectAll("td")
.data(function(row){return [row];});
var TDsEntered = TDSReselected
.enter()
.append("td");
var Merge1 = TDS.merge(TDsEntered);
var Merge2 = TRS.merge(RowsEntered);
var Merge3 = TDsEntered.merge(TDSReselected);
Merge3
.text(function fillTD(d){return d.name;});
But nope, doesn't work. Back to the same issue.
What gives?
Is there any way to tackle this without having my data() logic in two places? What am I doing wrong?
EDIT AFTER GERARDO'S EXPLANATION:
This works now. The key that Gerardo brought to light was that the rows needed merging first, and then the TDs' update&enter are built off the merged rows:
var TRS = d3.select("#queueTable tbody")
.selectAll("tr")
.data(data);
TRS.exit().remove();
var RowsEntered = TRS.enter()
.append("tr")
var RowsMerged = TRS.merge(RowsEntered);
var TDS = RowsMerged.selectAll("td")
.data(function(row){return [row];});
var TDsEntered = TDS
.enter()
.append("td");
var TDMerged = TDS.merge(TDsEntered);
TDMerged
.text(function fillTD(d){return d.name;});
This made everything work, and we no longer repeat any logic!
Note: I don't exit the TDs because in my specific case (TD count for a statically defined column count table will remain static), I don't need to. If my count of columns was dynamic, I would need to exit the TDs.
Upvotes: 1
Views: 67
Reputation: 102194
First of all, according to the very creator of the DRY principle, DRY is not exactly the same of just avoiding repetition of code:
“Most people take DRY to mean you shouldn't duplicate code. That's not its intention. The idea behind DRY is far grander than that” (source)
In D3 you'll find yourself repeating some snippets again and again. That's not a problem per se, as long as you know what you're doing.
Back to your question:
You can simplify your code and avoid creating a mess if you know clearly what each selection is doing. Better than just saying each selection, we should say each update pattern.
In your case, you have two elements that should be created, updated and removed: <tr>
and <td>
.
So, you can write a very clean pattern for tr
s:
let tr = table.selectAll("tr")
.data(data);
const trExit = tr.exit().remove();
const trEnter = tr.enter()
.append("tr");
tr = trEnter.merge(tr);
And another one for the td
s:
let td = tr.selectAll("td")
.data(function(d) {
return [d]
});
const tdExit = td.exit().remove();
const tdEnter = td.enter()
.append("td");
td = tdEnter.merge(td);
td.text(function(d) {
return d.name
});
Since you're appending just one <td>
in each <tr>
(single-column), the last pattern can be greatly simplified. But let's keep it that way for now.
Also, don't forget to give meaningful names for the selections:
const body = d3.select("body");
const table = body.append("table");
Here is a demo:
const body = d3.select("body");
const table = body.append("table");
const data1 = [{
name: "foo"
}, {
name: "bar"
}, {
name: "baz"
}];
const data2 = [{
name: "foobar"
}, {
name: "barbaz"
}, {
name: "bazfoo"
}, {
name: "foobaz"
}];
d3.selectAll("button").on("click", function(_, i) {
createTable(i ? data1 : data2)
})
function createTable(data) {
let tr = table.selectAll("tr")
.data(data);
const trExit = tr.exit().remove();
const trEnter = tr.enter()
.append("tr");
tr = trEnter.merge(tr);
let td = tr.selectAll("td")
.data(function(d) {
return [d]
});
const tdExit = td.exit().remove();
const tdEnter = td.enter()
.append("td");
td = tdEnter.merge(td);
td.text(function(d) {
return d.name
});
};
table {
font-family: verdana, arial, sans-serif;
font-size: 11px;
color: #333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #dedede;
}
td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Data 1</button><button>Data 2</button>
Regarding your main question: "what am I doing wrong?" Many things, depending on the snippet. For instance, sometimes you're merging an enter selection based on an outer selection which was not merged... A comprehensive answer would be too long. Just have a look at my snippet and see that we create the update pattern for the outer level, and then the update pattern for the inner level, and so on...
Upvotes: 1