Selecting SVGs in D3 - svg

I've got a little visualisation going where I have pairs of rectangles. Each pair shares the same id attribute. So currently, I've set it up so that if I hover over either one, both change colour. That's great, but, as I've done it, I need a new select("svg #whatever) command for each different pair. That can't be right.
So what I was thinking was setting up a variable, which would be the id of the element I was mousing over, then putting that inthe selct command, something like this
svg.selectAll("rect")
.on("mouseover", function(d) {
var thisid = d3.select(this).attr("id")
svg.selectAll("svg #"+thisid+)
.attr("fill", "red")
Except that doesn't work. Is it just the syntax - ie I've got the + + wrong - or am I making a fundamental mistake here?

Your idea is good, but as Robert Longson pointed out, you can't use id to link two related objects, since id has to be unique for the entire webpage.
You can, however, add any attribute you want to your data element (other than id) and preferably with an attribute name that starts "data-", and then use a CSS attribute selector to find other elements with the same attribute. Like this:
//when you create the rectangles:
rect.attr("data-id", function(d) {
return d.id;/* or however you figure out this id */
});
//Your event handler
svg.selectAll("rect")
.on("mouseover", function(d) {
var thisid = d3.select(this).attr("data-id")
svg.selectAll("svg rect[data-id='" + thisid + "']")
//note the single quotes inside double quotes
//the final selector should look like "svg rect[data-id='23']"
.attr("fill", "red");
});
The only downside it that browsers don't index all attributes for fast access like they do with classnames and ids, so it could be slow if you had a large number of rectangles and mouse-over them rapidly. Using a class would make selection faster, but adds a complication if you have multiple classes -- you can't just access the one class value you're interested in by calling .attr("class"). You could, however, re-access whichever data variable you are using to define data-id in the first place. Like this:
//when you create the rectangles:
rect.attr("data-id", function(d) {
return "data-rectangle " + /* any other classes you are adding, + */
"data-id-" + d.id;/* or however you figure out this id */
});
//Your event handler
svg.selectAll("rect")
.on("mouseover", function(d) {
var thisid = d.id; /* or however you figure out this id */
//d3 automatically passed the element's data object to your event handler
svg.selectAll("svg rect.data-id-" + thisid)
//the final selector should look like "svg rect.data-id-23"
.attr("fill", "red");
});

Related

How to access Object3D material after adding the GLTF

I want to duplicate my GLTF models with different positions/colors dynamically, to do so I have done:
const L_4_G = new Object3D();
...
const multiLoad_4 = (result, position) => {
const model = result.scene.children[0];
model.position.copy(position);
model.scale.set(0.05, 0.05, 0.05);
//
L_4_G.add(model.clone())
scene.add(model);
};
...
function duplicateModel4() {
L_4_G.translateX(-1.2)
L_4_G.translateY(0.0)//0.48
L_4_G.translateZ(1.2)
L_4_G.rotateY(Math.PI / 2);
scene.add(L_4_G);
}
I didn't find out how can I change the Object3D color from the documentation, can you please tell me how can I do that? thanks in advance.
Here is the full code that I'm using, and here are the models
Update
I have seen this solution, to store a set of colors in the object's userData and choose the color later:
L_2_G.userData.colors = {green : #00FF00, red : ..., ...}
L_2_G.children[0].material.color(userData.colors["green"])
But I'm getting an error that children[0] undefined, but I can see that this object has a child and a material, and color via the console: console.log(L_2_G.children), console.log(L_2_G.children.length)--> 0
Also I have tried getObjectByName as explained here:
scene.getObjectByName(name).children[0].material.color.set(color);
which also reslts: children[0] is undefined, scene.getObjectByName(name).children.length is 0.
THREE.Object3D is a base class for anything that can go in a scene graph, including lights, cameras, and empty objects. Not all Object3D instances have geometry or materials. You may be looking for the THREE.Mesh subclass which does have materials and colors.
In general, code like getObjectByName(...) and model = result.scene.children[0] is very content-specific. The file might contain many nested objects, and .children[0] just grabs the first part. It's usually best to traverse the scene graph instead, looking for the objects you want to modify (e.g. looking for all Meshes, or Meshes with a particular name).
const model = result.scene;
model.traverse((object) => {
if (object.isMesh) {
object.material.color.setHex( 0x404040 );
}
});
Then you can either add the entire group to your scene (scene.add(model)), or just add parts of it. Keep in mind that adding meshes to a new parent removes them from their previous parent, and you shouldn't do that while traversing the previous parent. Instead you can make a list of meshes, and add them in a second step:
const meshes = [];
result.scene.traverse((object) => {
if (object.isMesh) {
meshes.push(object);
}
});
for (const mesh of meshes) {
scene.add(mesh);
}
Finally, the position of an object is inherited from its parents. By removing the object from its original parents you might change its position in the scene. If you are planing to assign a new position to the object anyway, that is fine.

svg.js with existing svg

I'm trying to implement svg.js to make a map of a clickable floorplan.
But I cannot make this work properly, or this works but doesn't work like I expected:
const map = SVG.get('mysvg');
map.click(function( event ) {
this.fill({ color: '#000' });
console.log(this);
console.log(event.target);
});
When I try to click on some area on the map, instead of change fill color I get nothing.
Actually svgjs triggers 'create' as you can see in console with inspector.
Not sure what am I doing wrong here?
I would expect that the area will change fill color?
https://codepen.io/bobz-zg/pen/LdyXBe
You can use the select function as described here to create a Set. You'd then use the each() method from Set to iterate over each entry and apply the click event handler. For example:
const map = SVG.get("mysvg");
var restaurants = map.select('[id*="restaurant"]:not(g)'); // Create an svg.js Set, but do not capture the group in the set
restaurants.each(function() {
this.click(function(event) {
this.fill({ color: "#000" });
});
});
If you can edit the svg that you are using as an input, I would suggest adding a class to each of the clickable elements to use that as the reference for select. Otherwise, you'll have to do something like selecting all element id 'types' (i.e. restaurant, retail etc.) separately and concatenating the Sets using Set.add() before you do the loop to add the click listener, but that could get messy.

Select and manipulate particular paths within a path group fabric.js

I am making an app that involves shapes like cylinders and boxes, and need to be able to do this with fabric.js. I know about three.js but for my purposes, it must be in 2D.
Initially I thought I would just create them in 3D software and render images which can then be added to the canvas, which I did successfully...
However, I have run into a hurdle where fabric only allows patterns to be filled onto paths or objects (rect, circle etc.)....not images (png).
Since I absolutely need patterns, I now need to create these cylinders in SVG. I have gotten as far as making the cylinders in Illustrator, saving them as SVG's and then using them on the canvas, then adding fill patterns on them. So far so good.
Now I want to be able to fill a different pattern for the top of the cylinder, and a different pattern to the side BUT still have it as one object.
So...How can I select and manipulate particular paths within a path group? Is there anyway to give each path within the group a custom attribute (eg. name) which I can then target? Do I need to create two seperate SVG files and then add them seperately, and if so, how can I do this and still have it as one object?
Here's how I am adding the svg to the canvas...
fabric.loadSVGFromURL("/shapes/50-250R.png", function(objects) {
var oImg = fabric.util.groupSVGElements(objects);
oImg.perPixelTargetFind = true;
oImg.targetFindTolerance = 4;
oImg.componentType = "Shape";
oImg.lockUniScaling = true;
oImg.lockScalingX = true;
oImg.lockScalingY = true;
oImg.setControlsVisibility({'tl': false, 'tr': false, 'bl': false, 'br': false});
canvas.add(oImg);
canvas.renderAll();
});
Here is how I am adding the pattern...
var textureIMG = new Image;
textureIMG.crossOrigin = "anonymous";
textureIMG.src = texture.image;
obj.setFill(); //For some reason, the fill doesn't happen without this line.
var pattern = new fabric.Pattern({
source: textureIMG,
repeat: 'repeat'
});
if (obj instanceof fabric.PathGroup) {
obj.getObjects().forEach(function(o) {
o.setFill(pattern);
});
} else {
obj.setFill(pattern);
}
canvas.renderAll();
Thanks in advance.
So I managed to figure this out. Each path within the path group is stored in the 'paths' array of the object.
I can now add a pattern to the top of the cylinder using...
var obj = canvas.getActiveObject();
obj.paths[0].fill = patternOne;
and to the sides using...
obj.paths[1].fill = patternTwo;

How to use Maki svg icons with Snap.svg?

I'm experimenting with Snap in order to use svg and need to be able to use the Maki icons defined in https://github.com/mapbox/maki.
My plan is to load the svg's I need, and then instantiate them for particular icons on a piece of Snap paper. But in order for this to work, I need to place the icon at a particular place on the paper, but I can't get translation to work. Neither one of the translation techniques below works; the code works as is, but always places the icon at the top left.
What am I missing? There's not enough documentation on Snap, and I don't know if the problem is with the way the Maki icon svg is defined, or my use of Snap.
var icon = Snap.load("maki/bicycle-24.svg", function(f) {
var g = f.select("g").clone();
// var g = f.select("#layer1").clone(); // also works
// g.transform("t120,120");
// var t = new Snap.Matrix();
// t.translate(120,120);
// g.transform(t);
paper.append(g);
});
The cloning needs to happen after the append, as when loading an svg in Snap its just a fragment.
So you will need to do something like...
paper.append(f);
var element = paper.select('#someId').clone();
element.transform( myTransform );
Thank you! That did the trick! And since Snap is so poorly documented, I'm going to insert the code here that allows a general solution.
// Base set from which markers are constructed
var iconSet = paper.group();
iconSet.attr({ class: 'hide' });
// Instantiations of icons
var markers = paper.g();
// Now, create SVG shape
var icon = Snap.load("maki/bicycle-24.svg", function(icon) {
// Add it to the icon set
iconSet.append(icon);
// Instantiate it and remove from main view
var element = paper.select('#svg4460'); // Copies it!
// var element = paper.select('#base'); // Selects something but doesn't work
// var element = paper.select('#layer1'); // Selects something but doesn't work
// var element = paper.select('#bicycle-24'); // Silent fail
element = element.clone();
element.remove();
// Clone this icon and move it
var t = new Snap.Matrix();
t.translate(10,120);
element.transform(t);
// Insert into main document view (markers)
markers.add(element);
});

d3 basic animations

Trying to follow along the tutorial here, but there isn't enough code presented to me to bridge the gaps.
Here is the fiddle I am working on to accomplish the same thing, with d3 loaded, however, the animation transitions are not concurrently happening, let alone at all, it is just changing the attributes, something I am already familiar with in SVG hard coding with JQuery selectors. So where am I going wrong, or missing the boat?
// example code doesn't work
var circle = svg.selectAll("circle");
circle.style("fill", "steelblue");
circle.attr("cy", 90);
circle.attr("r", 30);
// this does, but animations don't work
d3.selectAll('circle').style("fill", "steelblue");
d3.selectAll('circle').attr("cy", 90);
d3.selectAll('circle').attr("r", 30);
I am eventually trying to leverage the tweening of d3, but I cant get the basics off the ground. Thanks for you help in advance....
In the example code, svg is previously assigned to a d3 selection object:
var svg = d3.select("#chart-2").append("svg")
.attr("width", w)
.attr("height", h);
Therefore, you can use it to select the child elements, as in the original example.
Eg. your fiddle could be rewritten like so:
var svg = d3.select("#svg");
//svg is now a d3.selection object.
svg.on("click", function() {
var circle = svg.selectAll("circle");
circle.style("fill", "steelblue");
circle.attr("cy", 90);
circle.attr("r", 30);
});
Something to note about binding events using d3:
Within the anonymous function:
The event is bound to d3.event
The dom element - not the d3.selection object - is bound to this
If you pass an argument to the function, it will be bound to the data which is joined to the element.
Not really code, but should show what I mean:
var someD3Selection = d3.select("#someElement");
someD3Selection.on("click", function(boundData) {
d3.event.preventDefault(); // <-- here's the event
this; // <-- the actual dom element which is selected in someD3Selection
d3.select(this); // <-- same as someD3Selection.
});
Is this what you're looking for? The duration is optional but it's easier to see what's happening when it's a bit slower.
$('#svg').on('click', function() {
d3.selectAll('circle').style("fill", "grey").transition().duration(5000).style("fill", "steelblue").attr("cy", 90).attr("r", 30);
});

Resources