Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active March 4, 2019 16:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/8566245 to your computer and use it in GitHub Desktop.
Save nitaku/8566245 to your computer and use it in GitHub Desktop.
Categorical color scale generator (HCL)

A generator of categorical color scales (a differentiated palette of colors to distinguish representations of categorical variables), using the HCL color space. Change the number of colors by interacting with the inner circle, or rotate the wheel to change the offset.

Choosing a palette like this is useful to avoid the unintended highlighting of a category, at the expense of another.

The generated scales have all the same values for the chroma (40) and the luminance (70) components, giving a set of colors that differ only in hue. Colors in a set like this are perceived as having the same brightness, while colors that are equidistant in, for example, the HSL color space, can be seen as more bright (e.g. yellow) or less bright (e.g. blue). See also this example by Mike Bostock, containing links to more detailed explanations.

The values for the hue component are selected by a uniform sampling from 0 to 360, rotated by an offset that can be controlled by interacting with the wheel.

The wheel is implemented using the D3 svg arc generator in combination with the D3 implementation of HCL.

The code that handles the interactive rotation makes use of D3's drag behavior and some trigonometry, involving the use of Javascript's atan2 function.

svg = d3.select('svg')
# input
q = 3
offset = 0
c = 40
l = 70
# define an arc generator
arc_gen = d3.svg.arc()
.innerRadius(100)
.outerRadius(200)
# rotate the arcs to have the origin pointing up
arc_g = svg.append('g')
.attr('transform', 'rotate(90)')
# define a drag behavior to let the user rotate the arcs
pinch_offset = null
drag = d3.behavior.drag()
.on 'drag', () ->
if not pinch_offset?
pinch_offset = 180/Math.PI*Math.atan2(d3.event.y,d3.event.x)+180-offset
# current angle in positive degrees
angle = 180/Math.PI*Math.atan2(d3.event.y, d3.event.x)+180
offset = angle-pinch_offset
redraw()
.on 'dragend', () ->
pinch_offset = null
redraw = () ->
# update colors
delta = 360/q
colors = (d3.hcl((h*delta+offset)%360,c,l) for h in [0...q])
# update the arc generator
arc_gen
.startAngle((d)->(d.h-delta/2+270)*Math.PI/180)
.endAngle((d)->(d.h+delta/2+270)*Math.PI/180)
# redraw the arcs
arcs = arc_g.selectAll('.arc')
.data(colors)
arcs.enter().append('path')
.attr('class', 'arc')
.call(drag)
arcs.exit().remove()
arc_g.selectAll('.arc')
.attr('d', arc_gen)
.attr('fill', (d)->d)
# redraw the label
svg.select('#q')
.text(q)
# label
svg.append('text')
.attr('class','label')
.attr('id','q')
.attr('dy','0.30em')
.text(q)
# controls
ctrls = svg.append('g')
.attr('id','controls')
ctrls.append('circle')
.attr('r', 100)
.attr('fill', 'transparent')
ctrls.append('text')
.attr('class','label control')
.attr('dx','70px')
.attr('dy','0.30em')
.text('+')
.on 'click', () ->
q = if q is 36 then 1 else q+1
redraw()
ctrls.append('text')
.attr('class','label control')
.attr('dx','-70px')
.attr('dy','0.30em')
.text('-')
.on 'click', () ->
q = if q is 1 then 36 else q-1
redraw()
redraw()
.arc {
stroke: white;
stroke-width: 6px;
cursor: move;
}
.label {
font-family: Georgia;
text-anchor: middle;
fill: #444;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
cursor: default;
}
#q {
font-size: 80px;
}
.control {
font-size: 48px;
font-weight: bold;
cursor: pointer;
visibility: hidden;
}
#controls:hover .control {
visibility: visible;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Categorical color scale generator (HCL)"/>
<title>Categorical color scale generator (HCL)</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<link type="text/css" href="index.css" rel="stylesheet"/>
</head>
<body>
<svg width="960" height="500" viewBox='-480 -250 960 500'>
</svg>
</body>
<script src="index.js"></script>
</html>
(function() {
var arc_g, arc_gen, c, ctrls, drag, l, offset, pinch_offset, q, redraw, svg;
svg = d3.select('svg');
q = 3;
offset = 0;
c = 40;
l = 70;
arc_gen = d3.svg.arc().innerRadius(100).outerRadius(200);
arc_g = svg.append('g').attr('transform', 'rotate(90)');
pinch_offset = null;
drag = d3.behavior.drag().on('drag', function() {
var angle;
if (!(pinch_offset != null)) {
pinch_offset = 180 / Math.PI * Math.atan2(d3.event.y, d3.event.x) + 180 - offset;
}
angle = 180 / Math.PI * Math.atan2(d3.event.y, d3.event.x) + 180;
offset = angle - pinch_offset;
return redraw();
}).on('dragend', function() {
return pinch_offset = null;
});
redraw = function() {
var arcs, colors, delta, h;
delta = 360 / q;
colors = (function() {
var _results;
_results = [];
for (h = 0; 0 <= q ? h < q : h > q; 0 <= q ? h++ : h--) {
_results.push(d3.hcl((h * delta + offset) % 360, c, l));
}
return _results;
})();
arc_gen.startAngle(function(d) {
return (d.h - delta / 2 + 270) * Math.PI / 180;
}).endAngle(function(d) {
return (d.h + delta / 2 + 270) * Math.PI / 180;
});
arcs = arc_g.selectAll('.arc').data(colors);
arcs.enter().append('path').attr('class', 'arc').call(drag);
arcs.exit().remove();
arc_g.selectAll('.arc').attr('d', arc_gen).attr('fill', function(d) {
return d;
});
return svg.select('#q').text(q);
};
svg.append('text').attr('class', 'label').attr('id', 'q').attr('dy', '0.30em').text(q);
ctrls = svg.append('g').attr('id', 'controls');
ctrls.append('circle').attr('r', 100).attr('fill', 'transparent');
ctrls.append('text').attr('class', 'label control').attr('dx', '70px').attr('dy', '0.30em').text('+').on('click', function() {
q = q === 36 ? 1 : q + 1;
return redraw();
});
ctrls.append('text').attr('class', 'label control').attr('dx', '-70px').attr('dy', '0.30em').text('-').on('click', function() {
q = q === 1 ? 36 : q - 1;
return redraw();
});
redraw();
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment