amber
amber

Reputation: 1137

Pushing the top most object in an SVG container to the bottom

I know what I want to accomplish but I don't know how to go about it.

Background: I have an interactive report, using D3, that uses circles to provide a visual reference to application usage by user location on top of a rendered map of the US by zip code. In places, these circles overlap, obscuring those underneath and preventing those circles underneath from receiving events, like mouseover for example. This creates a stack of circles.

Objective: I want to be able to click on a circle to push it to the bottom of the stack, though not beneath any background object that makes up the map. Or, as in this sample, not behind the .bgCircle.

Here is some sample code I put together in JSFiddle to illustrate the issue.

The CSS:

.bgCircle {
  fill: blue;
  opacity: .25
}

.popCircle {
  fill: green;
  opacity: 0.5;
  stroke: black;
  stroke-width: 1px;
}

svg {
  border: 1px solid black;
}

The HTML

<svg width="500" height="500">
  <g>
    <circle class="bgCircle" cx="250" cy="250" r="250" />
    <circle id="A" class="popCircle" cx="200" cy="200" r="100" />
    <circle id="B" class="popCircle" cx="300" cy="200" r="100" />
    <circle id="C" class="popCircle" cx="300" cy="300" r="100" />
    <circle id="D" class="popCircle" cx="200" cy="300" r="100" />
  </g>
</svg>

The JavaScript

d3.selectAll('.popCircle')
  .on('click', function(d) {
    var text = 'force circle ' + d3.select(this).attr('id') + ' to bottom of the popCircle stack but above the bgCircle';
    alert(text);
  });

Sample Project

https://jsfiddle.net/MissAmberClark/ssghj37v/

Upvotes: 3

Views: 549

Answers (3)

Gerardo Furtado
Gerardo Furtado

Reputation: 102194

This is a solution for D3 v3.x, using vanilla JavaScript:

d3.selectAll("circle")
    .on("click", function() {
        var firstChild = this.parentNode.firstChild;
        this.parentNode.insertBefore(this, firstChild);
    })

Actually, the hint for doing this in D3 v3 is in D3 v4 API itself! It says:

selection.lower(): Re-inserts each selected element, in order, as the first child of its parent. Equivalent to:

selection.each(function() {
    this.parentNode.insertBefore(this, this.parentNode.firstChild);
});

Here is the demo (I'm using @Andrew's SVG here):

d3.selectAll("circle")
  .on("click", function() {
    var firstChild = this.parentNode.firstChild;
    this.parentNode.insertBefore(this, firstChild);
  })
#A {
  fill: orange;
}

#B {
  fill: steelblue;
}

#C {
  fill: pink;
}

#D {
  fill: lawngreen;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<svg width="500" height="500">
  <g>
    <circle class="bgCircle" cx="250" cy="250" r="250" />
  </g>
  <g>
    <circle id="A" class="popCircle" cx="200" cy="200" r="100" />
    <circle id="B" class="popCircle" cx="300" cy="200" r="100" />
    <circle id="C" class="popCircle" cx="300" cy="300" r="100" />
    <circle id="D" class="popCircle" cx="200" cy="300" r="100" />
  </g>
</svg>

Upvotes: 1

Lars-Kristian Johansen
Lars-Kristian Johansen

Reputation: 160

You should isolate the background circle in a separate group. This makes things easier.

Svg element are drawn in sequence so the first element will be behind everyone else. From your question it was unclear if you wanted the circle to be drawn in the back or put it in the last spot in the childeNode array. I added both solutions.

d3.selectAll('.popCircle')
  .on('click', function(d) {
    var parent = this.parentNode;
    parent.removeChild(this);
    
    //Insert behind
    parent.insertBefore(this, parent.firstChild);
    
    //Insert in front
    //parent.appendChild(this);
  });
.bgCircle {
  fill: blue;
  opacity: .25
}

.popCircle {
  fill: green;
  opacity: 0.5;
  stroke: black;
  stroke-width: 1px;
}

svg {
  border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="500">
  <g>
    <circle class="bgCircle" cx="250" cy="250" r="250" />
  </g>
  
  <g>
    <circle id="A" class="popCircle" cx="200" cy="200" r="100" />
    <circle id="B" class="popCircle" cx="300" cy="200" r="100" />
    <circle id="C" class="popCircle" cx="300" cy="300" r="100" />
    <circle id="D" class="popCircle" cx="200" cy="300" r="100" />
  </g>
</svg>

Upvotes: 1

Andrew Reid
Andrew Reid

Reputation: 38171

You can use selection.lower() or selection.raise() in d3v4 to move things up or down in the DOM (within the parent element). selection.lower() will place an element at the bottom of its parent, while selection.raise() will place an element at the top of its parent. These methods will therefore change the layering of svg elements as the ordering of the DOM sets the ordering of SVG elements.

So, in order to do this easily, the background elements should be in a separate <g> so that they remain unaffected. Then, just raise or lower elements on click as needed (only the topmost element will trigger with each click event in the snippet):

d3.selectAll("circle")
  .on("click",function() {
    d3.select(this).lower();
  })
#A { fill: orange; }
#B { fill: steelblue; }
#C { fill: pink; }
#D { fill: lawngreen; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="500">
  <g>
    <circle class="bgCircle" cx="250" cy="250" r="250" />
  </g>
  
  <g>
    <circle id="A" class="popCircle" cx="200" cy="200" r="100" />
    <circle id="B" class="popCircle" cx="300" cy="200" r="100" />
    <circle id="C" class="popCircle" cx="300" cy="300" r="100" />
    <circle id="D" class="popCircle" cx="200" cy="300" r="100" />
  </g>
</svg>

Upvotes: 3

Related Questions