D3 difference between ordinal and linear scales - javascript

var xScale = d3.scale.ordinal().domain([0, d3.max(data)]).rangeRoundBands([0, w], .1);
var yScale = d3.scale.linear().domain([0, data.length]).range([h, 0]);
I'm confused about when to use ordinal or linear scale in D3.
Below is what I've discovered from the API doc, still bit lost... if anyone can help, it would be much appreciated.
ordinal(x)
Given a value x in the input domain, returns the corresponding value in the output range.
If the range was specified explicitly (as by range, but not rangeBands, rangeRoundBands or rangePoints), and the given value x is not in the scale’s domain, then x is implicitly added to the domain; subsequent invocations of the scale given the same value x will return the same value y from the range.
d3.scale.linear()
Constructs a new linear scale with the default domain [0,1] and the default range [0,1]. Thus, the default linear scale is equivalent to the identity function for numbers; for example linear(0.5) returns 0.5.

As for Ordinal Scales:
Ordinal scales have a discrete domain, such as a set of names or categories.
An ordinal scale's values must be coercible to a string, and the stringified version of the domain value uniquely identifies the corresponding range value.
So, as an example, a domain of an ordinal scale may contain names, like so:
var ordinalScale = d3.scale.ordinal()
.domain(['Alice', 'Bob'])
.range([0, 100]);
ordinalScale('Alice'); // 0
ordinalScale('Bob'); // 100
Notice how all values are strings. They cannot be interpolated. What is between 'Alice' and 'Bob'? I don't know. Neither does D3.
Now, as for Quantitative Scales (e.g. Linear Scales):
Quantitative scales have a continuous domain, such as the set of real numbers, or dates.
As an example, you can construct the following scale:
var linearScale = d3.scale.linear()
.domain([0, 10])
.range([0, 100]);
linearScale(0); // 0
linearScale(5); // 50
linearScale(10); // 100
Notice how D3 is able to interpolate 5 even if we haven't specified it explicitly in the domain.
Take a look at this jsfiddle to see the above code in action.

In D3.js scales transform a number from the domain to the range. For a linear scale the domain will be a continuous variable, with an unlimited range of values, which can be then transformed to a continuous range. For ordinal scales there will be a discrete domain, for example months of the year where there are limited range of possible values that may be ordered but aren't continuous. The API docs on Github can probably explain the difference better than I have

OK, we can start learning it with using both with the same data to see differences(I'm using d3 v4), imagine we have the data below with using ordinal and linear scales:
const data = [1, 2, 3, 4, 5];
const scaleLinear = d3.scaleLinear()
.domain([0, Math.max(...data)]).range([1, 100]);
const scaleOrdinal = d3.scaleOrdinal()
.domain(data).range(['one', 'two', 'three', 'four', 'five']);
Now we start calling them to see the result:
scaleLinear(1); //20
scaleOrdinal(1); //one
scaleLinear(2); //40
scaleOrdinal(2); //two
scaleLinear(5); //100
scaleOrdinal(5); //five
Look at the functions and the results we get, as you see in the ordinal one we map the data to our range, while in the linear one we stretch to the range, so in these cases for example scaleLinear(1) will return 20... our domain max is 100 and 100 divided by 5 is equal 20, so scaleLinear(1) is 20 and scaleLinear(2) is 40...
But as you see, scaleOrdinal(1) is map to the array in the range, so it's equal to one and scaleOrdinal(2) it's equal to two...
So that's how you can use these scales, scaleLinear is useful for many things including present the scale on page, but scaleOrdinal more useful for getting the data in order, that's how it's explained in the documentation:
# d3.scaleLinear() <>
Constructs a new continuous scale with the unit domain [0, 1], the
unit range [0, 1], the default interpolator and clamping disabled.
Linear scales are a good default choice for continuous quantitative
data because they preserve proportional differences. Each range value
y can be expressed as a function of the domain value x: y = mx + b.
d3.scaleOrdinal([range]) <>
Constructs a new ordinal scale with an empty domain and the specified
range. If a range is not specified, it defaults to the empty array; an
ordinal scale always returns undefined until a non-empty range is
defined.
Also this is a good example from d3 in depth using both ordinal and linear scales at the same time:
var myData = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
var linearScale = d3.scaleLinear()
.domain([0, 11])
.range([0, 600]);
var ordinalScale = d3.scaleOrdinal()
.domain(myData)
.range(['black', '#ccc', '#ccc']);
d3.select('#wrapper')
.selectAll('text')
.data(myData)
.enter()
.append('text')
.attr('x', function(d, i) {
return linearScale(i);
})
.text(function(d) {
return d;
})
.style('fill', function(d) {
return ordinalScale(d);
});
body {
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 14px;
color: #333;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<svg width="800" height="60">
<g id="wrapper" transform="translate(100, 40)">
</g>
</svg>

Related

D3 axis: set domain based on numbers but display them as strings

I have for dataset an array of objects like so
[
{
time: '20:07',
seconds: 7620,
value: 49,995
},
...
]
I'm currently creating X axis like so
this.x = d3.scaleLinear()
.domain(this.values.map(function(d) { return d.seconds}))
.range([ 0, width])
this.chart.append('g')
.call(d3.axisBottom(this.x))
what happens now is following
so this correct domain, 7620 is smallest seconds value and 7800 highest, but now instead of showing seconds I wish to display 'time' value. First label on x axis would be then '20:07' instead of 7620.
In a nutshell, create domain based on seconds but show time instead. How can I do that ?
You can transform the tick labels using tickFormat:
this.chart.append('g')
.call(d3.axisBottom(this.x).tickFormat(d3.timeFormat("%H:%M")))
this.x = d3.scaleBand()
.domain(array.map(value=>value.time))
.range([ 0, width])
for custom labels i prefer to use scaleBand()

Always show negative values on Y axis using D3js

I have a dataset which is is something like
[{key: 'abc', series: [1000,2500,3000]}, {key: 'xyz', series: [-20, 0,0]}]
In this case when I plot my bar chart with d3.js, The 'y' label ticks ignores negative values since they are not in the range of other numbers (1000,2500,3000). Is there a way to force the negative number ticks to be shown. Or if my y tick range is [0,200,400,800] then it should be [-200, 0, 200,400,800].
You may hard code your y axis range using:
d3.scale.linear().range([-200,800]);

Best d3 scale for mapping integers range

I want to construct a scale that maps a range of successive integers (indexes of characters in a string) to regular intervals in another range of integers (pixels, say 0-600). That is, I would like to assign characters to pixels and conversely as regularly as possible, the length of one not being necessarily a multiple of the other.
For instance, mapping [0,1,2,3] to 400 pixels, I would expect
0 -> 0-99
1 -> 100-199
2 -> 200-299
3 -> 300-399
and conversely
0-99 -> 0
100-199 -> 1
200-299 -> 2
300-399 -> 3
while for mapping 0-4000 to 400 pixels, I would expect
0-9 -> 0
10-19 -> 1
etc.
What is the best scale to use for this in d3 ?
On one hand I am afraid that discrete scales will not use the fact that the domain is equally separated and generate a huge switch statement if the number of elements is big. Since I will use the scale on every element to draw an image, I am worried about performance.
On the other hand, a linear scale such as
d3.scaleLinear()
.domain([0,399]) // 400 pixels
.rangeRound([0,3]) // 4 elements
gives me
0 0
66 0 // 1st interval has 66 pixels
67 1
199 1 // other intervals have 132 pixels
200 2
332 2
333 3 // last interval has 66 pixels
400 3
(fiddle)
so the interpolator returns unequal intervals (shorter at the ends).
Edit: not using d3, it is not hard to implement:
function coordinateFromSeqIndex(index, seqlength, canvasSize) {
return Math.floor(index * (canvasSize / seqlength));
}
function seqIndexFromCoordinate(px, seqlength, canvasSize) {
return Math.floor((seqlength/canvasSize) * px);
}
Too bad only if it does not come with d3 scales, since it would become much more readable.
The d3 Quantize Scale is the best option if you want to map onto an interval. The scale maps between discrete values and a continuous interval, though. I am not 100% clear on what you want to do, but let's look at how I could do a few of the things you mention with the quantize scale.
Mapping integers to intervals is straightforward, as long as you know that d3 uses half-open intervals [,) to break up the continuous domain.
var s1 = d3.scaleQuantize()
.domain([0,400])
.range([0,1,2,3]);
s1.invertExtent(0); // the array [0,100] represents the interval [0,100)
s1.invertExtent(1); // [100,200)
s1.invertExtent(2); // [200,300)
s1.invertExtent(3); // [300,400)
You could also enumerate the discrete values:
var interval = s.invertExtent(0);
d3.range(interval[0], interval[1]); // [0, 1, ... , 399]
These are nice values you've given though, and since you want to map integers to intervals of integers, we will need rounding when numbers aren't divisible. We can just use Math.round though.
var s2 = d3.scaleQuantize()
.domain([0,250])
.range([0,1,2,3]);
s2.invertExtent(0); // [0, 62.5)
s2.invertExtent(0).map(Math.round); // [0,63) ... have to still interpret as half open
There is no mapping from the interval itself to the integer, but the scale maps a point in an interval from the domain (which is continuous) to its value in the range.
[0, 99, 99.9999, 100, 199, 250, 399, 400].map(s1); // [0, 0, 0, 1, 1, 2, 3, 3]
I also suspect you switched the output of rangeRound from the linear scale with something else. I get
var srr = d3.scaleLinear()
.domain([0,3]) // 4 elements
.rangeRound([0,399]);
[0,1,2,3].map(srr); // [0, 133, 266, 399]
and
var srr2 = d3.scaleLinear()
.domain([0,4]) // 4 intervals created with 5 endpoints
.rangeRound([0,400]);
[0,1,2,3,4].map(srr2); // [0, 100, 200, 300, 400]
The output looks like a scale to us with a bar graph with 50% padding (then each position would be the midpoint of an interval that is 132 pixels). I am going to guess the cause is that rangeRound uses round to interpolate, rather than floor.
You could use a function designed for bar graphs also, if you want the width of the interval.
var sb = d3.scaleBand().padding(0).domain([0,1,2,3]).rangeRound([0,400]);
[0,1,2,3].map(sb); // [0, 100, 200, 300]
sb.bandwidth(); // 100
Not that any of this makes the code simpler.
Once I get to the functions you implement, it seems like the requirements are much simpler. There aren't any intervals involved really. The problem is that there isn't a one-to-one mapping. The best solution is either what you have done or to just use two linear scales with a custom interpolator (to find the floor, rather than rounding.
var interpolateFloor = function (a,b) {
return function (t) {
return Math.floor(a * (1 - t) + b * t);
};
}
var canvasSize = 400;
var sequenceLength = 4000;
var coordinateFromSequenceIndex = d3.scaleLinear()
.domain([0, sequenceLength])
.range([0, canvasSize])
.interpolate(interpolateFloor);
var seqIndexFromCoordinate = d3.scaleLinear()
.domain([0, canvasSize ])
.range([0, sequenceLength])
.interpolate(interpolateFloor);

D3.js - How to map continuous domains to discrete ranges with scales?

I'm trying to make a scale thats relates a continuous domain with a discrete range. My attempt is this one:
var scale = d3.scale.linear()
.domain([0, 15.43])
.range([0, 1, 2, 3, 4]);
So the domain will be from 0 to 15.43 taking all possible float numbers. How can I tell the scale to take all numbers from 0 to 15.43?
You are looking for scale.quantize:
var q = d3.scale.quantize().domain([0, 1]).range(['a', 'b', 'c']);
console.log(q(0));
console.log(q(0.3));
console.log(q(0.35 ));
console.log(q(0.5 ));
console.log(q(1));
Of course, the range values can also be numeric.

google visualizations align 0 axis with two different y-axes

I'm creating a combochart with google's visualization library. I'm charting a store's traffic and revenue over the course of a day. I have set my draw options to
var options = {
seriesType: "bars",
series:{0:{targetAxisIndex:0},1:{targetAxisIndex:1}},
vAxes:{0:{title: "Revenue"},1:{title: "Traffic"}},
hAxis: {title: "Time", showTextEvery: 1},
};
which sets up the Revenue on a different Y-axis than the traffic. A sample of the data might look like this:
var data = [
// Time Revenue Traffic
['10:00-10:30', '132.57', '33'],
['10:30-11:00', '249.23', '42'],
['11:00-11:30', '376.84', '37'],
[... etc ..]
];
the problem I'm having is that Traffic values will always be positive whereas Revenue could be a negative number if there were returns. If that happens my Revenue axis will start at a negative value like -50 while Traffic starts at 0 and the horizontal baselines don't line up. I would like to have it so that even if Revenue has values less than 0 it's 0 axis will line up with the Traffic 0 axis.
Here's an example to show what's happening. See how the Traffic 0 axis is on the same level as the Revenue's -50 axis. I would like to know how to raise the Traffic baseline to the same level as the Revenue 0 axis.
I have a method that I am reasonably certain will always produce axis values with the same 0 point (I haven't proved that it can't produce axes with different 0 points, but I haven't encountered any).
To start off, get the range of the two date series (for our purposes, column 1 is "revenue" and column 2 is "traffic"):
var range1 = data.getColumnRange(1);
var range2 = data.getColumnRange(2);
For each series, get the max value of the series, or 1 if the max is less than or equal to 0. These values will be used as the upper bounds of the chart.
var maxValue1 = (range1.max <= 0) ? 1 : range1.max;
var maxValue2 = (range2.max <= 0) ? 1 : range2.max;
Then calculate a scalar value relating the two upper bounds:
var scalar = maxValue2 / maxValue1;
Now, calculate the lower bounds of the "revenue" series by taking the lower of range1.min and 0:
var minValue1 = Math.min(range1.min, 0);
then multiply that lower bound by the scalar value to get the lower bound of the "traffic" series:
var minValue2 = minValue1 * scalar;
Finally, set the vAxis minValue/maxValue options for each axis:
vAxes: {
0: {
maxValue: maxValue1,
minValue: minValue1,
title: 'Revenue'
},
1: {
maxValue: maxValue2,
minValue: minValue2,
title: 'Traffic'
}
}
The net result is that positive and negative proportions of each series are equal (maxValue1 / (maxValue1 - minValue1 == maxValue2 / (maxValue2 - minValue2 and minValue1 / (maxValue1 - minValue1 == minValue2 / (maxValue2 - minValue2), which means the chart axes should end up with the same positive and negative proportions, lining up the 0's on both sides.
Here's a jsfiddle with this working: http://jsfiddle.net/asgallant/hvJUC/. It should work for any data set, as long as the second data series has no negative values. I'm working on a version that will work with any data sets, but this should suffice for your use case.

Categories