🕷 zenspider.com

by ryan davis



sitemap
Looking for the Ruby Quickref?

Bold Colors

Published 2010-11-02 @ 15:11

Tagged toys

I often create complex visualizations using graphviz and my graph gem (I still mourn vcg–vastly superior, but unsupported). I often want to use color to help with the visualization, but picking colors that are distinct is hard.

GV by default supports X11 colors, which have a ton of useless gradients like antiquewhite[1-4], aquamarine[1-4], azure[1-4], etc. Further, many of these colors are simply unusable on a white background. Anything pale or yellow just seems to disappear. I decided to see what I could do to fix that.

There are 655 colors in the X11 color list (for GV, at least). The easiest thing to do is to strip all numbered colors. That brings drops me way down to 141 colors. Much more manageable, but many of those are pale weak colors that don’t hold up to a white background.

So at this point, the best thing that I could think of was to convert all the RGB values to HSV and strip out all the unsaturated (and all yellow) colors. I also wanted to spread out like colors as much as possible so that I get maximum contrast as I walk through and use the colors in a graph. With those parameters, I wind up with 47 stark spread out colors:

% ./colors.rb 
47
%w[black brown mediumblue blueviolet orange magenta darkgreen
   maroon violetred purple greenyellow deeppink midnightblue
   firebrick darkturquoise mediumspringgreen chartreuse navy
   lightseagreen chocolate lawngreen green indigo darkgoldenrod
   darkviolet red springgreen saddlebrown mediumvioletred
   goldenrod tomato cyan forestgreen darkorchid crimson coral
   deepskyblue seagreen peru turquoise orangered dodgerblue sienna
   limegreen royalblue darkorange blue]

Here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def rgb2hsb(r, g, b)
  h     = 0.0
  min   = [r, g, b].min
  max   = [r, g, b].max
  delta = (max - min).to_f

  v = max # naming v to not collide w/ blue

  s = if max != 0 then
        255.0 * delta / max
      else
        0.0
      end

  h = if s != 0 then
        if r == max then
          0.0 + (g - b) / delta
        elsif g == max then
          2.0 + (b - r) / delta
        elsif b == max then
          4.0 + (r - g) / delta
        end
      else
        -1.0
      end

  h *= 60
  h += 360.0 if h < 0
  s *= 100.0 / 255
  v *= 100.0 / 255

  return [h, s, v]
end

seen = {}
good = {}

DATA.readlines.map { |l| l.split }.each do |rgb, name|
  next if name =~ /\d$/
  next if seen[rgb]

  h, s, v = rgb2hsb(*rgb.scan(/../).map { |s| s.to_i(16) })

  next unless s > 66.67 unless v == 0 # skip weak colors, black is not weak
  next if (50..80).include? h         # skip yellowish colors, weak on white

  seen[rgb] = true
  good[name] = [h, s, v]
end

names = good.sort_by { |n, (h, s, v)| [v, h] }.map { |n, _| n }

# 6 colors on the color hexagon, but we took out yellow. Spread out
# all colors as evenly as possible. We can't transpose non-rectangular
# matricies, so we have to fill it in with nils and remove them later.

bucket_size, leftover = names.size.divmod 5
groups = names.enum_slice(bucket_size).to_a
filler = [nil] * (bucket_size - leftover) # must be rectangular to transpose
groups.last.push(*filler)
groups = groups.transpose.flatten.compact

p groups.size
puts "%w[#{groups.join(" ")}]"

__END__
# ...huge color list excluded...