lucky1928
lucky1928

Reputation: 8935

d3.js - generate color table from array

I would like to generate the d3 color table with multiple rows, in below code, it looks like the color table not updated at all.

color_table()
function color_table() {
 var categorical = [
  { "name" : "schemeAccent", "n": 8},
  { "name" : "schemeDark2", "n": 8},
  { "name" : "schemePastel2", "n": 8},
  { "name" : "schemeSet2", "n": 8},
  { "name" : "schemeSet1", "n": 9},
  { "name" : "schemePastel1", "n": 9},
  { "name" : "schemeCategory10", "n" : 10},
  { "name" : "schemeSet3", "n" : 12 },
  { "name" : "schemePaired", "n": 12},
  //{ "name" : "schemeCategory20", "n" : 20 },
  //{ "name" : "schemeCategory20b", "n" : 20},
  //{ "name" : "schemeCategory20c", "n" : 20 }
]

var width = 400,
    height = 500;

var colorScale = null

var n = 0,
    unit = 0

var svg = d3.select('body').selectAll('cols')
  .data(categorical)
  .enter()
  .append('svg')
  .attr('width',width)
  .attr('height',height/categorical.length)
  .style('border','1px solid red')
  
var bars = svg.selectAll(".bars")
    .data((d,i) => {
      n = d.n
      unit = width/n
      //console.log(n)
      colorScale = d3.scaleOrdinal(d3[d.name]); 
      return d3.range(n)})
  .enter().append("rect")
    .attr("class", "bars")
    .attr("x", function(d, i) { return i * unit; })
    .attr("y", 0)
    .attr("height", height)
    .attr("width", unit)
    .style("fill", (d,i) => colorScale(d) )
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

Upvotes: 1

Views: 79

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38231

Your color scales are all the same as you reference the same variable for each: colorScale. By evaluating colorScale = d3.scaleOrdinal(d3[d.name]); for each datum and only afterwards adding the rectangles, they are colored based on the last processed datum to define colorScale. This is also why each series has the same rectangle width.

To fix this, we can use d3.local, which appears to be designed for this exact situation:

D3 locals allow you to define local state independent of data. For instance, when rendering small multiples of time-series data, you might want the same x-scale for all charts but distinct y-scales to compare the relative performance of each metric. (docs)

With d3 local we can hold the distinct color scales and width values for each series:

var localScale = d3.local();
var localWidth = d3.local();

And we can set their values for each parent node as follows using local.set, which sets a value for a specified node (2nd and 1st parameters respectively):

var svg = d3.select('body').selectAll('cols')
  .data(categorical)
  .enter()
  .append('svg')
  .each(function(d) {
    localScale.set(this,d3.scaleOrdinal(d3[d.name]));
    localWidth.set(this,width/d.n);
  })

To get the value we can use local.get(node), for the child nodes. While we haven't defined a local value for the child nodes, local.get will find the nearest ancestor with a defined value, so we can supply the rectangle to local.get:

var bars = svg.selectAll(".bars")
   .data(d => d3.range(d.n))
   .enter().append("rect")
   .attr("x", function(d, i) { 
        return i * localWidth.get(this); 
   })
   .attr("width", function() {
     return localWidth.get(this)
   })
   .style("fill", function(d) {
      return localScale.get(this)(d) ;
  })
  ...

I've changed the data being passed to simply d3.range(d.n) as we no longer need to calculate anything else here.

Altogether this gives us:

color_table()
function color_table() {
 var categorical = [
  { "name" : "schemeAccent", "n": 8},
  { "name" : "schemeDark2", "n": 8},
  { "name" : "schemePastel2", "n": 8},
  { "name" : "schemeSet2", "n": 8},
  { "name" : "schemeSet1", "n": 9},
  { "name" : "schemePastel1", "n": 9},
  { "name" : "schemeCategory10", "n" : 10},
  { "name" : "schemeSet3", "n" : 12 },
  { "name" : "schemePaired", "n": 12},
]

var width = 400,
    height = 500;

var localScale = d3.local();
var localWidth = d3.local();

var n = 0,
    unit = 0

var svg = d3.select('body').selectAll('cols')
  .data(categorical)
  .enter()
  .append('svg')
  .attr('width',width)
  .attr('height',height/categorical.length)
  .style('border','1px solid red')
  .each(function(d) {
    localScale.set(this,d3.scaleOrdinal(d3[d.name]));
    localWidth.set(this,width/d.n);
  })
  
var bars = svg.selectAll(".bars")
    .data(d => d3.range(d.n))
  .enter().append("rect")
    .attr("class", "bars")
    .attr("x", function(d, i) { return i * localWidth.get(this); })
    .attr("y", 0)
    .attr("height", height)
    .attr("width", function() {
      return localWidth.get(this)
    })
    .style("fill", function(d) {
        return localScale.get(this)(d) ;
    })
 
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

Providing:

enter image description here

An alternative approach would be to store the scale on the datum itself, either the parent or the child, but d3.local is designed for this purpose and is likely the simplest solution.

Upvotes: 3

Related Questions