Using d3's pack layout, I made some bubbles associated with states. Current test script: https://jsfiddle.net/80wjyxp4/4/. They're colored according to region. You'll note Texas is in the "NW", California in the "SE", etc.
Question:
How would you geographically sort the circles in pack-layout?
One hack way might use the default d3.layout.pack.sort(null). This sort starts with the first data point (in this case, AK) and then adds bubbles in a counterclockwise direction. I could hand-sort data to be input in an order that approximates state positions, and add in blank circles to move edge circles around.
I'm interested about better ideas. Would it be better to use a modified force layout, like http://bl.ocks.org/mbostock/1073373? The d3.geom.voronoi() seems useful.
On this http://bl.ocks.org/mbostock/1073373 look at these lines:
states.features.forEach(function(d, i) {
if (d.id === 2 || d.id === 15 || d.id === 72) return; // lower 48
var centroid = path.centroid(d); // <===== polygon center
if (centroid.some(isNaN)) return;
centroid.x = centroid[0]; <==== polygon lat
centroid.y = centroid[1]; <==== polygon lng
centroid.feature = d;
nodes.push(centroid); <== made node array of centroids
});
----------------
force
.gravity(0)
.nodes(nodes) <==== asign array nodes to nodes
.links(links)
.linkDistance(function(d) { return d.distance; })
.start();
Each state acts like a multi-foci layout. Like this one: http://bl.ocks.org/mbostock/1021841
Started with pack layout, to get circle radii
used state centroids from this modified force layout http://bl.ocks.org/mbostock/1073373
Contrived to avoid overlaps using collisions https://bl.ocks.org/mbostock/7881887
The code is here: https://jsfiddle.net/xyn85de1/. It does NOT run because I can't get data for the topojson file, at http://bl.ocks.org/mbostock/raw/4090846/us.json, from the link, and the file is too large to copy-paste. Download to your own server then run.
It has a hover-title text and transitions in, but ends up looking like this:
Spacing and stuff is modifiable with different parameters. The circles are approximately sorted geographically. MO is that big gold one in the middle; AK and HI are dark blue to the left; CA is lower left in pink; TX is at the bottom in light blue. And so on.
Code:
After collecting all data (location data for init position of circles), along with the state name, value, area (for color coding) into the nodes variable:
// circles
var circles = svg.selectAll("g") //g
.data(nodes)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + -d.x + "," + -d.y + ")";
})
.append("circle")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("r", function(d) {return d.r;})
.attr("fill", function(d) { return color(d.area); })
.call(force.drag); // lets you change the orientation
// title text
circles.append("title")
.text(function(d) { return d.state + ": " + format(d.value) + " GWh"; });
// // text // doesn't work :/ porque?
// circles.append("text")
// .attr("dy", ".3em")
// //.style("text-anchor", "middle")
// .text(function(d) { return d.state.substring(0, d.r / 3); });
// circle tick function
function tick(e) {
circles
.each(collide(collision_alpha))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// force
force.nodes(nodes)
.on("tick", tick)
.start();
Related
I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)
Is it possible to add a piece of text in the centre of multiple rectangles in a D3 treemap chart? So what i'm after is something like this:
As you can see by the picture, I want to have text appear once in the middle of each “section” (different colours) on the chart. Is it possible to do this with D3? If so how would I achieve it.
Currently I've managed to have it appear on each individual rectangle, like so:
cell.append("svg:text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.text("test")
.style("opacity", function(d) { console.log(this.getComputedTextLength());d.w = this.getComputedTextLength(); return d.dx > d.w ? 1 : 0; });
Full code is here: http://jsfiddle.net/noobiecode/9ev9qjt3/74/
Any help will be appreciated.
For appending these texts, we first need to find the corresponding data. Right now, nodes have data regarding all the different depths (parents and children), but for these texts we only need the data for the second depth (depth == 2).
So, we first get the data:
var parents = treemap.nodes(root)
.filter(function(d) { return d.depth == 2 });
It gives us an array with three objects, so we are in the right way. This array (parents) have all we need to position the texts (x, y, dx, dy, name).
Then, we bind this data to the texts:
var parentsText = svg.selectAll(".parentsText")
.data(parents)
.enter()
.append("text");
After that, we append the texts:
parentsText.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("x", function(d){ return d.x + d.dx/2})
.attr("y", function(d){ return d.y + d.dy/2})
.text(function(d){ return d.name})
.attr("class", "parentText");
And this is the resulting fiddle: http://jsfiddle.net/gerardofurtado/9ev9qjt3/94/
You can also have the texts for each individual rectangle and for the group (but, in that case, you may want to move away some overlapping texts): http://jsfiddle.net/gerardofurtado/9ev9qjt3/95/
I am trying to use the bubble chart example as a template to build a visualisation. I have my JSON as a flat-hierarchy, such that there is one element called children and that holds an array of objects that I want to visualise.
The JSON looks like this:
{
"children":[
{
"acc":"Q15019",
"uid":"SEPT2_HUMAN",
"sym":"SEPT2",
"name":"Septin-2",
"alt_ids":"",
"ratio":0.5494271087884398,
"pval":0.990804718
},
...,
{
"acc":"Q16181",
"uid":"SEPT7_HUMAN",
"sym":"SEPT7",
"name":"Septin-7",
"alt_ids":"",
"ratio":1.1949912048567823,
"pval":0.511011887
}
]
}
I have modified the example code as follows:
var diameter = 960,
format = d3.format(",d"),
color = d3.scale.quantile().range(colorbrewer.RdBu[9]);
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("datagraph.json", function(datagraph) {
var node = svg.selectAll(".node")
.data(bubble.nodes(datagraph))
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.acc; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
//node.append("title").text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return 30; })
.style("fill", function(d) {
if(d.ratio == null)
return "#ffffff";
else
return color(d.ratio);
});
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.acc; });
});
The resultant HTML has a ton of <g> tags responding to each element except they are never translated to the right position, but instead sort of sit on top of each other on the top left corner. By investigating in Firebug, I figured this happens presumably because the pack() algorithm does not get the objects one at a time, but the whole array as a single element, thus individual elements don't get .x and .y values.
If I change the .nodes() argument to datagraph.children I get the elements one at a time in nodes() iteration, but oddly enough I get a single <g> object. Since I don't need to flatten a hierarchy I skipped the classes(root) function, in the example. What I am wondering is whether or not the packageName attribute plays any role in the nodes()?
How can I resolve this issue?
You haven't specified a value accessor:
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5)
.value(function(d) { return d.pval; }) //<- must return a number
Example here.
I am having trouble with dynamically adding nodes on to a d3.js force directed graph and I was wondering if anyone here could shed some light on the subject.
The problem I am having is that I want the tick function to transform all nodes on the graph and not just the newly added ones.
Below are the functions I use for adding nodes and handling the transformation:
// Function to handle tick event
function tick() {
tick_path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "L" +
d.target.x + "," +
d.target.y;
});
tick_node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
/**
* Append nodes to graph.
*/
function addNodes2Graph(){
// define the nodes
// selection will return none in the first insert
// enter() removes duplicates (assigning nodes = nodes.enter() returns only the non-duplicates)
var nodes = viz.selectAll(".node")
.data(g_nodes)
.enter().append("g")
.on("click", nodeClick)
.call(force.drag);
// From here on, only non-duplicates are left
nodes
.append("circle")
.attr("r", 12)
.attr("class", "node");
nodes.append("svg:image")
.attr("class", "circle")
.attr("xlink:href", "img/computer.svg")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
// add the text
nodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.ip; });
return nodes;
}
// Execution (snippet)
// Add new nodes
tick_node = addNodes2Graph();
// Add new paths
tick_path = addPaths2Graph();
// Restart graph
force
.nodes(g_nodes) // g_nodes is an array which stores unique nodes
.links(g_edges) // g_edges stores unique edges
.size([width, height])
.gravity(0.05)
.charge(-700)
.friction(0.3)
.linkDistance(150)
.on("tick", tick)
.start();
I think the problem is that I only get non-duplicated results returned from addNodes2Graph which I then use in the tick function but I'm not sure how I could achieve this without adding duplicated nodes on to the graph.
At the moment, it's either adding duplicated elements on to the graph or transform only the new nodes on tick.
Thank you very much for your help in advance.
It looks like you're adding nodes only to the DOM, not to the force layout. To recap, here's what you need to do to add nodes to the force layout.
Add elements to the array that the force layout uses for its nodes. This needs to be the same array that you passed in initially, i.e. you can't create a new array and pass that it if you want smooth behaviour. Modifying force.nodes() should work fine.
Do the same for the links.
Add the new DOM elements using .data().enter() with the new data.
No change to the tick function should be required as adding the nodes and DOM elements is done elsewhere.
After adding the new nodes/links, you need to call force.start() again to have it take them into account.
Hello I am working with d3 diagonal diagram and would like to add a gradient to path which links my circles...
I am generating my tree with:
var width = 800,
height = 700;
element.html('');
var color = d3.interpolateLab("#008000", "#c83a22");
var scale = d3.scale.linear().domain([0, 100]).range(["red", "green"]);
var cluster = d3.layout.cluster()
.size([height, width - 160]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select('#tab-manageAccess').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(40,0)');
/*svg.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", y(0))
.attr("x2", 0).attr("y2", y(1000))
.selectAll("stop")
.data([
{offset: "0%", color: "red"},
{offset: "40%", color: "red"},
{offset: "40%", color: "black"},
{offset: "62%", color: "black"},
{offset: "62%", color: "lawngreen"},
{offset: "100%", color: "lawngreen"}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; });*/
var nodes = cluster.nodes(scope.accessTree),
links = cluster.links(nodes);
var link = svg.selectAll('.link')
.data(links)
.enter().append('path')
.attr('class', 'link')
.attr('d', diagonal);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; });
node.append('circle')
.attr('r', 4.5);
node.append('text')
.attr('dx', function(d) { return d.children ? -8 : 8; })
.attr('dy', 3)
.style('text-anchor', function(d) { return d.children ? 'end' : 'start'; })
.style('font-weight', 'bold')
.attr('fill', function (d) {
var color = '#4D7B88';
if (d.depth === 0) {
color = '#7F3762';
} else if(d.depth === 1) {
color = '#83913D';
}
return color;
})
.text(function(d) { return d.name; });
d3.select(self.frameElement).style('height', height + 'px');
I found this example: https://gist.github.com/mbostock/4163057 co I created variable color with d3.interpolateLab("#008000", "#c83a22"); and then added .style("fill", function(d) { return color(d.t); })
.style("stroke", function(d) { return color(d.t); }) to path element but it doesn't work :( can anyone help me?
The aspect of Mike Bostock's code that you're missing is where he divides the path up into hundreds of different sub-paths and sets the color on each one separately. Go to the live version at http://bl.ocks.org/mbostock/4163057 and check the DOM to see what's really going on.
Why does he do that? Because, while you can set the stroke of an SVG line or path to a gradient, you can't tell it to make the gradient follow the slope or curve of that line. The angle of the gradient is defined when the gradient is created, based on either:
the rectangular bounding box for the element that uses it
(if gradientUnits is set to ObjectBoundingBox), or
the user coordinate system where the object is drawn
(if gradientUnits is set to userSpaceOnUse).
The way you have it set up (in your commented out code) basically creates a hidden gradient background over the entire image, and then lets it show through wherever you draw your lines. Clearly not what you wanted.
Hence, Mike's complex function and the hundreds of sub-paths it creates. Probably not what you want, either, especially if you want the graph to be interactive.
For simple lines, there is another way to get gradients to line up correctly from start to finish of your line.
I've got a very simple example with plain SVG (no D3) up here: http://codepen.io/AmeliaBR/pen/rFtGs
In short, you have to define your line to go in the direction that matches up with the gradient, and then use transforms (scale/rotate/translate) to actually position the line where you want it.
How tricky that would be to implement in D3 depends on how complex your layout is. If you were just using simple lines, I think this would work:
calculate the length of the line and its slope using simple geometry from the (x1,y1) and (x2,y2) values,
draw the line from (0,0) to (0,length) (assuming a vertical gradient),
add a transform attribute of translate(x1,y1) rotate(slope)
With paths, you'd need to know what type of path you're dealing with and use regular expressions to parse and edit the path's d attribute. Very messy.
Maybe just try line markers for start and end?