How do you get selected datums in brush "brush" event handler? - d3.js

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.)

Related

d3.js v5 : Unable to get data points to show on map of US

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).

d3js stacked bar chart with groups

I'm new to learning d3 and so any advice on improving any inefficiencies in my charts would be greatly appreciated. I am using d3js to produce a horizontal stacked bar chart with two color scales.
In this example, there are two teams and a total of 8 players. We are charting the players first based on total points, then points per game. Then, I am also using two different color scales to show which team they belong to.
I did this using this stack bar example as reference: http://bl.ocks.org/mbostock/3886208
... but in order to color each player (the y axis) using the color scale associated with the team, I had to first remove the fill that was being applied to each of the wrapping groups made for each game. Then I had to add the key for each stack() series to each data array in that series.
When drawing the individual s I had to do an if and hard code the if condition that tells it which color scale to use in which case.
Here is my block: https://bl.ocks.org/Ognami/2128772d2d7c1d2708d973b9401e8e1f
My question, most importantly, is there an easier way to pass this key along to all of the data arrays in each series? And is there a way around the conditionals for choosing which scale?
Here's some small optimizations. Store the color scales in a object:
var z = {
'a': d3.scaleOrdinal()
.range(["#8cb6d9", "#4787ba", "#216297"]),
'b': d3.scaleOrdinal()
.range(["#ff5555", "#ff1122", "#990022"])
};
Assign there domains as:
for (key in z){
z[key].domain(key);
}
Just pass the data down to subselection and get the "key" from the parent:
g.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.selectAll("rect")
.data(d => d)
.enter().append("rect")
.attr("fill", function(d, i) {
var key = d3.select(this.parentNode).datum().key;
return z[d.data.team](key);
});
Now as the number of teams increases or decreases, you only need to edit the z to include the the colors for that team.
Full code here.

How to correctly transition stacked bars in d3.js

I'm trying to get a stacked bar chart to animate correctly as bars come and go. There's probably a good example of this somewhere (maybe I'll ask as a separate question), but the examples I'm finding don't show transitions with individual stack elements exiting and entering I want to make sure that as bars are exiting, they drag down the bars above them, and as they're entering, they push up the bars above them. And I don't want any gaps or overlaps midway through the transition.
Can anyone point me to an example that does this?
Correcting my wrong-headed question:
Ashitaka answered the question with a helpful jsfiddle. His answer prompted me to look at the d3 stack layout more closely, where I read:
In the simplest case, layers is a two-dimensional array of values. All of the 2nd-dimensional arrays must be the same length.
So, I concluded I was going about this all wrong. I shouldn't have been trying to remove stack bars at all. If bars in my data were going to disappear, I should leave them in the data and change their height to zero. That way the transitions work great. I haven't yet had to deal with new bars appearing.
One confusing aspect of transitioning stacked charts (and working with SVG in general) is that the coordinate system origin is at the top-left corner, which means that y increases downwards.
First, our data should have 2 y related attributes:
y, the height of the bar
And y0, the baseline or the y position of the bar when it's on top of other bars. This should be calculated by d3.layout.stack().
Then, we should create 2 scales:
One for height, which works exactly as expected:
var heightScale = d3.scale.linear()
.domain([0, maxStackY])
.range([0, height]);
And one for the y position, which works in the reverse way:
var yScale = d3.scale.linear()
.domain([0, maxStackY])
.range([height, 0]);
With these two scales, we can create some functions to calculate the appropriate y positions and heights of our bars:
var barBaseY = function (d) { return yScale(d.y0); };
var barTopY = function (d) { return yScale(d.y0 + d.y); };
var barHeight = function (d) { return heightScale(d.y); };
Next, it's critical that we create a key function so that elements are bound to the correct data:
var joinKey = function (d) { return d.name; };
Without this function D3 would join the data using its index, which would break everything.
Now, to remove or add a set of bars from the stack, we take these steps:
Recalculate the stack:
var newStack = stack(enabledSeries());
Join the new stack with the current selection of layers with the data function:
layers = layers.data(newStack, joinKey);
With our key function, D3 determines the bars that are to be added, removed or updated.
Access the appropriate bars:
layers.enter() contains the "enter selection", that is, the new set of bars to be added.
layers.exit() contains the "exit selection", that is, the set of bars to be removed.
And simply layers contains the "update selection", that is, the bars that are to be updated. However, after enter.append the "update selection" is modified to contain both entering and updating elements. This has changed in D3 v4 though.
Animate the bars:
For added bars, we create them with height 0 and y position barBaseY.
Then we animate all the bars' height and y attributes.
For removed bars, we animate them to height 0 and y position barBaseY, the exact opposite of adding bars. Then we animate all the remaining bars' height and y attributes. D3 is smart enough to render all these animations at the same time.
Here's a pared down version of the stacked chart I linked to in my first comment.
And here's a visual explanation of why you have to animate both y and height attributes to simulate a bar diminishing in size "going down".

draw a grid or rectangles using a scale

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.

How to add a rectangle to specified axis in D3

I have a zoomable area plot done in D3, which works well. Now I am trying to add a rectangle to the specified location along x-axis in the middle of the plot. However, I can't seem to figure out how to do that. "rect" element is specified using absolute (x,y) of the plot and so when using zooms it stays in the same position.
So I was wondering if there is a way to tie "rect" to the axis when plotting, so that it benefits from all the zoom and translate behaviour or do I need to manually edit the x,y,width and length of the rectangle according to translation as well as figuring out where the corresponding x and y coordinates are on the graph? I am trying to use "rect" because it seems the most flexible element to use.
Thanks
Alex
I'm not sure how you are doing the zooming, but I am guessing you are changing the parameters of the scales you use with your axis? You should be able to use the same scales to place your rectangle.
If you are starting with plot coordinates then maybe using the invert function on the scale will help (available at least for quantitive scales), e.g. https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_invert
You should be able to take initial plot coordinates and invert them to determine data coordinates that can then move with changes in the scale.
If the scale is linear you can probably invert the length and width too, but you will have to compute offsets if your domain does not include 0. Easiest is to compute the rectangle's end points, something like:
var dataX0 = xScale.invert(rect.x);
var dataX1 = xScale.invert(rect.x + rect.width);
var dataWidth = dataX1 - dataX0;
If you have the data in axes coordinates already you should be able to do something like:
var rectData = [{x: 'April 1, 1999', y: 10000, width: 100, height:100}];
svg.selectAll('rect.boxy')
.data(rectData)
.enter().append('rect').classed('boxy', true)
.style('fill','black');
svg.selectAll('rect.boxy')
.attr('x', function(d) { return x(new Date(d.x));} )
.attr('y', function(d) { return y(d.y);})
.attr('width', function(d) { return d.width;} )
.attr('height', function(d) { return d.height;} );
Based on the example you shared where x and y (as functions) are the scales the axes are based on.

Resources