How to place rectangles behind text in d3.js - javascript

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.

Related

D3 draw n circles in a tree

I have the following data, using which i plotted a d3 tree, where each node is a rectangle. Now, I want to draw n circles on each rectangle(where n is value of 'minor' in each node(see data), but not sure what's the easiest way to achieve this.
// Data
{
"minor": 1,
"critical": 0,
"children": [{
"minor": 2,
"critical": 0,
"children": [{
"minor": 3,
"critical": 0,
},
{
"minor": 2,
"critical": 1,
},
{
"minor": 1,
"critical": 1,
}]
}]
}
// JS
const nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.style('font-size', '12px')
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Add Rectangle for the nodes
nodeEnter.append('rect')
.attr('class', 'node')
.attr("width", function (d) { return d.children || d._children ? 150 : 350 })
.attr("height", 70)
.attr('stroke', '#ccc')
.attr('stroke-width', '1');
You can use d3.range to create an array of a specific length. Then you can treat this array just like any other d3 data structure.
const nCircles = 5;
const radius = 8;
const padding = 2;
d3.select('svg')
.selectAll('circle')
.data(d3.range(nCircles))
.enter()
.append('circle')
.attr('cx', function(_, i) {
return radius + padding + i * 2 * (radius + padding);
})
.attr('cy', radius + padding)
.attr('r', radius)
.attr('fill', 'darkred');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Alternatively, you can use a fully ES6 solution, but it's unnecessarily complex:
const array = new Array(nCircles).fill(undefined).map(function(_, i) { return i; });
From your data, I gather that each node has the attributes minor and critical. Now, to update the balls of your newly inserted nodes, you can use
nodeEnter
.selectAll('.minor')
// Here you take the data from the node and transform it into
// the data for the circles. This is what makes it generic.
.data(function(d) { d3.range(d.minor))
.enter()
.append('circle')
.classed('minor', true)
.attr('cx', function(_, i) {
return radius + padding + i * 2 * (radius + padding);
})
.attr('cy', radius + padding)
.attr('r', radius)
.attr('fill', 'darkyellow');
// And for critical messages:
nodeEnter
.selectAll('.critical')
.data(function(d) { d3.range(d.critical))
.enter()
.append('circle')
.classed('critical', true)
.attr('cx', function(_, i) {
// To position it next to the minor circles, we need to
// know how many there are
const nMinorCircles = d3.select(this).select('.minor').size();
return radius + padding + 2 * (radius + padding) * (i + nMinorCircles);
})
.attr('cy', radius + padding)
.attr('r', radius)
.attr('fill', 'darkred');

d3 filtering bar chart with legend toggling

I am trying to filter/update a bar chart with legend toggling. I am unsure how to set active states on the bars during initialization - then trying to deactivate - exclude the required datasets on toggle, but restore them when the active states come back.
http://jsfiddle.net/5ruhac83/5/
//legend toggling
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
})
.on("click", function(name) {
var active = false;
newState = active ? "active" : "inactive";
// Hide or show the elements
d3.select(this).attr("class", newState);
//set active state
console.log("name", name)
toggleBar(name)
});
//animating the bars - with a pruned data set
function toggleBar(name) {
var hiddenClassName = 'hidden',
bar = chartHolder.selectAll('.bars'),
currentBars = bar.selectAll('[value="' + name + '"]')
currentBars.classed(hiddenClassName, !currentBars.classed(hiddenClassName))
var barData = data.map(item => {
item.valores = item.valores.map(valor => {
return Object.assign({}, valor, {
value: bar.selectAll('[value="' + valor.name + '"]').classed(hiddenClassName) ?
0 : item[valor.name]
})
})
return item;
})
var barData = [{
label: "a",
"Current Period": 20
}, {
label: "b",
"Current Period": 15
}, {
label: "c",
"Current Period": 25
}, {
label: "d",
"Current Period": 5
}];
var options = getOptions(barData);
barData = refactorData(barData, options);
console.log("barData", barData)
bar
.data(barData)
var rect = bar.selectAll("rect")
.data(function(d) {
return d.valores;
})
rect
.transition()
.duration(1000)
.delay(100)
.attr("width", x0.rangeBand() / 2)
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
rect.exit().remove();
/*
var bar = bar.selectAll("rect")
bar.transition()
//.attr("id", function(d){ return 'tag'+d.state.replace(/\s|\(|\)|\'|\,+/g, '');})
.attr("x", function(d) { return x1(d.name); })
.attr("width", x0.rangeBand())
.attr("y", function(d) {
return 0;
//return y(d.value);
})
.attr("height", function(d) {
return 0;
//return height - y(d.value);
});
//bar.exit().remove();
*/
}
Here's a chart which resets the domain with new options based on the toggled legend:
JS Fiddle DEMO
function toggleBar(name, state) {
data.forEach(function(d) {
_.findWhere(d.valores, {name: name}).hidden = state;
});
var filteredOptions;
if(state) {
filteredOptions = options.filter(function(d) { return d !== name; });
} else {
filteredOptions = options;
}
x1.domain(filteredOptions).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.valores.filter(function(k) { return !k.hidden;}), function(d) {
return d.value;
});
})]);
Changes:
You don't need to reset the data on every toggle. I just added a hidden attribute to the "valores" and the while resetting the domain in the toggleBar function, filtered the data based on non-hidden options and set the domain accordingly.
I'd recommend to get used to d3's "enter, update and exit" methods. I hope the code helps you understand that as well.
drawBars() is a function that does that.
Changed the way the tooltip is rendered as well. Instead of using querySelector for hovered elements (that's definitely one way), you can just use the parent node's data using the datum() function.
Legends: I've added a stroke for every legend and to indicate whether the corresponding option is hidden or not, the fill-opacity is toggled on every click.
Used a separate color scale with an ordinal domain of options and range to be same as previous colors so that the colors are based on the names and not indices (as before)
Added simple transitions.
Used underscore.js in toggleBars() function. You could switch back to pure JS as well.
And to answer your question on active states, please check for toggling of the "clicked" classnames.
Please go through the code and let me know if you any part of it is unclear. I'll add some comments too.
:)
here is a solution for grouped bar chart legend toggling with animation.
//jsfiddle - http://jsfiddle.net/0ht35rpb/259/
var $this = this.$('.barChart');
var w = $this.data("width");
var h = $this.data("height");
//var configurations = $this.data("configurations");
var data = [{
"State": "a",
"AA": 100,
"BB": 200
}, {
"State": "b",
"AA": 454,
"BB": 344
},{
"State": "c",
"AA": 140,
"BB": 500
}, {
"State": "d",
"AA": 154,
"BB": 654
}];
var yLabel = "Count";
var svg = d3.select($this[0]).append("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom,
g = svg
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups:
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
// The scale for spacing each group's bar:
var x1 = d3.scaleBand()
.padding(0.05);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f", "#8bf41b"]);
var keys = d3.keys(data[0]).slice(1);
x0.domain(data.map(function(d) {
return d.State;
}));
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(keys, function(key) {
return d[key];
});
})]).nice();
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) {
return "translate(" + x0(d.State) + ",0)";
})
.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
.enter().append("rect")
.attr("x", function(d) {
return x1(d.key);
})
.attr("y", function(d) {
return y(d.value);
})
.attr("width", x1.bandwidth())
.attr("height", function(d) {
return height - y(d.value);
})
.attr("fill", function(d, i) {
return z(d.key);
});
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
g.append("g")
.attr("class", "yaxis")
.call(d3.axisLeft(y).ticks(null, "s"))
.append("text")
.attr("x", 2)
.attr("y", y(y.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text(yLabel);
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 17)
.attr("width", 15)
.attr("height", 15)
.attr("fill", z)
.attr("stroke", z)
.attr("stroke-width", 2)
.on("click", function(d) {
update(d)
});
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) {
return d;
});
var filtered = [];
////
//// Update and transition on click:
////
function update(d) {
//
// Update the array to filter the chart by:
//
// add the clicked key if not included:
if (filtered.indexOf(d) == -1) {
filtered.push(d);
// if all bars are un-checked, reset:
if (filtered.length == keys.length) filtered = [];
}
// otherwise remove it:
else {
filtered.splice(filtered.indexOf(d), 1);
}
//
// Update the scales for each group(/states)'s items:
//
var newKeys = [];
keys.forEach(function(d) {
if (filtered.indexOf(d) == -1) {
newKeys.push(d);
}
})
x1.domain(newKeys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(keys, function(key) {
if (filtered.indexOf(key) == -1) return d[key];
});
})]).nice();
//g.select(".yaxis")
//.call(d3.axisLeft(y).ticks(null, "s"));
var t0 = svg.transition().duration(250);
var t1 = t0.transition();
t1.selectAll(".yaxis").call(d3.axisLeft(y).ticks(null, "s"));
//
// Filter out the bands that need to be hidden:
//
var bars = svg.selectAll(".bar").selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
bars.filter(function(d) {
return filtered.indexOf(d.key) > -1;
})
.transition()
.attr("x", function(d) {
return (+d3.select(this).attr("x")) + (+d3.select(this).attr("width")) / 2;
})
.attr("height", 0)
.attr("width", 0)
.attr("y", function(d) {
return height;
})
.duration(500);
//
// Adjust the remaining bars:
//
bars.filter(function(d) {
return filtered.indexOf(d.key) == -1;
})
.transition()
.attr("x", function(d) {
return x1(d.key);
})
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
})
.attr("width", x1.bandwidth())
.attr("fill", function(d, i) {
return z(d.key);
})
.duration(500);
// update legend:
legend.selectAll("rect")
.transition()
.attr("fill", function(d, i) {
if (filtered.length) {
if (filtered.indexOf(d) == -1) {
return z(d);
} else {
return "white";
}
} else {
return z(d);
}
})
.duration(100);
}

Text invisible yet appearing in inspector

My last question was answered so quickly and smoothly I thought I'd return with another issue I'm failing to figure out on my own.
I used one of the examples to create this graph:
data = [{ "label": "1", "value": 20 },
{ "label": "2", "value": 50 },
{ "label": "3", "value": 30 },
{ "label": "4", "value": 45 }];
var width = 400,
height = 450;
var outerRadius = 200,
innerRadius = outerRadius / 3,
color = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function (d) { return d.value; });
var pieData = pie(data);
var arc = d3.svg.arc()
.innerRadius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Title[enter image description here][1]");
svg.selectAll("path")
.data(pieData)
.enter().append("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill", function (d, i) { return color(i); })
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150));
svg.selectAll("path")
.append("text")
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function (d, i) { return data[i].label; });
function arcTween(outerRadius, delay) {
return function () {
d3.select(this).transition().delay(delay).attrTween("d", function (d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function (t) { d.outerRadius = i(t); return arc(d); };
});
};
}
The idea being that when you hover over a section on the pie chart (donut chart?) it expands. However, this made my labels dissapear and I can't manage to make them come back. I either get an error, or they just don't show up on the screen (even though I see the tag in the inspector). Any obvious thing I'm missing?
Thanks!
You cannot append a <text> element to a <path> element. It simply doesn't work in an SVG. Even not working, the <text> element will be appended.
That being said, a solution is creating a new "enter" selection for the texts:
svg.selectAll(null)
.data(pieData)
.enter()
.append("text")
.attr("transform", function(d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function(d, i) {
return data[i].label;
});
Here is your updated code:
data = [{
"label": "1",
"value": 20
}, {
"label": "2",
"value": 50
}, {
"label": "3",
"value": 30
}, {
"label": "4",
"value": 45
}];
var width = 400,
height = 450;
var outerRadius = 200,
innerRadius = outerRadius / 3,
color = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
});
var pieData = pie(data);
var arc = d3.svg.arc()
.innerRadius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Title[enter image description here][1]");
svg.selectAll("path")
.data(pieData)
.enter().append("path")
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr("fill", function(d, i) {
return color(i);
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150));
svg.selectAll(null)
.data(pieData)
.enter()
.append("text")
.attr("transform", function(d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function(d, i) {
return data[i].label;
});
function arcTween(outerRadius, delay) {
return function() {
d3.select(this).transition().delay(delay).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) {
d.outerRadius = i(t);
return arc(d);
};
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Legend in D3 circle pack diagram

I need to create a legend for the bubble/circle pack chart. I'm displaying the values inside the circle. I need the names as the legend. For an instance, in the below provided data, if the value is 60, i need that name "Petrol" in the legend. How could i achieve it?
Snippet:
var diameter = 200,
format = d3.format(",d"),
color = ["#7b6888", "#ccc", "#aaa", "#6b486b"];
var bubble = d3.layout.pack().size([diameter, diameter]);
var svg = d3.select("#bubbleCharts").append("svg")
.attr("width", diameter + 10)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("flare.json", function(error, root) {
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; }))
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + 20 + "," + d.y + ")"; });
node.append("circle").attr("r", function(d) { return d.r+ 7; })
.style("fill", function(d,i) { return color[i];} );
node.append("text").attr("dy", ".3em").style("text-anchor", "middle")
.text(function(d) { return d.value+"%"; });
});
function classes(root) {
var classes = [];
function recurse(name, node) {
if (node.children)
node.children.forEach(function(child){
recurse(node.name, child);
});
else
classes.push({packageName: name, value: node.value});
}
recurse(null, root);
return {children: classes};
}
var legend = d3.select("#bubbleChart").append("svg")
.selectAll("g").data(node.children).enter().append("g")
.attr("class","legend")
.attr("width", radius)
.attr("height", radius * 2)
.attr("transform", function(d, i) {return "translate(" + i *10 + "0" + ")"; });
legend.append("rect").attr("width", 18).attr("height", 10)
.style("fill", function(d, i) { return color[i];});
legend.append("text").attr("x", 24).attr("y", 5).attr("dy", ".35em")
.text(function(d) { return d; });
My data:
{
"name": "Spending Activity",
"children": [
{"name": "Petrol", "value": 10},
{"name": "Travel", "value": 60},
{"name": "Medical", "value": 25},
{"name": "Shopping", "value": 5}
]
}
How would i take the values from json and create a legend? Thanks.
You can simply iterate through your data set and add those values:
legend.selectAll("circle").data(data.children)
.enter()
.append("circle")
.attr("cy", function(d,i) { return (i+1) * 10; })
.attr("r", function(d) { return d.r+ 7; })
.style("fill", function(d,i) {
return color[i];
});
legend.selectAll("text").data(data.children)
.enter()
.append("text")
.attr("transform", function(d,i) {
return "translate(10," + ((i+1) * 10) + ")";
});

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