2

I want to use d3 to add smiley (or frowny) faces to an existing SVG, containing many circle elements.

So far, I have been able to achieve this by appending elements directly to the SVG root. It works, but only because their coordinates happen to be set in the correct way.

I would like to extend it to be able to add a smiley face to any number of circles, wherever they are.

I have tried selecting circles, and appending to them, but it does not work.

Here is what I have achieved so far:

let svg = d3.select("#mySvg");

let appendedTo = svg;
//let appendedTo = svg.select(".mainCircle");

appendedTo.append("circle")
.attr("cx",13)
.attr("cy",15)
.attr("r",5);

appendedTo.append("circle")
.attr("cx",37)
.attr("cy",15)
.attr("r",5);


var arc = d3.svg.arc()
    .innerRadius(10)
    .outerRadius(11)
    .startAngle(3*(Math.PI/2)) //converting from degs to radians
    .endAngle(5 * (Math.PI/2)) //just radians
    
appendedTo.append("path")
    .attr("d", arc)
    .attr("transform", "translate(25,40)");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width = 50 height = 50 id="mySvg">
  <circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
</svg>

The issue is that is the position of the circle changes on the HTML page, the smiley would not be positionned correctly.

Could you give me some pointers, to 'anchor' the smiley to the circle element?

Edit:

An example of SVG:

<svg width = 500 height = 500 id="mySvg">
  <circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
  <circle class="mainCircle" cx=125 cy=65 r=50 fill="red"></circle>
  <circle class="mainCircle" cx=200 cy=12 r=10 fill="red"></circle>
  <circle class="mainCircle" cx=210 cy=300 r=90 fill="red"></circle>
  <circle class="mainCircle" cx=320 cy=25 r=5 fill="red"></circle>
  <circle class="mainCircle" cx=400 cy=120 r=50 fill="red"></circle>
  <circle class="mainCircle" cx=410 cy=230 r=25 fill="red"></circle>        
</svg>
Maxime
  • 1,245
  • 2
  • 14
  • 24

2 Answers2

2

I reckon that a good solution here would be creating a function to which you can pass the circles' attributes (cx, cy and r), which would create the smileys based only on those values.

Creating the circles yourself

So, for instance, suppose that our circle's data has x, y, and r as those attributes. We can create a function, here named makeSmileys, that draws the circles and the path in the container group:

function makeSmileys(group, xPos, yPos, radius) {

  //left eye
  group.append("circle")
    .attr("cx", xPos - radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  //right eye
  group.append("circle")
    .attr("cx", xPos + radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  arc.innerRadius(radius / 2)
    .outerRadius(radius / 2.2);

  //mouth
  group.append("path")
    .attr("d", arc)
    .attr("transform", "translate(" + xPos + "," + yPos + ")");
}

As you can see, the position of the two eyes (circles) and the mouth (path) is based on the arguments only. You can tweak those positions the way you want.

For this function to work, we have to create the container groups and then call it on those respective selections:

circlesGroup.each(function(d) {
  d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})

Because I'm using selection.call, the first argument (which is group) is the selection itself. As an alternative, if you don't want to use selection.call, just call the function as a normal JavaScript function, passing the container to it.

Here is a demo, with 10 randomly generated circles:

const svg = d3.select("svg");

const data = d3.range(10).map(function(d) {
  return {
    x: 50 + Math.random() * 500,
    y: 50 + Math.random() * 300,
    r: Math.random() * 50
  }
});

const arc = d3.arc()
  .startAngle(1 * (Math.PI / 2))
  .endAngle(3 * (Math.PI / 2));

const circlesGroup = svg.selectAll(null)
  .data(data)
  .enter()
  .append("g");

circlesGroup.each(function(d) {
  d3.select(this).append("circle")
    .attr("cx", d => d.x)
    .attr("cy", d => d.y)
    .attr("r", d => d.r)
    .style("fill", "yellow")
    .style("stroke", "black")
})

circlesGroup.each(function(d) {
  d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})

function makeSmileys(group, xPos, yPos, radius) {
  group.append("circle")
    .attr("cx", xPos - radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  group.append("circle")
    .attr("cx", xPos + radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  arc.innerRadius(radius / 2)
    .outerRadius(radius / 2.2);

  group.append("path")
    .attr("d", arc)
    .attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="600" height="400"></svg>

Using pre-existing circles

If you have an existing SVG (as you made clear in the edited question), you can select all circles with a selector...

const circles = svg.selectAll("circle");

...then get their attributes and finally call the function:

circles.each(function() {
  const x = +d3.select(this).attr("cx");
  const y = +d3.select(this).attr("cy");
  const r = +d3.select(this).attr("r");
  makeSmileys(x, y, r)
});

Mind the unary plus here, because the getters return strings for those attributes.

Here is the demo:

const svg = d3.select("svg");

const arc = d3.arc()
  .startAngle(1 * (Math.PI / 2))
  .endAngle(3 * (Math.PI / 2));

const circles = svg.selectAll("circle");

circles.each(function() {
  const x = +d3.select(this).attr("cx");
  const y = +d3.select(this).attr("cy");
  const r = +d3.select(this).attr("r");
  makeSmileys(x, y, r)
})

function makeSmileys(xPos, yPos, radius) {
  svg.append("circle")
    .attr("cx", xPos - radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  svg.append("circle")
    .attr("cx", xPos + radius / 3)
    .attr("cy", yPos - radius / 3)
    .attr("r", radius / 8)
    .style("fill", "black");

  arc.innerRadius(radius / 2)
    .outerRadius(radius / 2.2);

  svg.append("path")
    .attr("d", arc)
    .attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="500" id="mySvg">
  <circle class="mainCircle" cx=25 cy=25 r=25 fill="yellow"></circle>
  <circle class="mainCircle" cx=125 cy=65 r=50 fill="yellow"></circle>
  <circle class="mainCircle" cx=200 cy=12 r=10 fill="yellow"></circle>
  <circle class="mainCircle" cx=210 cy=300 r=90 fill="yellow"></circle>
  <circle class="mainCircle" cx=320 cy=25 r=5 fill="yellow"></circle>
  <circle class="mainCircle" cx=400 cy=120 r=50 fill="yellow"></circle>
  <circle class="mainCircle" cx=410 cy=230 r=25 fill="yellow"></circle>   
</svg>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • It basically looks like the HTML part of the snippet in the question, except with more circles. A bunch of `circle` elements, children of the `svg` root. What I want to achieve is to add eyes and mouth to the face :) – Maxime Aug 29 '18 at 14:22
  • Please, share this SVG, or make a simplified version of it. – Gerardo Furtado Aug 29 '18 at 14:24
  • 1
    And I have edited my answer. Have in mind that `` is not valid. – Gerardo Furtado Aug 29 '18 at 14:34
  • 1
    Awesome. Thanks a lot for your help, in addition to solving my problem, I have learned a lot :) – Maxime Aug 29 '18 at 14:36
  • Much better to add a `g` with a transform to `cx,cy` and add the circles and path as child nodes – rioV8 Aug 29 '18 at 15:49
-2

Instead of using a unique identifier on your SVG elements, use a class instead like this:

<svg width="50" height="50" class="face">

Then in your D3 you can reference all the instances of this class like so:

let svg = d3.selectAll(".face");
Jackson
  • 3,476
  • 1
  • 19
  • 29