I'm trying to combine click-to-zoom functionality (e.g. http://bl.ocks.org/mbostock/2206590) with d3's force layout (e.g. http://bl.ocks.org/mbostock/1093130). The idea being that after clicking on a node in a force layout diagram, a zoom transition is applied with the node clicked moving to the centre (while any other force-related "tick-ing" is also allowed to continue).
Does anyone know of existing examples combining these pieces of functionality? I've been trying to adapt the click-to-zoom example myself but although the clicked() function seems pretty simple, I'm stuck on the role of the "path" variable and how it seems to be linked to here...
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("id", "state-borders")
.attr("d", path);
For example, I have a force layout where all nodes/links are appended to a parent g-element, do I really need an equivalent datum line above? And what is the function of the d attribute that the path variable is being fed into? I know I'm doing something wrong because at the moment when the clicked function evaluates "path.centroid(d)" it's always NaN.
Thanks for any thoughts at all!
Adding my current simplified implementation of automated click/zoom (triggered from within my nodeClick function) that works perfectly until the force layout moves nodes further (due to paramaters like charge, gravity, friction etc)...
var k;
if (d && centered !== d) {
k = 4;
centered = d;
} else {
k = 1;
centered = null;
}
svg.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -d.x + "," + -d.y + ")");
Related
I've been playing around with this example here for a little while. What I'm trying to do is highlight a single node/circle in the plot (by making it larger with a border; later I want to add text or a letter inside it too).
Currently, I've made the circle for Bhutan larger in the plot like the following:
.attr("r",
function(d){return ( d.countryName === "Bhutan" ? r + 4 : r);})
.attr("stroke", function(d){if (d.countryName==="Bhutan"){return "black"}})
However, it overlaps with the other circles. What would be the best approach to avoid these collisions/overlaps? Thanks in advance.
Link to Plunkr - https://plnkr.co/edit/rG6X07Kzkg9LeVVuL0PH?p=preview
I tried the following to add a letter inside the bhutan circle
//find bhutan circle and add a "B" to it
countriesCircles
.data(data)
.enter().append("text")
.filter(function(d) { return d.countryName === "Bhutan"; })
.text("B");
Updated Plunkr - https://plnkr.co/edit/Bza5AMxqUr2HW9CYdpC6?p=preview
This is a slightly different problem than in this question here: How to change the size of dots in beeswarm plots in D3.js
You have a few options that I can think of:
Set the forceCollide to be your largest possible radius * 1.33, e.g. (r + 4) * 1.33. This will prevent overlapping, but spread things out a lot and doesn't look that great.
Add the radius property to each entry in your array and make the collide work based off that, which will look a bit better but not perform as awesomely for large sets.
Here's an example of how to do that:
...
d3.csv("co2bee.csv", function(d) {
if (d.countryName === "Bhutan") {
d.r = r + 4;
} else {
d.r = r;
}
return d;
}, function(error, data) {
if (error) throw error;
var dataSet = data;
...
var simulation = d3.forceSimulation(dataSet)
...
.force("collide", d3.forceCollide(function(d) { return d.r * 1.33; }))
...
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", function(d){ return d.r; })
....
Use the row function in d3.csv to add a property to each member of the array called r, and check the country name to determine which one gets the larger value. Then use that value wherever you need to mess with the radius.
I guess it would've been possible to check the country name everywhere the radius was impacted (e.g. .force("collide", d3.forceCollide(function(d) { return d.countryName === "Bhutan" ? (r + 4) * 1.33 : r * 1.33; }), etc.). This feels a bit cleaner to me, but it might be cleaner still by abstracting out the radius from the data entries themselves...
Forked your plunk here: https://plnkr.co/edit/Tet1DVvHtC7mHz91eAYW?p=preview
Drag and Drop Example
I am trying to rewrite part of this example above to use in my code, specifically this piece:
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
However I am getting stuck since the v4 package has changed quite a bit. I wrote my zoomListener function to be
var zoomListener = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
function zoomed() {
transform = d3.event.transform;
console.log(d3.event);
svg.attr("transform", transform);
}
function centerNode(source){
t = transform;
console.log(t);
x = t.x*t.k; //I only want things to be centered vertically
y = (t.y + -source.x0)*t.k + (viewerHeight)/2 ;
svg.transition()
.duration(duration)
.attr("transform","translate(" + x + "," + y +")scale(" + t.k + ")");
transform.scale(t.k); //DOES NOT WORK
transform.translate([x, y]); //DOES NOT WORK
}
and I know that according to the doc things have changed and info are no longer are stored on what would be my zoomListener
D3 V4 release note on zoom I guess I am just confused on how I am suppose to do it with the new version. The last few lines of my centerNode function don't work which has for effect that when I center the node the zooming and panning reset...
Any suggestion?
So after much digging and trial and error I cam up with an answer that works pretty well for my purposes. Note that this code below is only the relevant part of my code not the whole code, certain variable were self explanatory so did not include them. ALSO THIS IS IN VERSION 4 of d3.js.
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight);
var zoomer = svg.append("rect")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
zoomer.call(zoom.transform, d3.zoomIdentity.translate(150,0)); //This is to pad my svg by a 150px on the left hand side
function zoomed() {
g.attr("transform", d3.event.transform);//The zoom and panning is affecting my G element which is a child of SVG
}
function centerNode(source){
t = d3.zoomTransform(zoomer.node());
console.log(t);
x = t.x;
y = source.x0;
y = -y *t.k + viewerHeight / 2;
g.transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
}
As per the examples for v4 on the d3.js page, I used a rectangle to apply the zoom to
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. Pan & Zoom Example
In the Center node function I am using d3.zoomTransform(zoomer.node()); to get the current transform applied to the page.
The purpose of this function is only to center the collapsible tree vertically not horizontally, so I am keeping the current transform.x (here t.x) the same.
The coordinate in my svg are flip hence why y= source.x0, source is a what node was clicked in my collapsible tree. ("Look to the example referenced to the top of this thread to understand what I am trying to convert to version 4)
I am apply the transformation to my G element and then I want to commit those changes to the zoom transform, to do so I use the .on("end", function(){}) otherwise it was doing weird behavior with the transition, by doing that all it does is setting the current state of the transform.
zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))
This line above is applying a translation of x and y and a scale -- that is equal to what the current state -- to the identiy matrix has to get a new transform for G, i then apply it to zoomer which is the element I called zoom on earlier.
This worked like a charm for me!
Calling transform.scale and transform.translate returns a new transform, and modifies nothing. Therefore:
transform = transform.translate([x, y]).scale(k)
svg.call(zoomListener.transform, newTransform)
(At this point zoomListener is a pretty inaccurate name for this, but regardless...)
k, x, and y can be derived from source, maybe as you show, but I'm not sure, because I don't know what source is. But to me, t.x*t.k looks suspicious, because it's multiplying the existing transforms x by its scale. Seems like it would cause a feedback loop.
For more into about the zoom in v4, check out this related StackOverflow post, or this example by mbostock demonstrating programmatic control over the zoom transform of an element (canvas in this case) and includes transitions.
I have written the following code designed to display a d3js tree layout but encountered some difficulty when trying to get the generated svg to resize according to its aspect ratio. I was able (in the attached demo) to get the svg to scale the way I had desired but the code I have written is constrained by const ASPECT_RATIO, as seen here:
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
and again, further down, here:
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
Is there a way to circumvent this? I would prefer not having to change this value by hand every time the aspect ratio of the svg changes (that is, any time new content is added).
Regards,
Brian
tl;dr: Help me eliminate const ASPECT_RATIO.
Code:
/// <reference path="d3.d.ts" />
"use strict";
/* (c) brianjenkins94 | brianjenkins94.me | MIT licensed */
// Get JSON, impose synchronicity
d3.json("js/data.json", function(error, treeData) {
if (!error) {
// Instantiate canvas
var canvas = d3.select("#canvas");
// Aspect ratio nonsense
const ASPECT_RATIO = 1.89260808926;
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
// Update
update();
function update() {
// Add an SVG group element
canvas.append("g");
// Instantiate group
var group = canvas.select("g");
// Translate group right
group.attr("transform", "translate(64, 0)");
// Instantiate layout tree
var layout = d3.layout.tree();
// Initialize layout dimensions
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
// Instantiate rotation diagonal
var diagonal = d3.svg.diagonal();
// Rotate projection 90 degrees about the diagonal
diagonal.projection(function(d) { return [d.y, d.x]; });
// Initialize node array
var nodes = layout.nodes(treeData);
// Initialize link array
var links = layout.links(nodes);
// Select all paths in group
group.selectAll("path")
// For each link, create a path
.data(links).enter().append("path")
// Provide the specific diagonal
.attr("d", diagonal);
// Select all groups in group
var node = group.selectAll("g")
// For each node, create a group
.data(nodes).enter().append("g")
// Translate accordingly
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// Add a circle at every node
node.append("circle")
.attr("r", 3);
// Add label
node.append("text")
// To the left if the node has children, otherwise right
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 0)
// Branch if the node has children
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
}
} else {
console.log("There was a connection error of some sort.");
}
});
Demo:
https://rawgit.com/brianjenkins94/html5tree/master/index.html
Here's what I learned:
While an svg can be "dimensionless" it can't be "aspect-ratio-less." There must be some core aspect ratio to govern the resize.
The tree.size() and tree.nodeSize() functions create an "arbitrary coordinate system" that "is not limited screen coordinates."
What the aspect ratio is, is up to the developer and can be expressed as "arbitrary coordinates" or, in accordance with responsive design, can be expressed as relativistic measurements.
My solution is as follows:
// Viewbox & preserveAspectRatio
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (2 * window.innerWidth));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
...
// Initialize layout dimensions
layout.size([(2 * window.innerWidth), (window.innerWidth - 128)]);
Thus eliminating the dependence on const ASPECT_RATIO in favor of relative measurements based off of browser dimensions.
This can potentially (and almost certainly will) cause rendering inconsistency across multiple viewports, but can be handled accordingly by querying the viewport prior to rendering and employing cosmetic adjustments.
I want to reimplement the following Processing sketch with d3js.
Recursion Thing
This wonderful sketch recursively builds up the graph, using a complex pushMatrix, popMatrix hierarchy.
How could this be implemented in d3.js as we there always work on the DOM immediately when appending a shape or transformation. But in a logic like in this sketch the appending part seems to have to be held back for the respective popMatrix to come. It feels like I have to implement my own transform and shape stack to temporary remember the transformation and shapes to be added until the popMatrix comes but that seems so not d3.js.
Any suggestion highly appreciated
ps:
i dont want to use processing.js as i want to work with svg, not canvas.
Interesting problem! Here's my take on it: http://jsfiddle.net/Y48BL/
This is more a proof of concept; I didn't do all the different colours and such. Nevertheless, it demonstrates the general approach. The general idea is to use g elements instead of the matrices that processing uses. Both are used for local transformations of the coordinate system; in the case of the g elements by setting transform accordingly. New gs (matrices) are created inside the recursive function and then passed on to the next level of the recursion. This would correspond to pushMatrix(). Coming back up, we continue to use the original g, corresponding to popMatrix().
The translation of the drawing of the circles and lines is fairly straightforward -- I find the D3 code much easier to read.
So I came up with this helper "class" to get this done, maybe a bit of a overkill but I will have more use cases for this.
var TransformStack = (function () {
function TransformStack() {
this.stack = [];
}
TransformStack.prototype.getCurrentElement = function () {
return this.stack[this.stack.length - 1];
};
TransformStack.prototype.setCurrentElement = function (element) {
this.stack[this.stack.length - 1] = element;
};
TransformStack.prototype.push = function (transformElement) {
this.stack.push(transformElement);
};
TransformStack.prototype.pushAndTransform = function (transformAttr) {
this.push(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.transform = function (transformAttr) {
this.setCurrentElement(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.pop = function () {
return this.stack.pop();
};
return TransformStack;
})();
Basically a stack to push/pop g elements which replaces the matrices approach in processing as Lars already pointed out. With this the main routine looks something like
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + gScale + ")");
var tstack = new TransformStack();
tstack.push(svg);
doIt(nRecursions);
function doIt(n) {
// Circle
tstack.getCurrentElement()
.append("circle")
.attr("r", theSize)
.style("fill", "#fe6b0c")
.style("stroke", "0")
.style("stroke-width", "2")
.style("opacity", 0.3);
if (n != nRecursions) {
for (var i = 0; i < 4; i++) {
tstack.getCurrentElement().append("line")
.style("stroke", "red")
.style("opacity", 0.16)
.attr("x1", Math.random() * 4 - 2)
.attr("x2", Math.random() * 4 - 2)
.attr("y1", theSize / 2.0 + Math.random() * 4 - 2)
.attr("y2", distance - (theSize * theScale / 2.0) - 8.0 + Math.random() * 4 - 2);
}
}
var rot = 0;
tstack.pushAndTransform("scale(" + theScale + ")");
for (var i = 0; i < n; i++) {
if (n > 0) {
tstack.pushAndTransform("translate(0," + distance + ")");
doIt(n - 1);
tstack.pop();
rot = 360 / n;
tstack.transform('rotate(' + rot + ')');
}
}
tstack.pop();
}
}
Just wanted to share this, maybe of some use for some. The main point was given by Lars already.
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!