Transform <g> attribute on map of d3.js - svg

I have prepared one example of map in d3.js. I wanted to implement zoom on map with and node(contains circle, smiley and text. as of now i putted circle and smiley) on map shows the city of different countries. When i zoom over map i could not able to transform the tag so smiley got misplace as per my logic. so how to transform only g tags on map. I don't want to transform shape(circle, images) inside tag.
My Jsfiddle link
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".node")
.attr("width", function(){
var self = d3.select(this);
var r = 28 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
g.selectAll(".circle")
.attr("r", function(){
var self = d3.select(this);
var r = 8 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
});
Please anybody help me to solve my issue.

To do semantic zooming, one would need to adjust the width and height of the smiley faces as well as adjust the x and y locations since the adjustments will change relative to the width/height:
g.selectAll(".node")
.attr("x", function(d) { return (projection([d.lon, d.lat])[0]) - (8 / d3.event.scale); })
.attr('y', function(d) { return (projection([d.lon, d.lat])[1]) - (8 / d3.event.scale); })
.attr('width', function () { return 20 / d3.event.scale; })
.attr('height', function () { return 20 / d3.event.scale; })
Demo: http://jsfiddle.net/ktee4dLp/

Related

html canvas clip but with an image

I have been working with html canvas compositing trying to clip a pattern with a mask.
The main issue that I have is that the mask I have comes from an svg with transparencies within the outer most border. I want the entire inside from the outer most border to be filled with the pattern.
Take this SVG for example you can see that there is a single pixel border, then some transparency, and then an opaque red inner blob. The compositing I have done works as the documentation says it should, the single pixel border and the red inner portion pick up the pattern that I want to mask into this shape. The problem is that I want to mask the entire innards starting from the single pixel border.
This is where I think clip might help. But it seems clip only works with manually drawn paths, not paths from an svg (at least that I am aware of).
Is there a way to accomplish what I am trying to do?
Regards,
James
The Path2D constructor accepts an SVG path data argument, that it will parse as the d attribute of an SVG <path> element.
You can then use this Path2D object with the clip() method:
(async () => {
// fetch the svg's path-data
const markup = await fetch("https://upload.wikimedia.org/wikipedia/commons/7/76/Autism_spectrum_infinity_awareness_symbol.svg").then(resp => resp.ok && resp.text());
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
const pathData = doc.querySelector("[d]").getAttribute("d");
// build our Path2D object and use it
const path = new Path2D(pathData);
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.clip(path);
// draw something that will get clipped
const rad = 30;
for(let y = 0; y < canvas.height; y += rad * 2 ) {
for(let x = 0; x < canvas.width; x += rad * 2 ) {
ctx.moveTo(x+rad, y);
ctx.arc(x, y, rad, 0, Math.PI*2);
}
}
ctx.fillStyle = "red";
ctx.fill();
})().catch(console.error);
<canvas width="792" height="612"></canvas>
If you need to transform this path-data (e.g scale, or rotate), then you can create a second Path2D object, and use its .addPath(path, matrix) method to do so:
// same as above, but smaller
(async () => {
const markup = await fetch("https://upload.wikimedia.org/wikipedia/commons/7/76/Autism_spectrum_infinity_awareness_symbol.svg").then(resp => resp.ok && resp.text());
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
const pathData = doc.querySelector("[d]").getAttribute("d");
const originalPath = new Path2D(pathData);
const path = new Path2D();
// scale by 0.5
path.addPath(originalPath, { a: 0.5, d: 0.5 });
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.clip(path);
// draw something that will get clipped
const rad = 15;
for(let y = 0; y < canvas.height; y += rad * 2 ) {
for(let x = 0; x < canvas.width; x += rad * 2 ) {
ctx.moveTo(x+rad, y);
ctx.arc(x, y, rad, 0, Math.PI*2);
}
}
ctx.fillStyle = "red";
ctx.fill();
})().catch(console.error);
<canvas width="396" height="306"></canvas>

how to drag a svg rectangle in d3.js?

Here I am creating a rectangle named resize_icon within another rectangle positioning it at the lower right corner of the big rectangle.I want this resize_icon rectangle to be dragged freely within big rectangle but it should not cross the bigger rectangles top edge and its left edge but can be dragged anywhere else. resize[] is an array which contains id, x and y position of resize_icon rectangle.Please have a look at the following drag code and please tell me why is it not working. Also if anyone can suggest a proper code would be really helpful.
c.append('rect').attr('class','resize_icon').attr('fill','#000')
.attr('id','resize_icon'+i).attr('height',5).attr('width',5)
.attr('x', x+wd+35).attr('y',y+rectHeight-5).on(drag1)
.on('click',function(){
d3.selectAll(".selected").classed("selected",false);
d3.select(this.parentNode).classed("selected",true);
resize_rect(i);
});
var drag1 = d3.behavior.drag().origin(function()
{
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("dragstart", dragstart)
.on("drag", dragon)
.on("dragend", dragstop);
function dragstart() {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("drag", true);
}
function dragon() {
d3.select(this).select('.resize_icon')
.attr("x", d3.event.x)
.attr("y", d3.event.y);
id = d3.select(this).select('.resize_icon').attr("id");
a = id.replace("resize_icon", "");
resize[a].x = d3.event.x;
resize[a].y = d3.event.y;
}
function dragstop() {
d3.select(this).classed("drag", false);
}
Here is my code to drag between a boundary :
function button_drag2(xLower,xHigher,yLower,yHigher){
return d3.behavior.drag()
.origin(function() {
var g = this;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function(d) {
g = this;
translate = d3.transform(g.getAttribute("transform")).translate;
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
if(x<xLower){x=xLower;}
if(x>xHigher){x=xHigher;}
if(y<yLower){y=yLower;}
if(y>yHigher){y=yHigher;}
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
}
}
Then call on your selection :
var selection = "#rectangle"; //change this to whatever you want to select
d3.select(selection).call(button_drag2(-100,100,-100,100));
// this lets you drag the rectangle 100px left, right, up and down. Change to what ever you want :)
Hope this helps you out :)

add images to bubble layout in D3.js

Unable to add images to bubble layout in D3.js . I am trying to append images to the circles in bubble layout but it doesnt works out . the image is not getting transformed.
I want to have look and feel of this:-
http://www.cloudshapes.co.uk/labs/attention-hungry-cabinet-ministers/
here is the fiddle link for what I have been trying to do:
http://jsfiddle.net/Ankitb/eYGCY/4/
var force = d3.layout.force()
.charge(-300)
.size([w, h])
.nodes(nodes)
.on("tick", tick)
.start();
function tick() {
svg.selectAll("circle")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
var interval = setInterval(function () {
var d = {
x: w / 4 + 2 *( Math.random() - 1),
y: h / 4 + 2 *( Math.random() - 1)
};
var personDot = svg.append("g")
.attr("class", "g-person-dots")
.selectAll("g")
.data([d])
.enter().append("g")
.attr("transform", function (d) { return "translate(" + d.x+ "," + d.y + ")"; });
personDot.append("circle")
.data([d]).attr("r", 40)
//.attr("r", 1e-6)
.attr("cx",0).attr("cy",0)
.transition().style("stroke", "gray").style("fill","white")
.ease(Math.sqrt);
personDot.append("image").data([d])
.attr("xlink:href", "PeopleProfilePicture.jpg")
// .attr("x", function (d, i) { return -mugDiameter / 2 - mugDiameter * (i % 9); })
//.attr("y", function (d, i) { return -mugDiameter / 2 - mugDiameter * (i / 9 | 0); })
.attr("width", 80)
.attr("height", 80)
.attr("transform", function (d) { return "translate(" + -d.x / 10 + "," + -d.y / 10 + ")"; });
if (nodes.push(d) > 10) clearInterval(interval);
else { force.start(); }
}, 30);
The translation of an element is relative to its parent element. That is, by default the element will be in the same position as its parent. Therefore, the translation you need to do does not depend on the dynamic data that you pass in, but only on the dimensions of the image. You need to set transform as follows.
.attr("transform", "translate(-40,-40)");
You may also want to make the background of your images transparent such that you can still see the circle.

D3js - Circle position vs radius of circle or curved path

I created a data visualization of a solar system using D3.js.
In doing so, I noticed a weird inconsistency when setting the x,y position of a circle element and the radius of a circle element or curved path element.
To place the planets down, I do:
planetEnter.append("circle")
.attr("r", function (d) {
return planetScale(d.radius); })
.attr("class", "body")
.attr("fill", "url(#gradePlanet)")
.attr("filter", "url(#glowPlanet)")
.attr("transform", function (d) {
// Position of planet in relation to the sun at (0,0)
// x and y are linear scales
return "translate(" + x(d.orbital_radius) + ", " + y(0) + "), scale(.05)"; });
Now to create the orbital lines, I do:
var orbital_arc = d3.svg.arc()
.startAngle(0)
.endAngle(6.28318531) // 360 degrees
.innerRadius(function (d) {
return x(d.orbital_radius); })
.outerRadius(function (d) {
return x(d.orbital_radius); });
Now you would think that this would work and the radius of the arc would match the position of the planet, but it does not. The radius ends up MUCH bigger. To compensate, I found this magic number through trial and error:
var orbital_arc = d3.svg.arc()
.startAngle(0)
.endAngle(6.28318531) // 360 degrees
.innerRadius(function (d) {
return x(d.orbital_radius) - 470; }) // Magic number.
.outerRadius(function (d) {
return x(d.orbital_radius) - 470; }); // Magic number.
That number consistently works for every orbital line and I cannot figure why.
And it's not just the path element, the radius of a circle ends up much bigger too:
planetEnter.append("circle")
.attr("r", function (d) {
return x(d.orbital_radius); })
.attr("class", "body")
.attr("transform", function (d) {
return "translate(" + x(0) + ", " + y(0) + ")"; });
Here are the jsfiddles demostrating this (pan and zoom if you need a better view):
Solar System with Magic Number
Solar System without Magic Number
So why do I need this magic number?
Angles in D3 are set in radians, so you can have a function that does...
function degreesToRadians(degrees) {
return degrees * (Math.PI/180);
}
But you're always using circles, so this is more done more elegantly simply by...
var tau = Math.PI * 2; //this is your first magic number
var orbital_arc = d3.svg.arc()
.startAngle(0)
.endAngle(tau)
As for the second magic number (470) it's half of your width, so putting it all together you can do...
var tau = Math.PI * 2; //this is your first magic number
var orbital_arc = d3.svg.arc()
.startAngle(0)
.endAngle(tau)
.innerRadius(function (d) { return x(d.orbital_radius) - width/2; })
.outerRadius(function (d) { return x(d.orbital_radius) - width/2; });

Svg image representing a node, varying node size in a force-directed graph

So far I've used d3.svg.symbol() in a force-directed graph for distinguishing different node types from one another.
I'd now like to distinguish different node types by displaying node as a svg image. Following one of the examples I used the following code to display the node images:
var node = svg.selectAll("image.node").data(json.nodes);
var nodeEnter = node.enter().append("svg:g").append("svg:image")
.attr("class", "node")
.attr("xlink:href", function(d)
{
switch( d.source )
{
case "GEXP": return "img/node_gexp.svg";
case "CNVR": return "img/node_cnvr.svg";
case "METH": return "img/node_meth.svg";
case "CLIN": return "img/node_clin.svg";
case "GNAB": return "img/node_gnab.svg";
case "MIRN": return "img/node_mirn.svg";
case "SAMP": return "img/node_samp.svg";
case "RPPA": return "img/node_rppa.svg";
}
})
.attr("x", function(d) { return d.px; } )
.attr("y", function(d) { return d.py; } )
.attr("width", "50")
.attr("height", "50")
.attr("transform", "translate(-25,-25)")
.on("click", function(d) {
console.log("nodeclick");
} )
.on("mouseover", fade(0.10) )
.on("mouseout", fade(default_opacity) )
.call(force.drag);
This does display the svg images but I've two problems:
1) I want to scale the node sizes based on an attribute. From what I understand this can be done by supplying the "scale" attribute
transform="scale(something)"
to a suitable place, like to the image tag itself or to the group containing the image:
var node = svg.selectAll("image.node").data(json.nodes);
var nodeEnter = node.enter()
.append("svg:g")
.attr("transform", function(d)
{
var str = "scale(";
if( d.gene_interesting_score && d.gene_interesting_score > 0 )
{
return str + ( (d.gene_interesting_score - minScore ) / ( maxScore - minScore ) ) + ")";
}
return str + 0.7 + ")";
})
.append("svg:image")
....
As it happens, the scale() transform displaces the images: they are no longer on the endpoint of the edge. How do I resize the images properly when initializing the graph, preferrably so that the controlling mechanism is within a single function (e.g. so that I don't have to control x,y,width,height separately)?
2) When the graph is zoomed, Chrome blurs the images whereas in Firefox the image remains crispy (picture). How can this blur be avoided?
Edit: Based on duopixel's suggestions, the code is now:
var nodeGroup = svg.selectAll("image.node").data(json.nodes).enter()
.append("svg:g")
.attr("id", function(d) { return d.id; })
.attr("class", "nodeGroup")
.call(force.drag);
var node = nodeGroup.append("svg:image")
.attr("viewBox", "0 0 " + nodeImageW + " " + nodeImageH)
.attr("class", "node")
.attr("xlink:href", function(d)
{
switch( d.source )
{
case "GEXP": return "img/node_gexp.svg";
case "CNVR": return "img/node_cnvr.svg";
case "METH": return "img/node_meth.svg";
case "CLIN": return "img/node_clin.svg";
case "GNAB": return "img/node_gnab.svg";
case "MIRN": return "img/node_mirn.svg";
case "SAMP": return "img/node_samp.svg";
case "RPPA": return "img/node_rppa.svg";
}
})
.attr("width", nodeImageW)
.attr("height", nodeImageH)
.attr("transform", function(d)
{
var matrix = "matrix(";
var scale = 0.7; // sx & sy
if( d.gene_interesting_score && d.gene_interesting_score > 0 )
{
scale = ( (d.gene_interesting_score - minScore ) / ( maxScore - minScore ) ) * 0.5 + 0.25;
}
//console.log("id=" + d.id + ", score=" + scale );
matrix += scale + ",0,0," + scale + "," + ( d.x - ( scale*nodeImageW/2 ) ) + "," + ( d.y - ( scale*nodeImageH/2 ) ) + ")";
return matrix;
})
// .attr("transform", "translate(-25,-25)")
.on("click", function(d) {
console.log("nodeclick");
} )
.on("mouseover", fade(0.10) )
.on("mouseout", fade(node_def_opacity) );
It solves the problem #1, but not the second one: by selecting
var nodeImageH = 300;
var nodeImageW = 300;
The resulting svg image contains a lot empty space (seen by selecting the image with firebug's selection tool). The images were created in Inkscape, canvas size cropped to 50x50 which should be the correct viewing pixel size.
#1 You need to set the transform origin. In SVG this means that you are going to have to use a transform matrix as answered here: https://stackoverflow.com/a/6714140/524555
#2 To solve the blurry scaling modify the viewBox, width, height of your svg files to start as large images (i.e. <svg viewBox="0 0 300 300" width="300" height="300">.

Resources