Appending multiple svg text with d3 - svg

I have been stuck on this problem for days.
So I have a dataset of objects of the following form:
dataset = [{metric:"revenue",value:0.03},{metric:"sales", value:0.15},{metric:"churn", value: 0.06},{metric:"logins", value: 0.45}]
The following code would display the 4 metric names in a grid pattern (meshy, meshx are the coordinates points of the grid and meshsize is the size of the grid, so this is just putting the text in the middle of a grid square):
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d){
return d.metric;
})
.attr("y",function(d,i){
return meshy[i] + meshsize/2;
})
.attr("x", function(d,i){
return meshx[i] + meshsize/2;
})
.attr("font-size",25)
.attr("font-family","serif")
.attr("text-anchor","middle")
.attr("font-weight","bold");
Now I would like to put the value of the metric right underneath the metric name like so:
svg.append("text")
.data(dataset)
.text(function(d){
return (d.value);
})
.attr("y",function(d,i){
return meshy[i] + meshsize/2 + 20;
})
.attr("x", function(d,i){
return meshx[i] + meshsize/2 ;
})
.attr("font-size",25)
.attr("font-family","serif")
.attr("text-anchor","middle")
.attr("font-weight","bold");
But this only returns the value underneath the metric name for the FIRST metric, the other 3 value texts are not even in the DOM. I have tried multiple approaches including replacing .text with .html as described here:https://github.com/mbostock/d3/wiki/Selections#wiki-html with no success. I have also tried appending paragraph elements instead - this works but the p elements are positioned below the svg body in a list with no obvious way to move them into the right position. The code above is the closest I have come to getting what I need, but for some reason only the first value text shows up. However, I am open to any approach in d3 that gets the job done: 4 metric names with the values right underneath them

In your second block of code, you are only appending one text element, hence only one of them is appearing. What you need to do is to append the text similar to your first block, i.e. with the .enter() selection. For this, you have two choices. You can either save and reuse the .enter() selection, or assign different classes to the two kinds of text to be able to distinguish between them.
Option 1:
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function(d){
return d.metric;
})
// set position etc.
texts.append("text")
.text(function(d){
return d.value;
})
// set position etc.
Option 2:
svg.selectAll("text.title")
.data(dataset)
.enter()
.append("text")
.attr("class", "title")
.text(function(d){
return d.metric;
})
// set position etc.
svg.selectAll("text.value")
.data(dataset)
.enter()
.append("text")
.attr("class", "value")
.text(function(d){
return d.value;
})
// set position etc.
The first option is obviously shorter, but depending on what else you want to do, the second option may be preferable -- if you want to modify the text afterwards, it will be a lot easier if you can distinguish between the two kinds of text. You can also use the different classes to give different CSS styles.

Related

D3 js skip symbol conditionally

following code adds symbols on multi series line chart.
I want to skip symbols for specific series among all. Can we conditionally skip a single series from being added in multi series line chart?
lineChart.selectAll("g.dot")
.data(d3Data)
.enter().append("g")
.attr("class", "dot")
.style("fill", "red")
.style("stroke", "red")
.selectAll("svg:path")
.data(function(d) { return d.data; })
.enter().append("svg:path")
.attr("transform",function(d){return "translate("+x(d.key)+","+y(d.value)+")";})
.attr("d", d3.svg.symbol().type(d.seriesSymbol).size(22));
If I set value in d.seriesSymbol as blank of any garbage value then d3 by default displays circle. I dont want even this circle to be displayed. I am writing d3.js code in nodejs.
Do we have "none" kind of keyword to skip symbol?

how to get text bounding box in famo.us

I am attempting to draw an SVG bezier curve that starts at the end of a text string that is in a Surface. I can set the size of the Surface to [true, true], which is supposed to make the size equal the text bounding box. But if I later try "mySurface.size[0]" in order to get the width, it returns "true"! I need to get a number for the width and height of that bounding box, in order to calculate the end point of my bezier curve! The equivalent DOM approach would just be to use the .getBBox() function.. how do I do this in Famo.us?
this is maybe because the surface hasn't rendered yet. there are a few similar questions here, one of them from me:
how can we get the size of a surface within famo.us?
you could also try deferring or using a setTimeout or Engine.nextTick() to check the size on the next loop through.
if you find an elegant solution let us know as this is a big problem in many places using famous - having to do multiple highjinks where you can't really position a scene on the initial setup - you have to let it render and then adjust...
You can use the 'getSize' function and specify 'true' to get the real size of the surface:
var realSize = surface.getSize(true);
#ljzerenHein, thanks for the hint.. unfortunately, surface.getSize(true) returns null!
#dcsan, thanks for the link. I believe you may be right, however the solution linked to ends up being much too involved for me.
After much searching, hacking, and experimenting, I've settled on the following approach:
-] use the DOM to get untransformed bounding boxes for text strings
-] format the text strings in SVG form
-] make it so the strings are invisible (set fill and stroke to none)
-] reuse the same "div" element for all the strings that I want to measure
-] once I have the untransformed bounding box, then set the famous surface size to that and then apply modifiers.
-] if I need the bounding box after all transforms have been applied, then get the total accumulated transforms for the surface and multiply that with the original untransformed bounding box
Here's the code to create the DOM element, insert SVG text, then get the bounding box:
//Do this part once, of creating a DOM element and adding it to the document
var el1 = document.createElement("div");
document.body.appendChild(el1); //only need to append once -- just set innerHTML after
//now set the SVG text string -- from this point down can be repeated for multiple
// strings without removing or re-adding the element, nor fiddling with the DOM
var text1_1_1_SVG = '<svg> <text x="0" y="0" style="font-family: Arial; font-size: 12;fill:none;stroke:none" id="svgText1">' + myFamousSurface.content + '</text> </svg>';
//note the id is inside the text element! Also the fill and stroke are null so nothing paints
el1.innerHTML = text1_1_1_SVG;
//now get the element -- this seems to be what triggers the bounding box calc
var test = document.getElementById("svgText1"); //this is ID in the text element
//get the box, take the values out of it, and display them
var rect = test.getBoundingClientRect();
var str = "";
for (i in rect) { //a trick for getting all the attributes of the object
str += i + " = " + rect[i] + " ";
}
console.log("svgText1: " + str);
FYI, all of the SVGTextElement methods seem to be callable upon gotElem.
SVGTextElement docs here: http://msdn.microsoft.com/en-us/library/ie/ff972126(v=vs.85).aspx
#seanhalle
I'm pretty sure .getSize(true) is returning null because the element has not yet been added to the DOM. Keep in mind that famo.us is synchronized with animation-frames, and updates to the DOM happen don't happen instantly. Accesssing the DOM directly (aka pinging) is strongly disadviced because you will loose the performance benefits that famo.us promises.
What I would do is create a custom view to wrap your surface inside and implement a render-method in it. In the render-method, use getSize(true) to get the size. If it returns null,
you know it has not yet been committed to the DOM.
view in action as jsfiddle
define('MyView', function (require, exports, module) {
var View = require('famous/core/View');
var Surface = require('famous/core/Surface');
function MyView() {
View.apply(this, arguments);
this.surface = new Surface();
this.add(this.surface);
}
MyView.prototype = Object.create(View.prototype);
MyView.prototype.constructor = MyView;
MyView.prototype.render = function() {
var size = this.getSize(true);
if (size) {
if (!this.hasSize) {
this.hasSize = true;
console.log('now we have a size: ' + size);
}
this.surface.setContent('Size: ' + JSON.stringify(size));
} else {
console.log('no size yet');
}
return this._node.render();
};
module.exports = MyView;
});

How to right/end align text along an textPath inside an arc using d3.js?

Here's the fiddle: http://jsfiddle.net/DevChefOwen/CZ6Dp/
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
.style("text-anchor","left") // using "end", the entire text disappears
.text("some text");
I've tried a number of different things to no avail. The left align is the easy part. If you did a middle, though, you see only "text" instead of "some text", implying that "some" is just hidden because it went "out of span" for the given arc.
If, however, I added:
.attr("startOffset","39%")
(as in here: http://jsfiddle.net/DevChefOwen/2H99c/)
It would look right aligned, but outside of programmatically trying to get the width/height of the text element and look for sharp changes in width/height (which seems wrong and likely error-prone), I can't seem to find a way to right align the text.
I've also tried using an SVG path (essentially a curved arc line) and the same disappearing act happens with the text when "text-anchor" is set to "left".
Thanks ahead for your time!
The question is somewhat confusing matters. The issue isn't aligning text at the end of the path -- that's easy to do with "text-anchor"="end" and "startOffset"="100%".
However, using those settings with the path created by the d3 arc function, you end up with the text cornering around the end of the inside curve and the left straight edge, to the end of the path as defined by the arc function:
http://jsfiddle.net/CZ6Dp/8/
The real issue is that the path that you want the text to be aligned along (the outside arc of the shape) is only one segment of the path that defines the shape.
(By the way, "left" and "right" are not valid values for the "text-anchor" property, and will just be ignored).
The answer by #defghi1977 gives one way to approach the problem, by figuring out the length of the path segment that you do want to use and adjusting the start offset accordingly.
Another way to approach the problem is to create a separate path (not drawn on screen) that represents only the part of the path that you want to be used for positioning text.
There are a number of possible ways to create a path that only represents the outside arc (some example code here). #defghi1977's approach of grabbing it from the existing path with regular expressions is probably the most efficent for your situation. But instead of just creating a temporary element to calculate a length, I actually have to add the new path to the DOM so it can be used as the reference path for the <textPath> element. (Which I suppose is the downside to this approach -- twice as many DOM elements!)
var path = g.append("svg:path")
.attr("d", arct)
.style("fill","#ccc")
.attr("transform", "translate("+cfg.w/2+","+cfg.h/2+")")
.each(function(d,i) {
var justArc = /(^.+?)L/;
//grab everything up to the first Line statement
var thisSelected = d3.select(this);
var arcD = justArc.exec( thisSelected.attr("d") )[1];
defs.append("path")
.attr("id", "yyy") //normally the id would be based on the data or index
.attr("d", arcD)
.attr("transform", thisSelected.attr("transform") );
//if you can avoid using transforms directly on the path element,
//you'll save yourself having to repeat them for the text paths...
});
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
.style("text-anchor","end")
.attr("startOffset","100%")
.text("some text");
http://jsfiddle.net/CZ6Dp/9/
Again, factoring in the extra DOM load #defghi1977's method is probably slightly preferrable, although this version has the benefit of not being dependent on browser support for getTotalLength. But as far as I know that method is fairly well implemented.
So just consider this an alternate approach for completeness' sake.
This path is constructed by 4(or 5) path segments.
So, this probrem will be solved to get first arc path length.
But I don't know how to get sub path length by using d3.js, thus I use svgdom directly.
I tried to fix your code. If this code is not what you hope, I'm sorry.
path-anchor attribute to end.
define function to get startOffset value.
var path = g.append("svg:path")
.attr("id","yyy")
.attr("d", arct)
.style("fill","#ccc")
.attr("transform", "translate("+cfg.w/2+","+cfg.h/2+")");
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
//.style("text-anchor","left") // using "end", the entire text disappears
.attr("text-anchor", "end")
.text("some text")
.attr("startOffset",function(){
var d = document.getElementById("yyy").getAttribute("d");
var tmp = document.createElementNS("http://www.w3.org/2000/svg" ,"path");
//get the arc segment of path
var arc = d.match(/(^.+?)L/)[1];
tmp.setAttribute("d", arc);
//return offset position
return tmp.getTotalLength();
});
I think the confusion comes from the meaning of text-anchor - it's not "relative to where on the parent will I justify" but rather "what part of me should I align to the start".
You're right to try to use startOffset to move the origin. Since the outer radius of your path is longer than the inner radius, the correct start offset is a little more than half of the path (around 53%).
Just a little more twiddling with your settings and you should have it. Here's a fiddle with my interpretation of what you're looking for.

Why labels on chart are not shown when drawn after axes

I'm making a simple line chart with in-chart labels using d3.js.
When I draw the axes before the labels, the latter do not get added to the svg element, but they do if I draw them first:
In the second chart, the labels are not just hidden. They are not being appended to the DOM at all.
Here's the bl.ock. The only difference between both scripts is that before.js writes labels before axes, while after.js does it afterwards.
Why does this happen?
The problem is this line when adding the labels:
svg.selectAll('text')
If you have drawn the axes at this point. There will be text elements and the selection above will not be empty. Calling .data() on it causes D3 to match data elements to DOM elements in the selection. In this case, everything is matched and therefore the .enter() selection is empty and no new labels are added.
It works if you run this code first because there are no text elements, the selection is empty and no data is matched. To prevent this, you can for example identify the label text elements explicitly with a class:
svg.selectAll('text.label')
.data(dataset)
.enter()
.append('text')
.attr("class", "label")
.text(Math.floor)
.attr('class', 'data-label')
.attr('x', function (d, i) { return x(i); })
.attr('y', function (d, i) { return y(d + 1); });
With this code, it doesn't matter whether you run it before or after adding the axes.

Nesting data using D3.js (heatmap)

So I am new to working with Javascript (especially the D3 library) and I am trying to do something not unlike the following example: http://mbostock.github.com/d3/talk/20111116/iris-splom.html. In my case though each cell is the same thing a 4 x 4 grid with exactly the same scale.
So in my case the top level element is a plate. Each plate has rows and columns at the intersection of a row and column a value (a heatmap if you will). I able to create the proper plate elements; however, the data for ALL plates is present under each element rather than properly nested. I tried to attach an image so you can see that each "plate" is the same, if you look at the underlying document structure it is the same and essentially each rectangle is two overlaid data points.
In looking more closely at Mike's example (link above), it looks like he uses a cross function to help out with the nesting of data, I am wondering if that is where my code falls down. Thank you for any help you all can provide.
I have posted my code below
d3.csv("plateData.csv", function(data) {
var m = 20,
w = 400,
h = 300,
x_extent = d3.extent(data, function(d){return d.Rows}),
y_extent = d3.extent(data, function(d){return d.Columns}),
z_extent = d3.extent(data, function(d){return d.Values});
var x_scale = d3.scale.linear()
.range([m, w-m])
.domain(x_extent)
var y_scale = d3.scale.linear()
.range([h-m,m])
.domain(y_extent)
var ramp=d3.scale.linear()
.domain(z_extent)
.range(["blue","red"]);
// Nest data by Plates
var plates = d3.nest()
.key(function(d) { return d.Plate; })
.entries(data);
// Insert an svg element (with margin) for each plate in our dataset.
var svg = d3.select("body").selectAll("svg")
.data(plates)
.enter().append("svg:svg")
.attr("width", w)
.attr("height", h)
//add grouping and rect
.append("svg:g")
.selectAll('rect')
.data(data)
.enter()
.append('svg:rect')
.attr('x', function(d){return x_scale(d.Rows)})
.attr('y', function(d){return y_scale(d.Columns)})
.attr('width', 10)
.attr('height', 10)
.style('fill', function(d){return ramp(d.Values)});
}
);
AND example Data:
Plate,Rows,Columns,Values
12345,1,1,1158.755
12345,1,2,1097.768
12345,1,3,1097.768
12345,1,4,914.807
12345,2,1,1189.249
12345,2,2,1128.261
12345,2,3,1433.197
12345,2,4,701.352
12345,3,1,914.807
12345,3,2,1433.197
12345,3,3,1189.249
12345,3,4,1402.703
12345,4,1,1158.755
12345,4,2,1067.274
12345,4,3,701.352
12345,4,4,1372.21
56987,1,1,20.755
56987,1,2,97.768
56987,1,3,97.768
56987,1,4,14.807
56987,2,1,89.249
56987,2,2,28.261
56987,2,3,33.197
56987,2,4,15.352
56987,3,1,2000.807
56987,3,2,14.197
56987,3,3,89.249
56987,3,4,402.703
56987,4,1,158.755
56987,4,2,3067.274
56987,4,3,701.352
56987,4,4,182.21
You problem has two sources:
You are inserting one svg element for each nested group, but you don't have defined a position for each element (all the elements are in the body).
You are selecting all the svg elements in the body, and appending one rectangle for each data element, without considering the nested structure.
One solution is to create one svg element inside the body, one group for each plate, define a position for each plate, translate the group to its position (in the svg element) and then create the rectangles for the heatmap.

Resources