class AnalyticsCharts::CustomPie

Attributes

label_hash[RW]
label_offset[RW]
label_start_x[RW]
label_start_y[RW]
pie_center_x[RW]
pie_center_y[RW]
pie_label_hash[RW]
pie_radius[RW]

Public Class Methods

new(image_path, label_hash, pie_label_hash) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 14
def initialize(image_path, label_hash, pie_label_hash)
  @base_image = Image.read(image_path)[0]
  @columns = @base_image.columns
  @rows = @base_image.rows
  @d = Draw.new
  @data = Hash.new # Value is array with two items
  @aggregate = Array([0,0,0,0]) # Cluster brands into categories
  @thresholds = Array(["","","",""]) # Will populate with integer thresholds
  @label_hash = Hash.new
  @pie_label_hash = Hash.new
  @label_hash = label_hash if label_hash
  @pie_label_hash = pie_label_hash if pie_label_hash
  set_pie_colors(%w(#AD1F25 #BE6428 #C1B630 #1E753B #FFFFFF))
end

Public Instance Methods

draw() click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 100
def draw
  total_sum = @aggregate.inject(:+) + 0.0 # Sum elements and make it a float
  if total_sum == 0
    @d.stroke_width(2)
    @d = @d.stroke "#FFFFFF"
    @d = @d.fill "#000000"
    start_x, start_y = @pie_center_x + @pie_radius, @pie_center_y - @pie_radius
    end_x, end_y = @pie_center_x - @pie_radius, @pie_center_y + @pie_radius
    @d = @d.arc(start_x, start_y, end_x, end_y, 0, 360)
    @d.draw(@base_image)
    # If we don't refresh draw, future "@d.draw(@base_image)" will redraw the circle,
    # overlapping on the text written below
    @d = Draw.new
    insert_text(@pie_center_x - 30, @pie_center_y, "No Data",
      @label_hash.merge({'fill'=> '#FFFFFF', 'font_weight'=> 700 }))
    return
  end
  if @data.size > 0
    @d.stroke_width(@pie_radius)
    prev_degrees = 60.0
    @d.fill_opacity(0) # VERY IMPORTANT, otherwise undesired artifact can result.
    degrees = Array([0,0,0,0])
    label_offset_degrees =  Array([0,0,0,0])
    @aggregate.each_with_index do |data_row, index|
      degrees[index] = (data_row / total_sum) * 360.0
    end
    num_small_slices = 0
    small_slice_index = Array([0,0,0,0])
    for i in 0..3
      if degrees[i] != 0 and degrees[i] < 18.0
        num_small_slices += 1
        small_slice_index[i] = 1
      end
    end
    for i in 0..3 # First draw slices
      next if degrees[i] == 0
      @d = @d.stroke @colors[i]
      # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
      # therefore, in order to draw a circle of the proper diameter we must center the stroke at
      # half the radius for both x and y
      @d = @d.ellipse(@pie_center_x, @pie_center_y,
                @pie_radius / 2.0, @pie_radius / 2.0,
                prev_degrees, prev_degrees + degrees[i] + 1.0) # <= +0.5 'fudge factor' gets rid of the ugly gaps
      prev_degrees += degrees[i]
    end
    # If less than two small slices, or there are two small slices that are not adjacent
    if num_small_slices < 2 or (num_small_slices == 2 and small_slice_index[0] == small_slice_index[2])
      #Do nothing
    # If two adjacent small slices, push them apart. Non-adjacent case is taken care of above.
    # I also push back the other labels too. The logic is condensed. To see original logic,
    # consult appendix.html
    elsif num_small_slices == 2
      if small_slice_index[1] == 1
        label_offset_degrees[0] = -15
        label_offset_degrees[2] = 15
      else
        label_offset_degrees[0] = 15
        label_offset_degrees[2] = -15
      end
      if small_slice_index[2] == 1
        label_offset_degrees[1] = -15
        label_offset_degrees[3] = 15
      else
        label_offset_degrees[1] = 15
        label_offset_degrees[3] = -15
      end
    # In this case, push apart only the outside small slices.
    elsif num_small_slices == 3
      if small_slice_index[0] == 0
        label_offset_degrees[1] = -15
        label_offset_degrees[3] = 15
      elsif small_slice_index[1] == 0
        label_offset_degrees[2] = -15
        label_offset_degrees[0] = 15
      elsif small_slice_index[2] == 0
        label_offset_degrees[3] = -15
        label_offset_degrees[1] = 15
      elsif small_slice_index[3] == 0
        label_offset_degrees[0] = -15
        label_offset_degrees[2] = 15
      end
    end
    prev_degrees = 60.0 # Now focus on labels
    @aggregate.each_with_index do |cluster_data_value, i|
      next if degrees[i] == 0
      half_angle = prev_degrees + degrees[i] / 2
      label_string = process_pie_label_data(cluster_data_value)
      draw_pie_label(@pie_center_x,@pie_center_y, half_angle + label_offset_degrees[i],
        @pie_radius, label_string, i)
      prev_degrees += degrees[i]
    end
  end
end
draw_labels() click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 220
def draw_labels
  @d.align = LeftAlign
  sorted_data = @data.sort_by{|key,value| -value[1]} # Sort by descending quality
  x_offset = @label_start_x + 15
  y_offset = @label_start_y
  @label_hash = @label_hash.merge(label_attributes)
  for data in sorted_data
    has_data = false
    if data[1][0] > 0 # Amount > 0
      text = data[0]
      has_data = true
    else
      text = data[0]
    end
    if has_data
      case data[1][1]
      when 3
        # label_hash gets merged and overrided by fill and font_weight.
        insert_text_with_circle(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#1E753B'}))
      when 2
        insert_text_with_circle(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#C1B630'}))
      when 1
        insert_text_with_circle(x_offset, y_offset, text,
         @label_hash.merge({'fill'=> '#BE6428'}))
      when 0
        insert_text_with_circle(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#AD1F25'}))
      end
    else
      case data[1][1]
      when 3
        # label_hash gets merged and overrided by fill and font_weight.
        insert_text(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#1E753B'}))
      when 2
        insert_text(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#C1B630'}))
      when 1
        insert_text(x_offset, y_offset, text,
         @label_hash.merge({'fill'=> '#BE6428'}))
      when 0
        insert_text(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#AD1F25'}))
      end
    end
    y_offset += self.label_height
  end
  insert_text_with_circle(x_offset, y_offset, '= purchased by you',
    @label_hash.merge({'fill'=> '#252525', 'font_weight'=> 900 }))
end
draw_pie_label(center_x, center_y, angle, radius, percent, index) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 273
def draw_pie_label(center_x, center_y, angle, radius, percent, index)
  #気を付けて、get_type_metrics depends on font and pointsize, image res, AND font_weight so need to set those first
  # See more at http://studio.imagemagick.org/RMagick/doc/draw.html#get_type_metrics
  @d.font = @pie_label_hash['font'] if @pie_label_hash['font']
  @d.pointsize = @pie_label_hash['pointsize'] if @pie_label_hash['pointsize']
  ascent =  @d.get_type_metrics(@base_image, percent.to_s).ascent
  descent =  @d.get_type_metrics(@base_image, percent.to_s).descent
  width = @d.get_type_metrics(@base_image, percent.to_s).width
  radians = angle * Math::PI / 180.0
  x = center_x +  radius * Math.cos(radians)
  # By default, text is centered at bottom, so need to shift vertically to center it
  y =  center_y + ascent / 2.0 + radius * Math.sin(radians)
  # Imagine the text box around the text
  # Shift text box so a corner is tangent to circle
  if x > center_x
    x += width / 2.0 + 6
  end
  if x < center_x
    x -= width / 2.0 + 6
  end
  if y > center_y
    y += ascent / 2.0 + 6
  end
  if y < center_y
    y -= ascent / 2.0 + 6
    # descent value retrieved is negative, so sub instead of add
  end
  @d.align = CenterAlign

  # Provide default fill of black
  insert_text(x, y, percent, {'fill'=> @colors[index]}.merge(@pie_label_hash))# {'fill'=> 'black', 'font_weight'=> 700, 'pointsize'=>48})
end
highest_score(index, score) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 45
def highest_score(index, score)
  @thresholds[index] = score
end
insert_pie_data(name, amount, quality) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 49
def insert_pie_data(name, amount, quality)
  #convert all '&#39; instances to an apostrophe
  name = name.gsub(/&#39;/, "\'")
  # Figure out whether to give name a 0,1,2, or 3
  [0,1,2,3].each do |rank|
    next if @thresholds[rank].is_a?(String)
    if quality <= @thresholds[rank]
      @data[name] =  [amount, rank]
      @aggregate[rank] += amount
      break
    end
  end
end
insert_text(x_offset, y_offset, text, features = {}) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 91
def insert_text(x_offset, y_offset, text, features = {})
  features.each { |feature, attribute|
    set_feature(feature, attribute)
  }
  # Double quotes automatically escaped in rails db. Refer to Rmagick doc for escaping stuff
  text = text.gsub(/['%]/, '%' => '%%', "'" => "\'")
  @d.annotate(@base_image, 0 ,0, x_offset, y_offset, text)
end
insert_text_with_circle(x_offset, y_offset, text, features = {}) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 79
def insert_text_with_circle(x_offset, y_offset, text, features = {})
  insert_text(x_offset, y_offset, text, features)
  height = @d.get_type_metrics(@base_image, text).ascent
  y_offset -= height / 2
  circle_xpos = x_offset - 10
  radius = 5
  @d.stroke_width(radius)
  @d.stroke features["fill"] unless features["fill"].nil?
  @d = @d.ellipse(circle_xpos, y_offset,
    radius / 2.0, radius / 2.0, 0, 400) # Need bigger overlap for smaller circle
end
label_attributes() click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 67
def label_attributes
  @label_attributes ||= {}
end
label_attributes=(label = {}) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 63
def label_attributes=(label = {})
  @label_attributes = label
end
label_height() click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 75
def label_height
  @label_height ||= 12
end
label_height=(height) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 71
def label_height=(height)
  @label_height = height
end
process_pie_label_data(cluster_data_value) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 194
def process_pie_label_data(cluster_data_value)
  if cluster_data_value < 10**3
    return '$' + cluster_data_value.round(0).to_s
  elsif cluster_data_value < 10**6
    cluster_data_value /= 10**3 # Truncates decimals
    return '$' + cluster_data_value.round(0).to_s + 'K'# Round is unnecessary, but add anyways
  elsif cluster_data_value < 10**9
    cluster_data_value /= 10**6
    return '$' + cluster_data_value.round(0).to_s  + 'M'
  elsif cluster_data_value < 10**12
    cluster_data_value /= 10**9
    return '$' + cluster_data_value.round(0).to_s + 'B'
  elsif cluster_data_value < 10**15
    cluster_data_value /= 10**12
    return '$' + cluster_data_value.round(0).to_s + 'T'
  else
    return '>1T'
  end
end
recalibrate_metrics_for_labels() click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 306
def recalibrate_metrics_for_labels
  label_attributes.each { |feature, attribute|
    set_feature(feature, attribute)
  }
end
set_feature(feature, attribute) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 311
def set_feature(feature, attribute)
  begin
    case feature
      when 'fill'
        @d.fill = attribute
      when 'font'
        @d.font = attribute
      when 'font_family'
        @d.font_family = attribute
      when 'font_stretch'
        @d.font_stretch = attribute
      when 'font_style'
        @d.font_style = attribute
      when 'font_weight'
        @d.font_weight = attribute
      when 'stroke'
        @d.stroke = attribute
      when 'pointsize'
        @d.pointsize = attribute
      when 'text_undercolor'
        @d.undercolor = attribute
    end
  rescue
    puts "Tried to set #{feature} to #{attribute}"
    puts $!, $@
  end
end
set_label_values(label_start_x, label_start_y, label_offset) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 29
def set_label_values(label_start_x, label_start_y, label_offset)
  @label_start_x = label_start_x
  @label_start_y = label_start_y
  @label_offset = label_offset
end
set_pie_colors(list) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 41
def set_pie_colors(list)
  @colors = list
end
set_pie_geometry(x, y, radius) click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 35
def set_pie_geometry(x, y, radius)
  @pie_center_x = x
  @pie_center_y = y
  @pie_radius = radius
end
write(filename='graph.png') click to toggle source
# File lib/analytics_charts/custom_pie.rb, line 214
def write(filename='graph.png')
  draw
  draw_labels
  @base_image.write(filename)
end