Class: AnalyticsCharts::CustomPie

Inherits:
Object
  • Object
show all
Includes:
Magick
Defined in:
lib/analytics_charts/custom_pie.rb

Direct Known Subclasses

PieAndLabels

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(image_path, label_hash, pie_label_hash) ⇒ CustomPie

Returns a new instance of CustomPie.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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

Instance Attribute Details

#label_hashObject

Returns the value of attribute label_hash.



8
9
10
# File 'lib/analytics_charts/custom_pie.rb', line 8

def label_hash
  @label_hash
end

#label_offsetObject

Returns the value of attribute label_offset.



12
13
14
# File 'lib/analytics_charts/custom_pie.rb', line 12

def label_offset
  @label_offset
end

#label_start_xObject

Returns the value of attribute label_start_x.



10
11
12
# File 'lib/analytics_charts/custom_pie.rb', line 10

def label_start_x
  @label_start_x
end

#label_start_yObject

Returns the value of attribute label_start_y.



11
12
13
# File 'lib/analytics_charts/custom_pie.rb', line 11

def label_start_y
  @label_start_y
end

#pie_center_xObject

Returns the value of attribute pie_center_x.



5
6
7
# File 'lib/analytics_charts/custom_pie.rb', line 5

def pie_center_x
  @pie_center_x
end

#pie_center_yObject

Returns the value of attribute pie_center_y.



6
7
8
# File 'lib/analytics_charts/custom_pie.rb', line 6

def pie_center_y
  @pie_center_y
end

#pie_label_hashObject

Returns the value of attribute pie_label_hash.



9
10
11
# File 'lib/analytics_charts/custom_pie.rb', line 9

def pie_label_hash
  @pie_label_hash
end

#pie_radiusObject

Returns the value of attribute pie_radius.



7
8
9
# File 'lib/analytics_charts/custom_pie.rb', line 7

def pie_radius
  @pie_radius
end

Instance Method Details

#drawObject



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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_labelsObject



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# 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) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# 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) ⇒ Object



45
46
47
# File 'lib/analytics_charts/custom_pie.rb', line 45

def highest_score(index, score)
  @thresholds[index] = score
end

#insert_pie_data(name, amount, quality) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
# 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 = {}) ⇒ Object



91
92
93
94
95
96
97
98
# 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 = {}) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
# 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_attributesObject



67
68
69
# File 'lib/analytics_charts/custom_pie.rb', line 67

def label_attributes
  @label_attributes ||= {}
end

#label_attributes=(label = {}) ⇒ Object



63
64
65
# File 'lib/analytics_charts/custom_pie.rb', line 63

def label_attributes=(label = {})
  @label_attributes = label
end

#label_heightObject



75
76
77
# File 'lib/analytics_charts/custom_pie.rb', line 75

def label_height
  @label_height ||= 12
end

#label_height=(height) ⇒ Object



71
72
73
# File 'lib/analytics_charts/custom_pie.rb', line 71

def label_height=(height)
  @label_height = height
end

#process_pie_label_data(cluster_data_value) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# 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_labelsObject



306
307
308
309
310
# 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) ⇒ Object



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# 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) ⇒ Object



29
30
31
32
33
# 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) ⇒ Object



41
42
43
# File 'lib/analytics_charts/custom_pie.rb', line 41

def set_pie_colors(list)
  @colors = list
end

#set_pie_geometry(x, y, radius) ⇒ Object



35
36
37
38
39
# 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') ⇒ Object



214
215
216
217
218
# File 'lib/analytics_charts/custom_pie.rb', line 214

def write(filename='graph.png')
  draw
  draw_labels
  @base_image.write(filename)
end