There is no built-in way in D3 to select an element by its text contents, which is due to the fact that D3 internally uses Element.querySelector()
and Element.querySelectorAll()
to select elements from the DOM. Those methods take a CSS Selector string as a single parameter which is defined by the Selectors Level 3 specification. Unfortunately, there is no way of selecting an element based on its contents (this once was possible via the :contains()
pseudo-class, which is gone, though).
Therefore, to build your selection you have to resort to passing a function to .select()
which selects and returns the <text>
element you are interested in. There are various ways of doing this, but I would like to suggest a not so obvious yet elegant approach. This makes use of the little-known NodeIterator
interface which can be used to create an iterator over a list of nodes from the DOM which meet your filter criteria.
The NodeIterator
instance is created by calling Document.createNodeIterator()
which takes three arguments:
- The root of the DOM sub-tree from where the search is performed.
- A bitmask determining which types of nodes you are interested in; for your purposes this would be
NodeFilter.SHOW_TEXT
.
- An object implementing the
.acceptNode()
method of the NodeFilter
interface. This method is presented each node of the specified type in document order and must return NodeFilter.FILTER_ACCEPT
for any matching node and NodeFilter.FILTER_REJECT
any other nodes. At this point your implementation would look for a match of the id value with the text contents of the actual text element.
You can then call .nextNode()
on the created node iterator to walk through the list of matching nodes. For your task this could be something along the following lines:
document.createNodeIterator(
this, // The root node of the searched DOM sub-tree.
NodeFilter.SHOW_TEXT, // Look for text nodes only.
{
acceptNode(node) { // The filter method of interface NodeFilter
return new RegExp(value).test(node.textContent) // Check if text contains string
? NodeFilter.FILTER_ACCEPT // Found: accept node
: NodeFilter.FILTER_REJECT; // Not found: reject and continue
}
})
.nextNode() // Get first node from iterator.
.parentElement; // Found node is a "pure" text node, get parent <text> element.
Once having this node at hand it is easy to apply whatever modifications you need for that element—i.e. append elements, set attributes… This is also easily adapted to handling multiple nodes, if you were not looking for a unique value but for multiple elements matching the same string. You would just have to return an array of the nodes found by the iterator which could then be directly passed on to D3's .selectAll()
for creating a selection of multiple nodes.
For a working demo have a look at the following snippet:
function nodeIterator(value) {
return function() {
return document.createNodeIterator(
this, // The root node of the searched DOM sub-tree.
NodeFilter.SHOW_TEXT, // Look for text nodes only.
{
acceptNode(node) { // The filter method of interface NodeFilter
return new RegExp(value).test(node.textContent) // Check if text contains string
? NodeFilter.FILTER_ACCEPT // Found: accept node
: NodeFilter.FILTER_REJECT; // Not found: reject and continue
}
})
.nextNode() // Get first node from iterator.
.parentElement; // Found node is a "pure" text node, get parent <text> element.
}
}
const filter = nodeIterator("id0");
const sel = d3.select("svg").select(filter);
// Manipulate the selection:...
// sel.append("g")
// .attr("transform", "...");
console.log(sel.node());
<script src="https://d3js.org/d3.v5.js"></script>
<svg>
<g>
<g>
<text> id3 </text>
<text> 73% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
<g>
<text> id0 </text>
<text> 11% </text>
<svg></svg>
</g>
<g>
<text> id1 </text>
<text> 66% </text>
<svg></svg>
</g>
<g>
<svg></svg>
</g>
</g>
</svg>