build specific icon from data - javascript

I want to display a specific icon in different boxes using d3.
I have a data as an array of [x0, y0, x1, y1, vx, vy] where:
x0, y0 is the first corner of a box,
x1, y1 the second corner and
vx, vy two parameters (velocity)
that will be used to generate a SVG path.
I am using:
var boxes = nodeSVGGroup.selectAll("rect").data(nodes).enter().append("rect");
to generate my boxes and this is working well
My problem comes when I want to create the SVG path (icon) and properly render it in each box (it needs to be generated, translated and rotated to fit the center of each box.
I am using a similar pattern i.e.
var selection = nodeSVGGroup.selectAll(".barb").data(nodes);
selection.enter()
.append('g')
.each(function(node, i) {
var vx = node[4];
var vy = node[5];
var speed = Math.sqrt(vx*vx + vy*vy);
var angle = Math.atan2(vx, vy);
// generate a path based on speed. it looks like
// M2 L8 2 M0 0 L1 2 Z
var path = ...
var scale = 0.5*(node[1]-node[0])/8;
var g = d3.select(this).data([path,angle,scale]).enter().append('g');
// still need to add following transforms
// translate(' + node[2] + ', ' + node[3] + ')
// scale(' + scale + ')
// rotate(' + angle + ' ' + 0 + ' ' + 0 + ')
// translate(-8, -2)',
g.append('path').attr('d', function(d){console.log(d); return d[0];})
.attr('vector-effect', 'non-scaling-stroke');
})
.attr('class', 'wind-arrow');
I get the error Uncaught TypeError: Cannot read property 'ownerDocument' of null(…) which seems to be related to this line
.each(function(node, i) {
What am I doing wrong?
The full code is here

It's not entirely clear why are you trying to create an "enter" selection inside an each. I'm not sure if I understand your goals, but you can simply use d3.select(this) to append the path to each group:
d3.select(this).append('path')
.attr('d', path)
Here is the updated fiddle: https://jsfiddle.net/9x169eL1/

Related

d3: replace a number value with text on x axis

I am trying to make a multiline chart in d3 v3 reusing the code taken mainly from here since I'm a begginner in js and it seemed to fit my needs the most.
However, what I do have now on my x axis are IDs and I would like to have there the text values (NAME from data shown below).
This is the code concerning x axis:
chartObj.formatAsNumber = d3.format(".0f"); // since now ID are represented as numbers
chartObj.xFormatter = chartObj.formatAsNumber; //TODO as text value
chartObj.bisect = d3.bisector(chartObj.xFunct).left; // used for tooltip
chartObj.xScale = d3.scale.linear().range([0, chartObj.width]).domain(d3.extent(chartObj.data, chartObj.xFunct));
// chartObj.xScale = d3.scale.ordinal().range([0, chartObj.width]).domain(dataset.map(function(d) { return d."NAME"; })); // (?)
chartObj.xAxis = d3.svg.axis().scale(chartObj.xScale).orient("bottom").tickFormat(chartObj.xFormatter);
chartObj.svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + chartObj.height + ")").call(chartObj.xAxis).append("text").attr("class", "label").attr("x", chartObj.width / 2).attr("y", 30).style("text-anchor", "middle").text(chartObj.xAxisLabel);
And the function for mousemove:
function mousemove() {
var x0 = chartObj.xScale.invert(d3.mouse(this)[0]), i = chartObj.bisect(dataset, x0, 1), d0 = chartObj.data[i - 1], d1 = chartObj.data[i];
try {
var d = x0 - chartObj.xFunct(d0) > chartObj.xFunct(d1) - x0 ? d1 : d0;
} catch (e) { return;}
minY = chartObj.height;
for (var y in yObjs) {
yObjs[y].tooltip.attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + "," + chartObj.yScale(yObjs[y].yFunct(d)) + ")");
yObjs[y].tooltip.select("text").text(chartObj.yFormatter(yObjs[y].yFunct(d)));
minY = Math.min(minY, chartObj.yScale(yObjs[y].yFunct(d)));
}
focus.select(".focus.line").attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + ")").attr("y1", minY);
}
And the data I use looks like this:
[{"ID":"2","NAME":"version:BA01","min":"44.8","max":"44.8"},{"ID":"6","NAME":"version:BA10","min":"44.7","max":"44.9"},{"ID":"7","NAME":"version:BA21","min":"45.0","max":"45.1"},{"ID":"28","NAME":"version:BA25","min":"44.9","max":"44.9"}]
I would really appreciate if you could possibly explain how to replace ID (number) with NAME (text) value on the x axis not loosing the tooltip.
Change chartObj.xFormatter = chartObj.formatAsNumber; to chartObj.xFormatter = chartObj. NAME;
It will be easier if you can produce a JSFiddle and share the link if the solution doesn't work.

d3 Force: Calculating position of text on links where links between nodes are arcs

One way to apply links to text is to use the force.links() array for the text elements and centre the text on the midpoint of the link.
I have some nodes with bidirectional links, which I've rendered as paths that bend at their midpoint to ensure it's clear that there's two links between the two nodes.
For these bidirectional links, I want to move the text so that it sits correctly over the bending path.
To do this, I've attempted to calculate the intersection(s) of a circle centred on the centre point of the link and a line running perpendicular to the link that also passes through its centre. I think in principal this makes sense, and it seems to be half working, but I'm not sure how to define which coordinate returned through calculating the intersections to apply to which label, and how to stop them jumping between the curved links when I move the nodes around (see jsfiddle - https://jsfiddle.net/sL3au5fz/6/).
The function for calculating coordinates of text on arcing paths is as follows:
function calcLinkTextCoords(d,coord, calling) {
var x_coord, y_coord;
//find centre point of coords
var cp = [(d.target.x + d.source.x)/2, (d.target.y + d.source.y)/2];
// find perpendicular gradient of line running through coords
var pg = -1 / ((d.target.y - d.source.y)/(d.target.x - d.source.x));
// define radius of circle (distance from centre point text will appear)
var radius = Math.sqrt(Math.pow(d.target.x - d.source.x,2) + Math.pow(d.target.y - d.source.y,2)) / 5 ;
// find x coord where circle with radius 20 centred on d midpoint meets perpendicular line to d.
if (d.target.y < d.source.y) {
x_coord = cp[0] + (radius / Math.sqrt(1 + Math.pow(pg,2)));
} else {
x_coord = cp[0] - (radius / Math.sqrt(1 + Math.pow(pg,2)));
};
// find y coord where x coord is x_text and y coord falls on perpendicular line to d running through midpoint of d
var y_coord = pg * (x_coord - cp[0]) + cp[1];
return (coord == "x" ? x_coord : y_coord);
};
Any help either to fix the above or propose another way to achieve this would be appreciated.
Incidentally I've tried using textPath to line my text up with my links but I don't find that method to be performant when displaying upward of 30-40 nodes and links.
Update: Amended above function and now works as intended. Updated fiddle here:https://jsfiddle.net/o82c2s4x/6/
You can calculate the projection of the chord to x and y axis and add it to the source node coordinates:
function calcLinkTextCoords(d,coord) {
//find chord length
var dx = (d.target.x - d.source.x);
var dy = (d.target.y - d.source.y);
var chord = Math.sqrt(dx*dx + dy*dy);
//Saggita
// since radius is equal to chord
var sag = chord - Math.sqrt(chord*chord - Math.pow(chord/2,2));
//Find the angles
var t1 = Math.atan2(sag, chord/2);
var t2 = Math.atan2(dy,dx);
var teta = t1+t2;
var h = Math.sqrt(sag*sag + Math.pow(chord/2,2));
return ({x: d.source.x + h*Math.cos(teta),y: d.source.y + h*Math.sin(teta)});
};
Here is the updated JsFiddle

Show D3 link text right-side up

I have built a D3 force directed visualization with text labels along the links. The one problem I'm running into is these labels appearing upside down when the links are to the left of their source node. Example here:
The code where I position the path and text looks like so:
var nodes = flatten(data);
var links = d3.layout.tree().links(nodes);
var path = vis.selectAll('path.link')
.data(links, function(d) {
return d.target.id;
});
path.enter().insert('svg:path')
.attr({
class: 'link',
id: function(d) {
return 'text-path-' + d.target.id;
},
'marker-end': 'url(#end)'
})
.style('stroke', '#ccc');
var linkText = vis.selectAll('g.link-text').data(links);
linkText.enter()
.append('text')
.append('textPath')
.attr('xlink:href', function(d) {
return '#text-path-' + d.target.id;
})
.style('text-anchor', 'middle')
.attr('startOffset', '50%')
.text(function(d) {return d.target.customerId});
I know I will need to somehow determine the current angle of each path and then set the text position accordingly, but I am not sure how to.
Here is a link to a block based on this issue: http://blockbuilder.org/MattDionis/5f966a5230079d9eb9f4
The answer below has got me about 90% of the way there. Here is what my original visualization looks like with text longer than a couple digit number:
...and here is what it looks like utilizing the tips in the below answer:
So while the text is now "right-side up", it no longer follows the arc.
The arcs you draw are such that their tangent in the middle is exactly the direction of the baseline of the text, AND it is also colinear with the vector that separates the two tree nodes.
We can use that to solve the problem.
A bit of math is needed. First, let's define a function that returns the angle of a vector v with respect to the horizontal axis:
function xAngle(v) {
return Math.atan(v.y/v.x) + (v.x < 0 ? Math.PI : 0);
}
Then, at each tick, let's rotate the text in place by minus the angle of its baseline. First, a few utility functions:
function isFiniteNumber(x) {
return typeof x === 'number' && (Math.abs(x) < Infinity);
}
function isVector(v) {
return isFiniteNumber(v.x) && isFiniteNumber(v.y);
}
and then, in your tick function, add
linkText.attr('transform', function (d) {
// Checks just in case, especially useful at the start of the sim
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of the text element
var box = this.getBBox();
var center = {
x: box.x + box.width/2,
y: box.y + box.height/2
};
// Get the tangent vector
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Rotate about the center
return 'rotate('
+ (-180/Math.PI*xAngle(delta))
+ ' ' + center.x
+ ' ' + center.y
+ ')';
});
});
edit: added pic:
edit 2 With straight lines instead of curved arcs (simply <text> instead of <textPath> inside of <text>), you can replace the part of the tick function that concerns linkText with this:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the direction of the link along the X axis
var dx = d.target.x - d.source.x;
// Flip the text if the link goes towards the left
return dx < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ')')
: '';
});
and this is what you get:
Notice how the text gets flipped as the link goes from pointing more to the right to pointing more to the left.
The problem with this is that the text ends up below the link. That can be fixed as follows:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the vector of the link
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Get a unitary vector orthogonal to delta
var norm = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
var orth = {
x: delta.y/norm,
y: -delta.x/norm
};
// Replace this with your ACTUAL font size
var fontSize = 14;
// Flip the text and translate it beyond the link line
// if the link goes towards the left
return delta.x < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ') translate('
+ (orth.x * fontSize) + ' '
+ (orth.y * fontSize) + ')')
: '';
});
and now the result looks like this:
As you can see, the text sits nicely on top of the line, even when the link points towards the left.
Finally, in order to solve the problem while keeping the arcs AND having the text right side up curved along the arc, I reckon you would need to build two <textPath> elements. One for going from source to target, and one for going the opposite way. You would use the first one when the link goes towards the right (delta.x >= 0) and the second one when the link goes towards the left (delta.x < 0) and I think the result would look nicer and the code would not be necessarily more complicated than the original, just with a bit more logic added.

How to make a d3 sankey link flow out the bottom of the diagram?

With a d3.js sankey diagram, how can I make a link flow out of a node and off the diagram towards either the bottom or top? This necessitates a 90deg turn in the link and for it to not end at a node (or at least not one in the diagram) and symbolizes a flow going out of the modeled system.
I want it to look something like the 'Finished Petroleum Products' link in this diagram.
Specifically I'm using this boilerplate, but a more generic answer is fine I'm stuck on which methods I can use to make this happen but once I know what to use and how to use them I can implement it.
Update: Basically, what I'm wondering is how to re-write one of the d3 functions (which?) to make a link terminate with a horizontal line (ie flush with the SVG ceiling or floor) instead of a vertical line (ie another node's side face).
The code below is a function I used to create a link path which leaves a node on its right, goes horizontally briefly, bends upwards with a quarter-circle and exits the ceiling of the diagram. I was naive in that the solution had nothing to do with re-writing a D3 function..it just took learning how to use the SVG path language.
function drawPathLeavingDiagram(link) {
var x0 = link.source.x + link.source.width,
x1 = x0 + link.source.width / 4,
x2 = x1 + link.source.width / 2,
y0 = link.source.y + link.sourceY + link.thickness / 2,
y1 = y0 - link.source.width / 2,
rx = link.source.width / 2,
ry = link.source.width / 2;
return "M" + x0 + "," + y0 + "L" + x1 + "," + y0 +
"A" + rx + "," + ry + "," + 0 + "," + 0 + "," + 0 + "," + x2 + "," + y1 +
"V" + 0;
}

Updating Text Labels in D3JS

I have been struggling with this issue for the past couple days: I have a force directed graph that labels its edges just like this example does it. The problem I am facing is that when the graph updates (ie: a node on the graph is added upon a user's click) it updates the graph but it leaves the old edge labels that I wrote previously behind:
BEFORE & AFTER A NEW GRAPH IS APPENDED:
As you can see, my edge labels are hanging around after an update. I have a function that is called everytime new data comes in, and in this function I have the following code that draws the labels:
path_text = svg.selectAll(".path")
.data(force.links(), function(d){ return d.name;})
.enter().append("svg:g");
path_text.append("svg:text")
.attr("class","path-text")
.text(function(d) { return d.data.label; });
The svg variable is declared once at a top level closure like so:
var svg = d3.select("body").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
My graph has a tick() function that calculates the location of each label like so:
function tick()
{
// Line label
path_text.attr("transform", function(d)
{
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y);
var dr = Math.sqrt(dx * dx + dy * dy);
var sinus = dy/dr;
var cosinus = dx/dr;
var l = d.data.label.length * 6;
var offset = (1 - (l / dr )) / 2;
var x=(d.source.x + dx*offset);
var y=(d.source.y + dy*offset);
return "translate(" + x + "," + y + ") matrix("+cosinus+", "+sinus+",
"+-sinus+", "+cosinus+", 0 , 0)";
});
.
.
.
I have tried moving this svg declaration down into the update function, so that this is instantiated each time there is a graph change. This actually works - but it makes an entire duplicate of the entire graph. The first, original copy still keeps the old labels - but the second copy acts exactly how I want it to. Is there a way, perhaps, instead of appending svg, there is a way of replacing? I have also tried calling exit().remove() without any luck as well.
Thank you so much for your time. This has been killing me as to how I'm supposed to do this.
I placed the svg declaration inside my graph update function, attached it to a div, and clear the div before appending it again:
jQuery('#v').empty();
var svg = d3.select("#v").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
Not the cleanest solution in my opinion, but will go with this unless you all have a better solution!

Categories

Resources