block by mbostock 310c99e53880faec2434

A Less-Angry Rainbow

Full Screen

Experimenting with a rainbow color scale that is cylical but has better perceptual properties. The HCL rainbow has roughly-constant luminance, but is ugly. The cubehelix rainbow, inspired by Matteo Niccoli’s perceptual rainbow but extended to 360°, varies in brightness but is prettier.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.space {
  position: absolute;
}

.space canvas {
  float: left;
}

.space div {
  position: absolute;
  top: 0;
  left: 20px;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="cubehelix.min.js"></script>
<script>

var spaces = [
  {
    name: "Rainbow (HSL)",
    color: function(t) {
      return d3.hsl(t * 360, 1, .5);
    }
  },
  {
    name: "Rainbow (HCL)",
    color: function(t) {
      return d3.hcl(t * 360, 100, 55);
    }
  },
  {
    name: "Rainbow (Cubehelix)",
    color: d3.scale.cubehelix()
        .domain([0, .5, 1])
        .range([
          d3.hsl(-100, 0.75, 0.35),
          d3.hsl(  80, 1.50, 0.80),
          d3.hsl( 260, 0.75, 0.35)
        ])
  }
];

var y = d3.scale.ordinal()
    .domain(spaces.map(function(d) { return d.name; }))
    .rangeRoundBands([0, 500], .04);

var margin = y.range()[0],
    width = 960 - margin - margin,
    height = y.rangeBand();

var space = d3.select("body").selectAll(".space")
    .data(spaces)
  .enter().append("div")
    .attr("class", "space")
    .style("width", width + "px")
    .style("height", height + "px")
    .style("left", margin + "px")
    .style("top", function(d) { return y(d.name) + "px"; });

space.append("canvas")
    .attr("width", width)
    .attr("height", 1)
    .style("width", width + "px")
    .style("height", height / 2 + "px")
    .each(render(function(color) { return color; }));

space.append("canvas")
    .attr("width", width)
    .attr("height", 1)
    .style("width", width + "px")
    .style("height", height / 2 + "px")
    .each(render(function(color) { color = d3.hcl(color); color.c = 0; return color; }));

space.append("div")
    .style("line-height", height / 2 + "px")
    .text(function(d) { return d.name; });

function render(color) {
  return function(d) {
    var context = this.getContext("2d"),
        image = context.createImageData(width, 1);
    for (var i = 0, j = -1, c; i < width; ++i) {
      c = d3.rgb(color.call(this, d.color(i / width)));
      image.data[++j] = c.r;
      image.data[++j] = c.g;
      image.data[++j] = c.b;
      image.data[++j] = 255;
    }
    context.putImageData(image, 0, 0);
  };
}

</script>

cubehelix.min.js

!function(){function t(t){return function(e,i){e=d3.hsl(e),i=d3.hsl(i);var r=(e.h+120)*a,h=(i.h+120)*a-r,s=e.s,l=i.s-s,o=e.l,u=i.l-o;return isNaN(l)&&(l=0,s=isNaN(s)?i.s:s),isNaN(h)&&(h=0,r=isNaN(r)?i.h:r),function(a){var e=r+h*a,i=Math.pow(o+u*a,t),c=(s+l*a)*i*(1-i);return"#"+n(i+c*(-.14861*Math.cos(e)+1.78277*Math.sin(e)))+n(i+c*(-.29227*Math.cos(e)-.90649*Math.sin(e)))+n(i+c*1.97294*Math.cos(e))}}}function n(t){var n=(t=0>=t?0:t>=1?255:0|255*t).toString(16);return 16>t?"0"+n:n}var a=Math.PI/180;d3.scale.cubehelix=function(){return d3.scale.linear().range([d3.hsl(300,.5,0),d3.hsl(-240,.5,1)]).interpolate(d3.interpolateCubehelix)},d3.interpolateCubehelix=t(1),d3.interpolateCubehelix.gamma=t}();