score:2

Within an SVG in order to color individual words you'll need to use tspans within text elements. This means searching through text elements, finding matching strings, and replacing them with new child tspan elements containing the matching word.

Highlight (change color) of specified phrase/word in text elements:

One approach could be:

function highlight(selection,word) {
  selection.each(function() {
    this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
  })
}

This function takes a selection of (text) elements, and a word to find. It searches the text element's text content to find matching strings and replaces matching strings with a tspan containing the matching string. It is case insensitive in matching text but preserves the case in the original text.

In the snippet below just type into the text box to dynamically highlight text:

var svg = d3.select("svg");
var data = [
  "You can't direct the wind, but you can adjust your sails",
  "If you chase two rabbits, you will lose them both.",
  "If you speak the truth, have a foot in the stirrup.",
  "One doesn't discover new lands without losing sight of the shore.",
  "The whole is more than the sum of its parts."
]


var textElements = svg.selectAll(null)
  .data(data)
  .join("text")
  .text(d=>d)
  .attr("x",20)
  .attr("y",(d,i) => i* 30 + 30);
  
function highlight(selection,word) {
  selection.each(function() {
    this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
  })
}


d3.select("#text").on("keyup", function() {
  textElements.call(highlight, this.value);
   //alternatively:  highlight(textElements,this.value);
})
tspan {
  fill: orange;
  stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" id="text"/></div>
<svg width="500" height="250"></svg>

Highlight with a rectangle:

We could also be a bit fancier and use a highlighter stroke, which requires adding rectangles, to do this we could use the tspans as the data for a join to create the rectangles:

var svg = d3.select("svg");
var data = [
  "You can't direct the wind, but you can adjust your sails",
  "If you chase two rabbits, you will lose them both.",
  "If you speak the truth, have a foot in the stirrup.",
  "One doesn't discover new lands without losing sight of the shore.",
  "The whole is more than the sum of its parts.",
]


var textElements = svg.selectAll(null)
  .data(data)
  .join("text")
  .text(d=>d)
  .attr("x",20)
  .attr("y",(d,i) => i* 30 + 30);
  
function highlight(selection,word,rectContainer) {
  selection.each(function() {
    this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
  })
  // join, color, positoin the rectangles:
  rectContainer.selectAll(".highlight")
    .data(selection.selectAll("tspan").nodes())
    .join("rect")
    .attr("class","highlight")
    .datum(d=>d.getBBox())
    .attr("x", d=>d.x)
    .attr("y", d=>d.y)
    .attr("width", d=>d.width)
    .attr("height", d=>d.height)
    .attr("fill","yellow")
    .lower();

}


d3.select("#text").on("keyup", function() {
  textElements.call(highlight, this.value, svg);
   //alternatively:  highlight(textElements,this.value);
})
tspan {
  fill: orange;
  stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" id="text"/></div>
<svg width="500" height="250"></svg>

Highlight multiple phrases/words with one highlighting style:

If we want to highlight multiple phrases with the same color, we can modify the regex:

  let ex = new RegExp(words.join("|"),"gi");
  selection.each(function() {
    this.innerHTML = this.textContent.replace(ex, "ig"),(w)=>"<tspan>"+w+"</tspan>")
  })

var svg = d3.select("svg");
var data = [
  "You can't direct the wind, but you can adjust your sails",
  "If you chase two rabbits, you will lose them both.",
  "If you speak the truth, have a foot in the stirrup.",
  "One doesn't discover new lands without losing sight of the shore.",
  "The whole is more than the sum of its parts.",
]


var textElements = svg.selectAll(null)
  .data(data)
  .join("text")
  .text(d=>d)
  .attr("x",20)
  .attr("y",(d,i) => i* 30 + 30);
  
function highlight(selection,words,rectContainer) {
      let ex = new RegExp(words.join("|"),"gi");
      selection.each(function() {
        this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
      })
  // join, color, positoin the rectangles:
  rectContainer.selectAll(".highlight")
    .data(selection.selectAll("tspan").nodes())
    .join("rect")
    .attr("class","highlight")
    .datum(d=>d.getBBox())
    .attr("x", d=>d.x)
    .attr("y", d=>d.y)
    .attr("width", d=>d.width)
    .attr("height", d=>d.height)
    .attr("fill","yellow")
    .lower();

}



d3.selectAll("input").on("keyup", function() {
  var words = d3.selectAll("input").nodes().map(function(n) { return n.value; });
  textElements.call(highlight, words, svg);
})

d3.select("input").dispatch("keyup");
tspan {
  fill: orange;
  stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" value="you" id="text1"/></div>
<div><input type="text" value="two" id="text2"/></div>
<svg width="500" height="250"></svg>

Highlight multiple phrases/words with different styles for each:

The challenge is a bit more difficult when dealing with multiple tspans and rectangles for different phrases. For this we need to modify the highlight function to match multiple strings, to allow flexibility we'll avoid hard coded classes as well. This requires a bit better data management than before, so there a few changes to the highlighting function to accommodate this.

To do accomplish this task we'll iterate through the list of words provided and assign them colors based on specified arrays of colors for rectangle fill and text fill. We'll add tspans and use both the content of the tspan and the bbox of the tspan as data to pass forward to the join used to add the rectangles.

For both rect and text we'll use the highlight function to give elements fills directly, eg:

function highlight(selection,words,rectContainer) {
  // create a pool of colors available:
  let textColors = ["crimson","steelblue"];
  let rectColors = ["yellow","#ccc","orange","#eee"];

  // assign colors to words:
  let colors = {}
  words.forEach((w,i)=>{
    colors[w.toLowerCase()] = {
      text: textColors[i%textColors.length],
      rect: rectColors[i%rectColors.length]
    }
  })
  // create a regex experssion:
  let ex = new RegExp(words.join("|"),"gi");
  
  // Create the tspans: 
  selection.each(function() {
    this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
  })
  
  // Select the tspans, bind data to them, color them:
  let tspans = selection.selectAll("tspan")
    .datum((d,i,n)=>{     
        return {word:n[i].textContent.toLowerCase()}
     })
    .attr("fill", d=>colors[d.word].text)
    .each((d,i,n)=>{ d.bbox = n[i].getBBox() })

  // Conduct a join of rectangles, color them, place them:
  rectContainer.selectAll(".highlight")
    .data(tspans.data())
    .join("rect")
    .attr("class","highlight")
    .attr("x", d=>d.bbox.x)
    .attr("y", d=>d.bbox.y)
    .attr("width", d=>d.bbox.width)
    .attr("height", d=>d.bbox.height)
    .attr("fill", d=>colors[d.word].rect)
    .lower();

}

var svg = d3.select("svg");
var data = [
  "You can't direct the wind, but you can adjust your sails",
  "If you chase two rabbits, you will lose them both.",
  "If you speak the truth, have a foot in the stirrup.",
  "One doesn't discover new lands without losing sight of the shore.",
  "The whole is more than the sum of its parts."
]


var textElements = svg.selectAll(null)
  .data(data)
  .join("text")
  .text(d=>d)
  .attr("x",20)
  .attr("y",(d,i) => i* 30 + 30);
  
  

function highlight(selection,words,rectContainer) {
  // create a pool of colors available:
  let textColors = ["crimson","steelblue"];
  let rectColors = ["yellow","#ccc","orange","#eee"];

  // assign colors to words:
  let colors = {}
  words.forEach((w,i)=>{
    colors[w.toLowerCase()] = {
      text: textColors[i%textColors.length],
      rect: rectColors[i%rectColors.length]
    }
  })
  // create a regex experssion:
  let ex = new RegExp(words.join("|"),"gi");
  
  // Create the tspans: 
  selection.each(function() {
    this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
  })
  
  // Select the tspans:
  let tspans = selection.selectAll("tspan")
    .datum((d,i,n)=>{     
        return {word:n[i].textContent.toLowerCase()}
     })
    .attr("fill", d=>colors[d.word].text)
    .each((d,i,n)=>{ d.bbox = n[i].getBBox() })

  // Conduct a join of rectangles:
  rectContainer.selectAll(".highlight")
    .data(tspans.data())
    .join("rect")
    .attr("class","highlight")
    .attr("x", d=>d.bbox.x)
    .attr("y", d=>d.bbox.y)
    .attr("width", d=>d.bbox.width)
    .attr("height", d=>d.bbox.height)
    .attr("fill", d=>colors[d.word].rect)
    .lower();

}


// cycle through some words:
let wordlist = [
  ["you","the","can"],
  ["stirrup","chase","discover","whole"],
  ["if"]
]

let i = 0;
highlight(textElements,wordlist[i++%3],svg)
setInterval(function(){  
     highlight(textElements,wordlist[i++%3],svg) }, 
     1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width="500" height="250"></svg>

The above should produce:

enter image description here


Related Query

More Query from same tag