Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 370
JulienD Avatar asked Aug 10 '16 08:08

JulienD


1 Answers

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);
like image 158
Steve Clanton Avatar answered Sep 28 '22 19:09

Steve Clanton