Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.scale.category20 is too smart for me

Can anybody explain to me why these two expressions return different values...

log1.text(c20(1)); // "#aec7e8"
log2.text(d3.scale.category20()(1)); // "#1f77b4"

... in the following context


Working example...

    var c20 = d3.scale.category20(),
      col = d3.range(20).map(function(c) {
        return c20(c).replace("#", "0x")
      }),
      log1 = d3.select("#log1"),
      log2 = d3.select("#log2");

    log1.text(c20(1)); // "#aec7e8"
    log2.text(d3.scale.category20()(1)); // "#1f77b4"
    $("#user-agent").text(navigator.userAgent);
#log div {
  display: inline-block;
  margin: 0 0 0 10px;
  background: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>

<div id="log">
  <div id="log1"></div>
  <div id="log2"></div>
</div>
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title></title>
</head>

<body>
  <div id="container"></div>
  <p id="user-agent"></p>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
  <div id="log1"></div>
  <div id="log2"></div>
</body>

</html>

The user agent reported in my system is

Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36


I kind of get the above behaviour but this is very strange...

Why is this...

// method 1
d3.range(20).map(d3.scale.category20())

0   #1f77b4
1   #aec7e8
2   #ff7f0e
3   #ffbb78
4   #2ca02c
5   #98df8a
6   #d62728
7   #ff9896
8   #9467bd
9   #c5b0d5
10  #8c564b
11  #c49c94
12  #e377c2
13  #f7b6d2
14  #7f7f7f
15  #c7c7c7
16  #bcbd22
17  #dbdb8d
18  #17becf
19  #9edae5

different from this...

// method 2
d3.range(20).map(function(d, i) {
    return d3.scale.category20()(i);
})  

0   #1f77b4
1   #1f77b4
2   #1f77b4
3   #1f77b4
4   #1f77b4
5   #1f77b4
6   #1f77b4
7   #1f77b4
8   #1f77b4
9   #1f77b4
10  #1f77b4
11  #1f77b4
12  #1f77b4
13  #1f77b4
14  #1f77b4
15  #1f77b4
16  #1f77b4
17  #1f77b4
18  #1f77b4
19  #1f77b4

var c20 = d3.scale.category20(),
  log1 = d3.select("#log1"),
  log2 = d3.select("#log2");

log1.text(c20(1)); // "#aec7e8"
log2.text(d3.scale.category20()(1)); // "#1f77b4"

d3.select("#t1").selectAll(".logs")
  .data(d3.range(20).map(d3.scale.category20()))
  .enter().append("tr").selectAll("td").data(function(d) {
    return [d]
  })
  .enter().append("td")
  .attr("class", "logs")
  .text(function(d, i, j) {
    return [j, d].join("\t")
  })

d3.select("#t2").selectAll(".logs")
  .data(d3.range(20).map(function(d, i) {
    return d3.scale.category20()(i);
  }))
  .enter().append("tr").selectAll("td").data(function(d) {
    return [d]
  })
  .enter().append("td")
  .attr("class", "logs")
  .text(function(d, i, j) {
    return [j, d].join("\t")
  })
#log div {
  display: inline-block;
  margin: 0 0 10px 10px;
  background: #ccc;
}
#t1,
#t2 {
  background: #ccc;
  display: inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>

<div id="log">
  <div id="log1"></div>
  <div id="log2"></div>
</div>
<div id="t1"></div>
<div id="t2"></div>

Just to explain, the reason I wanted to use method 2 above was because I needed to convert the hex strings into properly formatted hex numbers so I had to process the domain values on the way through. The actual use case is this:

var col = d3.range(20).map(function(c){
        return d3.scale.category20()(c).replace("#", "0x")
    });  

which doesn't work (and I still don't get why not), which is why I had to do this:

var c20 = d3.scale.category20(),
    col = d3.range(20).map(function(c){
        return c20(c).replace("#", "0x")
    });
like image 696
Cool Blue Avatar asked Sep 06 '15 10:09

Cool Blue


1 Answers

You can think of the palette 'building up' as it's being used. If you create the palette on the top, e.g.

var palette = d3.scale.category20();

and apply the palette different values in an iteration (e.g.

selection.style('fill', function(d, i) {return palette(i);});

then on each invocation, the palette checks if it already assigned a color for that value; if not, it'll attempt to give a new color (or recycle if you run out of colors).

In contrast, if you apply the value to a fresh palette in your iteration, it'll always just pull one value from that one specific palette:

selection.style('fill', function(d, i) {return d3.scale.category20()(i);});

The undesirable result is that all colors will be the same.

In other words, the d3.scale.category20 isn't a pure function; it implicitly keeps track of its state. It's similar to e.g. using a random number generation that accepts a seed, i.e. deterministic: you don't want to recreate it in an iteration, otherwise the random number you pull will always be the same.

This issue (pre D3v4) speaks to the general value of functional programming, as there's anticipation that a function called with some values will always depend on just the supplied arguments, making testing easier too.

like image 99
Robert Monfera Avatar answered Sep 27 '22 23:09

Robert Monfera