How to append multiple items to a force simulation node? - javascript

I'm using D3 v4 and can't seem to get multiple items to append to a node. In the code below I'm trying to get text to appear with the image as part of my force simulation. Both the image and text need to move together around the screen. It works perfectly if I only append either the image or the text but I can't get it to group both. When I run this it just shows 1 node in the corner.
this.node = this.d3Graph.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(Nodes)
.enter()
.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50)
.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
this.force.on('tick', this.tickActions);
tickActions() {
this.node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
this.force
.restart()
}

You cannot append a <text> element to an <image> element. You have to append the <text> to the <g>.
The easiest solution is breaking your selection:
this.node = this.d3Graph.selectAll(null)
.data(Nodes)
.enter()
.append("g")
.attr("class", "nodes");
this.node.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50);
this.node.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
Here we use the data to create <g> elements in the enter selection. Then, to each <g> element, we append an <image> and a <text> as children.

Related

d3-zoom breaks when cursor is over an inner svg element

I have implemented d3-zoom by following this brief tutorial.
I'm using https://d3js.org/d3.v5.min.js. This is my first project with d3.
My goal is to have a kind of floor plan showing booth tables at a venue. Similar to the tutorial, I've drawn shape elements from an array. In my case I've entered an array of booth information into a grid of elements.
The zoom functionality works just fine, except when my cursor is over the border or fill of one of my rectangles, or on the text of a element. If the point of my cursor is touching any of these elements, the zooming behavior stops working.
Try to zoom with the mousewheel with your cursor in blank space versus touching a shape or text.
I've tried to fit a console.log in somewhere to see what's not getting passed in the event, but have had trouble even finding where I can get the event argument.
Any help greatly appreciated! Here is my code:
var svg = d3.select("#venue-svg"); // this is my svg element
// the zoom rectangle. from the tutorial: 'The zoom behavior is applied
// to an invisible rect overlaying the SVG element; this ensures that it
// receives input, and that the pointer coordinates are not affected by
// the zoom behavior’s transform.'
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all")
.call(
d3
.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed)
);
function zoomed() {
g.attr("transform", d3.event.transform);
}
// a parent <g> that holds everything else and is targeted
// for the transform (from the tutorial).
var g = svg.append("g");
// the groups that hold each booth table, associated org name, etc.
var tables = g
.selectAll("g")
.data(venueBooths)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + " " + d.y + ")";
});
var tableRects = tables
.append("rect")
.attr("stroke", "steelblue")
.attr("stroke-width", "2px")
.attr("width", function(d) {
return d.w;
})
.attr("height", function(d) {
return d.h;
})
.attr("fill", "none")
.attr("fill", function(d) {
return $.isEmptyObject(d.reservation) ? "none" : "#FF5733";
})
.attr("id", function(d) {
return "table-" + d.id;
});
tables
.append("text")
.text(function(d) {
return "Booth " + d.id;
})
.attr("dx", 5)
.attr("dy", 60)
.attr("font-size", "8px");
tables
.append("text")
.text(function(d) {
return d.reservation.orgName ? d.reservation.orgName : "Available";
})
.attr("dy", 15)
.attr("dx", 5)
.attr("font-size", "9px")
.attr("font-weight", "bold");
Try creating the rect in the end such that the DOM looks like this:
<svg>
<g></g>
<rect></rect>
</svg>
Since the zoom function is attached to the large rectangle, creating the smaller boxes above it prevents a zoom event from propagating to the large rectangle below them. It works for the boxes with a fill: none; since it behaves like a hollow box.
Try modifying the code to something like:
var svg = d3.select("#venue-svg"); // this is my svg element
// the zoom rectangle. from the tutorial: 'The zoom behavior is applied
// to an invisible rect overlaying the SVG element; this ensures that it
// receives input, and that the pointer coordinates are not affected by
// the zoom behavior’s transform.'
function zoomed() {
g.attr("transform", d3.event.transform);
}
// a parent <g> that holds everything else and is targeted
// for the transform (from the tutorial).
var g = svg.append("g");
// the groups that hold each booth table, associated org name, etc.
var tables = g
.selectAll("g")
.data(venueBooths)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + " " + d.y + ")";
});
var tableRects = tables
.append("rect")
.attr("stroke", "steelblue")
.attr("stroke-width", "2px")
.attr("width", function(d) {
return d.w;
})
.attr("height", function(d) {
return d.h;
})
.attr("fill", "none")
.attr("fill", function(d) {
return $.isEmptyObject(d.reservation) ? "none" : "#FF5733";
})
.attr("id", function(d) {
return "table-" + d.id;
});
tables
.append("text")
.text(function(d) {
return "Booth " + d.id;
})
.attr("dx", 5)
.attr("dy", 60)
.attr("font-size", "8px");
tables
.append("text")
.text(function(d) {
return d.reservation.orgName ? d.reservation.orgName : "Available";
})
.attr("dy", 15)
.attr("dx", 5)
.attr("font-size", "9px")
.attr("font-weight", "bold");
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all")
.call(
d3
.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed)
);

html label inside d3 element

I have written the following code (it's a toy example, here on jsfiddle) to show a label when hovering a shape:
var lbl;
var svg = d3.select("#chart").append("svg")
.attr("width", 200)
.attr("height", 200);
svg.append("rect")
.attr("x", 50)
.attr("y", 0)
.attr("width",50)
.attr("height",50)
.attr("label","first line"+"<br/>"+"second line")
.style("fill", "yellow")
.on("mouseover",function() {
var th=d3.select(this);
lbl=svg.append("text")
.attr("x", th.attr("x")*1+th.attr("width")/2)
.attr("y", th.attr("y")*1+th.attr("height")/2)
.attr("dy", "0.5em")
.style("text-anchor", "middle")
.style("visibility", "visible")
.attr("pointer-events", "none")
.text(th.attr("label"));
})
.on("mouseout",function(){
lbl.remove();
});
How can I render the label in html format?
You can make a div and give css position:absolute
so that you can now position it w.r.t. the top and left calculated.
Also set the text within the div like .html("your html<br> text")
var th=d3.select(this);
lbl=d3.select("#chart").append("div")
.attr("class", "tooltip")
.style("left", (th.attr("x")*1+th.attr("width")/2) + "px")
.style("top", (th.attr("y")*1+th.attr("height")/2) + "px")
.html(th.attr("label"));
working code here

D3.js IE vs Chrome SVG not showing

I have a simple D3 donut diagram with a .mouseover() event that updates an SVG:text element at the center of the donut hole. It works great...
Until I encounter users with IE 9, 10 and 11. These browsers won't render the center label. Is there a way to accommodate IE and show the center label in both browsers?
The HTML page is based on HTML5BoilerPlate with the various shims to detect old browsers.
The D3 script seems pretty straight forward.
d3.json("data/census.php", function(error, dataset) {
var h = 220, w = 295;
var outerRadius = h / 2, innerRadius = w / 4;
var color = d3.scale.category20b();
var svg= d3.select("#dailycensus")
.append("svg")
.data([dataset])
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.layout.pie()
.value(function(d,i) { return +dataset[i].Census; });
var arcs = svg.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
var syssum = d3.sum(dataset, function(d,i) { return +dataset[i].Census; });
var tip = d3.tip()
.attr("class", "d3-tip")
.html(String);
var formatter = d3.format(".1%");
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html("Health System Census"); // Default label text
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(syssum); // Default label value
arcs.append("svg:path")
.call(tip) // Initialize the tooltip in the arc context
.attr("fill", function(d,i) { return color(i); }) // Color the arc
.attr("d", arc)
.on("mouseover", function(d,i) {
tip.show( formatter(dataset[i].Census/syssum) );
// Update the doughnut hole label with slice meta data
svg.select("#hospital").remove();
svg.select("#census").remove();
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html(dataset[i].Facility);
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(+dataset[i].Census);
})
.on("mouseout", function(d) {
tip.hide();
// Return the doughnut hole label to the default label
svg.select("#hospital").remove();
svg.select("#census").remove();
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html("Health System Census");
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(syssum);
})
Replace all the .html calls with .text calls. Generally innerHTML is for HTML things although browsers are giving it SVG support as everybody keeps expecting it to work.
It's not immediately clear what is causing the issue, however setting the .text property instead resolves the issue after testing with Fiddler:
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.text(dataset[i].Facility);
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.text(+dataset[i].Census);
})
After investigating the <text /> elements directly in the Developer Tools you can see that setting the .innerHTML property doesn't render the results you'd expect, however .textContent does.
If this is working as expected in both Chrome and Firefox, I'll gladly open up an interop bug for the IE team to look into. We've been doing some SVG work lately, so I may find that this has already been discussed.
I had the same issue and innerSvg polyfill helps me. Now html() in SVG works in IE.

Placing labels at the center of nodes in d3.js

I am starting with d3.js, and am trying to create a row of nodes each of which contains a centered number label.
I am able to produce the desired result visually, but the way I did it is hardly optimal as it involves hard-coding the x-y coordinates for each text element. Below is the code:
var svg_w = 800;
var svg_h = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", svg_w)
.attr("weight", svg_h);
var dataset = [];
for (var i = 0; i < 6; i++) {
var datum = 10 + Math.round(Math.random() * 20);
dataset.push(datum);
}
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d, i) {
return (i * 70) + 50;
})
.attr("cy", svg_h / 2)
.attr("r", 20);
var labels = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("dx", function(d, i) {
return (i * 70) + 42
})
.attr("dy", svg_h / 2 + 5)
.text(function(d) {
return d;
});
The node class is custom CSS class I've defined separately for the circle elements, whereas classes nodes and labels are not explicitly defined and they are borrowed from this answer.
As seen, the positioning of each text label is hard-coded so that it appears at the center of the each node. Obviously, this is not the right solution.
My question is that how should I correctly associate each text label with each node circle dynamically so that if the positioning of a label changes along with that of a circle automatically. Conceptual explanation is extremely welcome with code example.
The text-anchor attribute works as expected on an svg element created by D3. However, you need to append the text and the circle into a common g element to ensure that the text and the circle are centered with one another.
To do this, you can change your nodes variable to:
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
// Add one g element for each data node here.
.append("g")
// Position the g element like the circle element used to be.
.attr("transform", function(d, i) {
// Set d.x and d.y here so that other elements can use it. d is
// expected to be an object here.
d.x = i * 70 + 50,
d.y = svg_h / 2;
return "translate(" + d.x + "," + d.y + ")";
});
Note that the dataset is now a list of objects so that d.y and d.x can be used instead of just a list of strings.
Then, replace your circle and text append code with the following:
// Add a circle element to the previously added g element.
nodes.append("circle")
.attr("class", "node")
.attr("r", 20);
// Add a text element to the previously added g element.
nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Now, instead of changing the position of the circle you change the position of the g element which moves both the circle and the text.
Here is a JSFiddle showing centered text on circles.
If you want to have your text be in a separate g element so that it always appears on top, then use the d.x and d.y values set in the first g element's creation to transform the text.
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
The best answer came from the asker himself:
just a further observation: with only .attr("text-anchor", "middle")
for each text element, the label is at the middle horizontally but
slightly off vertically. I fixed this by adding attr("y", ".3em")
(borrowed from examples at d3.js website), which seems to work well
even for arbitrary size of node circle. However, what exactly this
additional attribute does eludes my understanding. Sure, it does
something to the y-coordinate of each text element, but why .3em in
particular? It seems almost magical to me...
Just add .attr("text-anchor", "middle") to each text element.
Example:
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
This page describes what's going on under the svg hood when it comes to text elements. Understanding the underlying machinery and data structures helped me get a better handle on how I had to modify my code to get it working.

Nested SVG node creation in d3.js

I'v just started playing with d3js and find it strange that I have to create multiple selectors for each element I want to link to the background data structure for example separate selectors such as one for overlay text and one for rectangles to make an annotated bar graph.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr('y',function(d,i){return i*10;})
.attr('height',10)
.attr('width',function(d){return d.interestingValue})
.fill('#00ff00');
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr('y',function(d,i){return i*10;})
.fill('#0000ff')
.text(function(d){return d.interestingValue});
Is there a more convenient way of combining these into a single selection and enter() chain that creates both the rects and the text elements?
Use a G (group) element. Use a single data-join to create the G elements, and then append your rect and text element. For example:
var g = svg.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(0," + i * 10 + ")"; });
g.append("rect")
.attr("height", 10)
.attr("width", function(d) { return d.interestingValue; });
g.append("text")
.text(function(d) { return d.interestingValue; });

Categories

Resources