Hi I am trying to spawn new circles in a set time interval (e.g., double the amount of existing circles every ten seconds) with D3.forceSimulation. I am using forceSimulation to make sure the circles do not overlap. My goal is to also have the new circles spawn in a position near the existing circles.
My initial approach is to append new {} elements into the nodes array using a setInterval function. Basically check the length of the nodes array every ten seconds and append new {} elements so that the new length of the array is double the previous length.
However, I don't think I am understanding/using nodes and d3.forceSimulation correctly. In the code below I see five circles appearing and moving away from each other. But I didn't pass any x or y positions to the circle elements that are joined to the nodes data? Are default/random positions being assigned to the circles?
I know if I add .force('center', d3.forceCenter(width / 2, height / 2)) the five circles will appear near the center of the screen before moving away. But I'm not sure how d3.forceSimulation is setting the initial positions of the five circles when they spawn in.
var nodes = [{}, {}, {}, {}, {}]
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('circle')
.data(nodes)
u.enter()
.append('circle')
.attr('r', 5)
.merge(u)
.attr('cx', function(d) {
return d.x
})
.attr('cy', function(d) {
return d.y
})
u.exit().remove()
}
Yes, there are default position which are assigned to the circles.
Here is a quote from d3js docs:
The position ⟨x,y⟩ and velocity ⟨vx,vy⟩ may be subsequently modified
by forces and by the simulation. If either vx or vy is NaN, the
velocity is initialized to ⟨0,0⟩. If either x or y is NaN, the
position is initialized in a phyllotaxis arrangement, so chosen to
ensure a deterministic, uniform distribution around the origin.
Related
I can successfully get a map of the US to render however, my data points do not. (I understand that d3.js made some significant changes with v5 so please note that similar questions previously asked do not apply)
$(document).ready(function () {
var us = d3.json('https://unpkg.com/us-atlas#1/us/10m.json');
var meteoriteData = d3.json('https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/meteorite-strike-data.json');
var svg = d3.select("svg")
.style("width", "1110px")
.style("height", "714px");
var path = d3.geoPath();
Promise.all([us, meteoriteData]).then(function (values) {
var map = values[0];
console.log("map", map);
var meteoriteData = values[1];
console.log("meteoriteData", meteoriteData);
svg.append("g")
.attr("fill", "#ccc")
.selectAll("path")
.data(topojson.feature(map, map.objects.states).features)
.enter().append("path")
.attr("d", path),
svg.append("path")
.datum(topojson.mesh(map, map.objects.states, (a, b) => a !== b))
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("pointer-events", "none")
.attr("d", path),
svg.selectAll("circle")
.data(meteoriteData)
.enter()
.append("circle")
.attr("class", "circles")
.attr("cx", function (d) { return ([d.geometry.coordinates[0], d.geometry.coordinates[1]])[1]; })
.attr("cy", function (d) { return ([d.geometry.coordinates[0], d.geometry.coordinates[1]])[0]; })
.attr("r", "1px");
});
});
And a working copy can be found here.. https://codepen.io/lady-ace/pen/PooORoy
There's a number of issues here:
Passing .data an array
First, when using
svg.selectAll("circle")
.data(meteoriteData)
selectAll().data() requries you to pass a function or an array. In your case you need to pass it the data array - however, meteoriteData is an object. It is a geojson feature collection with the following structure:
{
"type": "FeatureCollection",
"features": [
/* features */
]
}
All the individual geojson features are in an array inside that object. To get the array of features, in this case features representing meteors, we need to use:
svg.selectAll("circle")
.data(meteoriteData.features)
Now we can create one circle for every feature in the feature collection. If you do this, you can find the circles when inspecting the SVG element, but they won't placed correctly.
Positioning Points
If you make the above change, you won't see circles in the right places. You are not positioning the circles correctly here:
.attr("cx", function (d) { return ([d.geometry.coordinates[0], d.geometry.coordinates[1]])[1]; })
This is the same as:
.attr("cx", function(d) { return d.geometry.coordinates[1]; })
Two issues here: One, geojson is [longitude,latitude], or [x,y] (you are getting the y coordinate here, but setting the x value).
But, the bigger concern is you are not projecting your data. This is a raw coordinate from your geojson:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-113,
54.21667
]
...
You are taking the longitude and directly turning it into a pixel value. But your geojson uses a 3D coordinate space (unprojected points on a 3D globe) with units measures in degrees. If we simply convert this to pixels, cx = -113, your circle will appear off screen to the left of your SVG.
Using a Projection
You need to project your data, to do so we would define a projection function and use something like:
.attr("cx", function(d) { return projection(d.geometry.coordinates)[0] })
This gets both longitude and latitude and passes them to a projection function and then grabs the returned x value and sets it as the value for cx.
A projection takes an unprojected coordinate in 3 dimensional space (points on a globe or long/lat pairs) with units in degrees, and returns a point in 2 dimensional space with units in pixels.
But, this now brings us to the most difficult part:
What projection should you use?
We need to align our points with the US features that have already been drawn, but you don't define a projection for the US states - if you do not supply a projection to d3.geoPath, it uses a null projection, it takes supplied coordinates and plots them on the SVG as though they are pixel coordinates. No transform takes place. I know that your US features are projected though, because Alaska isn't where it is supposed to be, the coordinate values in the topojson exceed +/- 180 degrees in longitude, +/- 90 in latitude, and the map looks like it is projected with an Albers projection.
If the geoPath is not projecting the data with a d3 projection but the data is drawn as though projected, then the data is pre-projected - this topojson stores already projected points.
We have projected data (the US states) and unprojected data (meteor strikes), mixing them is always a challenge.
The challenge here is creating a projection that replicates the projection function used to create the US states dataset. We can replicate that projection, as it is a problematic file that leads to many questions. How to do so is explained here. But this is more complicated than is should be: mixing projected and unprojected data is burdensome, inflexible, and more complicated than need be.
I would suggest you use unprojected data for both the US and the meteors:
var projection = d3.geoAlbersUsa(); // create an Albers USA projection
var path = d3.geoPath().projection(projection); // set the projection for the path
We can draw the paths the same way, provided we find an unprojected topojson/geojson of the US, and we can place points with:
.attr("cx", function(d) { return projection(d.geometry.coordinates)[0]; })
.attr("cy", function(d) { return projection(d.geometry.coordinates)[1]; })
As for finding an unprojected US topojson, here's one.
And here's a working version using the above approach and projecting all data (I also filter the features to get rid of ones without coordinates, those that the USA Albers might break on, as it is a composite projection).
I'm building my first line graph in d3:
http://jsfiddle.net/j94RZ/
I want to know how to utilize either the scale or axis allow me to draw a grid (of, presumably rectangles) where I can set a different background colour for each of the section of the grid...so I can alternate colours for each cell of the grid. I want the grid to be drawn and be constrained by the axes of my graph and then also adapt if the spacing of the axes ticks change (i.e. the axes changes like this: http://bl.ocks.org/mbostock/1667367). So if my graph has an x axis with 4 ticks and a y axis of 7 ticks then my graph will have a background grid that's 7 blocks high and 4 blocks wide.
I've been playing with the idea of using a range which starts at zero and ends at the full width of the graph but I don't know what value I can use for the step. Is there any way to sort of query the axis and return how many ticks there are?
var gridRange = d3.range(0, width, step?);
A better approach than your current solution would be to use scale.ticks() explicitly to get the tick values. The advantage of that is that it will still work if you change the number of ticks for some reason.
To get an alternating grid pattern instead of a single fill, you can use something like this code.
.attr("fill", function(d, i) {
return (i % 2) == 1 ? "green" : "blue";
})
Finally, to get the full grid pattern, you can either use an explicit loop as you've suggested, or nested selections. The idea here is to first pass in the y ticks, create a g element for each and then pass the x ticks to each one of these groups. In code, this looks something like this.
svg.selectAll("g.grid")
.data(y.ticks()).enter().append("g").attr("class", "grid")
.selectAll("rect")
.data(x.ticks()).enter().append("rect");
To set the position, you can access the indices within the top and bottom level data arrays like this.
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d, i, j) {
return yScale(j);
})
To set the x position, you need the index of the inner array (passed to the set of g elements), which can be accessed through the second argument of your callback. For the outer array, simply add another argument (j here).
And that's really all there is to it. Complete jsfiddle here. To update this grid dynamically, you would simply pass in the new tick values (gotten from scale.ticks()), match with the existing data, and handle the enter/update/exit selections in the usual manner.
If you want to do without the auxiliary scales (i.e. without .rangeBand()), you can calculate the width/height of the rectangles by taking the extent of the range of a scale and dividing it by the number of ticks minus 1. Altogether, this makes the code a bit uglier (mostly because you need one fewer rectangle than ticks and therefore need to subtract/remove), but a bit more general. A jsfiddle that takes this approach is here.
So after a few helpful comments above I've got close to a solution. Using Ordinal rangebands get me close to where I want to go.
I've created the range bands by using the number of ticks on my axis as a basis for the range of the input domain:
var xScale = d3.scale.ordinal()
.domain(d3.range(10))
.rangeRoundBands([0, width],0);
var yScale = d3.scale.ordinal()
.domain(d3.range(4))
.rangeRoundBands([0, height],0);
I've then tried drawing the rectangles out like so:
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
0
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
This gets me the desired effect but for only one row deep:
http://jsfiddle.net/Ny2FJ/2/
I want,somehow to draw the green blocks for the whole table (and also without having to hard code the amount of ticks in the ordinal scales domain). I tried to then apply the range bands to the y axis like so (knowing that this wouldn't really work though) http://jsfiddle.net/Ny2FJ/3/
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
return yScale(i);
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
The only way I can think to do this is to introduce a for loop to run the block of code in this fiddle http://jsfiddle.net/Ny2FJ/2/ for each tick of the y axis.
I am drawing circles by setting a fixed x position but a changing y position. The problem is the circles are overlapping since the radius of each circle is different.
Ideally in theory to solve that I would probably want to get the y position of the previous circle and add the radius of the current circle to it to get the y position of the current circle. Correct me if I am thinking it wrong.
Right now I am doing something like this now
var k = 10;
var circleAttributes = circles.attr("cx", '150')
.attr("cy", function (d) {
return (k++) * 10; //this is a very gray area
})
And I am getting an overlap. Ideally I would like to space the circles form each other. Even if the outer edges touch each other I could live with that. How should I approach it?
I am writing a range which i am using to get the radius
var rScale = d3.scale.linear()
.domain([min, max])
.range([10, 150]);
and simply passing that as the radius like this
.attr("r", function(d) { return rScale(d.consumption_gj_);})
This is my fiddle
http://jsfiddle.net/sghoush1/Vn7mf/27/
Did a solution here: http://tributary.io/inlet/6283630
The key was to keep track of the sum of the radius of all previous circles. I did that in a forEach loop:
data.forEach(function(d,i){
d.radius = rScale(d.consumption_gj_);
if (i !== 0){
d.ypos = d.radius*2 + data[i-1].ypos;
}
else {
d.ypos = d.radius*2;
}
})
then, when setting the attributes of the circles you can use your new d.radius and d.ypos
var circleAttributes = circles.attr("cx", '150')
.attr("cy", function (d,i) {
return d.ypos + 5*i;
})
.attr("r", function(d) { return d.radius;})
The Charge Property
The charge in a force layout refers to how nodes in the environment push away from one another or attract one another. Kind of like magnets, nodes have a charge that can be positive (attraction force) or negative (repelling force).
From the Documentation:
If charge is specified, sets the charge strength to the specified value. If charge is not specified, returns the current charge strength, which defaults to -30. If charge is a constant, then all nodes have the same charge. Otherwise, if charge is a function, then the function is evaluated for each node (in order), being passed the node and its index, with the this context as the force layout; the function's return value is then used to set each node's charge. The function is evaluated whenever the layout starts.
A negative value results in node repulsion, while a positive value results in node attraction. For graph layout, negative values should be used; for n-body simulation, positive values can be used. All nodes are assumed to be infinitesimal points with equal charge and mass. Charge forces are implemented efficiently via the Barnes–Hut algorithm, computing a quadtree for each tick. Setting the charge force to zero disables computation of the quadtree, which can noticeably improve performance if you do not need n-body forces.
A good tutorial that will help you see this in action:
http://vallandingham.me/bubble_charts_in_d3.html
I am attempting to create a vertical timeline using d3.js that is linked to a map so that any item(s) contained in the brush will also be displayed in the map. Kind of like http://code.google.com/p/timemap/ but with d3 instead of SIMILE and a vertical timeline rather than horizontal.
I can successfully create an svg with vertical bars representing time ranges, legend, ticks, and a brush. The function handling brush events is getting called and I can obtain the extent which contains the y-axis start and stop of the brush. So far so good...
How does one obtain the datums covered by the brush? I could iterate over my initial data set looking for items within the extent range but that feels hacky. Is there a d3 specific way of getting the datums highlighted by a brush?
var data = [
{
start: 1375840800,
stop: 1375844400,
lat: 0.0,
lon: 0.0
}
];
var min = 1375833600; //Aug 7th 00:00:00
var max = 1375919999; //Aug 7th 23:59:59
var yScale = d3.time.scale.utc().domain([min, max]).range([0, height])
var brush = d3.svg.brush().y(yScale).on("brush", brushmove);
var timeline = d3.select("#myDivId").append("svg").attr("width", width).attr("height", height);
timeline.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(datum, index) {return index * barSize})
.attr("y", function(datum, index) {return yScale(datum.start)})
.attr("height", function(datum, index) {return yScale(datum.end) - yScale(datum.start)})
.attr("width", function() {return barSize})
timeline.append("g")
.attr("class", "brush")
.call(brush)
.selectAll("rect")
.attr("width", width);
function brushmove() {
var extent = brush.extent();
//How do I get the datums contained inside the extent????
}
You'll need to do some kind of iteration to figure out what points live inside the brush extent. D3 doesn't automatically do this for you, probably because it can't know what shapes you're using to represent your data points. How detailed you get about what is considered "selected" and what isn't is quite application specific.
There are a few ways you can go about this:
As you suggest, you can iterate your data. The downside to this is that you would need to derive the shape information from the data again the same way you did when you created the <rect> elements.
Do a timeline.selectAll("rect") to grab all elements you potentially care about and use selection.filter to pare it down based on the x, y, height and width attributes.
If performance is a concern because you have an very large number of nodes, you can use the Quadtree helper to partition the surface and reduce the number of points that need to be looked at to find the selected ones.
Or try Crossfilter, there you pass the extent from the brush to a dimension filter and then you fetch filtered and sorted data by dimension.top(Infinity).
(A bit late answer, buy maybe useful for others, too.)
I need to create a d3 bar chart that can have negative values. Ideally the axis zero position should be calculated based on the extent of the data, but I'd settle for a solution that assumes symmetric positive and negative extent, i.e. that it would be always in the middle of the chart.
Here's an example of what I'd like to achieve.
OK, let's say you have an array of numbers as your dataset, and this includes some positive and negative values:
var data = [-15, -20, -22, -18, 2, 6, -26, -18];
You'll want two scales to construct a bar chart. You need one quantitative scale (typically a linear scale) to compute the bar positions along the x-axis, and a second ordinal scale to compute the bar positions along the y-axis.
For the quantitative scale, you typically need to compute the domain of your data, which is based on the minimum and maximum value. An easy way to do that is via d3.extent:
var x = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width]);
You might also want to nice the scale to round the extent slightly. As another example, sometimes you want the zero-value to be centered in the middle of the canvas, in which case you'll want to take the greater of the minimum and maximum value:
var x0 = Math.max(-d3.min(data), d3.max(data));
var x = d3.scale.linear()
.domain([-x0, x0])
.range([0, width])
.nice();
Alternatively, you can hard-code whatever domain you want.
var x = d3.scale.linear()
.domain([-30, 30])
.range([0, width]);
For the y-axis, you'll want to use rangeRoundBands to divide the vertical space into bands for each bar. This also lets you specify the amount of padding between bars. Often an ordinal scale is used with some identifying data—such as a name or a unique id. However, you can also use ordinal scales in conjunction with the data's index:
var y = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, height], .2);
Now that you've got your two scales, you can create the rect elements to display the bars. The one tricky part is that in SVG, rects are positioned (the x and y attributes) based on their top-left corner. So we need to use the x- and y-scales to compute the position of the top-left corner, and that depends on whether the associated value is positive or negative: if the value is positive, then the data value determines the right edge of the bar, while if it's negative, it determines the left edge of the bar. Hence the conditionals here:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return x(Math.min(0, d)); })
.attr("y", function(d, i) { return y(i); })
.attr("width", function(d, i) { return Math.abs(x(d) - x(0)); })
.attr("height", y.rangeBand());
Lastly, you can add an axis to display tick marks on top. You might also compute a fill style (or even a gradient) to alter the differentiate the appearance of positive and negative values. Putting it all together:
Bar Chart with Negative Values