Javascript find a word in text and change color dynamically

Viewed 799

I made a d3.js chart using this code bl.ocks.org/d3noob/8375092

I am trying to change the color of a word. In every instance of a word, I would like to have that color changed. For example, if you look at the chart, there is First Daughter of A and Daughter of A. I would like to change the color of the text "Daughter" to red. And I would like to change all of "Son of Level 2: B" to the color blue. (Ignore the large height and width -in my project I have 50 terms in my collapsible chart - but only included a few in this question).

I tried in CSS to change the color of all text, but it does not appear. When I inspect an element, the color will show as "blue" but the text appears black. I also tried in HTML to use , but I don't know what to do for the variable tree.Data, since it is in JavaScript.

I am a beginner in coding. Appreciate your help!

var treeData = [
  {
    "name": "Top Level",
    "parent": "null",
    "children": [
      {
        "name": "Level 2: A",
        "parent": "Top Level",
        "children": [
          {
            "name": "First Daughter of A",
            "parent": "Level 2: A"
          },
          {
            "name": "Daughter of A",
            "parent": "Level 2: A"
          }
        ]
      },
      {
        "name": "Level 2: B",
        "parent": "Top Level",
        "children": [
          {
            "name": "Son of Level 2: B",
            "parent": "Level 2: B"
          }
         ]
      }
    ]
  }
];


// ************** Generate the tree diagram  *****************
var margin = {top: 20, right: 120, bottom: 20, left: 250},
    width = 2000 - margin.right - margin.left,
    height = 2000 - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root;

var tree = d3.layout.tree()
    .size([height, width]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;

update(root);

d3.select(self.frameElement).style("height", "500px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = ++i); });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
      .on("click", click);

  nodeEnter.append("circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeEnter.append("text")
      .attr("x", function(d) { return d.children || d._children ? -13 : 13; })
      .attr("dy", ".35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.name; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  nodeUpdate.select("circle")
      .attr("r", 10)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeUpdate.select("text")
      .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.selectAll("path.link")
      .data(links, function(d) { return d.target.id; });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      });

  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}
.node {
  cursor: pointer;
}
.block{
  width:5%;
  height:10%;
}
.top{
  width: 95%;
  margin-left: 5%;
  color: black;
}
.no{
  color:black;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 3px;
}

.node text {
  color: Blue;
  font-weight: bold;
  font: 12px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 2px;
}
<div class="block"></div>
<div class="top">
  <h1>Title</h1>
  <h2>Click nodes to expand each level</h2>
  <h3>Key</h3>
  <div class="no">
    <h4>No capability</h4>
  </div>
  <div class="adhoc">
    <h4>Another heading</h4>
  </div>
  <h4>Another heading4</h4>
  <h4>Heading 4</h4>
  <h4>Final heading 4</h4>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="myChart.js"></script>
<link rel= "stylesheet" href="./myStyle.css">

1 Answers

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