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; });
Related
I am trying to load a CSV data set into d3 by assigning it to a variable, but it seems that I keep receiving an error saying that enter() is not a function. I think the issue lies in the way I'm loading the CSV data.
For reference, I'm following this tutorial: http://duspviz.mit.edu/d3-workshop/scatterplots-and-more/
Here is my code for reference.
var ratData = [];
d3.csv("rat-data.csv", function(d) {
return {
city : d.city, // city name
rats : +d.rats // force value of rats to be number (+)
};
}, function(error, rows) { // catch error if error, read rows
ratData = rows; // set ratData equal to rows
console.log(ratData);
createVisualization(); // call function to create chart
});
function createVisualization(){
// Width and height of SVG
var w = 150;
var h = 175;
// Get length of dataset
var arrayLength = ratData.length; // length of dataset
var maxValue = d3.max(ratData, function(d) { return +d.rats;} ); // get maximum
var x_axisLength = 100; // length of x-axis in our layout
var y_axisLength = 100; // length of y-axis in our layout
// Use a scale for the height of the visualization
var yScale = d3.scaleLinear()
.domain([0, maxValue])
.range([0, y_axisLength]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Select and generate rectangle elements
svg.selectAll( "rect" )
.data( ratData )
.enter()
.append("rect")
.attr( "x", function(d,i){
return i * (x_axisLength/arrayLength) + 30; // Set x coordinate of rectangle to index of data value (i) *25
})
.attr( "y", function(d){
return h - yScale(d.rats); // Set y coordinate of rect using the y scale
})
.attr( "width", (x_axisLength/arrayLength) - 1)
.attr( "height", function(d){
return yScale(d.rats); // Set height of using the scale
})
.attr( "fill", "steelblue");
// Create y-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 75)
.attr("x2", 30)
.attr("y2", 175)
.attr("stroke-width", 2)
.attr("stroke", "black");
// Create x-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 175)
.attr("x2", 130)
.attr("y2", 175)
.attr("stroke-width", 2)
.attr("stroke", "black");
// y-axis label
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.text("No. of Rats")
.attr("transform", "translate(20, 20) rotate(-90)")
.attr("font-size", "14")
.attr("font-family", "'Open Sans', sans-serif");
}; // end of function
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/
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 :)
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.
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">.