Class: Palette

Inherits:
Object
  • Object
show all
Includes:
Constants
Defined in:
lib/palette.rb,
lib/palette/colors.rb,
lib/palette/helper.rb,
lib/palette/version.rb,
lib/palette/constants.rb,
lib/palette/statistics.rb

Defined Under Namespace

Modules: Constants, Helper, Statistics

Constant Summary collapse

VERSION =
"0.5.0"

Constants included from Constants

Constants::MPL_QUAL_PALS, Constants::QUAL_PALETTE_SIZES, Constants::SEABORN_PALETTES

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(palette = nil, n_colors = nil, desaturate_factor: nil) ⇒ Palette

Return a list of colors defining a color palette

Parameters:

  • palette (nil, String, Palette) (defaults to: nil)

    Name of palette or nil to return current palette. If a Palette is given, input colors are used but possibly cycled and desaturated.

  • n_colors (Integer, nil) (defaults to: nil)

    Number of colors in the palette. If nil, the default will depend on how palette is specified. Named palettes default to 6 colors, but grabbing the current palette or passing in a list of colors will not change the number of colors unless this is specified. Asking for more colors than exist in the palette cause it to cycle.

  • desaturate_factor (Float, nil) (defaults to: nil)

    Propotion to desaturate each color by.



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
68
69
70
71
# File 'lib/palette.rb', line 28

def initialize(palette=nil, n_colors=nil, desaturate_factor: nil)
  case
  when palette.nil?
    @name = nil
    palette = Colors::ColorData::DEFAULT_COLOR_CYCLE
    n_colors ||= palette.length
  else
    palette = normalize_palette_name(palette)
    case palette
    when String
      @name = palette
      # Use all colors in a qualitative palette or 6 of another kind
      n_colors ||= QUAL_PALETTE_SIZES.fetch(palette, 6)
      case @name
      when SEABORN_PALETTES.method(:has_key?).to_proc # NOTE: to_proc needs for Ruby 2.4
        palette = self.class.seaborn_colors(@name)
      when "hls", "HLS", "hsl", "HSL"
        palette = self.class.hsl_colors(n_colors)
      when "husl", "HUSL"
        palette = self.class.husl_colors(n_colors)
      when /\Ach:/
        # Cubehelix palette with params specified in string
        args, kwargs = parse_cubehelix_args(palette)
        palette = self.class.cubehelix_colors(n_colors, *args, **kwargs)
      else
        begin
          palette = self.class.matplotlib_colors(palette, n_colors)
        rescue ArgumentError
          raise ArgumentError,
                "#{palette} is not a valid palette name"
        end
      end
    else
      n_colors ||= palette.length
    end
  end
  if desaturate_factor
    palette = palette.map {|c| Colors.desaturate(c, desaturate_factor) }
  end

  # Always return as many colors as we asked for
  @colors = palette.cycle.take(n_colors).freeze
  @desaturate_factor = desaturate_factor
end

Class Attribute Details

.defaultObject

Returns the value of attribute default.



169
170
171
# File 'lib/palette.rb', line 169

def default
  @default
end

Instance Attribute Details

#colorsObject (readonly)

Returns the value of attribute colors.



109
110
111
# File 'lib/palette.rb', line 109

def colors
  @colors
end

#desaturate_factorObject (readonly)

Returns the value of attribute desaturate_factor.



109
110
111
# File 'lib/palette.rb', line 109

def desaturate_factor
  @desaturate_factor
end

#nameObject (readonly)

Returns the value of attribute name.



109
110
111
# File 'lib/palette.rb', line 109

def name
  @name
end

Class Method Details

.blend_colors(colors, n_colors = 6, as_cmap: false) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/palette/colors.rb', line 110

def self.blend_colors(colors, n_colors=6, as_cmap: false)
  name = "blend"
  cmap = Colors::LinearSegmentedColormap.new_from_list(name, colors)
  if as_cmap
    return cmap
  else
    bins = Helper.linspace(0r, 1r, Integer(n_colors))
    colors = cmap[bins]
    return colors
  end
end

.cubehelix_colors(n_colors = 6, start: 0, rot:, gamma:, hue:, light:, dark:, reverse: false) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/palette/colors.rb', line 56

def self.cubehelix_colors(n_colors=6, start: 0, rot: 0.4r, gamma: 1.0r, hue: 0.8r,
                           light: 0.85r, dark: 0.15r, reverse: false)
  get_color_function = ->(p0, p1) do
    color = -> (x) do
      xg = x ** gamma
      a = hue * xg * (1 - xg) / 2
      phi = 2 * Math::PI * (start / 3 + rot * x)
      return xg + a * (p0 * Math.cos(phi) + p1 * Math.sin(phi))
    end
    return color
  end

  segmented_data = {
    red:   get_color_function.(-0.14861, 1.78277),
    green: get_color_function.(-0.29227, -0.90649),
    blue:  get_color_function.(1.97294, 0.0),
  }

  cmap = Colors::LinearSegmentedColormap.new("cubehelix", segmented_data)

  x = Helper.linspace(light, dark, n_colors)
  pal = cmap[x]
  pal.reverse! if reverse
  pal
end

.hsl_colors(n_colors = 6, h:, s:, l:) ⇒ Array<Colors::HSL>

Get a set of evenly spaced colors in HSL hue space.

Parameters:

  • n_colors (Integer) (defaults to: 6)

    The number of colors in the palette

  • h (Numeric)

    The hue value of the first color in degree

  • s (Numeric)

    The saturation value of the first color (between 0 and 1)

  • l (Numeric)

    The lightness value of the first color (between 0 and 1)

Returns:

  • (Array<Colors::HSL>)

    The array of colors



23
24
25
26
27
28
29
30
31
# File 'lib/palette/colors.rb', line 23

def self.hsl_colors(n_colors=6, h: 3.6r, s: 0.65r, l: 0.6r)
  hues = Helper.linspace(0, 1, n_colors + 1)[0...-1]
  hues.each_index do |i|
    hues[i] += (h/360r).to_f
    hues[i] %= 1
    hues[i] -= hues[i].to_i
  end
  (0...n_colors).map {|i| Colors::HSL.new(hues[i]*360r, s, l) }
end

.husl_colors(n_colors = 6, h:, s:, l:) ⇒ Array<Colors::HSL>

Get a set of evenly spaced colors in HUSL hue space.

Parameters:

  • n_colors (Integer) (defaults to: 6)

    The number of colors in the palette

  • h (Numeric)

    The hue value of the first color in degree

  • s (Numeric)

    The saturation value of the first color (between 0 and 1)

  • l (Numeric)

    The lightness value of the first color (between 0 and 1)

Returns:

  • (Array<Colors::HSL>)

    The array of colors



46
47
48
49
50
51
52
53
54
# File 'lib/palette/colors.rb', line 46

def self.husl_colors(n_colors=6, h: 3.6r, s: 0.9r, l: 0.65r)
  hues = Helper.linspace(0, 1, n_colors + 1)[0...-1]
  hues.each_index do |i|
    hues[i] += (h/360r).to_f
    hues[i] %= 1
    hues[i] *= 359
  end
  (0...n_colors).map {|i| Colors::HUSL.new(hues[i], s, l) }
end

.matplotlib_colors(name, n_colors = 6, as_cmap: false) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/palette/colors.rb', line 82

def self.matplotlib_colors(name, n_colors=6, as_cmap: false)
  if name.end_with?("_d")
    sub_name = name[0..-2]
    if sub_name.end_with?("_r")
      reverse = true
      sub_name = sub_name[0..-2]
    else
      reverse = false
    end
    colors = Palette.new(sub_name, 2).colors
    colors << Colors::RGB.parse("#333333")
    colors.reverse! if reverse
    cmap = blend_cmap(colors, n_colors, as_cmap: true)
  else
    cmap = Colors::ColormapRegistry[name]
  end

  return cmap if as_cmap

  bins = if MPL_QUAL_PALS.include?(name)
           Helper.linspace(0r, 1r, MPL_QUAL_PALS[name])[0, n_colors]
         else
           Helper.linspace(0r, 1r, Integer(n_colors) + 2)[1...-1]
         end
  colors = cmap[bins]
  return colors
end

.seaborn_colors(name) ⇒ Object



4
5
6
7
8
# File 'lib/palette/colors.rb', line 4

def self.seaborn_colors(name)
  SEABORN_PALETTES[name].map do |hex_string|
    Colors::RGB.parse(hex_string)
  end
end

Instance Method Details

#==(other) ⇒ Object

Two palettes are equal if they have the same colors, even if they have the different names and different desaturate factors.



117
118
119
120
121
122
123
124
# File 'lib/palette.rb', line 117

def ==(other)
  case other
  when Palette
    colors == other.colors
  else
    super
  end
end

#[](i) ⇒ Object



126
127
128
# File 'lib/palette.rb', line 126

def [](i)
  @colors[i % n_colors]
end

#n_colorsObject



111
112
113
# File 'lib/palette.rb', line 111

def n_colors
  @colors.length
end

#to_aryObject



134
135
136
# File 'lib/palette.rb', line 134

def to_ary
  @colors.dup
end

#to_colormap(n = self.n_colors) ⇒ Object



130
131
132
# File 'lib/palette.rb', line 130

def to_colormap(n=self.n_colors)
  Colors::ListedColormap.new(colors, n_colors: n)
end

#to_irubyObject



138
139
140
# File 'lib/palette.rb', line 138

def to_iruby
  ["image/svg", to_svg]
end

#to_svgObject



142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/palette.rb', line 142

def to_svg
  w = 55
  n = n_colors
  svg = %Q[<svg width="#{n*w}" height="#{w}">]
  @colors.each_with_index do |color, i|
    hex = color.to_rgb.to_hex_string
    svg << %Q[<rect x="#{i*w}" y="0" width="#{w}" height="#{w}" style="fill:#{hex};]
    svg << %Q[stroke-width:2;stroke:rgb(255,255,255)"/>]
  end
  svg << %Q[</svg>]
  svg
end