I have been successful in creating a Tree visualization, but now I want to decouple the additional code I needed to create the visualization. I have made modifications based on Tidy tree component / D3 | Observable. My hope is to just clean up the code by having the caller of the Tree handle the now-coupled code, and possibly to see if I can transition my data and most of the logic over to Oberservable’s Tree Plot. However, I don’t even know if I am on the right track.
Here is a working Visualization
To see what the modifications do to the visualization, enable Indicators and select a date.
But the modifications are as followed:
Node Fill Color
Node Radius
Node Text Font Weight
Node Text X Offset
I know the last 2 modifications can’t be moved over to Observable’s Tree Plot. At least not yet.
This allows me to call the code like the following. Notice I don’t have any logic for the above 4 items in the calling code.
Tree(find("XBB.1.5"), {
width: ...,
heightCoefficent: ...,
// highlightWeeks: ... This will be handled elsewhere in the d3 code
label: (d) => d.data.lineage.pango,
nodeRadius: (d) => nodeRadius(numberOfWeeks(d.data.data)),
nodeTextWeight: (d) => nodeTextWeight(numberOfWeeks(d.data.data)),
})
I moved the label out and that works find. However, I also tried to work on 2 other separate parts of my modified code. One part seemed to work (node radius), but doing something very similar to another part failed (node text weight). So I figure I am doing something wrong already.
I tried to make it as simple as possible, so I got rid of the ternary logic, which I will have to add back in.
I have to admit I’m not entirely clear about your goal. Can you explain what you mean by “moving the label out”? Are you trying to make the rendering of the tree composable, or do you want to avoid having to use data selector callbacks?
For Tree in SARS-CoV-2 Phylogenetic Tree / Christopher Rucinski | Observable, I have the following code, which coupled Tree to this particular data structure. If I want to use Tree with different data, I have to modify the Tree function, rather than modify the caller
function Tree(
data,
{
label = (d) => d.data.lineage.pango, // given a node d, returns the display name
...
} = {}
) {
...
}
Tree(find("XBB.1.5"), {
...
})
This allowed me to call Tree without needing to specify the label. This kind of coupling is also present in other aspects of the above Tree.
Right, but this is still just a data selector that doesn’t handle the actual rendering, so I wasn’t sure why you consider this to be “decoupled”. And since it’s already present in the original Tree widget it’s not clear to me what changes you had to make.
Instead of modifying the Tree function itself you can also wrap it in another function that provides your custom defaults (or any abstraction that you’d need):
Here is the code that I want to decouple and have the calling code handle. All the d3.scaleDiverging() is an implementation detail. The next time I use this code, I might not want to use a diverging scale. See the comments in the code for what modifications I made and what I want to do.
I also would like to not have to touch the inside of the Tree function anymore. So that means that the code I added to Tree over the last few weeks (compared to the original Tree from Tidy tree component / D3 | Observable), I want to move out of the function.
Setting the domain and range for each attribute (node radius, node color, node text weight, node text offset, etc…) shouldn’t be done inside of Tree. I only added it in there because I am new and was trying to figure out how this all works.
My goal is to minimalize the Tree function to more closely match the original but is a more plug-and-playable Tree which can easily be modified by the caller code. I believe I could even possibly use Oberservable’s Tree Plot for the node fill color and node radius
Current Coupled Tree Code
function Tree(
data,
{
...
weeks = 1
} = {}
) {
...
// I moved these 2 function out
function divergingDomainOn(numberOfWeeks) {
return [
1,
numberOfWeeks + 1,
Span("2021-11-24", new Date(Date.now())).numberWeeks
];
}
let spannedWeeks = function (node) {
return Span(node.data.lineage.designation_date, new Date(Date.now()))
.numberWeeks;
};
// I also moved these d3 code snippets out.
let fill_color = d3
.scaleDiverging()
.domain(divergingDomainOn(weeks))
.range(["#D52322", "lightgray", "black"]);
// .interpolator(d3.interpolateRdYlGn);
let node_radius = d3
.scaleDiverging()
.domain(divergingDomainOn(weeks))
.range([7, 3, 2]);
// .interpolator(d3.interpolateRdYlGn);
let font_weight = d3
.scaleDiverging()
.domain(divergingDomainOn(weeks))
.range([1000, 500, 400]);
// For the below, I don't want to do arrow functions here.
// I just want .attr("fill", nodeFill) and .attr("r", nodeRadius).
// nodeFill and nodeRadius would be options passed into Tree()
node
.append("circle")
.attr("fill", (d) => (weeks == 0 ? fill : fill_color(spannedWeeks(d.data))))
.attr("r", (d) => (weeks == 0 ? r : node_radius(spannedWeeks(d.data))));
if (title != null) node.append("title").text((d) => title(d.data, d));
// Same thing for x offset and font-weight.
if (L)
node
.append("text")
.attr("dy", "0.32em")
.attr("x", weeks == 0 ? 4 : 10)
.attr("text-anchor", "start")
.attr("paint-order", "stroke")
.attr("stroke", halo)
.attr("stroke-width", haloWidth)
.attr("font-weight", (d) =>
weeks == 0 ? 400 : font_weight(spannedWeeks(d.data))
)
.text((d, i) => L[i]);
return svg.node();
}
Expected Decoupled Tree Code
Here is a more minimalized version of Tree where the implementation detail is handled by the caller; not Tree.
I should not have said this, as it led my follow-up reply to you to be directed toward this. Please don’t focus on the label. That should carry no weight in my question. Sorry
… and where are you stuck? What’s preventing you from moving your scale related code out of your Tree function and just passing in the individual attribute callbacks?
I thought it would be fairly straightforward, but that doesn’t seem to be the case at all. First of all, I don’t understand why I need to use d.data.data and why that works for one and not the other.
Here is what I have so far
Tree(find("XBB.1.5"), {
width: width,
// Works with d.data
label: (d) => d.data.lineage.pango,
// Doesn't work with d.data. Requires d.data.data for some reason, but works.
nodeRadius: (d) =>
nodeRadius(numberOfWeeks(d.data.data.lineage.designation_date)),
// Doesn't work at all no matter what I do; no matter how similar it is to above
nodeTextWeight: (d) =>
nodeTextWeight(numberOfWeeks(d.data.data.lineage.designation_date)),
})
// These are in separate cells
highlightWeeks = 1
scale = d3.scaleDiverging().domain(divergingDomainOn(highlightWeeks))
nodeRadius = scale.range([7, 4, 2])
nodeTextWeight = scale.range([1000, 500, 400])
OK, so based on what you said, it tried passing in d.data to each function. That cleared up the double-data confusion. I had one more issue from above that I did end up fixing (see below), but that led me to realize that I could only pass in a function as the options and not values like "#aaa". So I am now doing the following…
And from this point, I can possibly do some abstractions as you pointed out. But I am curious if you have any pointers or concerns with the current approach before I start abstracting. I do notice a lot of “duplicate” code for the function arguments. Not sure if this can be reduced or if it even should be reduced if it can be.
The benefit of this approach is that it chooses the abstraction in a way that allows you to iterate faster when testing different parameters for your scales.
If you want even more flexibility you could also move some of the render code outside of your Tree function by utilizing d3-selection’s selection.each and expose the render callbacks as options.
This answer is well beyond what I was expecting but I am extremely happy you did put in that effort to go through the process.
I will be reviewing it this week and trying to understand a few parts here and there. JavaScript is quite a language. The abstractions you developed are amazing. Hopefully I can understand what is happening well enough to transfer this knowledge over to other areas!