Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/dfdf8d9e501ab10b405d to your computer and use it in GitHub Desktop.
Save nitaku/dfdf8d9e501ab10b405d to your computer and use it in GitHub Desktop.
HCL decomposition: Linear L*

This example shows Matteo Niccoli's linear L* color scale, an implementation of a palette originally found in Kindlmann et al. 2002, as well as its decomposition in the HCL color space (aka CIELCH). This perceptually-based color scale features a linear increase in lightness (depicted in the grayscale version below the scale), while also changing hue.

I am not sure about the reason but, at least in my implementation and with my screen, a weird band is clearly visible near the middle of the green band, both in color and in grayscale.

This scale can be used to perform a color encoding of a linear variable, for example when displaying a heat map. Classic choices for such a case are rainbow scales (which have the problem of artefacts, making the encoding excessively non-linear) or grayscales (which are fine, but lack color information, which can be useful to perceive small changes or better identify areas with a similar value).

ROYGBIV, rainbow, linear L* and gray color scales compared

This comparison (taken from this Matteo Niccoli's post) shows the use of ROYGBIV, rainbow, linear L* and gray color scales to map elevation data.

All palette data is available as CSV and other formats from this link.

palette = d3.csv.parse("""
R,G,B
4,4,4
10,3,8
13,4,11
16,5,14
18,5,16
21,6,18
22,7,19
24,8,21
26,8,22
27,9,24
28,10,25
30,11,26
31,12,27
32,12,28
33,13,29
35,14,31
36,14,32
37,15,32
38,15,33
39,16,34
40,17,35
41,17,36
42,18,38
43,19,38
44,19,39
46,20,41
46,20,45
46,21,50
45,21,55
45,21,60
45,22,64
45,23,67
45,23,71
45,24,75
45,24,77
45,25,81
45,25,84
44,26,87
44,27,90
45,27,92
45,28,95
44,29,98
44,29,100
44,30,103
44,31,106
44,31,109
44,32,110
44,33,113
44,34,116
43,34,118
42,35,121
40,38,120
38,40,119
36,42,120
34,44,120
33,46,120
32,47,120
31,49,121
30,50,122
30,51,123
29,52,123
29,53,125
28,55,125
28,56,126
27,57,127
28,58,128
28,59,129
27,60,129
27,61,131
27,62,132
27,63,133
28,64,134
27,65,135
27,66,136
27,68,137
27,69,138
25,71,136
22,73,134
21,74,133
20,76,131
17,78,129
16,79,128
15,81,126
14,82,125
10,84,123
10,85,122
9,87,120
8,88,119
7,89,118
6,91,117
4,92,115
4,94,114
4,95,114
3,96,112
1,98,111
1,99,110
0,100,109
0,101,108
0,103,107
0,104,106
0,105,105
0,107,104
0,108,101
0,110,100
0,111,99
0,112,98
0,114,96
0,115,95
0,116,93
0,118,92
0,119,90
0,120,89
0,121,88
0,123,86
0,124,85
0,125,83
0,127,82
0,128,80
0,129,79
0,131,77
0,132,75
0,133,73
0,134,72
0,136,70
0,137,68
0,138,66
0,139,65
0,141,64
0,142,63
0,143,61
0,145,60
0,146,60
0,147,58
0,149,57
0,150,56
0,151,55
0,153,53
0,154,52
0,155,51
0,157,50
0,158,48
0,159,47
0,160,45
0,162,44
0,163,42
0,164,41
0,165,39
0,167,36
0,168,34
0,169,31
0,170,23
0,169,8
9,170,0
20,171,0
29,172,0
35,173,0
40,174,0
45,175,0
48,176,0
52,177,0
55,178,0
59,179,0
61,180,0
64,181,0
66,182,0
68,183,0
71,184,0
73,185,0
76,186,0
78,187,0
79,188,0
81,189,0
83,190,0
85,191,0
87,192,0
92,193,0
99,193,0
106,193,0
114,193,0
119,194,0
125,194,0
130,194,0
135,195,0
140,195,0
145,195,0
149,196,0
153,196,0
157,197,0
161,197,0
165,197,0
169,198,0
172,198,0
176,199,0
180,199,0
184,199,0
186,200,0
190,201,0
193,201,0
197,201,0
200,202,0
201,201,24
203,202,51
206,202,65
207,203,77
209,203,87
212,203,95
213,204,103
215,205,109
218,205,116
219,206,121
221,207,127
223,207,132
226,207,138
227,208,143
229,209,147
231,209,151
232,210,155
235,211,159
237,211,164
238,212,168
240,212,172
243,213,175
243,214,179
245,214,183
248,215,186
248,216,189
248,218,193
247,219,195
247,220,198
247,222,201
248,223,204
247,224,206
247,226,209
247,227,211
247,229,214
247,230,216
247,231,218
247,232,220
248,234,224
247,235,225
247,236,229
247,238,231
247,239,232
248,240,235
248,242,237
247,243,239
248,244,241
248,246,244
248,247,246
248,248,248
249,249,249
251,251,251
252,252,252
253,253,253
254,254,254
255,255,255
""").map (d) -> d3.rgb(d.R, d.G, d.B)
svg = d3.select('svg')
svg.selectAll('.colored_band')
.data(palette)
.enter().append('rect')
.attr('class', 'colored_band')
.attr('x', (d,i)->32+3.5*i)
.attr('y', 21)
.attr('width', 3.5)
.attr('height', 120)
.attr('fill', (d)->d )
svg.selectAll('.hue_band')
.data(palette)
.enter().append('rect')
.attr('class', 'hue_band')
.attr('x', (d,i)->32+3.5*i)
.attr('y', 166)
.attr('width', 3.5)
.attr('height', 60)
.attr('fill', (d) ->
hcl = d3.hcl(d)
hcl.c = 60
hcl.l = 60
return hcl
)
svg.selectAll('.chroma_band')
.data(palette)
.enter().append('rect')
.attr('class', 'chroma_band')
.attr('x', (d,i)->32+3.5*i)
.attr('y', 237)
.attr('width', 3.5)
.attr('height', 116)
.attr('fill', (d) ->
hcl = d3.hcl(d)
hcl.l = hcl.c/1.4
hcl.h = 80
hcl.c = 25
return hcl
)
svg.selectAll('.luminance_band')
.data(palette)
.enter().append('rect')
.attr('class', 'luminance_band')
.attr('x', (d,i)->32+3.5*i)
.attr('y', 364)
.attr('width', 3.5)
.attr('height', 116)
.attr('fill', (d) ->
hcl = d3.hcl(d)
hcl.c = 0
return hcl
)
x = d3.scale.linear()
.domain([0,255])
.range([32, 928])
h_y = d3.scale.linear()
.domain([0,360])
.range([166+60, 166])
c_y = d3.scale.linear()
.domain([0,150])
.range([237+116, 237])
l_y = d3.scale.linear()
.domain([0,100])
.range([364+116, 364])
h_line = d3.svg.line()
.x((_, i) -> x(i))
.y((d) ->
h = d3.hcl(d).h
return h_y( if h > 0 then h else h+360 )
)
c_line = d3.svg.line()
.x((_, i) -> x(i))
.y((d) -> c_y( d3.hcl(d).c ) )
l_line = d3.svg.line()
.x((_, i) -> x(i))
.y((d) -> l_y( d3.hcl(d).l ) )
svg.append('path')
.datum(palette)
.attr('class', 'hue')
.attr('d', h_line)
svg.append('path')
.datum(palette)
.attr('class', 'chroma')
.attr('d', c_line)
svg.append('path')
.datum(palette)
.attr('class', 'luminance')
.attr('d', l_line)
h_axis = d3.svg.axis()
.scale(h_y)
.tickValues([0,360])
.orient('right')
.tickSize(3,5)
c_axis = d3.svg.axis()
.scale(c_y)
.ticks(4)
.orient('right')
.tickSize(3,5)
l_axis = d3.svg.axis()
.scale(l_y)
.ticks(2)
.orient('right')
.tickSize(3,5)
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(932,0)')
.call(h_axis)
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(932,0)')
.call(c_axis)
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(932,0)')
.call(l_axis)
body {
background: #272822;
margin: 0;
padding: 0;
}
svg {
background: white;
}
.label {
text-anchor: middle;
font-size: 24px;
}
rect {
shape-rendering: crispEdges;
}
path {
fill: none;
}
path.hue {
stroke: white;
stroke-width: 1;
}
path.chroma {
stroke: #FF3D00;
stroke-width: 1;
}
path.luminance {
stroke: black;
stroke-width: 0.6;
}
.axis .domain, .axis .tick line {
stroke: black;
shape-rendering: crispEdges;
}
.axis {
font-size: 8px;
font-family: sans-serif;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="HCL decomposition: Linear L*" />
<title>HCL decomposition: Linear L*</title>
<link rel="stylesheet" href="index.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500" width="960">
<text class="label" x="16" y="198" dy="0.35em">H</text>
<text class="label" x="16" y="295" dy="0.35em">C</text>
<text class="label" x="16" y="420" dy="0.35em">L</text>
</svg>
<script src="index.js"></script>
</body>
</html>
(function() {
var c_axis, c_line, c_y, h_axis, h_line, h_y, l_axis, l_line, l_y, palette, svg, x;
palette = d3.csv.parse("R,G,B\n4,4,4\n10,3,8\n13,4,11\n16,5,14\n18,5,16\n21,6,18\n22,7,19\n24,8,21\n26,8,22\n27,9,24\n28,10,25\n30,11,26\n31,12,27\n32,12,28\n33,13,29\n35,14,31\n36,14,32\n37,15,32\n38,15,33\n39,16,34\n40,17,35\n41,17,36\n42,18,38\n43,19,38\n44,19,39\n46,20,41\n46,20,45\n46,21,50\n45,21,55\n45,21,60\n45,22,64\n45,23,67\n45,23,71\n45,24,75\n45,24,77\n45,25,81\n45,25,84\n44,26,87\n44,27,90\n45,27,92\n45,28,95\n44,29,98\n44,29,100\n44,30,103\n44,31,106\n44,31,109\n44,32,110\n44,33,113\n44,34,116\n43,34,118\n42,35,121\n40,38,120\n38,40,119\n36,42,120\n34,44,120\n33,46,120\n32,47,120\n31,49,121\n30,50,122\n30,51,123\n29,52,123\n29,53,125\n28,55,125\n28,56,126\n27,57,127\n28,58,128\n28,59,129\n27,60,129\n27,61,131\n27,62,132\n27,63,133\n28,64,134\n27,65,135\n27,66,136\n27,68,137\n27,69,138\n25,71,136\n22,73,134\n21,74,133\n20,76,131\n17,78,129\n16,79,128\n15,81,126\n14,82,125\n10,84,123\n10,85,122\n9,87,120\n8,88,119\n7,89,118\n6,91,117\n4,92,115\n4,94,114\n4,95,114\n3,96,112\n1,98,111\n1,99,110\n0,100,109\n0,101,108\n0,103,107\n0,104,106\n0,105,105\n0,107,104\n0,108,101\n0,110,100\n0,111,99\n0,112,98\n0,114,96\n0,115,95\n0,116,93\n0,118,92\n0,119,90\n0,120,89\n0,121,88\n0,123,86\n0,124,85\n0,125,83\n0,127,82\n0,128,80\n0,129,79\n0,131,77\n0,132,75\n0,133,73\n0,134,72\n0,136,70\n0,137,68\n0,138,66\n0,139,65\n0,141,64\n0,142,63\n0,143,61\n0,145,60\n0,146,60\n0,147,58\n0,149,57\n0,150,56\n0,151,55\n0,153,53\n0,154,52\n0,155,51\n0,157,50\n0,158,48\n0,159,47\n0,160,45\n0,162,44\n0,163,42\n0,164,41\n0,165,39\n0,167,36\n0,168,34\n0,169,31\n0,170,23\n0,169,8\n9,170,0\n20,171,0\n29,172,0\n35,173,0\n40,174,0\n45,175,0\n48,176,0\n52,177,0\n55,178,0\n59,179,0\n61,180,0\n64,181,0\n66,182,0\n68,183,0\n71,184,0\n73,185,0\n76,186,0\n78,187,0\n79,188,0\n81,189,0\n83,190,0\n85,191,0\n87,192,0\n92,193,0\n99,193,0\n106,193,0\n114,193,0\n119,194,0\n125,194,0\n130,194,0\n135,195,0\n140,195,0\n145,195,0\n149,196,0\n153,196,0\n157,197,0\n161,197,0\n165,197,0\n169,198,0\n172,198,0\n176,199,0\n180,199,0\n184,199,0\n186,200,0\n190,201,0\n193,201,0\n197,201,0\n200,202,0\n201,201,24\n203,202,51\n206,202,65\n207,203,77\n209,203,87\n212,203,95\n213,204,103\n215,205,109\n218,205,116\n219,206,121\n221,207,127\n223,207,132\n226,207,138\n227,208,143\n229,209,147\n231,209,151\n232,210,155\n235,211,159\n237,211,164\n238,212,168\n240,212,172\n243,213,175\n243,214,179\n245,214,183\n248,215,186\n248,216,189\n248,218,193\n247,219,195\n247,220,198\n247,222,201\n248,223,204\n247,224,206\n247,226,209\n247,227,211\n247,229,214\n247,230,216\n247,231,218\n247,232,220\n248,234,224\n247,235,225\n247,236,229\n247,238,231\n247,239,232\n248,240,235\n248,242,237\n247,243,239\n248,244,241\n248,246,244\n248,247,246\n248,248,248\n249,249,249\n251,251,251\n252,252,252\n253,253,253\n254,254,254\n255,255,255").map(function(d) {
return d3.rgb(d.R, d.G, d.B);
});
svg = d3.select('svg');
svg.selectAll('.colored_band').data(palette).enter().append('rect').attr('class', 'colored_band').attr('x', function(d, i) {
return 32 + 3.5 * i;
}).attr('y', 21).attr('width', 3.5).attr('height', 120).attr('fill', function(d) {
return d;
});
svg.selectAll('.hue_band').data(palette).enter().append('rect').attr('class', 'hue_band').attr('x', function(d, i) {
return 32 + 3.5 * i;
}).attr('y', 166).attr('width', 3.5).attr('height', 60).attr('fill', function(d) {
var hcl;
hcl = d3.hcl(d);
hcl.c = 60;
hcl.l = 60;
return hcl;
});
svg.selectAll('.chroma_band').data(palette).enter().append('rect').attr('class', 'chroma_band').attr('x', function(d, i) {
return 32 + 3.5 * i;
}).attr('y', 237).attr('width', 3.5).attr('height', 116).attr('fill', function(d) {
var hcl;
hcl = d3.hcl(d);
hcl.l = hcl.c / 1.4;
hcl.h = 80;
hcl.c = 25;
return hcl;
});
svg.selectAll('.luminance_band').data(palette).enter().append('rect').attr('class', 'luminance_band').attr('x', function(d, i) {
return 32 + 3.5 * i;
}).attr('y', 364).attr('width', 3.5).attr('height', 116).attr('fill', function(d) {
var hcl;
hcl = d3.hcl(d);
hcl.c = 0;
return hcl;
});
x = d3.scale.linear().domain([0, 255]).range([32, 928]);
h_y = d3.scale.linear().domain([0, 360]).range([166 + 60, 166]);
c_y = d3.scale.linear().domain([0, 150]).range([237 + 116, 237]);
l_y = d3.scale.linear().domain([0, 100]).range([364 + 116, 364]);
h_line = d3.svg.line().x(function(_, i) {
return x(i);
}).y(function(d) {
var h;
h = d3.hcl(d).h;
return h_y(h > 0 ? h : h + 360);
});
c_line = d3.svg.line().x(function(_, i) {
return x(i);
}).y(function(d) {
return c_y(d3.hcl(d).c);
});
l_line = d3.svg.line().x(function(_, i) {
return x(i);
}).y(function(d) {
return l_y(d3.hcl(d).l);
});
svg.append('path').datum(palette).attr('class', 'hue').attr('d', h_line);
svg.append('path').datum(palette).attr('class', 'chroma').attr('d', c_line);
svg.append('path').datum(palette).attr('class', 'luminance').attr('d', l_line);
h_axis = d3.svg.axis().scale(h_y).tickValues([0, 360]).orient('right').tickSize(3, 5);
c_axis = d3.svg.axis().scale(c_y).ticks(4).orient('right').tickSize(3, 5);
l_axis = d3.svg.axis().scale(l_y).ticks(2).orient('right').tickSize(3, 5);
svg.append('g').attr('class', 'axis').attr('transform', 'translate(932,0)').call(h_axis);
svg.append('g').attr('class', 'axis').attr('transform', 'translate(932,0)').call(c_axis);
svg.append('g').attr('class', 'axis').attr('transform', 'translate(932,0)').call(l_axis);
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment