Replace space with new line charcter in texts d3js - d3.js

I have a script in which I make txts in the following way:
texts = svg.selectAll(null)
.data(data)
.enter()
.append('text')
.attr("text-anchor", "middle")
.text(function(d) {
return d.abbreviation;
})
.attr("pointer-events", "none")
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "black");
texts.each(function(d) {
console.log(this.getComputedTextLength());
d.size = this.getComputedTextLength() / 2 ;
});
simulation.nodes(data).on("tick", function() {
circles.attr("cx", function(d) {
return d.x = Math.max(20, Math.min(width - 20, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(20, Math.min(height - 20, d.y));
})
texts.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
});
I am using property abbreviation in data to put text over the bubbles but I want to replace all spaces in input with new line character.
I tried some soutions like given at this link: How to linebreak an svg text in javascript?
but all texts go to left most corner or if I remove the .attr("x", 0) property from this, then the alignment of texts in not right. See picture below:
"State" should come directly below "Iowa"
Updated script:
texts = svg.selectAll(null)
.data(data)
.enter()
.append('text')
.attr("text-anchor", "middle")
.each(function (d) {
var arr = d.abbreviation.split(" ");
for (i = 0; i < arr.length; i++) {
d3.select(this).append("tspan")
.text(arr[i])
.attr("dy", i ? "1.2em" : 0)
.attr("text-anchor", "middle")
.attr("class", "tspan" + i);
}
})
.attr("pointer-events", "none")
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "black");
What should I do to make the alignment right or is there any other way to do this?

There are different ways to fix this. An easy one is appending the <tspan> to a <g> element, setting all their x properties to 0 and text-anchor to middle.
Have a look at the demo:
var svg = d3.select("svg");
var data = [{
text: "some text"
}, {
text: "a longer text here"
}, {
text: "an even longer text here"
}, {
text: "short text"
}, {
text: "a long text"
}];
var texts = svg.selectAll(null)
.data(data)
.enter()
.append("g");
texts.append("text")
.attr("text-anchor", "middle")
.each(function(d) {
var arr = d.text.split(" ");
d3.select(this).selectAll(null)
.data(arr)
.enter()
.append("tspan")
.attr("text-anchor", "middle")
.attr("x", 0)
.attr("dy", function(d, i) {
return "1.2em"
})
.text(String)
})
var simulation = d3.forceSimulation(data)
.force("center", d3.forceCenter(200, 100))
.force("collide", d3.forceCollide(40))
.on("tick", tick);
function tick() {
texts.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="250"></svg>
PS: Don't use that for loop inside the each. That's not idiomatic D3. Instead of that, just use an enter selection (refer to the demo to see how to do it).

Related

Unable to change x position of legend text inside a group element using D3

I am trying to create a horizontal graph legend in D3.js. I am using a group element (g) as a container for all the legends and the individual legends (text) are also each wrapped inside a "g" element. The result is that the individual legends are stacked on top of each other rather than spaced out.
I have tried changing the x attribute on the legends and also transform/translate. Whilst the DOM shows that the x values are applied the legends don't move. So if the DOM shows the legend / g element is positioned at x = 200 it is still positioned at 0.
I have spent two days trying to solve this and probably looked at over 50 examples including anything I could find on StackExchange.
Below code is my latest attempt. It doesn't through any error and the x values are reflected in the DOM but the elements just won't move.
I have included the code covering the relevant bits (but not all code).
The legend container is added here:
/*<note>Add container to hold legends. */
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", Width)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
There is then a loop through a json array of objects:
var PrevElemLength = 0;
/*<note>Loop through each data series, call the Valueline variable and plot the line. */
Data.forEach(function(Key, i) {
/*<note>Add the metric line(s). */
Svg.append("path")
.attr("class", "line")
.attr("data-legend",function() { return Key.OriginId })
/*<note>Iterates through the data series objects and applies a different color to each line. */
.style("stroke", function () {
return Key.color = Color(Key.UniqueName); })
.attr("d", Valueline(Key.DataValues));
/*<note>Add a g element to the legend container. */
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function (d, i) {
if (i === 0) {
return "translate(0,0)"
} else {
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength) + ",0)"
}
});
/*<note>Adds a rectangle to pre-fix each legend. */
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function () {
return Key.color = Color(Key.UniqueName); });
/*<note>Adds the legend text. */
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
/*.attr("y", NetHeight + (Margin.bottom/2)+ 10) */
.attr("class", "legend text")
.style("fill", function () {
return Key.color = Color(Key.UniqueName); })
.text(Key.UniqueName);
Here is a screen shot of what the output looks like:
enter image description here
Any help on how to create a horizontal legend (without over lapping legends) would be much appreciated. Chris
The problem is you are using local variables d and i as function parameters while setting the transform attribute. Parameter i in local scope overrides the actual variable. The value of local variable i would be always zero as there is no data bind to that element.
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function (d, i) { //Remove i
if (i === 0) {
return "translate(0,0)"
} else {
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength) + ",0)"
}
});
I have also made slight updates to the code for improvements.
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
var PrevElemLength = 0;
var Data = [{
OriginId: 1,
UniqueName: "Some Long Text 1"
}, {
OriginId: 2,
UniqueName: "Some Long Text 2"
}];
/*<note>Loop through each data series, call the Valueline variable and plot the line. */
var Color = d3.scale.category10();
Data.forEach(function(Key, i) {
/*<note>Add a g element to the legend container. */
var Legend = LegendCanvas.append("g")
.attr("class", "legend container")
.attr("transform", function() {
if (i === 0) {
return "translate(0,0)"
} else {
var marginLeft = 5;
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength + marginLeft) + ",0)"
}
});
/*<note>Adds a rectangle to pre-fix each legend. */
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function() {
return Key.color = Color(Key.UniqueName);
});
/*<note>Adds the legend text. */
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
.attr("dy", "0.4em")
.attr("class", "legend text")
.style("fill", function() {
return Key.color = Color(Key.UniqueName);
})
.text(Key.UniqueName);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height=500 width=500></svg>
The d3 way of implementation(Using data binding) would be as follows
var LegendCanvas = d3.select("svg")
.append("g")
.attr("class", "legend canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.style("fill", "#ffcccc")
.attr("transform", "translate(0,15)");
var Data = [{
OriginId: 1,
UniqueName: "Some Long Text 1"
}, {
OriginId: 2,
UniqueName: "Some Long Text 2"
}];
var Color = d3.scale.category10();
var Legend = LegendCanvas.selectAll(".legend")
.data(Data)
.enter()
.append("g")
.attr("class", "legend container");
Legend.append("rect")
.attr("width", 5)
.attr("height", 5)
.style("fill", function(Key) {
return Key.color = Color(Key.UniqueName);
});
Legend.append("text")
.attr("x", function() {
return this.parentNode.getBBox().width + 5;
})
.attr("dy", "0.4em")
.attr("class", "legend text")
.style("fill", function(Key) {
return Key.color = Color(Key.UniqueName);
})
.text(function(Key){ return Key.UniqueName });
var PrevElemLength = 0;
Legend.attr("transform", function(d, i) {
if (i === 0) {
return "translate(0,0)"
} else {
var marginLeft = 5;
PrevElemLength += this.previousElementSibling.getBBox().width;
return "translate(" + (PrevElemLength + marginLeft) + ",0)"
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width=500 height=500></svg>
Try this :
//Legend
var legend = vis.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("image")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
return "../assets/images/dev/"+d+".png";
})
legend.append("text")
.attr("x", 910)
.attr("y", 78)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d) {
return d;
});

How Can I Use Symbols With a Legend

I've got a legend, with colored rectangles...
I'd like to replace the rectangles with symbols (i.e., circle, cross, diamond, square). I can't figure out how to do that.
I've been using variations of .attr("d", d3.svg.symbol().type('circle'). For instance, I tried:
legendRect
.attr("d", d3.svg.symbol().type(function (d) { return d[2] })
and I tried:
legendRect.append("svg:path")
.attr("d", d3.svg.symbol().type((d: any) => { return d[2] }))
d[2] is "supposed to be" pulling from legendData, as shown in the below code example...like it does with d[1] for the fill.
But I don't ever see anything change.
Here's the code I'm using for the legend, without the symbol stuff, below. What am I doing wrong and how can I change the rectangles to symbols? Where do I need to add what?
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var legend = this.svg.append("g")
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(-20,250)');
var legendRect = legend.selectAll('rect').data(legendData);
legendRect.enter()
.append("rect")
.attr("x", width - 65)
.attr("width", 10)
.attr("height", 10)
;
legendRect
.attr("y", function (d, i) {
return i * 20;
})
.style("fill", function (d) {
return d[1];
})
var legendText = legend.selectAll('text').data(legendData);
legendText.enter()
.append("text")
.attr("x", width - 52);
legendText
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d[0];
});
Here's how I would code it. Notice, that I data-bind to a wrapper g element and then place the symbol and text into it for each legend item. You can then position the g instead of positioning the text and "symbol" separately. This also removes the need for double-binding the data.
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var svg = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var legend = svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(20,20)');
var legendRect = legend
.selectAll('g')
.data(legendData);
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function(d,i){
return 'translate(0, ' + (i * 20) + ')';
});
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => { return d[2] }))
.style("fill", function (d) {
return d[1];
});
legendRectE
.append("text")
.attr("x", 10)
.attr("y", 5)
.text(function (d) {
return d[0];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
This is a implementation which uses symbols for your legend. You can use the symbols like the following:
svg.selectAll('.symbol')
.data(legendData)
.enter()
.append('path')
.attr('transform', function(d, i) {
return 'translate(' + (20) + ',' + ((i * 20) + 10) + ')';
})
.attr('d', d3.symbol().type(function(d, i) {
if (d[2] === "circle") {
return d3.symbolCircle;
} else if (d[2] === "cross") {
return d3.symbolCross;
} else if (d[2] === "diamond") {
return d3.symbolDiamond;
} else if (d[2] === "square") {
return d3.symbolSquare;
} else {
return d3.symbolTriangle;
}
})
.size(100))
.style("fill", function(d) {
return d[1];
});
Then you can set your legend labels like the following:
svg.selectAll('.label')
.data(legendData)
.enter()
.append('text')
.attr("x", "40")
.attr("y", function(d, i){ return ((i * 20)+15);})
.text(function(d) {
return d[0];
});
Check fiddle here - https://jsfiddle.net/zoxckLe3/
P.S. - Above solution uses d3 v4. To achieve the same in v3, use the following line .attr('d', d3.svg.symbol().type(function(d){return d[2];})) instead of the part where I match d[2] to the symbol name.
For adding image icons, you can use below code.
legend.append("**image**")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
**return "../assets/images/dev/"+d+".png";**
})
This works for me..

How to append value at the top of rect

I have made a vertical bar chart and have the following to append the text onto the bars.
If i set y attribute as 0,the text is shown at the top, irrespective of the length of bars i.e all values stick together on top even though bar length is 50,1,1,3.
What i need is that the value should display at the immediate top of the bar.
I tried text anchor end also but it didnt work.
sets.append("text")
.attr("class", "global")
//.attr("y", function(d) {
// return yScale(d.global) ;
// })
.attr("dy", 5)
.attr("dx",(xScale.rangeBand() / 4))
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("fill", "black")
.text(function(d) {
return ( commaFormat(d.global) > 0) ? commaFormat(d.global) : "";
});
here is the rect code
sets.append("rect")
.attr("class","global")
.attr("width", xScale.rangeBand()/2)
.attr("y", function(d) {
return yScale((d.global/total)*100);
})
.attr("height", function(d){
return h - yScale((d.global/total)*100);
})
.attr('fill', function (d, i) {
return color(d.global);
})
.append("text")
.text(function(d) {
return commaFormat((d.global/total)*100);
})
//.attr('x', function(d, i) {
// return xScale(i) + xScale.rangeBand() / 2;
//})
//.attr('y', function(d) {
// return h - yScale((d.global/total)*100) + 14;
//})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")

How to add text to Cluster Force Layout bubbles

I am trying to add some text from the name field in my JSON file to each bubble in a cluster.
https://plnkr.co/edit/hwxxG34Z2wYZ0bc51Hgu?p=preview
I have added what I thought was correct attributes to the nodes with
node.append("text")
.text(function (d) {
return d.name;
})
.attr("dx", -10)
.attr("dy", "5em")
.text(function (d) {
return d.name
})
.style("stroke", "white");
function tick(e) {
node.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("transform", function (d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
Everything works fine except the labels.
What am I missing here?
Thanks
Kev
For that you will need to make a group like this:
var node = svg.selectAll("g")
.data(nodes)
.enter().append("g").call(force.drag);//add drag to the group
To the group add circle.
var circles = node.append("circle")
.style("fill", function(d) {
return color(d.cluster);
})
To the group add text:
node.append("text")
.text(function(d) {
return d.name;
})
.attr("dx", -10)
.text(function(d) {
return d.name
})
.style("stroke", "white");
Add tween to the circle in group like this:
node.selectAll("circle").transition()
.duration(750)
.delay(function(d, i) {
return i * 5;
})
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) {
return d.radius = i(t);
};
});
Now the tick method will translate the group and with the group the circle and text will take its position.
working code here
The problem: a text SVG element cannot be child of a circle SVG element.
The solution is creating another selection for the texts:
var nodeText = svg.selectAll(".nodeText")
.data(nodes)
.enter().append("text")
.text(function (d) {
return d.name;
})
.attr("text-anchor", "middle")
.style("stroke", "white")
.call(force.drag);
Here is the plunker: https://plnkr.co/edit/qnx7CQox0ge89zBL9jxc?p=preview

How to draw data properties aside a graph node?

I'm drawing a graph with D3 in which the node is a picture with name above. I would now like display some properties on the right side of the node.
var node = svg.select(".nodes").selectAll(".node")
.data(nodes, function(d) {return d.id});
var nodeEnter = node.enter()
.append("g")
.attr("class", function(d) { return "node "+d.type})
.attr("id", function(d) { return d.label })
.call(force.drag);
nodeEnter.filter(function(d) {return (d.properties)}).append("rect")
.attr("x", -40)
.attr("y", -50)
.attr("width", 130)
.attr("height", 90);
nodeEnter.filter(function(d) {return (d.imgB64)}).append("image")
.attr("xlink:href", function(d) { return 'data:image/png;base64, ' + d.imgB64; })
.attr("x", -40)
.attr("y", -40)
.attr("width", 80)
.attr("height", 80);
d3.selectAll(node);
var textNode = nodeEnter.filter(function(d) { return (d.imgB64) && (d.name));
textNode.append("text")
.attr("class", function(d) { return "text "+d.type; })
.attr("text-anchor", function(d) { return (d.properties) ? "left" : "middle"})
.attr("dx", -38)
.attr("dy", function(d) { return (d.properties) ? -38 : 42})
.text(function(d) {
return d.name;
});
var textNode = nodeEnter.filter(function(d) { return (d.properties)});
textNode.append("text")
.attr("text-anchor", "left")
.attr("dx", 40)
.attr("dy", -30)
.style("font-size",9)
.text(function(d) { return "MW: "+d.properties.MW.toPrecision(3); });
The above code properly draw one property on the right side of the image. How could I loop through all available properties of each node and display them as a list.
Each node data is formatted as follow:
{imgB64: "...", name: "A", properties: {MW: 354, logP: 4.6, pKd: -6.2, logPapp: -5.99}}
I tried the following:
var textNode = nodeEnter.filter(function(d) { return (d.properties)});
textNode.append("text")
.data(function(d) { return d.properties })
.attr("text-anchor", "left")
.attr("dx", 40)
.attr("dy", function(d,i) { return (i*15-30) })
.style("font-size",9)
.text(function(d) { return "foo" });
I get the an error message "TypeError: undefined is not an object (evaluating 'd.properties')".
How should I create new data to handle the properties?
Code is shared on fiddle
Thanks
As suggested by echonax, d.properties was not an array. This can be addressed by
.data(function(d) { return jsonToArray(d.data) })
with
function jsonToArray(data) {
var a = [];
for (k in data) {
a.push({key: k, value: data[k]});
}
return a;
}
Thanks to echonax suggestion, I saved a lot if redundant code.

Resources