d3.js force directed graph sphere - javascript

I have adapted Christopher Manning's force-directed graph on a sphere. I would like to have the graph settle down and then rotate the sphere without changing the relationships among the points in the graph. Instead, when I drag, it seems to drag the graph, and not rotate the sphere. Dragging the graph activates the force start. If I turn off force.start() nothing changes.
var svg = d3.select("#cluster").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
.on("drag", function() { force.start(); var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))
From http://bl.ocks.org/mbostock/3795040, I found that I could rotate the graticule, but then all of my links and nodes disappear.
var path = d3.geo.path()
.projection(projection);
var λ = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
var φ = d3.scale.linear()
.domain([0, height])
.range([90, -90]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.on("mousemove", function() {
var p = d3.mouse(this);
projection.rotate([λ(p[0]), φ(p[1])]);
svg.selectAll("path").attr("d", path);
});
Links and nodes get added like this:
var link = svg.selectAll("path.link")
.data(graph.links)
.enter().append("path").attr("class", "link")
.attr ("stroke-width", function(d){return d.value/3});
var node = svg.selectAll("path.node")
.data(graph.nodes)
.enter()
.append("g")
.attr("class", "gnode")
.attr("text-anchor", "middle")
.append("path").attr("class", "node")
.style("fill", function(d) { return d.color; })
.style("stroke", function(d) { return d3.rgb(fill(d.group)).darker(); })
What I would like to accomplish: when the graph settles into position, I would like to use the drag gesture to rotate the sphere with the nodes and links on it.
How do I make this happen?

This could be done by adjusting the handler registered for the drag event on the svg which is defined in your first snippet. This requires two edits:
Get rid of force.start() because you don't want to restart the force on drag.
Trigger the rendering of nodes and links after the projection has been updated, which can easily be done by calling tick(). If the force had not been deactivated by step 1., this would be called repeatedly because the function is registered as the handler for the force layout's tick event. Now, that you have deactivated the force, you will have to call it explicitely.
The reformatted code will look like:
.on("drag", function() {
//force.start(); // 1. Don't restart the force.
var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]];
t0 = Date.now();
origin = r;
projection.rotate(r);
tick(); // 2. Trigger the rendering after adjusting the projection.
}))
Have a look at this working example.

This may rotate along one axis
var rotateScale = d3.scale.linear().domain([0, width]).range([-180,180]);//width of SVG
d3.select("svg").on("mousedown",startRotating).on("mouseup",stopRotating);
function startRotating() {
d3.select("svg").on("mousemove",function() {
var p = d3.mouse(this);
projection.rotate([rotateScale(p[0]),0]);
});
}
function stopRotating() {
d3.select("svg").on("mousemove",null);
}

Related

d3js v4: Scale circles with zoom

I have a world map made with d3js v4 and topojson which has Zoom / Drag / Circles. Everything seems fine except I cant scale the circles togheter with the zoom.
When I scroll into the map, my circles stay at the same size, which makes them way to big compared to the map.
How can I apply the transformation to the circles when I zoom?
var width = 660,
height = 400;
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var projection = d3.geoMercator()
.center([50, 10]) //long and lat starting position
.scale(150) //starting zoom position
.rotate([10,0]); //where world split occurs
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var path = d3.geoPath()
.projection(projection);
var g = svg.append("g");
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
d3.select(".zoom-in").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 1.2);
});
d3.select(".zoom-out").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 0.8);
});
// load and display the world and locations
d3.json("https://gist.githubusercontent.com/d3noob/5193723/raw/world-110m2.json", function(error, topology) {
var world = g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries).geometries)
.enter()
.append("path")
.attr("d", path)
;
var locations = g.selectAll("circle")
.data(devicesAll)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0];})
.attr("cy", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1];})
.attr("r", 2)
.style("fill", "black")
.style("opacity", 1)
;
var simulation = d3.forceSimulation()
.force('x', d3.forceX().x(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0]}))
.force('y', d3.forceY().y(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1]}))
.force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(2).iterations(2)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(devicesAll)
.on("tick", function(d){
locations
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
If i understood your problem correctly, you need to add it to your zoom behaviour.
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
here, you are applying your transformation to the elements, which is fine. However, you're not applying any logic to the radius.
That logic is up to you to make, and it will depend on the k property of the transform event (currentTransform.k).
I will use a some dummy logic for your radius. Your scale extent is between 1 and 10, you need a logic in which the radius decreases as the zoom increases (bigger k). It is also important that your radius doesn't go lower than 1, because the area of the circle will decrease much faster (remember the area depends on r^2, and r^2 < r for r < 1)
So my logic will be: the radius is 2.1 - (k / 10). Again, I'm oversimplifying, you can change it or tune it for your specific case.
In the end, it should look something like this:
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
g.selectAll("circle")
.attr("r", 2.1 - (currentTransform.k / 10))
}
I haven't tested the code, but tell me if this works! Maybe you can add it to a jsfiddle if needed

How to update bar graph data whenever array is updated in d3 v6.3.1?

I'm trying to update a bargraph created using d3.js to display values from a regularly updated array. Currently, I have a function d3Data that is called upon page load(using jQuery) and as a function invoked whenever buttons are clicked on the page. This d3 data updates the array and then calls another function d3New that is supposed to rerender the bar graph.
The bar graph is able to render along with the bar rectangles if hard coded data in the array is used. However, since I initialize the starting array as empty I am unable to see the rectangles as it seems my bar graph doesn't display rectangles based on updated values in this array.
Here is my logic for displaying the rectangles within the bar graph:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect")
rects.exit().remove()
rects.attr("x", function(d, i) { return (i * 2.0 + 1.3) * barWidth; })
.attr("y", function(d,i) {
return Math.min(yScale(0), yScale(d))
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("fill", function(d,i) { return color[i];})
I understand the enter() function intially joins the data to the rectangle elements and the exit function is used in order to remove any previous rectangle element values upon rectangle rerender. But, no rectangles are rendered to the screen and not sure why? Here is what it looks like:
Any help would be great
edit:
Here is some more of the two functions:
function d3Data() {
var dataArray = [];
for (var key in gradeFrequency) {
dataArray.push(gradeFrequency[key]);
}
d3New(dataArray);
}
function d3New(data) {
var height = 500;
var width = 500;
var margin = {left: 100, right: 10, top: 100, bottom: 20};
var color = ["#C6C7FF", "#8E8EFC", "#5455FF", "#8E8EFC", "#C6C7FF"];
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate("+ [margin.left + "," + margin.top] + ")");
var barWidth = 30;
var chartHeight = height-margin.top-margin.left;
var xScale= d3.scaleBand()
.domain(["A", "B", "C", "D", "F"])
.range([100, 450])
.padding([0.8])
// Draw the axis
svg.append("g")
.attr("transform", "translate(-100,300)")
.call(d3.axisBottom(xScale));
var yScale = d3.scaleLinear()
.domain([0, 1.0])
.range([chartHeight, 0]);
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
I figured out how to fix my problem. Had to add:
d3.selectAll("svg").remove();
to the start of the function in order to remove previous outdated graphs and also add the attributes for "rect" before the .exit().remove(). So instead of:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
rects.attr(..).attr(..).attr(..)
I did:
rects.enter().append("rect").merge("rect").attr(..).attr(..).attr(..) and so on.
rects.exit().remove()
Since the attributes for the rectangles need to be updated as well they had to go before the .exit() and .remove() calls

plots on multiple instances of graph

I'm using the d3 library to plot a bar graph with JSON objects recieved from the server through websockets. What is happening though is that each time the graph is plotted it draws a new instance of a graph. So I end up with multiple graphs.
But I want the JSON data to be all plotted onto the same one graph.
Here's my code:
ws = new WebSocket("ws://localhost:8888/dh");
var useData = []
//var chart;
var chart = d3.select("body")
.append("svg:svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 200);
ws.onmessage = function(evt)
{
var distances = JSON.parse(evt.data);
data = distances.miles;
console.log(data);
if(useData.length <= 10){
useData.push(data)
}
else
{
var draw = function(data){
// Set the width relative to max data value
var x = d3.scale.linear()
.domain([0, d3.max(useData)])
.range([0, 420]);
var y = d3.scale.ordinal()
.domain(useData)
.rangeBands([0, 120]);
var rect = chart.selectAll("rect")
.data(useData)
// enter rect
rect.enter().append("svg:rect")
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
// update rect
rect
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
var text = chart.selectAll("text")
.data(useData)
// enter text
text.enter().append("svg:text")
.attr("x", x)
.attr("y", function (d) { return y(d) + y.rangeBand() / 2; })
.attr("dx", -3) // padding-right
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text(String);
// update text
text
.data(useData)
.attr("x", x)
.text(String);
}
useData.length = 0;
}
}
How can I plot all points onto on graph which is being constantly updated?
It's a shame that d3 cannot handle data in real-time and update charts accordingly, or if it can that there's no clear tutorial/ explanation of how to.
Thanks
My guess is because you're creating a chart every time with:
var chart = d3.select("body")
.append("svg:svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 20 * useData.length);
Rather you need to check if chart exists, and if so, don't call that line.
// outside of .onmessage
var chart;
// inside of .onmessage
if (!chart) {
chart = d3.select("body")
.append("svg:svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 20 * useData.length);
}
Like Brian said you keep creating a new svg element at every onmessage event. However, in D3 you don't need to use an if statement to check for element existence; after you do a data join, the enter() selection will only contain the elements that did not exist yet:
// data join
var chart = d3.select("body").selectAll(".chart")
.data([useData]); // if you wanted multiple charts: .data([useData1, useDate2, useData3])
// enter
chart.enter().append("svg") // this will only execute if the .chart did not exist yet
.attr("class", "chart")
.attr("width", 420);
// update (both new and existing charts)
chart
.attr("height", function(d) { return 20 * d.length; });
The concepts of the data join, enter(), update(), and exit() selections are explained in the Thing with Joins article. See also the 3 General Update Pattern articles.
You have to use a similar approach when you update or add new rect elements in your chart. Assuming for the moment that useData contains all accumulated data (although the useData.length = 0 might mean that is not the case):
// data join
var rects = chart.selectAll("rect")
.data(function(d) { return d; }); // use the data bound to each chart
// enter
rects.enter().append("rect");
// update
rects
.attr("y", function(d) { return y(d.yValue); }) // not sure what your data structure looks like
.attr("width", function(d) { return x(d.xValue); })
.attr("height", y.rangeBand());
// exit
rects.exit().remove();
Some suggestions how to update a path with real time data are given in Path Transitions.

d3.js: limit size of brush

Is there a way to limit the size of a brush, even though the extent is larger?
I put together a brush with only an x-scale that can be moved and resized. I would like to be able to limit the extent to which it can be resized by the user (basically only up to a certain point).
In the following example, the brush function stops updating when the brush gets bigger than half the maximum extent. The brush itself, though, can still be extended. Is there a way to prevent this from happening? Or is there a better way of handling this?
Many thanks!
See this code in action here: http://bl.ocks.org/3691274 (EDIT: This demo now works)
bar = function(range) {
var x_range = d3.scale.linear()
.domain([0, range.length])
.range([0, width]);
svg.selectAll("rect.items").remove();
svg.selectAll("rect.items")
.data(range)
.enter().append("svg:rect")
.attr("class", "items")
.attr("x", function(d, i) {return x_range(i);})
.attr("y", 0)
.attr("width", width/range.length-2)
.attr("height", 100)
.attr("fill", function(d) {return d})
.attr("title", function(d) {return d});
}
var start = 21;
bar(data.slice(0, start), true);
var control_x_range = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
controlBar = svg.selectAll("rect.itemsControl")
.data(data)
.enter().append("svg:rect")
.attr("class", "itemsControl")
.attr("x", function(d, i) {return control_x_range(i);})
.attr("y", 110)
.attr("width", width/data.length-2)
.attr("height", 20)
.attr("fill", function(d) {return d});
svg.append("g")
.attr("class", "brush")
.call(d3.svg.brush().x(d3.scale.linear().range([0, width]))
.extent([0,1*start/data.length])
.on("brush", brush))
.selectAll("rect")
.attr("y", 110)
.attr("height", 20);
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
};
}
I eventually solved it by redrawing the brush to its maximum allowed size when the size is exceeded:
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
}
else {d3.event.target.extent([s[0],s[0]+0.5]); d3.event.target(d3.select(this));}
}
Demo: http://bl.ocks.org/3691274
I'm still interested in reading better solutions.
Here's another strategy using d3.v4 an ES6:
brush.on('end', () => {
if (d3.event.selection[1] - d3.event.selection[0] > maxSelectionSize) {
// selection is too large; animate back down to a more reasonable size
let brushCenter = d3.event.selection[0] + 0.5 * (d3.event.selection[1] - d3.event.selection[0]);
brushSel.transition()
.duration(400)
.call(brush.move, [
brushCenter - 0.49 * maxSelectionSize,
brushCenter + 0.49 * maxSelectionSize
]);
} else {
// valid selection, do stuff
}
});
If the brush selection size is too large when the user lets go of it, it animates back down to the specified maximum size (maxSelectionSize). At that point, the 'end' event will fire again, with an acceptable selection size.
Note the 0.49 scalar: this is required to prevent floating point / rounding errors that could cause an infinite loop if the brush is move()d to a size that is still too large.
Here is an example of limiting the brush's minimum width as 100px and maximum width as 200ps. I added a few lines into d3.v4.js, for setting the limitation of the brush width.
Added brush.limit([min, max]) for set the limitation:
var _limit = null;
brush.limit = function (l) {
_limit = l;
}
Break mouse move event in move() function:
if (_limit && e1 - w1 < _limit[0]) {
return;
}
(Demo) (Source Code)
Nice piece of code, but I found a little bug.
It does lock your brush whenever s[1]-s[0] < 0.5, but if you keep pressed the resize and bring all your brush to the oposite direction, it starts "moving" the brush without any action (i.e. it doesn't behave as it should).
I am pretty sure I can come up with a reasonable solution. If so, I'll re-post it here.
(sorry to post it here as an answer, but as you've already answered it once, I cannot post as a comment, I guess).

Basics of D3's Force-directed Layout

I'm plowing into the exciting world of force-directed layouts with d3.js. I've got a grasp of the fundamentals of d3, but I can't figure out the basic system for setting up a force-directed layout.
Right now, I'm trying to create a simple layout with a few unconnected bubbles that float to the center. Pretty simple right!? The circles with the correct are created, but nothing happens.
Edit: the problem seems to be that the force.nodes() returns the initial data array. On working scripts, force.nodes() returns an array of objects.
Here's my code:
<script>
$(function(){
var width = 600,
height = 400;
var data = [2,5,7,3,4,6,3,6];
//create chart
var chart = d3.select('body').append('svg')
.attr('class','chart')
.attr('width', width)
.attr('height', height);
//create force layout
var force = d3.layout.force()
.gravity(30)
.alpha(.2)
.size([width, height])
.nodes(data)
.links([])
.charge(30)
.start();
//create visuals
var circles = chart.selectAll('.circle')
.data(force.nodes()) //what should I put here???
.enter()
.append('circle')
.attr('class','circles')
.attr('r', function(d) { return d; });
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
Here was the problem. The array you feed into force.nodes(array) needs to be an array of objects.
So:
var data = [{number:1, value:20}, {number:2, value:30}...];
works. Or even just
var data=[{value:1},{value:2}];
makes the script work fine.
You need to update cx and cy in force.on handler.
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x - deltaX(d, e) ; })
.attr("cy", function(d) { return d.y - deltaY(d, e); });
});
And functions deltaX and deltaY depends on your force model.

Categories