How to add textPath labels to zoomable sunburst diagram in D3.js? - javascript

I have modified this sunburst diagram in D3 and would like to add text labels and some other effects. I have tried to adopt every example I could find but without luck. Looks like I'm not quite there yet with D3 :(
For labels, I would like to only use names of top/parent nodes and that they appear outside of the diagram (as per the image below). This doesn't quite work:
var label = svg.datum(root)
.selectAll("text")
.data(partition.nodes(root).slice(0,3)) // just top/parent nodes?
.enter().append("text")
.attr("class", "label")
.attr("x", 0) // middle of arc
.attr("dy", -10) // outside last children arcs
/*
.attr("transform", function(d) {
var angle = (d.x + d.dx / 2) * 180 / Math.PI - 90;
console.log(d, angle);
if (Math.floor(angle) == 119) {
console.log("Flip", d)
return ""
} else {
//return "scale(-1 -1)"
}
})
*/
.append("textPath")
.attr("xlink:href", function(d, i) { return "#path_" + i; })
.text(function(d) { return d.name + " X%"; });
I would also like to modify a whole tree branch on hover so that it 'shifts' outwards. How would I accomplish that?
function mouseover(d) {
d3.select(this) // current element and all its children
.transition()
.duration(250)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); });
// shift arcs outwards
}
function mouseout(d) {
d3.selectAll("path")
.transition()
.duration(250)
.style("fill", "#fff");
// bring arcs back
}
Next, I'd like to add extra lines/ticks on the outside of the diagram that correspond to boundaries of top/parent nodes, highlighting them. Something along these lines:
var ticks = svg.datum(root).selectAll("line")
.data(partition.nodes) // just top/parent nodes?
.enter().append("svg:line")
.style("fill", "none")
.style("stroke", "#f00");
ticks
.transition()
.ease("elastic")
.duration(750)
.attr("x1", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y1", function(d) { return Math.max(0, y(d.y + d.dy)); })
.attr("x2", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y2", function(d) { return Math.max(0, y(d.y + d.dy) + radius/10); });
Finally, I would like to limit zoom level so the last nodes in the tree do not fire zoom but instead launch a URL (which will be added in JSON file). How would I modify the below?
function click(d) {
node = d;
path.transition()
.duration(750)
.attrTween("d", arcTweenZoom(d));
}
My full pen here.
Any help with this would be much appreciated.

Related

How do I change the position of a circle svg element using the transform attribute?

I am currently building a sunburst chart in D3JS and am trying to append circles to each node. You can view current project here: https://jsfiddle.net/mhxuo260/.
I am trying to position each of the circles in the top right hand corner of their respective node. Currently they will only position in the center which covers the node labels. I have been scouring the net in search for a clue but just haven't come up with anything yet. Any suggestion would be appreciated.
d3.json("flare.json", function(error, root) {
if (error) throw error;
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
path = g.append("path")
.attr("d", arc)
.attr('stroke', 'white')
.attr("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", magnify)
.each(stash);
var text = g.append("text")
// .attr("x", function(d) { return d.x; })
// .attr("dx", "6") // margin
// .attr("dy", ".35em") // vertical-align
.text(function(d) {
return d.name;
})
.attr('font-size', function(d) {
return '10px';
})
.attr("text-anchor", "middle")
.attr("transform", function(d) {
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getStartAngle(d) + ")";
} else {
return null;
}
})
.on("click", magnify);
var circle = g.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.dy; })
.attr('r', '10')
.attr('fill', 'white')
.attr('stroke', 'lightblue')
.attr("transform", function(d) {
console.log(arc.centroid(d))
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getStartAngle(d) + ")";
} else {
return null;
}
});
You are using the ´´´arc.centroid´´´ function which always returns the x,y midpoint of the arc. All that function is doing is:
The midpoint is defined as (startAngle + endAngle) / 2 and (innerRadius + outerRadius) / 2
You just need to calculate a different position using these values depending on where you want it. Use the transform like this (sudo code):
.attr( "transform", function(d) {
var x = (startAngle + endAngle) / 2;
var y = (innerRadius + outerRadius) / 2;
return "translate(" + x +"," + y + ")";
});
You don't need to rotate your circle.
(FYI: javascript will convert arrays into strings by joining each number with commas, this is why arc.centroid returning an array works here)

how to change stack order of text label in JavaScript?

I am trying to plot a network graph using networkD3 in R. I wanted to make some changes to the display so that the text labels (which appears when mouseover) can be easily read.
Please refer to the link here for an example. Note: Jump to the d3ForceNetwork plot.
As seen in the example, the labels are hard to read due to its colour and it often gets obstructed by the surrounding nodes. I have been messing around with the JS file and managed to change the text label color to black. However, having no knowledge of JS or CSS (I can't even tell the difference between the 2 actually), I have no idea how I can change the stack order such that the text labels will always be displayed above any other objects.
Can anyone advise me on how I can achieve the desired outcome?
Below is the full JS file:
HTMLWidgets.widget({
name: "forceNetwork",
type: "output",
initialize: function(el, width, height) {
d3.select(el).append("svg")
.attr("width", width)
.attr("height", height);
return d3.layout.force();
},
resize: function(el, width, height, force) {
d3.select(el).select("svg")
.attr("width", width)
.attr("height", height);
force.size([width, height]).resume();
},
renderValue: function(el, x, force) {
// Compute the node radius using the javascript math expression specified
function nodeSize(d) {
if(options.nodesize){
return eval(options.radiusCalculation);
}else{
return 6}
}
// alias options
var options = x.options;
// convert links and nodes data frames to d3 friendly format
var links = HTMLWidgets.dataframeToD3(x.links);
var nodes = HTMLWidgets.dataframeToD3(x.nodes);
// get the width and height
var width = el.offsetWidth;
var height = el.offsetHeight;
var color = eval(options.colourScale);
// set this up even if zoom = F
var zoom = d3.behavior.zoom();
// create d3 force layout
force
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(options.linkDistance)
.charge(options.charge)
.on("tick", tick)
.start();
// thanks http://plnkr.co/edit/cxLlvIlmo1Y6vJyPs6N9?p=preview
// http://stackoverflow.com/questions/22924253/adding-pan-zoom-to-d3js-force-directed
var drag = force.drag()
.on("dragstart", dragstart)
// allow force drag to work with pan/zoom drag
function dragstart(d) {
d3.event.sourceEvent.preventDefault();
d3.event.sourceEvent.stopPropagation();
}
// select the svg element and remove existing children
var svg = d3.select(el).select("svg");
svg.selectAll("*").remove();
// add two g layers; the first will be zoom target if zoom = T
// fine to have two g layers even if zoom = F
svg = svg
.append("g").attr("class","zoom-layer")
.append("g")
// add zooming if requested
if (options.zoom) {
function redraw() {
d3.select(el).select(".zoom-layer").attr("transform",
"translate(" + d3.event.translate + ")"+
" scale(" + d3.event.scale + ")");
}
zoom.on("zoom", redraw)
d3.select(el).select("svg")
.attr("pointer-events", "all")
.call(zoom);
} else {
zoom.on("zoom", null);
}
// draw links
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return d.colour ; })
//.style("stroke", options.linkColour)
.style("opacity", options.opacity)
.style("stroke-width", eval("(" + options.linkWidth + ")"))
.on("mouseover", function(d) {
d3.select(this)
.style("opacity", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.style("opacity", options.opacity);
});
// draw nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.group); })
.style("opacity", options.opacity)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.call(force.drag);
node.append("circle")
.attr("r", function(d){return nodeSize(d);})
.style("stroke", "#fff")
.style("opacity", options.opacity)
.style("stroke-width", "1.5px");
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name })
.style("font", options.fontSize + "px " + options.fontFamily)
.style("opacity", options.opacityNoHover)
.style("pointer-events", "none");
function tick() {
node.attr("transform", function(d) {
if(options.bounded){ // adds bounding box
d.x = Math.max(nodeSize(d), Math.min(width - nodeSize(d), d.x));
d.y = Math.max(nodeSize(d), Math.min(height - nodeSize(d), d.y));
}
return "translate(" + d.x + "," + d.y + ")"});
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; });
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style('position','relative')
.style("opacity", 1);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d);});
d3.select(this).select("text").transition()
.duration(1250)
.attr("x", 0)
.style("font", options.fontSize + "px ")
.style("opacity", options.opacityNoHover);
}
function click(d) {
return eval(options.clickAction)
}
// add legend option
if(options.legend){
var legendRectSize = 18;
var legendSpacing = 4;
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = legendRectSize;
var vert = i * height+4;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.style('fill', 'darkOrange')
.text(function(d) { return d; });
}
// make font-family consistent across all elements
d3.select(el).selectAll('text').style('font-family', options.fontFamily);
},
});
I suspect I need to make some changes to the code over here:
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style("opacity", 1);
}
You need to resort the node groups holding the circles and text so the currently mouseover'ed one is the last in that group, and thus the last one drawn so it appears on top of the others. See the first answer here -->
Updating SVG Element Z-Index With D3
In your case, if your data doesn't have an id field you may have to use 'name' instead as below (adapted to use the mouseover function you've got):
function mouseover(d) {
d3.selectAll("g.node").sort(function (a, b) {
if (a.name != d.name) return -1; // a is not the hovered element, send "a" to the back
else return 1; // a is the hovered element, bring "a" to the front (by making it last)
});
// your code continues
The pain might be that you have to do this edit for every d3 graph generated by this R script, unless you can edit the R code/package itself. (or you could suggest it to the package author as an enhancement.)

How can I dynamically update text labels in d3?

I want to add labels to my vertical bar chart that display the current percentage value that corresponds to the current hight of the bar.
So I need to continuously update the percentage value and I also need a transition to make the text element move insync with the bar chart.
I tried this:
var percentageLabels = svg.selectAll(".percentage-label")
.data(dataset);
percentageLabels.remove();
percentageLabels
.enter()
.append("text")
.attr("class", "percentage-label")
.style("fill", "white")
.text(function(d) {
return d;
})
.attr("y", function(d) {
return y(d);
})
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w + w * 10/100;
})
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
});
Check out the fiddle
I'd make a couple changes here. First, wrap the rect and the text in a g, so you only need to data-bind once. Then you are free to transition them together:
var uSel = svg.selectAll(".input")
.data(dataset); //<-- selection of gs
uSel.exit().remove(); //<-- anybody leaving? remove g (both rect and text)
var gs = uSel
.enter()
.append("g")
.attr("class", "input"); //<-- enter selection, append g
gs.append("rect")
.attr("fill", "rgb(250, 128, 114)"); //<-- enter selection, rect to g
gs.append("text")
.attr("class", "percentage-label")
.style("fill", "white")
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w + w * 10/100;
}); //<-- enter selection, text to g
uSel.select("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
}); //<-- update rects with transition
uSel.select("text")
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.text(function(d) {
return d + "%";
}); //<-- update text with transition
Updated fiddle.
EDITS
To transition the text, you are probably going to have to use a custom tween function:
uSel.select("text")
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d); //<-- move the text
})
.tween("", function(d) {
var self = d3.select(this),
oldValue = y.invert(self.attr("y")), //<-- get the current value
i = d3.interpolateRound(oldValue, d); //<-- interpolate to new value
return function(t) {
self.text(i(t) + '%') <-- update the text on each iteration
};
});
Updated, updated fiddle.
From the docs:
The transition.each method can be used to chain transitions and apply shared timing across a set of transitions. For example:
d3.transition()
.duration(750)
.ease("linear")
.each(function() {
d3.selectAll(".foo").transition()
.style("opacity", 0)
.remove();
})
.transition()
.each(function() {
d3.selectAll(".bar").transition()
.style("opacity", 0)
.remove();
});
You might want to check out this: https://github.com/mbostock/d3/wiki/Transitions#tween

Issue in rendering circles in javascript

I am trying to make tooltip like: http://jsfiddle.net/6cJ5c/10/ for my graph and that is the result on my realtime graph: http://jsfiddle.net/QBDGB/52/ I am wondering why there is a gap between the circles and the graph and why at the beginning there is a vertical line of circles? When it starts the circles are close to the curve but suddendly they start to jump up and down !! I want the circles to move smooothly and stick on the surface of the curve. I think the problem is that they are not moving with the "path1" and so it does not recognize the circles and thats why they are moving separetly or maybe the value of tooltipis are different of the value of the curve so they do not overlap!. That is how the data is generated ( value and time) and the tooltip:
var data1 = initialise();
var data1s = data1;
function initialise() {
var arr = [];
for (var i = 0; i < n; i++) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
arr.push(obj);
}
return arr;
}
// push a new element on to the given array
function updateData(a) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
a.push(obj);
}
var formatTime = d3.time.format("%H:%M:%S");
//tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var blueCircles = svg.selectAll("dot")
.data(data1s)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", "white")
.style("stroke", "red")
.style("stroke-width", "2px")
.on("mousemove", function(d ,i) {
div.transition()
.duration(650)
.style("opacity", .9);
div.html(formatTime(new Date(d.time)) + "<br/>" + d.value)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d ,i ) {
div.transition()
.duration(650)
.style("opacity", 0);
});
blueCircles.data(data1s)
.transition()
.duration(650)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); });
Please kindly tell me your opinions since I really need it :(
As I said maybe I should add "mouseover and mouse move functions" to the "path" to make it recognize the tooltip. something like following. but I am nor really sure :(
var path1 = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.data([data1])
.attr("class", "line1")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
I think your problem lies in the interpolation of your paths. You set the interpolation between points on your var area to "basis", which I found is a B-spline interpolation. This means the area drawn does not go through the points in your dataset, as shown in this example:
The path your points move over, though, are just straight lines between the points in your dataset. I updated and changed the interpolation from basic to linear, to demonstrate that it will work that way. I also set the ease() for the movement to linear, which makes it less 'jumpy'. http://jsfiddle.net/QBDGB/53/

on click event D3.js only works first time

I have a table that has different rows. Every row is a different data set.
I have an on click event attached to the rows that gives an extra chart when you click on the specific row.
But it only works the first time. After you first click on a specific row that data is shown in the chart, but if you click on another row the chart doesn't change.
Here is some of my code:
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", w * 24)
.attr("height", h);
//saturday
var saturday = d3.select(".saturday")
.selectAll("td")
.data(d3.values(twitterDays[5][5]))
.enter()
.append("td")
.attr("class", function(d) { return "hour h" + color(d); });
d3.select(".saturday").on("click", function() {
chart.selectAll("rect")
.data(d3.values(twitterDays[5][5]))
.enter().append("rect")
.attr("x", function(d, i) { return x(i) - .5; })
.attr("y", function(d) { return h - y(d) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d); })
chart.append("line")
.attr("x1", 0)
.attr("x2", w * d3.values(twitterDays[5][5]).length)
.attr("y1", h - .5)
.attr("y2", h - .5)
.style("stroke", "#000");
});
//sunday
var sunday = d3.select(".sunday")
.selectAll("td")
.data(d3.values(twitterDays[6][6]))
.enter()
.append("td")
.attr("class", function(d) { return "hour h" + color(d); });
d3.select(".sunday").on("click", function() {
chart.selectAll("rect")
.data(d3.values(twitterDays[6][6]))
.enter().append("rect")
.attr("x", function(d, i) { return x(i) - .5; })
.attr("y", function(d) { return h - y(d) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d); })
chart.append("line")
.attr("x1", 0)
.attr("x2", w * d3.values(twitterDays[6][6]).length)
.attr("y1", h - .5)
.attr("y2", h - .5)
.style("stroke", "#000");
});
Your are only taking care of the so-called enter selection; meaning only the creation but not the update or removal of the rects is implemented in your code.
See the General Update Pattern: General Update Pattern, I
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().remove();
}
Make sure to have a look at Mike's Thinking with Joins.

Categories