class Charty::Plotters::CategoricalPlotter

Attributes

default_palette[R]
require_numeric[R]
dodge[R]
group_label[R]
group_names[R]
order[R]
orient[R]
plot_data[R]
saturation[R]
value_label[RW]
width[R]

Public Class Methods

default_palette=(val) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 7
def default_palette=(val)
  case val
  when :light, :dark
    @default_palette = val
  when "light", "dark"
    @default_palette = val.to_sym
  else
    raise ArgumentError, "default_palette must be :light or :dark"
  end
end
new(x, y, color, order: nil, orient: nil, width: 0.8r, dodge: false, **options, &block) click to toggle source
Calls superclass method Charty::Plotters::AbstractPlotter::new
# File lib/charty/plotters/categorical_plotter.rb, line 30
def initialize(x, y, color, order: nil, orient: nil, width: 0.8r, dodge: false, **options, &block)
  super

  setup_variables
  setup_colors
end
require_numeric=(val) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 20
def require_numeric=(val)
  case val
  when true, false
    @require_numeric = val
  else
    raise ArgumentError, "require_numeric must be ture or false"
  end
end

Public Instance Methods

dodge=(dodge) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 69
def dodge=(dodge)
  @dodge = check_boolean(dodge, :dodge)
end
order=(order) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 39
def order=(order)
  @order = order && Array(order).map(&method(:normalize_name))
end
orient=(orient) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 45
def orient=(orient)
  @orient = check_orient(orient)
end
saturation=(saturation) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 75
def saturation=(saturation)
  @saturation = check_saturation(saturation)
end
width=(val) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 63
def width=(val)
  @width = check_number(val, :width)
end

Private Instance Methods

annotate_axes(backend) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 353
        def annotate_axes(backend)
  backend.set_title(self.title) if self.title

  if orient == :v
    xlabel, ylabel = @group_label, @value_label
  else
    xlabel, ylabel = @value_label, @group_label
  end
  xlabel = self.x_label if self.x_label
  ylabel = self.y_label if self.y_label
  backend.set_xlabel(xlabel) unless xlabel.nil?
  backend.set_ylabel(ylabel) unless ylabel.nil?

  if orient == :v
    backend.set_xticks((0 ... @plot_data.length).to_a)
    backend.set_xtick_labels(@group_names)
  else
    backend.set_yticks((0 ... @plot_data.length).to_a)
    backend.set_ytick_labels(@group_names)
  end

  if orient == :v
    backend.disable_xaxis_grid
    backend.set_xlim(-0.5, @plot_data.length - 0.5)
  else
    backend.disable_yaxis_grid
    backend.set_ylim(-0.5, @plot_data.length - 0.5)
  end

  unless @color_names.nil?
    backend.legend(loc: :best, title: @color_title)
  end
end
check_orient(value) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 49
        def check_orient(value)
  case value
  when nil, :v, :h
    value
  when "v", "h"
    value.to_sym
  else
    raise ArgumentError,
          "invalid value for orient (#{value.inspect} for nil, :v, or :h)"
  end
end
check_saturation(value) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 79
        def check_saturation(value)
  case value
  when 0..1
    value
  when Numeric
    raise ArgumentError,
          "saturation is out of range (%p for 0..1)" % value
  else
    raise ArgumentError,
          "invalid value for saturation (%p for a value in 0..1)" % value
  end
end
color_offsets() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 333
        def color_offsets
  n_names = @color_names.length
  if self.dodge
    each_width = @width / n_names
    offsets = Charty::Linspace.new(0 .. (@width - each_width), n_names).to_a
    offsets_mean = Statistics.mean(offsets)
    offsets.map {|x| x - offsets_mean }
  else
    Array.new(n_names) { 0 }
  end
end
group_long_form(vals, groups, group_order) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 265
        def group_long_form(vals, groups, group_order)
  grouped_vals = vals.group_by(groups)

  plot_data = group_order.map do |g|
    grouped_vals[g] || Charty::Vector.new([])
  end

  if vals.respond_to?(:name)
    value_label = vals.name
  end

  return plot_data, value_label
end
infer_orient(x, y, orient, require_numeric) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 207
        def infer_orient(x, y, orient, require_numeric)
  x_type = x.nil? ? nil : variable_type(x)
  y_type = y.nil? ? nil : variable_type(y)

  nonnumeric_error = "%{orient} orientation requires numeric `%{dim}` variable"
  single_variable_warning = "%{orient} orientation ignored with only `%{dim}` specified"

  case
  when x.nil?
    case orient
    when :h
      warn single_variable_warning % {orient: "Horizontal", dim: "y"}
    end
    if require_numeric && y_type != :numeric
      raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
    end
    return :v
  when y.nil?
    case orient
    when :v
      warn single_variable_warning % {orient: "Vertical", dim: "x"}
    end
    if require_numeric && x_type != :numeric
      raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
    end
    return :h
  end

  case orient
  when :v
    if require_numeric && y_type != :numeric
      raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
    end
    return :v
  when :h
    if require_numeric && x_type != :numeric
      raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
    end
    return :h
  when nil
    case
    when x_type != :categorical && y_type == :categorical
      return :h
    when x_type != :numeric     && y_type == :numeric
      return :v
    when x_type == :numeric     && y_type != :numeric
      return :h
    when require_numeric && x_type != :numeric && y_type != :numeric
      raise ArgumentError, "Neither the `x` nor `y` variable appears to be numeric."
    else
      :v
    end
  else
    # must be unreachable
    raise RuntimeError, "BUG in Charty. Please report the issue."
  end
end
nested_width() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 345
        def nested_width
  if self.dodge
    @width / @color_names.length * 0.98r
  else
    @width
  end
end
normalize_name(value) click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 94
        def normalize_name(value)
  case value
  when String, Symbol
    value
  else
    value.to_str
  end
end
setup_colors() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 279
        def setup_colors
  if @color_names.nil?
    n_colors = @plot_data.length
  else
    n_colors = @color_names.length
  end

  if key_color.nil? && self.palette.nil?
    # Check the current palette has enough colors
    current_palette = Palette.default
    if n_colors <= current_palette.n_colors
      colors = Palette.new(current_palette.colors, n_colors).colors
    else
      # Use huls palette as default when the default palette is not usable
      colors = Palette.husl_colors(n_colors, l: 0.7r)
    end
  elsif self.palette.nil?
    if @color_names.nil?
      colors = Array.new(n_colors) { key_color }
    else
      raise NotImplementedError,
            "Default palette with key_color is not supported"
      # TODO: Support light_palette and dark_palette in red-palette
      # if default_palette is light
      #   colors = Palette.light_palette(key_color, n_colors)
      # elsif default_palette is dark
      #   colors = Palette.dark_palette(key_color, n_colors)
      # else
      #   raise "No default palette specified"
      # end
    end
  else
    case self.palette
    when Hash
      if @color_names.nil?
        levels = @group_names
      else
        levels = @color_names
      end
      colors = levels.map {|gn| self.palette[gn] }
    end
    colors = Palette.new(colors, n_colors).colors
  end

  if saturation < 1
    colors = Palette.new(colors, n_colors, desaturate_factor: saturation).colors
  end

  @colors = colors.map {|c| c.to_rgb }
  lightness_values = @colors.map {|c| c.to_hsl.l }
  lum = lightness_values.min * 0.6r
  @gray = Colors::RGB.new(lum, lum, lum)  # TODO: Use Charty::Gray
end
setup_single_data() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 202
        def setup_single_data
  raise NotImplementedError,
        "Single data plot is not supported yet"
end
setup_variables() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 107
        def setup_variables
  if x.nil? && y.nil?
    @input_format = :wide
    setup_variables_with_wide_form_dataset
  else
    @input_format = :long
    setup_variables_with_long_form_dataset
  end
end
setup_variables_with_long_form_dataset() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 146
        def setup_variables_with_long_form_dataset
  x = self.x
  y = self.y
  color = self.color
  if @data
    x &&= @data[x] || x
    y &&= @data[y] || y
    color &&= @data[color] || color
  end

  # Validate inputs
  [x, y, color].each do |input|
    next if input.nil? || array?(input)
    raise RuntimeError,
          "Could not interpret input `#{input.inspect}`"
  end

  x = Charty::Vector.try_convert(x)
  y = Charty::Vector.try_convert(y)
  color = Charty::Vector.try_convert(color)

  self.orient = infer_orient(x, y, orient, self.class.require_numeric)

  if x.nil? || y.nil?
    setup_single_data
  else
    if orient == :v
      groups, vals = x, y
    else
      groups, vals = y, x
    end

    if groups.respond_to?(:name)
      @group_label = groups.name
    end

    @group_names = groups.categorical_order(order)
    @plot_data, @value_label = group_long_form(vals, groups, @group_names)

    # Handle color variable
    if color.nil?
      @plot_colors = nil
      @color_title = nil
      @color_names = nil
    else
      # Get the order of color levels
      @color_names = color.categorical_order(color_order)

      # Group the color data
      @plot_colors, @color_title = group_long_form(color, groups, @group_names)
    end

    # TODO: Handle units
  end
end
setup_variables_with_wide_form_dataset() click to toggle source
# File lib/charty/plotters/categorical_plotter.rb, line 117
        def setup_variables_with_wide_form_dataset
  if @color
    raise ArgumentError,
          "Cannot use `color` without `x` or `y`"
  end

  # No color grouping with wide inputs
  @plot_colors = nil
  @color_title = nil
  @color_names = nil

  # No statistical units with wide inputs
  @plot_units = nil

  @value_label = nil
  @group_label = nil

  order = @order # TODO: supply order via parameter
  unless order
    order = @data.column_names.select do |cn|
      # TODO: Use Charty::Vector#numeric?
      @data[cn].all? {|x| Float(x, exception: false) }
    end
  end
  order ||= @data.column_names
  @plot_data = order.map {|cn| @data[cn] }
  @group_names = order
end