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.



13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/analytics_charts/custom_pie.rb', line 13

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
  @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



70
71
72
73
74
75
76
77
78
79
80
81
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
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
# File 'lib/analytics_charts/custom_pie.rb', line 70

def draw
  total_sum = @aggregate.inject(:+) + 0.0 # Sum elements and make it a float
  if total_sum == 0
    @d.stroke_width(@pie_radius)
    @d = @d.stroke "#FFFFFF"
    @d = @d.ellipse(@pie_center_x, @pie_center_y,
                @pie_radius / 2.0, @pie_radius / 2.0,
                0, 360 + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
    @d = @d.stroke "#000000"
    @d.stroke_width(5)
    @d = @d.line(@pie_center_x-30,@pie_center_y-30,@pie_center_x-14,@pie_center_y-14)
    @d = @d.line(@pie_center_x-30,@pie_center_y-14,@pie_center_x-14,@pie_center_y-30)
    @d = @d.line(@pie_center_x+14,@pie_center_y-30,@pie_center_x+30,@pie_center_y-14)
    @d = @d.line(@pie_center_x+14,@pie_center_y-14,@pie_center_x+30,@pie_center_y-30)
    @d.stroke_width(10)
    @d = @d.ellipse(@pie_center_x, @pie_center_y + 20,
                5,5,
                0, 360) # <= +0.5 'fudge factor' gets rid of the ugly gaps
    draw_pie_label(@pie_center_x,@pie_center_y, 45,
        @pie_radius, "$0", 4)
    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] + 0.5) # <= +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 |data_row, i|
      next if degrees[i] == 0
      half_angle = prev_degrees + degrees[i] / 2
      label_string = '$' + data_row.round(2).to_s
      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



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/analytics_charts/custom_pie.rb', line 175

def draw_labels
  @d.align = LeftAlign
  sorted_data = @data.sort_by{|key,value| -value[1]} # Sort by descending quality
  x_offset = @label_start_x
  y_offset = @label_start_y
  for data in sorted_data
    has_data = false
    if data[1][0] > 0 # Amount > 0
      font_weight = 900 # Very Bold
      text = data[0]
      has_data = true
    else
      text = data[0]
      font_weight = 900 # Very Bold
    end
    if has_data
      case data[1][1]
      when 3
        # label_hash gets merged and overrided by fill and font_weight.
        insert_text_with_arrow(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#1E753B', 'font_weight'=> font_weight }))
      when 2
        insert_text_with_arrow(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#C1B630', 'font_weight'=> font_weight }))
      when 1
        insert_text_with_arrow(x_offset, y_offset, text,
         @label_hash.merge({'fill'=> '#BE6428', 'font_weight'=> font_weight }))
      when 0
        insert_text_with_arrow(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#AD1F25', 'font_weight'=> font_weight }))
      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', 'font_weight'=> font_weight }))
      when 2
        insert_text(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#C1B630', 'font_weight'=> font_weight }))
      when 1
        insert_text(x_offset, y_offset, text,
         @label_hash.merge({'fill'=> '#BE6428', 'font_weight'=> font_weight }))
      when 0
        insert_text(x_offset, y_offset, text,
          @label_hash.merge({'fill'=> '#AD1F25', 'font_weight'=> font_weight }))
      end
    end
    y_offset += @label_offset
  end
end

#draw_pie_label(center_x, center_y, angle, radius, percent, index) ⇒ Object



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
# File 'lib/analytics_charts/custom_pie.rb', line 227

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
  end
  if x < center_x
    x -= width / 2.0
  end
  if y > center_y
    y += ascent / 2.0
  end
  if y < center_y
    y -= (ascent / 2.0 - descent) # descent to account for '$' descent,
    # 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

#insert_pie_data(name, amount, quality) ⇒ Object



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

def insert_pie_data(name, amount, quality)
  #convert all '&#39; instances to an apostrophe
  name = name.gsub(/&#39;/, "\'")
  @data[name] =  [amount, quality]
  @aggregate[quality] += amount
end

#insert_text(x_offset, y_offset, text, features = {}) ⇒ Object



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

def insert_text(x_offset, y_offset, text, features = {})
  features.each { |feature, attribute|
    set_feature(feature, attribute)
  }
  @d.annotate(@base_image, 0 ,0, x_offset, y_offset, text)
end

#insert_text_with_arrow(x_offset, y_offset, text, features = {}) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/analytics_charts/custom_pie.rb', line 49

def insert_text_with_arrow(x_offset, y_offset, text, features = {})
  features.each { |feature, attribute|
    set_feature(feature, attribute)
  }
  @d.annotate(@base_image, 0 ,0, x_offset, y_offset, text)
  height = @d.get_type_metrics(@base_image, text).ascent
  y_offset -= height / 2
  arrow_xpos = x_offset + @d.get_type_metrics(@base_image, text).width + 5
  @d.stroke_width(4)
  @d.stroke features["fill"]
  @d = @d.line(arrow_xpos+1,y_offset,arrow_xpos+12,y_offset+6)
  @d = @d.line(arrow_xpos,y_offset,arrow_xpos+30,y_offset)
  @d = @d.line(arrow_xpos+1,y_offset,arrow_xpos+12,y_offset-6)
end

#set_feature(feature, attribute) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/analytics_charts/custom_pie.rb', line 260

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



27
28
29
30
31
# File 'lib/analytics_charts/custom_pie.rb', line 27

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



39
40
41
# File 'lib/analytics_charts/custom_pie.rb', line 39

def set_pie_colors(list)
  @colors = list
end

#set_pie_geometry(x, y, radius) ⇒ Object



33
34
35
36
37
# File 'lib/analytics_charts/custom_pie.rb', line 33

def set_pie_geometry(x, y, radius)
  @pie_center_x = x
  @pie_center_y = y
  @pie_radius = radius
end

#write(filename = 'graph.png') ⇒ Object



169
170
171
172
173
# File 'lib/analytics_charts/custom_pie.rb', line 169

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