Greg Greg - 4 months ago 17
CSS Question

Is there a way to make an SVG USE subsequently modifiable (or a different technique)?

I have used an SVG defs group (created on the fly, using d3js) to create a complex item that I want to instantiate and then modify but from all my playing and researching, the only conclusion I can draw, is that each instance is effectively a linked instance, not an independent one. I'm sure there are deeply technical computer-science names for these but I'm sure you know what I mean :o)

I have built some sample code to illustrate the problem I have.
There is a group defined in an SVG DEFS section, that consists of two squares and a text element. Then a similar structure is built under the SVG container. Finally, I used a USE + XLINK:HREF to instantiate two copies of the group elements containing the two squares and text. Originally, all the declarations had classes which were also specified in the STYLE section.

To illustrate the problem and experiment with it, I added some element modifications, accessing the objects with jQuery (I found it easier than d3 or straight Javascript DOM manipulation). It seems I'm prohibited from querying inside the hierarchies that have been instantiated from DEFS, with USE but I can access the full hierarchy of the directly drawn areas. This is a problem in my project as I have a bunch of minor shape variations on frequently occurring sub elements but each of these will at least be differently labelled. I don't want to create each one long hand, unless I have to, even programmatically. Ideally I just want to switch a few sub-components to opacity=0 and insert text labels and descriptions using d3js. What alternative to DEFS/USE is there that means I can make a copied instance not a linked instance?

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Testing how to navigate 'DEFS' &amp; 'USE' to change attributes of the elements in the instance
</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<!--script src="https://code.jquery.com/ui/1.12.0/jquery-ui.js"></script-->

<style>
.pinkBox {
fill: #fdc4fe;
stroke: black;
stroke-width: 2px;
stroke-linecap: square;
stroke-linejoin: round;
opacity: 1;
}
.redBox {
fill: #fb198e;
stroke: black;
stroke-width: 2px;
stroke-linecap: square;
stroke-linejoin: round;
opacity: 1;
}
.lightGreenBox {
fill: #bdf07c;
stroke: black;
stroke-width: 2px;
stroke-linecap: square;
stroke-linejoin: round;
opacity: 1;
}
.GreenBox {
fill: #4a9a03;
stroke: black;
stroke-width: 2px;
stroke-linecap: square;
stroke-linejoin: round;
opacity: 1;
}
.greyText{
font-family:sans-serif;
font-size: 12px;
fill: grey;
}
</style>
</head>
<body>
<script type="text/javascript">
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 1024)
.attr("height", 768);

var BG = svgContainer.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "#2d2525");

var reusables = svgContainer.append("defs")

var USEgrp = reusables.append("g")
//.attr("id","Ugrp")
.attr("transform","translate(20,20)");

var RB = USEgrp.append("rect")
//.attr("id","RB")
.attr("x","10")
.attr("y","10")
.attr("height","100")
.attr("width","100")
.attr("rx","10")
.attr("ry","10")
//.attr("class","redBox")
;

var BB = USEgrp.append("rect")
//.attr("id","BB")
.attr("x","120")
.attr("y","10")
.attr("height","100")
.attr("width","100")
.attr("rx","10")
.attr("ry","10")
//.attr("class","blueBox")
;

var Txt = USEgrp.append("text")
//.attr("id","TxtU")
.attr("x",250)
.attr("y",70)
//.attr("class","greyText")
//.attr("fill","white")
.text("These boxes are 'USEed' from the prototype in the 'DEFS' section");

var canvasgrp = svgContainer.append("g")
.attr("id","Cgrp")
.attr("transform","translate(0,0)");

var GB = canvasgrp.append("rect")
.attr("id","GB")
.attr("x","30")
.attr("y","350")
.attr("height","100")
.attr("width","100")
.attr("rx","10")
.attr("ry","10")
.attr("class","greenBox")
;

var OB = canvasgrp.append("rect")
.attr("id","OB")
.attr("x","140")
.attr("y","350")
.attr("height","100")
.attr("width","100")
.attr("rx","10")
.attr("ry","10")
.attr("class","orangeBox")
;

var Txt2 = canvasgrp.append("text")
.attr("id","TxtC")
.attr("x",270)
.attr("y",400)
.attr("class","greyText")
.attr("fill","white")
.text("These boxes drawn directly to SVG canvas");

var inst1 = svgContainer.append("use")
.attr("xlink:href","#Ugrp")
.attr("id","inst1")
.attr("transform","translate(0,0)");

var inst2 = svgContainer.append("use")
.attr("xlink:href","#Ugrp")
.attr("id","inst1")
.attr("transform","translate(0,150)");

var USEbox1 = ($("g").filter("#Ugrp").children().first().attr("stroke","yellow").attr("stroke-width","6"));
var USEtext = ($("g").filter("#Ugrp").children().last().attr("fill","brown"));
var USEbox2 = ($("g").filter("#Ugrp").children().first().next().attr("style",".GreenBox"));


var SVGbox1 = ($("g").filter("#Cgrp").children().first().attr("stroke","pink").attr("stroke-width","6"));
var SVGtext = ($("g").filter("#Cgrp").children().last().attr("fill","cyan"));
var SVGbox2 = ($("g").filter("#Cgrp").children().first().next().attr("stroke","magenta").attr("stroke-width","1"));

</script>




Answer

Robert was right with clone() but I wanted to explore a variety of ideas and what their opportunities and limitations were, so I built a little d3js code to try them out. I decided to define a complex assembly to instantiate and then try to make changes to the sub-components of the assembly. The model was a group, containing two rects and a text. The different alternatives I explored were:

Create my complex hierarchy directly in the SVG container. This represented the worst case scenario, where everything has to be hand crafted.

  var canvasgrp = svgContainer.append("g")
      .attr("id","Cgrp")
      ...snipped
      );

  var BA = canvasgrp.append("rect")
      .attr("id","BA")
      ...snipped);

  var BB = canvasgrp.append("rect")
      .attr("id","BB")
      ...snipped);

  var BC = canvasgrp.append("text")
      .attr("id","BC")
      ...snipped
      .text("These boxes drawn directly to SVG canvas");

Create a <defs> section, also containing the same complex hierarchy, so the content won't be rendered. The top level group for the hierarchy is 'USEgrp'.

  var reusables = svgContainer.append("defs")

  var USEgrp = reusables.append("g")
      .attr("id","Ugrp")
  var AA = USEgrp.append("rect")...etc
  var AB = USEgrp.append("rect")...etc
  var AC = USEgrp.append("text")...etc

Directly clone USEgrp, followed by code to set a new unique ID for the group and position it:

  var clonedInst1 = svgContainer
     .append(function(){ return sidingTemplate.cloneNode(true); })
     .attr("id","clonedInst1")
     .attr("transform","translate(20,380)");

Use an object method to clone USEgrp, with the possibility to make an object with methods to create, control and dispose of a complex hierarchy, defined once in the <defs> section. This was my goal.

var boxCollection = {
      new: function (parent) {
        var defaultID = "instance";
        var ID = '_'+ defaultID + "-" + Math.random().toString(36).substr(2, 9);
        var template = document.getElementById("Ugrp");
        var objInst = svgContainer.append(function(){ return template.cloneNode(true); })
        .attr("id",ID)
        .attr("transform","translate(20,140)");
      }

along with var newInstance = boxCollection.new("svgContainer"); to use it to instantiate a copy and set it up.

Finally, I tried the .append('use') command, which proved to be almost entirely not appropriate for generating different instances of a complex hierarchy where each instance needed some predictable customisation. Firefox did allow per instance changes to the rectangle fill and stroke but not to the text (so far as I could tell), while Chrome allowed the text to change and the fill and stroke (with a great deal of flickering) but only to all instances of the defined object, simultaneously.

var inst1 = svgContainer.append("use")
      .attr("xlink:href","#Ugrp")
      .attr("id","inst1")
      .attr("transform","translate(20,260)");

I tested the ability to access the rectangles and text by using rollover and rollout events to change and restore the fills, strokes, stroke-widths & text.

The code is available on https://jsfiddle.net/nohjose/1mktmbvp/9/

Comments