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.
Related
im trying to understand what tools i need to use as im new to d3 and didnt find any thing related...
i need a area chart that is like bars but can float and be on multiple values both on the x and y axis.
in this example the values are days but it might be hours/months etc...
need to know the direction i need to go.. / the right term to search...
There's no significant difference between drawing this chart and a normal bar chart.
And you need to define some scales that will map the values in your data to co-ordinates on your chart.
You need to draw some rect shapes.
So, in the above example you would define a time scale that, given an input date, will map that to a certain x co-ordinate on your chart. You can then use that to determine both the x co-ordinate for where the left-hand-side of a rectangle will be, and to work out how wide the rectangle needs to be.
const xScale = d3.scaleTime()
.domain([d3.min(dateValuesInMyDataset, d => d.date), d3.max(dateValuesInMyDataset, d => d.date)])
.range([0, widthOfMyChart]);
The above xScale if given the earliest date in your dataset would return the value 0, because this is the x co-ordinate representing that date.
Similarly, you would want to construct a linear scale which defines how to map the numerical range of values in your dataset, to the y co-ordinates in your chart. Then you can use the scale to determine the y value and height of all of the rectangles in your chart.
There are lots of good examples of this on ObservableHQ.com that you can browse and see the code for.
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".
I have a dataset which I am plotting. I've modelled it as a network, and have used a force-directed layout to display it, except that I have constrained the layout such that on the x-axis, the nodes are arranged according to time.
An example of what I've done so far is here on my own website: http://www.ericmajinglong.com/force/force.html
As you can see, I have one time axis. The axis scale is derived from the data. However, you'll notice a big gap in the middle.
I understand the concept of scales, where I have a domain and a range, and a scale basically maps the domain to the range. I have a few questions, however.
I was wondering if it might be possible, without creating two horizontal time axes, to exclude empty months?
Instead of an linear scale, would I have to go to an ordinal scale?
Would there be any disadvantages to going to an ordinal scale instead of a time scale?
Code is not posted here for brevity, but I have it at: http://www.ericmajinglong.com/force/force.js
You can probably use an ordinal scale, but in that case you should make sure that the domain is sorted, and add some mark between the two intervals so the user of the visualization understand that there is a a period not shown. Another option is to create a custom scale that automatically shorten gaps in the data, but will still to add special markers to indicate missing time periods.
If you use an ordinal scale instead of a time scale, you will need to format the axis manually.
EDIT: Add a small example of how a custom scale may be implemented
I would implement a custom scale as a closure with accessors.
function customScale() {
// Scale attributes
var domain = [0, 1], // Default domain
range = [0, 1]; // Default range
function scale(x) {
var y = 0;
// Compute the output value...
return y;
}
// Domain and Range Accessors
scale.domain = function(value) {
if (!arguments.length) { return domain; }
domain = value;
return scale;
};
// range accessor...
return scale;
}
And then configure and use the scale
var scale = customScale()
.domain([0, 10])
.range([2, 3]);
console.log(scale(5));
Using a custom scale will probably implies to create custom axis as well.
Well, I'm starting with D3 and I'm trying to create a vertical bar chart.
I really don't know what's happening but some things are not working as expected for me (maybe because I'm just a noob on the matter).
I'm using line scales, works pretty well with axes, but it's miscalculating the height of the bars, for instance the higher values are not displayed (because of the low value of the result).
I've used the d3.max to determine the range. I really don't get what's happening.
var yScaleLeft = d3.scale.linear()
.domain([0, d3.max(stats)])
.range([realHeight, 0]);
.attr("height", function(d) {
return yScaleLeft(d);
});
Here is the code: http://jsfiddle.net/aKhhb/ Look at * Scales * and // Stats bars
(Just forget about the x-alignement of the bars, I will see that later, I want to set its height properly)
Thanks a lot! Y saludos desde Chile :)
The issue is that your input and output ranges are mirrored -- that is, the largest input value maps to the smallest output value. That is fine, but you need to take it into account when calculating the y and height attributes. Essentially, you had the calculations for both reversed.
Fixed fiddle here. I've also fixed the x axis by adding your margin and half of the bar width to the computed x positions. Oh and you don't need parseInt() when doing calculations, only when you actually want to parse an integer from a string.
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).