Time axis: Remove scale points with no data - d3.js

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.

Related

dcjs dynamic zooming to fit range of values

I have a rowchart in DCjs that plots the top N values of a given parameter. However, for the unfiltered data these differ from each other by a very small number.
I've had to label each row with it's unique identifier, as my random generator produced two identical names, meaning that if I use name as the dimension, I end up with one of ten thousand data points having a value greater than 100%.
However, the main problem here is that the difference between each row is tiny, and can be around 0.0001.
However if I zoom in on that part of the x-axis using
var max = dim.top[1][0].value;
var min = dim.top(10)[9].value;
chart
.dimension(dim)
.group(group)
.x(d3.scaleLinear()
.range([-100, chart.width()])
.domain([min-(max-min)*0.1,max])
)
.cap(10)
.othersGrouper(null)
.colors(['#ff0000']);
Firstly I loose the ID label on the left. Secondly as I also have to use .elasticX(false) for the zooming to work, it means that when I add filters, the range of the x-axis doesn't change with the values e.g.
Is there a way to get dynamic zooming such that the range of the x-axis depends on the range of values presented?
elasticX is a really simple feature which does pretty much what your code does, although it locks the min or max to zero depending if the data is positive or negative:
var extent = d3.extent(_rowData, _chart.cappedValueAccessor);
if (extent[0] > 0) {
extent[0] = 0;
}
if (extent[1] < 0) {
extent[1] = 0;
}
(calculateAxisScale source)
This code gets called (indirectly) before each render and redraw.
Here's some general purpose advice for when elasticX or elasticY doesn't do exactly what you want. I've never seen it fail! (Which is saying something in such a quirky codebase as dc.js.)
First, disable elasticX. Then create a function which calculates the X domain and sets it:
function custom_elastic(chart) {
var max = chart.dimension().top[1][0].value;
var min = chart.dimension().top(10)[9].value;
chart.x().domain([min,max]);
}
I've parameterized it on the chart for generality.
Now we can have this function called on the preRender and preRedraw events. These events will pass the chart when they fire:
chart.on('preRender', custom_elastic)
.on('preRedraw', custom_elastic);
And that should do it!
BTW, you probably don't want to set the range of the scale - this is set automatically by the chart, and it's a little more complicated than you have it since it takes margins into account.
Debugging the min and max
Looking at your fiddle I realized that I hadn't given a second look to how you are calculating the min and max.
I also hadn't noticed that you had the range start at -100.
Good first step logging it; it reports
min: 0.81, max: 0.82
which is incorrect. The top ten are from 0.96 to 1.
The issue is that the dimension's key is the id, so the rows returned by .top() are in reverse alphabetical order (the "largest" strings).
Again you're on the right track with
console.log(Group.top(Infinity))
Yes! The group will give you the top 10 by value.
var top10 = thischart.group().top(10);
var max = top10[0].value;
var min = top10[9].value;
Nice!
fiddle
But wait, doesn't it look like the bars are stretching off the left side of the chart?
Hacking the row chart with a renderlet to draw bars starting at the left edge
Okay now it's clear that the row chart doesn't support this. Here is a hack to resize the bars to the left edge:
chart.on('renderlet', function(chart) {
var transform = chart.select('g.row rect').attr('transform');
var tx = +transform.split('(')[1].split(',')[0];
chart.selectAll('g.row rect')
.attr('transform', null)
.attr('width', function(d) {
return +d3.select(this).attr('width') + tx;
})
chart.selectAll('g.row text.row')
.attr('transform', null);
})
All the row rects are going to be offset by a large negative number, which we grab first in tx. Then we remove the transform from both the rects and the text, and add tx to the width of the row rects.
fiddle
Great! But where's the last bar? Well, we took the top ten values for the min and max, so the tenth bar is the minimum value.
You'll have to figure out what works for you, but I found that looking at the top 20 values left the top 10 at good sizes:
var N = 20;
var topN = thischart.group().top(N);
var max = topN[0].value;
var min = topN[N-1].value;
final fiddle
This hack does not play well with the built-in transitions, so I turned them off for this chart:
chart.transitionDuration(0)
It would be a lot more work to hack that part, and better to fix it in the chart itself.

d3 floating grouped bar with ranged values in a timeline

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.

How to get the boundaries of currently visible time scale (after panning and zooming)?

I would like to load in additional data points for the graph only if it scales or translates. Suppose I have a graph for the specific time range (http://codepen.io/jayarjo/pen/gzfyj), now if the user pans or zooms it to a wider range I want to load a wider data corresponding to that range and plot additional data points. I'm not sure how to get the currently visible date range from the D3...?
Simply retrieve inverted values of the beginning and ending points of your scale, which apparently are 0 and width of the scale:
var startDate = x.invert(0);
var endDate = x.invert(width); // where width is the visible width of the x scale

How to pass different arrays to d3.svg.line() function?

I have seen an example of d3.svg.line() being as follows:
var altitude = some_array() // I read this from a .json file
y = d3.scale.linear().domain([0, d3.max(altitude)]).range([0 + margin, h - margin]),
x = d3.scale.linear().domain([0, altitude]).range([0 + margin, w - margin])
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return -1 * y(d); })
some_svg_group.append("svg:path").attr("d", line(altitude));
although I didn't quite understood what is actually happening, I get that this is a sort of generator function, where line().x and line().y are sort of generator functions, and the idiom shown is a way to have an equally spaced x array versus the actual values in the y array.
What I would like to do, though, is to pass TWO arrays to d3.svg.line(), say distance and altitude, more or less like this:
var line = d3.svg.line()
.x(function_that_gives_distance)
.y(function_that_fives_altitude)
some_svg_group.append("svg:path").attr("something", line(some_other_thing));
Any suggestion on what to replace those placeholders with? Or, if they are wrong, how to achieve that at all?
line.x and line.y are actually accessors. The array you pass into the line generator represents a set of data points and the accessor functions are used to map each data point onto appropriate x and y pixel coordinates.
So with that in mind, it probably doesn't make sense represent the same data points using 2 separate arrays. You should try to transform distance and altitude into a single array of data points ahead of time. Then, you can defined your x and y accessors to map the properties on your data point to the correct pixel locations. Using your placeholder terminology, I would think that function_that_gives_distance and function_that_gives_altitude would actually be invoked as part of the construction of the data points, not in the line generator's accessors.
To summarize, break apart the workflow into 2 steps:
Transform the data into a set of data points you want to base the line on.
Use the line generator accessor functions to do a projection of these data points onto the drawing system's coordinates (i.e. pixel positions).

What does this.__chart__ refer to in a d3 selection?

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.

Resources