I am trying to combine the implementation of a semantic zoom with a brush selection tool with a quadtree on a scatterplot in D3.
The idea is that the zoom is active by default. On input selection, A brush is created and the mousedown event of the zoom is deactivated. To speed the brushing, I would like to organize the data that is visible (i.e. the data that has been zoomed on) in a quadtree (c.f. http://bl.ocks.org/mbostock/4343214). But I have no idea how to create the quad tree from the zoom extent only.
I tried creating the quadtree as such:
var scaleX = d3.scale.linear()
.domain(...)
.range(...);
var scaleY = d3.scale.linear()
.domain(...)
.range(...);
// var data is an array of Objects containing the coordinates (x,y)
var dataCoords = data.map(function(d){return [scaleX(d.x) scaleY(d.y)];});
/******
Define d3.zoom.extent which does not exist (contrary to d3.brush.extent())
*******/
var s= d3.event.scale;
var t = d3.event.translate;
//top left corner
var tl = ...;
//bottom right corner
var br = ...;
d3Zoom.extent = [tl, br];
/******
Create quadtree from extent
*******/
var quadtree = d3.geom.quadtree()
.extent(d3Zoom.extent)(dataCoords);
But it does not work. The only way I managed to get a quadtree was to pre-create it using the full svg as extent, before I zoom:
var quadtree = d3.geom.quadtree()
.extent([[-1, -1], [size + 1, size + 1]])(dataCoords);
When I pre-create the quadtree before zooming on the entire data, and zoom afterward, the quadtree desynchronizes with the newly zoomed data because the values in data depend on the axes scale which are updated during the zoom event (semantic zooming).
I haven't been able to combine http://bl.ocks.org/mbostock/4343214 and Zooming and brushing in d3 force directed graph, and I haven't found anything relevant in http://techslides.com/over-2000-d3-js-examples-and-demos or http://bl.ocks.org or SO.
Could someone please give me a fiddle that shows how to brush on a quadtree that contains only the subset of the data that is zoomed upon?
Related
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".
My understanding of D3's projection functions are that they do the same thing as the scale functions. They map GPS coordinates to pixel coordinates. This is the projection I'm currently using. (I don't understand all the variables in detail but I fiddled around with them until the map showed up)
var projection = d3.geo.albers()
.center([-122.436269,37.798107])
.rotate([0, 0, 0])
.parallels([36, 38])
.scale(300000);
This draws the map fine:
.attr("d", d3.geo.path().projection(projection))
When I try to plot points though the numbers are crazy.
.attr("cx",function(d) {
return projection([d._lon,d._lat])[0];
})
.attr("cy",function(d) {
return projection([d._lon,d._lat])[1];
});
How do I properly do this?
Here are some examples of the points I'm getting through the projection function:
[5175.3799972560955, 1808.5108650794136]
[5158.315547249338, 1823.564395627589]
[5143.958532762888, 1831.9879789081751]
On a 1280x800 screen these are way off. Even if I scale them by dividing by 100, they'll still mostly just stack on top of each other. I have a 700*700 svg positioned using twitter bootstrap. Not sure how those are taken into account. I just assumed that if the map if fine, then the same projection should work for the points.
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've been poking around in d3.box and d3.bullet. In both cases, the current scale is retrieved using something like the following...
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
...where x1 is the current scale object. I realise that this idiom is used to update the box plot or bullet chart, but could anyone explain how? What does this.__chart__ refer to? A scale object? Why the conditional? (||) Why does x0's domain cover the range 0 to infinity when the current scale is unlikely to have such a large range?
Apologies if my questions are poorly specified. Any help would be greatly appreciated.
The this context is the DOM element that contains the chart: i.e. a g element. Binding some variable to the DOM element, e.g. this.myvar = state, provides a way to deal with chart specific state. Multiple update calls on one specific chart g element, will then all have access to the same variable.
Mike and Jason have used the property name __chart__ in various charts and also in the d3 axis component, for keeping track of chart specific state.
You're right that in this case it's the scale that is being stored in the g element's __chart__ property. See excerpt from bullet.js:
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], markerz[0], measurez[0])])
.range(reverse ? [width, 0] : [0, width]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
So, a scale x1 is determined based on the current data. It will be stored in __chart__, to be used some time in the future when this chart is updated with new data.
The previous scale is taken from this.__chart__ and kept in x0. The this.__chart__ will return undefined when the chart is just being constructed (i.e. the enter phase). In that case x0 will instead become d3.scale.linear().domain([0, Infinity]).range(x1.range()). See short circuit eval.
The old scale is needed for smooth transition. When new data points are entered, we first want to plot them on the chart using the old scale. After that we will transition all points (new and updated) according to the new scale.
Regarding the [0, Infinity] domain. So, a scale with this domain will only be used when the chart is just constructed. That means it provides a way to setup the initial transition when introducing the chart. A infinite domain with a finite range means that all points are scaled to 0. So, when the chart is set up, all points will be plotted at 0 and transition to the proper values according the x1 scale.
I've created nycMap, a project that uses angularJS (MVC), yeoman (build), d3 (mapping) and geoJSON (geo data).
Everything works very nicely, but I did have to spend quite some time getting the right scale and translation. I was wondering how I can automatically figure out at what scale the map will show its best and what x and y values go into the translation?
'use strict';
japanAndCo2App.controller('MainCtrl', function($scope) {
function makeJapanAll(){
var path, vis, xy;
xy = d3.geo.mercator().scale(16000).translate([-5600,2200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#japanAll").append("svg:svg").attr("width", 1024).attr("height", 700);
d3.json("data/JPN_geo4.json", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill",function(d,i){ return d.properties.color || "transparent"});
});
}
makeJapanAll();
});
(If you are interested in the code, it's all on github. The code for the map is in scripts/controllers/main.js which is the same as shown above.)
I've had the same problems. But it is very easy to do when you have a bounding box, which can be determined from the GeoJSON (like meetamit said), or while creating the GeoJson. And the width of the wanted SVG.
I'll start with the variables lattop, lonleft, lonright, width and height for the bounding box of the geojson and the dimensions of the image. I haven't yet occupied myself with calculating a good height from the difference in latutude. So the height is just estimated to be big enough to fit the image. The rest should be clear from the code:
var xym = d3.geo.mercator();
// Coordinates of Flanders
var lattop = 51.6;
var lonleft = 2.4;
var lonright = 7.7;
var width = 1500;
var height =1000;
// make the scale so that the difference of longitude is
// exactly the width of the image
var scale = 360*width/(lonright-lonleft);
xym.scale(scale);
// translate the origin of the map to [0,0] as a start,
// not to the now meaningless default of [480,250]
xym.translate([0,0]);
// check where your top left coordinate is projected
var trans = xym([lonleft,lattop]);
// translate your map in the negative direction of that result
xym.translate([-1*trans[0],-1*trans[1]]);
var path = d3.geo.path().projection(xym);
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height);
Note, if you go over the date line (180 degrees), you will have to take the overflow into account.
Given this:
xy = d3.geo.mercator().scale(someScale).translate([0, 0]);
someScale is the pixel width of the entire world when projected using the mercator projection. So, if your json data had outlines for the whole world – spanning from lat/lng -180,90 to latLng 180,-90 – and if someScale was 1024, then the world would be drawn such that it exactly fits within a 1024x1024-pixel square. That's what you see on in this Google Maps view (well... sort of... not quite... read on...).
That's not enough though. When the world is drawn at 1024px, without any translation, lat/lng 0,0 (i.e. the "middle" of the world) will sit at the 0,0 pixel of the projected map (i.e. the top left). Under these conditions, the whole northern hemisphere and western hemisphere have negative x or y values, and therefore fall outside the drawn region. Also, under these conditions, the bottom right of the world (i.e. lat/lng -90, 180) would sit at the exact middle of the 1024x1024 square (i.e. at pixel 512,512).
So, in order to center the world in the square described here, you need to translate the map by half its width in the X and Y directions. I.e. you need
xy = d3.geo.mercator().scale(1024).translate([512, 512]);
That'll give you exactly the Google Map view I linked to.
If your json data only has part of the world (like, nyc or NY state) drawing it with this xy projection will render the outlines in the correct geographic position relative to the entire 1024x1024 world-spanning region. So it would appear rather small, with lots of whitespace.
The challenge is how to scale and translate the projection such that the area in question fills up the 1024x1024 square. And... so far I haven't answered this question, but I hope that this explanation points you in the right direction towards figuring out this math. I'll also try to continue the answer later, when I have more time. :/
There's an example here that gets the bounds of countries from geojson and then scales and translates the map to that country. The code is a bit ugly; there're however efforts to make this easier in the future (see this and this issue).