d3: A sub array of objects - javascript

I have the following structure:
[
{ 'length': 10, attributes: [1,2,3] },
{ 'length': 7, attributes: [1,3,4,5] },
{ 'length': 12, attributes: [3,5,7,9,10] },
]
and I am doing the following:
x = d3.scale.linear().domain([0, maxHeight]).range([50, w]),
y = d3.scale.linear().domain([0, maxHeight]).range([h, 20]);
z = d3.scale.linear().domain([0, maxHeight]).range([0, h - 20]);
var chart = svg.selectAll("g.chart")
.data(items)
.enter()
.append("svg:g")
.attr("class", "chart");
chart.append("svg:rect")
.attr("fill", 'darkblue')
.attr("class", 'data')
.attr("x", function(d, i) { return x(i+1); })
.attr("y", function(d, i) { return bottom - z(d['length']) + 15 })
.attr("width", 4)
.attr("height", function(d, i) { return z(d['length']) - z(d['min']); })
What I would like to do is add circles on each of these rectangles which corresponds to the attributes in my structure. Basically, (for one 'item'}, I should see something like this:
<g class="chart">
<rect fill="darkblue" class="data" x="626.1538461538462" y="15" width="6" height="530"></rect>
<circle cx="626.1538461538462" cy="(y1)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
<circle cx="626.1538461538462" cy="(y2)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
<circle cx="626.1538461538462" cy="(y3)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
</g>
The only thing I can think of is looping over the attributes and adding them element by element:
for (z=0; z< 3; ++z)
{
chart.append("svg:circle")
.data(items[z]['attributes'])
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(i+1); })
.attr("cy", function(d, i)
{
console.log(d);
return bottom - 15;
})
.attr("r", 5);
}
Is there a better way to do this?

You can created a nested selection instead of looping:
chart.selectAll("svg:circle")
.data(function(item) { return item.attributes; })
.enter()
.append("svg:circle")
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(i+1); })
.attr("cy", function(d, i)
{
console.log(d);
return bottom - 15;
})
.attr("r", 5);
Example:
To keep the cx the same for each parent rect, you can pass the parent_idx through
chart.selectAll("svg:circle")
.data(function(item, parent_idx) {
return item.attributes.map(function (attr_val) {
return { attr_val: attr_val, parent_idx: parent_idx };
});
})
.enter()
.append("svg:circle")
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(d.parent_idx); })
.attr("cy", function(d, i)
{
return y(d.attr_val);
})
.attr("r", 5);

You can use nested selections. The primary selection will create the groups, each group will have a data item bound to it.
var data = [
{name: 'A', items: [1, 2]},
{name: 'B', items: [2, 3, 4]}
];
var cScale = d3.scale.category10()
.domain(d3.range(10));
var grp = svg.selectAll('g.main')
.data(data)
.enter()
.append('g')
.attr('class', 'main')
.attr('transform', function(d, i) {
return 'translate(0,' + i * 20 + ')';
});
Then, you can create a nested selection, passing an accessor function to the data method. I have an example with rect elements, but with circles is the same:
grp.selectAll('rect')
.data(function(d) { return d.items; })
.enter()
.append('rect')
.attr('x', function(d) { return 10 * d; })
.attr('width', 8)
.attr('height', 10)
.attr('fill', function(d) { return cScale(d); });
You may found the article Nested Selections useful. I wrote a small jsfiddle too: http://jsfiddle.net/pnavarrc/h2YVd/

Related

Find original color of element before mouseover

I have a bubble chart in which I make bubbles in the following way:
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("opacity", 0.3)
.attr("r", 20)
.style("fill", function(d){
if(+d.student_percentile <= 40){
return "red";
}
else if(+d.student_percentile > 40 && +d.student_percentile <= 70){
return "yellow";
}
else{
return "green";
}
})
.attr("cx", function(d) {
return xscale(+d.student_percentile);
})
.attr("cy", function(d) {
return yscale(+d.rank);
})
.on('mouseover', function(d, i) {
d3.select(this)
.transition()
.duration(1000)
.ease(d3.easeBounce)
.attr("r", 32)
.style("fill", "orange")
.style("cursor", "pointer")
.attr("text-anchor", "middle");
texts.filter(function(e) {
return +e.rank === +d.rank;
})
.attr("font-size", "20px");
}
)
.on('mouseout', function(d, i) {
d3.select(this).transition()
.style("opacity", 0.3)
.attr("r", 20)
.style("fill", "blue")
.style("cursor", "default");
texts.filter(function(e) {
return e.rank === d.rank;
})
.transition()
.duration(1000)
.ease(d3.easeBounce)
.attr("font-size", "10px")
});
I have given colors red, yellow, green to the bubbles based on the student percentile. On mouseover, I change the color of bubble to 'orange'. Now the issue is, on mouseout, currently I am making colors of bubbles as 'blue' but I want to assign the same color to them as they had before mouseover, i.e., red/green/yellow. How do I find out what color, the bubbles had?
One way is to obviously check the percentile of student and then give color based on that(like I have initially assigned green/yellow/red colors), but is there any direct way of finding the actual color of bubble?
Thanks in advance!
There are several ways for doing this.
Solution 1:
The most obvious one is declaring a variable...
var previous;
... to which you assign to the colour of the element on the mouseover...
previous = d3.select(this).style("fill");
... and reuse in the mouseout:
d3.select(this).style("fill", previous)
Here is a demo:
var svg = d3.select("svg");
var colors = d3.scaleOrdinal(d3.schemeCategory10);
var previous;
var circles = svg.selectAll(null)
.data(d3.range(5))
.enter()
.append("circle")
.attr("cy", 75)
.attr("cx", function(d, i) {
return 50 + 50 * i
})
.attr("r", 20)
.style("fill", function(d, i) {
return colors(i)
})
.on("mouseover", function() {
previous = d3.select(this).style("fill");
d3.select(this).style("fill", "#222");
}).on("mouseout", function() {
d3.select(this).style("fill", previous)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Solution 2:
However, D3 has a nice feature, called local variables. You simply have to define the local...
var local = d3.local();
..., set it on the mouseover:
local.set(this, d3.select(this).style("fill"));
And then, get its value on the mouseout:
d3.select(this).style("fill", local.get(this));
Here is the demo:
var svg = d3.select("svg");
var colors = d3.scaleOrdinal(d3.schemeCategory10);
var local = d3.local();
var circles = svg.selectAll(null)
.data(d3.range(5))
.enter()
.append("circle")
.attr("cy", 75)
.attr("cx", function(d, i) {
return 50 + 50 * i
})
.attr("r", 20)
.style("fill", function(d, i) {
return colors(i)
})
.on("mouseover", function() {
local.set(this, d3.select(this).style("fill"));
d3.select(this).style("fill", "#222");
}).on("mouseout", function() {
d3.select(this).style("fill", local.get(this));
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Solution 3:
Since DDD (also known as D3) means data-driven documents, you can use the bound datum to get the previous colour.
First, you set it (in my demo, using the colors scale):
.style("fill", function(d, i) {
return d.fill = colors(i);
})
And then you use it in the mouseout. Check the demo:
var svg = d3.select("svg");
var colors = d3.scaleOrdinal(d3.schemeCategory10);
var circles = svg.selectAll(null)
.data(d3.range(5).map(function(d) {
return {
x: d
}
}))
.enter()
.append("circle")
.attr("cy", 75)
.attr("cx", function(d) {
return 50 + 50 * d.x
})
.attr("r", 20)
.style("fill", function(d, i) {
return d.fill = colors(i);
})
.on("mouseover", function() {
d3.select(this).style("fill", "#222");
}).on("mouseout", function(d) {
d3.select(this).style("fill", d.fill);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
For using this solution #3, the element's datum has to be an object.
PS: drop that bunch of if...else for setting the style of the bubbles. Use a scale instead.

How to place rectangles behind text in d3.js

I am having difficulty trying to place rectangles behind text as background in d3.js. I read that in order to do that you have to append to the same g element but in my case it does not work like that.
my code: plunker
var urls = [{
"wor": "Nordmerika",
"number": "10.9",
"lon": "-100.33",
"lat": "47.61"
}, {
"wor": "Latinamerika",
"number": "14.2",
"lon": "-56.62",
"lat": "-8.53"
}, {
"wor": "Afrika",
"number": "51.8",
"lon": "24.5085",
"lat": "8.7832"
}, {
"wor": "Asien",
"number": "27.5",
"lon": "104.238281",
"lat": "34.51561"
}, {
"wor": "GUS | Russland",
"number": "3.4",
"lon": "62.753906",
"lat": "47.923705"
}, {
"wor": "Europa | MSOE",
"number": "10.9",
"lon": "15.2551",
"lat": "54.526"
}]
//starting map
var margin = {
top: 10,
left: 10,
bottom: 10,
right: 10
},
width = parseInt(d3.select('#map').style('width')),
width = width - margin.left - margin.right,
mapRatio = .5,
height = width * mapRatio;
//Map projection
var projection = d3.geo.equirectangular()
.scale(width / 5.8)
.translate([width / 2, height / 2]) //translate to center the map in view
//Generate paths based on projection
var path = d3.geo.path()
.projection(projection);
//Create an SVG
var svg = d3.select("#map")
.append("svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMinYMin");
//Group for the map features
var features = svg.append("g")
.attr("class", "features");
var labelWidths = [];
d3.json("countries.topojson", function(error, geodata) {
if (error) return console.log(error); //unknown error, check the console
var layerOne = svg.append("g");
var layerTwo = svg.append("g");
var layerThree = svg.append("g");
//Create a path for each map feature in the data
features.selectAll("path")
.data(topojson.feature(geodata, geodata.objects.subunits).features) //generate features from TopoJSON
.enter()
.append("path")
.attr("d", path)
.on("click", clicked)
.style('fill', '#cdd5db')
.style('stroke', '#ffffff')
.style('stroke-width', '0.5px')
.on('mouseover', function(d, i) {
d3.select(this).style('stroke-width', '2px');
})
.on('mouseout', function(d, i) {
d3.select(this).style('stroke-width', '0.5px');
});
var bubbles = layerOne.attr("class", "bubble")
.selectAll("circle")
.data(urls)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d, i) {
return projection([d.lon, d.lat])[1];
})
.attr("r", function(d) {
if (width >= 1000) {
return (d.number)
} else {
return d.number
}
})
.style('fill', function(d) {
if (d.wor == 'Afrika') {
return '#dc0f6e'
} else {
return '#3e3e3e'
}
});
var text = layerTwo
.attr('class', 'text')
.selectAll('text')
.data(urls)
.enter()
.append('text')
.attr('x', function(d, i) {
if (d.number < 10) {
return projection([d.lon, d.lat])[0] + 60;
} else {
return projection([d.lon, d.lat])[0]
}
})
.attr('y', function(d, i) {
return projection([d.lon, d.lat])[1];
})
.text(function(d) {
return d.number;
})
.attr("dy", function(d) {
if (this.getBBox().width > d.number * 3) {
return '-2em'
} else {
return "0.3em"
}
})
.attr("text-anchor", "middle")
.style('fill', '#fff')
.style('font-weight', 'bold')
.style('font-size', '1em');
var labels = layerThree
.attr('class', 'labels')
.selectAll('text')
.data(urls)
.enter()
.append('text')
.attr('x', function(d, i) {
return projection([d.lon, d.lat])[0] + 60;
})[plunker][1]
.attr('y', function(d, i) {
return projection([d.lon, d.lat])[1];
})
.attr("text-anchor", "middle")
.text(function(d) {
return d.wor;
})
.attr('dy', function(d) {
labelWidths.push(this.getBBox().width)
var radius = d.number * 2
if (radius > 10) {
return d.number * 4;
} else {
return '-0.5em'
}
})
.style('font-size', '1em')
.style('font-weight', 'bold');
var rect = layerThree
.attr('class', 'rectlabels')
.selectAll('rect')
.data(urls)
.enter()
.append('rect')
.attr('x', function(d, i) {
return projection([d.lon, d.lat])[0] + 60;
})
.attr('y', function(d, i) {
return projection([d.lon, d.lat])[1];
})
.attr('dy', function(d) {
return '1em'
})
.style('fill', '#ffffff')
.attr('width', function(d, i) {
return labelWidths[i] / 10 + 'em'
})
.attr('height', '1em');
function clicked(d, i) {
}
});
It looks like the rectangles don't line up correctly with the text, there are a couple of reasons for this:
Your <text> is anchored at the middle, and your <rects> are anchored at the top-left point of the <rect>.
You're setting dy on your <text> elements at varying amounts, this moves the text down by that distance, but all your <text> elements have dy=1em, so they're not moving down by the same amount.
I would suggest that you create 6 <g> elements, one for each label, each containing the <rect> element and the <text> element. Then you only need to set x & y attributes on the <g> element, and it's children should stay together.
I think you should also try removing the text-anchor attribute, and instead move the <g> element left by half its width ((labelWidths[i] / 10) / 2) to get it to center over the coordinates. This is made difficult by the fact that your width is in em units, so you might need to do it in pixels and adjust accordingly.
Try it without the dy elements too and see if that helps with the vertical alignment.

Chaining transitions

I'm trying to chain a transition in D3 and I can't quite figure out how to make it work properly. I've read through some of the examples and I feel like I'm missing something with regards to the selections (possibly because my selections are across different layers).
You can see an example below, clicking 'Objectives' should animate a pulse of light to the "Service" node. Once the pulse arrives I want the Service node to fill to orange with a transition. At the moment I'm aware of the fact my selection will fill both circles - I'll fix that shortly.
What happens however is that when the pulse arrives nothing happens:
var t0 = svg.transition();
var t1 = t0.selectAll(".pulse")
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; });
t1.selectAll(".node")
.style("fill", "#F79646");
The only way I seem to be able to get a change is if I change the final bit of code to:
t0.selectAll(".node")
.style("fill", "#F79646");
However that causes the node to fill instantly, rather than waiting for the pulse to arrive. It feels like the selection isn't "expanding" to select the .node instances, but I'm not quite sure
var nodes = [
{ x: 105, y: 105, r: 55, color: "#3BAF4A", title: "Objectives" },
{ x: 305, y: 505, r: 35, color: "#F79646", title: "Service" }
];
var links = [
{ x1: 105, y1: 105, x2: 305, y2: 505 }
];
var svg = d3.select("svg");
var relationshipLayer =svg.append("g").attr("id", "relationships");
var nodeLayer = svg.append("g").attr("id", "nodes");
// Add the nodes
var nodeEnter = nodeLayer.selectAll("circle").data(nodes).enter();
var nodes = nodeEnter.append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";})
.on("click", function (d) {
d3.select(this)
.select("circle")
.transition()
.style("stroke", "#397F42")
.style("fill", "#3BAF4A");
pulse(d);
});
var circles = nodes.append("circle")
.attr("class", "node")
.attr("r", function (d) { return d.r; })
.style("fill", "#1C1C1C")
.style("stroke-width", "4px")
.style("stroke", function (d) { return d.color; });
var texts = nodes.append("text")
.text(function (d) { return d.title; })
.attr("dx", function(d) { return -d.r / 2; })
.style("fill", "white");
function pulse(d) {
function distanceFunction(x1, y1, x2, y2) {
var a = (x2 - x1) * (x2 - x1);
var b = (y2 - y1) * (y2 - y1);
return Math.sqrt(a + b);
};
var lineFunction = d3.svg.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; })
.interpolate("linear");
var lines = relationshipLayer
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("x1", function(d) { return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; })
.attr("stroke-dasharray", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); })
.attr("stroke-dashoffset", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); });
var pulse = relationshipLayer
.selectAll(".pulse")
.data(links)
.enter()
.append("circle")
.attr("class", "pulse")
.attr("cx", function(d) { return d.x1; })
.attr("cy", function(d) { return d.y1; })
.attr("r", 50);
lines.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("stroke-dashoffset", 0);
var t0 = svg.transition();
var t1 = t0.selectAll(".pulse")
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; });
t1.selectAll(".node")
.style("fill", "#F79646");
};
svg {
background: #222234;
width: 600px;
height: 600px;
font-size: 10px;
text-align: center;
font-family: 'Open Sans', Arial, sans-serif;
}
circle {
fill: url(#grad1);
}
line {
fill: none;
stroke: #fff;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="svg">
<defs>
<radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="5%" style="stop-color:rgb(255,255,255); stop-opacity:1" />
<stop offset="10%" style="stop-color:rgb(255,255,255); stop-opacity:0.8" />
<stop offset="20%" style="stop-color:rgb(255,255,255); stop-opacity:0.6" />
<stop offset="60%" style="stop-color:rgb(255,255,255);stop-opacity:0.0" />
</radialGradient>
</defs>
</svg>
The reason why you're not seeing a change for the second transition is that it's not applied to anything. The selection for your first transition contains all the elements with class pulse, and then you're selecting the elements with class node from the elements of this first selection. There are no elements that have both classes, therefore your selection is empty and the change is applied to no elements.
In general, you can't chain transitions in the way that you're currently using when changing selections. Instead, use the .each() event handler of the transition, which allows you to install a handler function that is executed when the transition finishes. In your case, this would look like this:
svg.selectAll(".pulse")
.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; })
.each("end", function() {
svg.selectAll(".node")
.transition()
.duration(2000)
.style("fill", "#F79646");
});
This will select all the elements that have class node and change their fill to orange with a transition.
There are two problems with the above code -- first, as you have already observed, it changes the fill of all the nodes and not just the target, and second, the end event handler is executed for each element in the transition, not just once. For your particular example, this isn't a problem because you have only one link that's animated, but if you had several, the function (and therefore the transition) would be executed more than once.
Both problems can be fixed quite easily with the same code. The idea is to filter the selection of node elements to include only the target of the line. One way of doing this is to compare the target coordinates of the line with the coordinates of the elements in the selection:
svg.selectAll(".pulse")
.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; })
.each("end", function(d) {
svg.selectAll(".node")
.filter(function(e) {
return e.x == d.x2 && e.y == d.y2;
})
.transition()
.duration(2000)
.style("fill", "#F79646");
});
The argument d to the handler function is the data bound to the element that is being transitioned, which contains the target coordinates. After the filter() line, the selection will contain only the circle that the line moves towards. It is safe to execute this code several times for multiple lines as long as their targets are different.
Complete demo here.

Stopping a Line At the Outer Edge of a Circle

Hey guys I recently started learning D3.js and have ran into a problem: http://i.stack.imgur.com/Nqghl.png. How can I stop drawing the line at the outer edge of these circles?
Possibly another solution could be to re-arrange the layers so that the circle one is on top of the line one.
Here is my sample code
data = [{name: 'one', parent: 'one', a: 1}, {name: 'two', parent: 'one', a: 2}, {name: 'three', parent: 'one', a: 2}]
r = 30;
var centerX = function (d, i) {
return (i * ((r * 2) + 20)) + r;
};
var centerY = function (a, i) {
return (a * 160) + (r * 2);
}
var global = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500)
global.selectAll("circle")
.data(data)
.enter()
.append("circle")
.style("stroke", "gray")
.style("fill", "aliceblue")
.attr('r', r)
.attr('cx', function(d, i ) {return centerX(r, i)})
.attr('cy', function(d, i) {return centerY(d.a, i)})
.attr('id', function(d) { return 'one'});
global.selectAll("line")
.data(data)
.enter()
.append("line")
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 4)
.style('stroke-opacity', .4)
.attr('x1', function(d, i) {return centerX(r, i)})
.attr('y1', function(d, i) {return centerY(d.a, i)})
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
})
Any ideas? Thanks in advance!
You are right about switching the order in which you append the lines and circles, lines first, then circles. You just have to be careful to preserve the line selection in a variable that you can use to apply the line attributes that are dependent on the circles AFTER drawing the circles.
FIDDLE example
lines
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
});

D3 force directed graph, different shape according to data and value given?

I've made a force directed graph and I wanted to change shape of nodes for data which contains "entity":"company" so they would have rectangle shape, and other one without this part of data would be circles as they are now.
You can see my working example with only circle nodes here: http://jsfiddle.net/dzorz/uWtSk/
I've tried to add rectangles with if else statement in part of code where I append shape to node like this:
function(d)
{
if (d.entity == "company")
{
node.append("rect")
.attr("class", function(d){ return "node type"+d.type})
.attr("width", 100)
.attr("height", 50)
.call(force.drag);
}
else
{
node.append("circle")
.attr("class", function(d){ return "node type"+d.type})
.attr("r", function(d) { return radius(d.value) || 10 })
//.style("fill", function(d) { return fill(d.type); })
.call(force.drag);
}
}
But then I did not get any shape at all on any node.
What Is a proper way to set up this?
The whole code looks like this:
script:
var data = {"nodes":[
{"name":"Action 4", "type":5, "slug": "", "value":265000},
{"name":"Action 5", "type":6, "slug": "", "value":23000},
{"name":"Action 3", "type":4, "slug": "", "value":115000},
{"name":"Yahoo", "type":1, "slug": "www.yahoo.com", "entity":"company"},
{"name":"Google", "type":1, "slug": "www.google.com", "entity":"company"},
{"name":"Action 1", "type":2, "slug": "",},
{"name":"Action 2", "type":3, "slug": "",},
{"name":"Bing", "type":1, "slug": "www.bing.com", "entity":"company"},
{"name":"Yandex", "type":1, "slug": "www.yandex.com)", "entity":"company"}
],
"links":[
{"source":0,"target":3,"value":10},
{"source":4,"target":3,"value":1},
{"source":1,"target":7,"value":10},
{"source":2,"target":4,"value":10},
{"source":4,"target":7,"value":1},
{"source":4,"target":5,"value":10},
{"source":4,"target":6,"value":10},
{"source":8,"target":4,"value":1}
]
}
var w = 560,
h = 500,
radius = d3.scale.log().domain([0, 312000]).range(["10", "50"]);
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
vis.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("refX", 17 + 3) /*must be smarter way to calculate shift*/
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0,0 V 4 L6,2 Z"); //this is actual shape for arrowhead
//d3.json(data, function(json) {
var force = self.force = d3.layout.force()
.nodes(data.nodes)
.links(data.links)
.distance(100)
.charge(-1000)
.size([w, h])
.start();
var link = vis.selectAll("line.link")
.data(data.links)
.enter().append("svg:line")
.attr("class", function (d) { return "link" + d.value +""; })
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
.attr("marker-end", function(d) {
if (d.value == 1) {return "url(#arrowhead)"}
else { return " " }
;});
function openLink() {
return function(d) {
var url = "";
if(d.slug != "") {
url = d.slug
} //else if(d.type == 2) {
//url = "clients/" + d.slug
//} else if(d.type == 3) {
//url = "agencies/" + d.slug
//}
window.open("//"+url)
}
}
var node = vis.selectAll("g.node")
.data(data.nodes)
.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", function(d){ return "node type"+d.type})
.attr("r", function(d) { return radius(d.value) || 10 })
//.style("fill", function(d) { return fill(d.type); })
.call(force.drag);
node.append("svg:image")
.attr("class", "circle")
.attr("xlink:href", function(d){ return d.img_href})
.attr("x", "-16px")
.attr("y", "-16px")
.attr("width", "32px")
.attr("height", "32px")
.on("click", openLink());
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
//});
css:
.link10 { stroke: #ccc; stroke-width: 3px; stroke-dasharray: 3, 3; }
.link1 { stroke: #000; stroke-width: 3px;}
.nodetext { pointer-events: none; font: 10px sans-serif; }
.node.type1 {
fill:brown;
}
.node.type2 {
fill:#337147;
}
.node.type3 {
fill:blue;
}
.node.type4 {
fill:red;
}
.node.type5 {
fill:#1BC9E0;
}
.node.type6 {
fill:#E01B98;
}
image.circle {
cursor:pointer;
}
You can edit my jsfiddle linked on beginning of post...
Solution here: http://jsfiddle.net/Bull/4btFx/1/
I got this to work by adding a class to each node, then using "selectAll" for each class to add the shapes. In the code below, I'm adding a class "node" and a class returned by my JSON (d.type) which is either "rect" or "ellipse".
var node = container.append("g")
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function(d) {
return d.type + " node";
})
.call(drag);
Then you can add the shape for all elements of each class:
d3.selectAll(".rect").append("rect")
.attr("width", window.nodeWidth)
.attr("height", window.nodeHeight)
.attr("class", function(d) {
return "color_" + d.class
});
d3.selectAll(".ellipse").append("rect")
.attr("rx", window.nodeWidth*0.5)
.attr("ry", window.nodeHeight*0.5)
.attr("width", window.nodeWidth)
.attr("height", window.nodeHeight)
.attr("class", function(d) {
return "color_" + d.class
});
In the above example, I used rectangles with radius to draw the ellipses since it centers them the same way as the rectangles. But it works with other shapes too. In the jsfiddle I linked, the centering is off, but the shapes are right.
I implemented this behavior using the filter method that I gleaned from Filtering in d3.js on bl.ocks.org.
initGraphNodeShapes() {
let t = this;
let graphNodeCircles =
t.graphNodesEnter
.filter(d => d.shape === "circle")
.append("circle")
.attr("r", 15)
.attr("fill", "green");
let graphNodeRects =
t.graphNodesEnter
.filter(d => d.shape === "rect")
.append("rect")
.attr("width", 20)
.attr("height", 10)
.attr("x", -10) // -1/2 * width
.attr("y", -5) // -1/2 * height
.attr("fill", "blue");
return graphNodeCircles.merge(graphNodeRects);
}
I have this inside of initGraphNodeShapes call because my code is relatively large and refactored. The t.graphNodesEnter is a reference to the data selection after the data join enter() call elsewhere. Ping me if you need more context. Also, I use the d => ... version because I'm using ES6 which enables lambdas. If you're using pre-ES6, then you'll have to change it to the function(d)... form.
This is an older post, but I had the same trouble trying to get this concept working with D3 v5 in July of 2020. Here is my solution in case anyone else is trying to build a force-directed graph, I used both circle and rectangle elements to represent different types of nodes:
The approach was to create the elements, and then position them separately when invoking the force simulation (since a circle takes cx, cy, and r attributes, and the rect takes x, y, width and height). Much of this code follows the example in this blog post on medium: https://medium.com/ninjaconcept/interactive-dynamic-force-directed-graphs-with-d3-da720c6d7811
FYI I've declared 'svg' previously as the d3.select("some div with id or class"), along with a few helper functions not shown that read the data (setNodeSize, setNodeColor). I've used the D3.filter method to check for boolean field in the data - is the node initial or no?
Force simulation instance:
const simulation = d3.forceSimulation()
//the higher the strength (if negative), greater distance between nodes.
.force('charge', d3.forceManyBody().strength(-120))
//places the chart in the middle of the content area...if not it's top-left
.force('center', d3.forceCenter(width / 2, height / 2))
Create the circle nodes:
const nodeCircles = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.filter(d => d.initial)
.append('circle')
.attr('r', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)
Then create the rectangle nodes:
const nodeRectangles = svg.append('g')
.selectAll('rect')
.data(nodes)
.enter()
.filter(d => !d.initial)
.append('rect')
.attr('width', setNodeSize)
.attr('height', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)
And then when invoking the simulation:
simulation.nodes(nodes).on("tick", () => {
nodeCircles
.attr("cx", node => node.x)
.attr("cy", node => node.y)
nodeRectangles
.attr('x', node => node.x)
.attr('y', node => node.y)
.attr('transform', 'translate(-10, -7)')
Of course there's more to it to add the lines/links, text-labels etc. Feel free to ping me for more code. The medium post listed above is very helpful!
I am one step ahead of you :)
I resolved your problem with using "path" instead of "circle" or "rect", you can look my solution and maybe help me to fix problem which I have...
D3 force-directed graph: update node position

Categories

Resources