gruff-0.6.0/0000755000004100000410000000000012540054077012662 5ustar www-datawww-datagruff-0.6.0/Rakefile0000644000004100000410000001451612540054077014336 0ustar www-datawww-datarequire 'rubygems' require 'bundler/gem_tasks' require 'rake/testtask' require 'rake/clean' CLEAN << %w(pkg test/output/*) desc 'Run tests' task :default => :test task :gem => :build Rake::TestTask.new namespace :test do desc 'Run mini tests' task :mini => :clean do Dir['test/test_mini*'].each do |file| system "ruby #{file}" end end end desc 'Generate release docs for a given milestone' task :release_docs do raise "\n This task requires Ruby 1.9 or newer to parse JSON as YAML.\n\n" if RUBY_VERSION == '1.8.7' categories, grouped_issues, milestone, milestone_description, milestone_name = get_github_issues puts '=' * 80 puts release_doc = < 0 ? '%8d' % count : ' ' * 8 end puts end puts "\nTotal: #{total}\n\n" puts "\nRubyGems download statistics per month:" years = counts_per_month.keys puts ' ' + years.map { |year| '%-12s' % year }.join (0..20).each do |l| print (l % 10 == 0) ? '%4d' % ((20-l) * 100) : ' ' years.each do |year| (1..12).each do |month| count = counts_per_month[year][month] if [year, month] == [Date.today.year, Date.today.month] count *= (Date.new(Date.today.year, Date.today.month, -1).day.to_f / Date.today.day).to_i end print count > ((20-l) * 100) ? '*' : ' ' end end puts end puts ' ' + years.map { |year| '%-12s' % year }.join puts "\nTotal: #{total}\n\n" end def get_github_issues puts 'GitHub login:' begin require 'rubygems' require 'highline/import' user = ask('login : ') { |q| q.echo = true } pass = ask('password: ') { |q| q.echo = '*' } rescue Exception print 'user name: '; user = STDIN.gets.chomp print ' password: '; pass = STDIN.gets.chomp end require 'uri' require 'net/http' require 'net/https' require 'openssl' require 'yaml' host = 'api.github.com' base_uri = "https://#{host}/repos/topfunky/gruff" https = Net::HTTP.new(host, 443) https.use_ssl = true https.verify_mode = OpenSSL::SSL::VERIFY_NONE milestone_uri = URI("#{base_uri}/milestones") req = Net::HTTP::Get.new(milestone_uri.request_uri) req.basic_auth(user, pass) res = https.start { |http| http.request(req) } milestones = YAML.load(res.body).sort_by { |i| Date.parse(i['due_on']) } puts milestones.map { |m| "#{'%2d' % m['number']} #{m['title']}" }.join("\n") if defined? ask milestone = ask('milestone: ', Integer) { |q| q.echo = true } else print 'milestone: '; milestone = STDIN.gets.chomp end uri = URI("#{base_uri}/issues?milestone=#{milestone}&state=closed&per_page=1000") req = Net::HTTP::Get.new(uri.request_uri) req.basic_auth(user, pass) res = https.start { |http| http.request(req) } issues = YAML.load(res.body).sort_by { |i| i['number'] } milestone_name = issues[0] ? issues[0]['milestone']['title'] : "No issues for milestone #{milestone}" milestone_description = issues[0] ? issues[0]['milestone']['description'] : "No issues for milestone #{milestone}" milestone_description = milestone_description.split("\r\n").map{|l|wrap l}.join("\r\n") categories = { 'Features' => 'feature', 'Bugfixes' => 'bug', 'Support' => 'support', 'Documentation' => 'documentation', 'Pull requests' => nil, 'Internal' => 'internal', 'Rejected' => 'rejected', 'Other' => nil } grouped_issues = issues.group_by do |i| labels = i['labels'].map { |l| l['name'] } cat = nil categories.each do |k, v| if labels.include? v cat = k break end end cat ||= i['pull_request'] && i['pull_request']['html_url'] && 'Pull requests' cat ||= 'Other' cat end return categories, grouped_issues, milestone, milestone_description, milestone_name end def wrap(string, indent = 0) string.scan(/\S.{0,72}\S(?=\s|$)|\S+/).join("\n" + ' ' * indent) end gruff-0.6.0/Manifest.txt0000644000004100000410000000377412540054077015204 0ustar www-datawww-dataHistory.txt MIT-LICENSE Manifest.txt README.txt Rakefile assets/bubble.png assets/city_scene/background/0000.png assets/city_scene/background/0600.png assets/city_scene/background/2000.png assets/city_scene/clouds/cloudy.png assets/city_scene/clouds/partly_cloudy.png assets/city_scene/clouds/stormy.png assets/city_scene/grass/default.png assets/city_scene/haze/true.png assets/city_scene/number_sample/1.png assets/city_scene/number_sample/2.png assets/city_scene/number_sample/default.png assets/city_scene/sky/0000.png assets/city_scene/sky/0200.png assets/city_scene/sky/0400.png assets/city_scene/sky/0600.png assets/city_scene/sky/0800.png assets/city_scene/sky/1000.png assets/city_scene/sky/1200.png assets/city_scene/sky/1400.png assets/city_scene/sky/1500.png assets/city_scene/sky/1700.png assets/city_scene/sky/2000.png assets/pc306715.jpg assets/plastik/blue.png assets/plastik/green.png assets/plastik/red.png init.rb lib/gruff.rb lib/gruff/accumulator_bar.rb lib/gruff/area.rb lib/gruff/bar.rb lib/gruff/bar_conversion.rb lib/gruff/base.rb lib/gruff/bullet.rb lib/gruff/deprecated.rb lib/gruff/dot.rb lib/gruff/line.rb lib/gruff/mini/bar.rb lib/gruff/mini/legend.rb lib/gruff/mini/pie.rb lib/gruff/mini/side_bar.rb lib/gruff/net.rb lib/gruff/photo_bar.rb lib/gruff/pie.rb lib/gruff/scene.rb lib/gruff/side_bar.rb lib/gruff/side_stacked_bar.rb lib/gruff/spider.rb lib/gruff/stacked_area.rb lib/gruff/stacked_bar.rb lib/gruff/stacked_mixin.rb rails_generators/gruff/gruff_generator.rb rails_generators/gruff/templates/controller.rb rails_generators/gruff/templates/functional_test.rb test/gruff_test_case.rb test/test_accumulator_bar.rb test/test_area.rb test/test_bar.rb test/test_base.rb test/test_bullet.rb test/test_dot.rb test/test_legend.rb test/test_line.rb test/test_mini_bar.rb test/test_mini_pie.rb test/test_mini_side_bar.rb test/test_net.rb test/test_photo.rb test/test_pie.rb test/test_scene.rb test/test_side_bar.rb test/test_sidestacked_bar.rb test/test_spider.rb test/test_stacked_area.rb test/test_stacked_bar.rb gruff-0.6.0/Gemfile0000644000004100000410000000032312540054077014153 0ustar www-datawww-datasource 'http://rubygems.org' # Specify your gem's dependencies in gruff.gemspec gemspec group :test do if RUBY_VERSION =~ /^1\.9\./ || RUBY_VERSION =~ /^2\./ gem 'minitest-reporters', '<1.0.0' end end gruff-0.6.0/RELEASE.md0000644000004100000410000000103712540054077014265 0ustar www-datawww-dataSubject: [ANN] Gruff 0.5.1 released! The Gruff team is pleased to announce the release of Gruff 0.5.1. New in version 0.5.1: Skip packaging the test images. This reduces the gem from 20MB+ to 300KB+. Bugfixes: * Issue #92 Reduce the gem size by not shipping the test images. You can find a complete list of issues here: * https://github.com/topfunky/gruff/issues?state=closed&milestone=7 Installation: gem install gruff You can find an introductory tutorial at https://github.com/topfunky/gruff Enjoy! -- The Gruff Team gruff-0.6.0/MIT-LICENSE0000644000004100000410000000211412540054077014314 0ustar www-datawww-dataCopyright (c) 2005-2012 Topfunky Corporation boss@topfunky.com MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gruff-0.6.0/History.txt0000644000004100000410000001403312540054077015065 0ustar www-datawww-data== 0.5.1 Skip packaging the test images. This reduces the gem from 20MB+ to 300KB+. Bugfixes: * Issue #92 Reduce the gem size by not shipping the test images. == 0.5.0 We have added a couple of cosmetic changes: Multiple marker lines both vertically and horizontally, and multi-line titles, or no title at all if you want more space for the chart. Features: * Issue #86 Added support for multiple references lines along both axes to Line Graph * Issue #89 Allow multiline and empty titles Documentation: * Issue #61 Remove the "BETA Software" warning in the README. Pull requests: * Issue #90 Added missing parenthesis in base.rb == 0.4.0 All old branches and pull requests have been merged or deleted. Over 40 issues have been resolved! Ruby 2.0 compatibility has been confirmed. Several new features. Features: * Issue #38 Separate themes into Gruff::Themes module * Issue #39 Add staggered labels * Issue #40 Added spacing factor to bar graphs * Issue #41 Add rotation to spider chart * Issue #65 Add RMagick and RMagick4J respectively as dependencies to the Gruff gem * Issue #81 Ensure Ruby 2.0 compatibility Bugfixes: * Issue #17 Baseline drawn at incorrect position * Issue #21 Division By Zero Error on Documented Example * Issue #36 When writing the same chart multiple times, it should not be rendered again. * Issue #44 XY Datasets are inconvenient to use. (documentation and/or code is wrong) * Issue #46 issue with markers that are floats * Issue #51 line with nil in dataset * Issue #52 Wrong direction in y_axis_label * Issue #54 Explicitly specify overlapping for lines (Gruff::Line) * Issue #58 Clean up data sorting * Issue #59 Escape '%' in labels * Issue #60 Correct DOT graph drawing * Issue #63 Some charts are drawn with transparent text when running with JRuby/RMagick4J * Issue #66 Marker line for 54.0 is missing on bar_set_marker.png example * Issue #68 Y-axis label for bar_x_y_labels.png should be rotated when drawn with JRuby Support: * Issue #4 Scaling a graph leads to fuzzy pictures * Issue #8 zero-width bar entries drawn * Issue #11 Feature: Data value markers * Issue #18 Set encoding to uft-8 * Issue #24 Gruff::SideStackedBar * Issue #35 Add option for line height in legend. * Issue #49 font directive is not setting font in charts * Issue #53 Is this project still alive? Documentation: * Issue #57 Build failing on Travis and no Build Status image in Readme Pull requests: * Issue #9 Add gradient background direction * Issue #12 Bar updates * Issue #14 Make use of the TEXT_OFFSET_PERCENTAGE const variable * Issue #16 Fix exception when y_axis_increment is used in line bars * Issue #26 Fix bug with additional point included on area charts * Issue #27 Added marker_shadow_color to allow to draw shadows below marker lines * Issue #37 Update lib/gruff/base.rb: set legend under the graph * Issue #42 Update lib/gruff/base.rb: set legend under the graph * Issue #43 Fixed issue #25 * Issue #48 add license information to the gemspec * Issue #56 Fixed gem file generation problem Internal: * Issue #80 Remove the .rmvrc file from the project == 0.3.7 * ??? == 0.3.6 * Fixed manifest to list dot graph [theirishpenguin] * Fixed color cycling error [Gunnar Wolf] * Handle case where a line graph data set only has one value [Ron Colwill] == 0.3.5 * Added dot graph from Erik Andrejko == 0.3.4 * Reverted DEBUG=true. Will add a check in the release process so this doesn't happen again. * Future releases will end in an odd number for development (topfunky-gruff on GitHub) or even for production releases. == 0.3.3 * Legend line wrapping [Mat Schaffer] * Stacked area graph fixes [James Coglan] == 0.3.2 * Include init.rb for use as a Rails plugin. == 0.3.1 * Fixed missing bullet graph bug (experimental, will be in a future release). == 0.3.0 * Fixed bug where pie graphs weren't drawing their label correctly. == 0.2.9 * Patch to make SideBar accurate instead of stacked [Marik] * Will be extracting net, pie, stacked, and side-stacked to separate gem in next release. == 0.2.8 * New accumulator bar graph (experimental) * Better mini graphs * Bug fixes == 0.2.7 * Regenerated Manifest.txt * Added scene sample to package * Added mini side_bar (EXPERIMENTAL) * Added @zero_degree option to Gruff::Pie so first slice can start somewhere other than 3 o'clock * Increased size of numbers in Gruff::Mini::Pie * Added legend_box_size accessor == 0.2.6 * Fixed missing side_bar.rb in Manifest.txt == 0.2.5 * New mini graph types (Experimental) * Marker lines can be different color than text labels * Theme definition cleanup == 0.2.4 * Added option to hide line numbers * Fixed code that was causing warnings == 0.2.3 * Cleaned up measurements so the graph expands to fill the available space * Added x-axis and y-axis label options == 0.1.2 * minimum_value and maximum_value can be set after data() to manually scale the graph * Fixed infinite loop bug when values are all equal * Added experimental net and spider graphs * Added non-linear scene graph for a simple interface to complex layered graphs * Initial refactoring of tests * A host of other bug fixes == 0.0.8 * NEW Sidestacked Bar Graphs. [Alun Eyre] * baseline_value larger than data will now show correctly. [Mike Perham] * hide_dots and hide_lines are now options for line graphs. == 0.0.6 * Fixed hang when no data is passed. == 0.0.4 * Added bar graphs * Added area graphs * Added pie graphs * Added render_image_background for using images as background on a theme * Fixed small size legend centering issue * Added initial line marker rounding to significant digits (Christian Winkler) * Line graphs line width is scaled with number of points being drawn (Christian Winkler) == 0.0.3 * Added option to draw line graphs without the lines (points only), thanks to Eric Hodel * Removed font-minimum check so graphs look better at 300px width == 0.0.2 * Fixed to_blob (thanks to Carlos Villela) * Added bar graphs (initial functionality...will be enhanced) * Removed rendered test output from gem == 0.0.1 * Initial release. * Line graphs only. Other graph styles coming soon. gruff-0.6.0/.travis.yml0000644000004100000410000000033112540054077014770 0ustar www-datawww-datalanguage: ruby sudo: false rvm: - "1.8.7" - "1.9.3" - "2.0.0" - "2.1" - "2.2" - jruby-18mode - jruby-19mode - jruby-20mode - jruby-head - rbx - rbx-2 notifications: email: - uwe@kubosch.no gruff-0.6.0/lib/0000755000004100000410000000000012540054077013430 5ustar www-datawww-datagruff-0.6.0/lib/gruff/0000755000004100000410000000000012540054077014541 5ustar www-datawww-datagruff-0.6.0/lib/gruff/photo_bar.rb0000644000004100000410000000540212540054077017044 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' # EXPERIMENTAL! # # Doesn't work yet. # class Gruff::PhotoBar < Gruff::Base # TODO # # define base and cap in yml # allow for image directory to be located elsewhere # more exact measurements for bar heights (go all the way to the bottom of the graph) # option to tile images instead of use a single image # drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line # # The name of a pre-packaged photo-based theme. attr_reader :theme # def initialize(target_width=800) # super # init_photo_bar_graphics() # end def draw super return unless @has_data return # TODO Remove for further development init_photo_bar_graphics() #Draw#define_clip_path() #Draw#clip_path(pathname) #Draw#composite....with bar graph image OverCompositeOp # # See also # # Draw.pattern # define an image to tile as the filling of a draw object # # Setup spacing. # # Columns sit side-by-side. spacing_factor = 0.9 @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns @norm_data.each_with_index do |data_row, row_index| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| data_point = 0 if data_point.nil? # Use incremented x and scaled y left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index))) left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1 right_x = left_x + @bar_width * spacing_factor right_y = @graph_top + @graph_height - 1 bar_image_width = data_row[DATA_COLOR_INDEX].columns bar_image_height = right_y.to_f - left_y.to_f # Crop to scale for data bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height) @d.gravity = NorthWestGravity @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image) # Calculate center based on bar_width and current row label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0) draw_label(label_center, point_index) end end @d.draw(@base_image) end # Return the chosen theme or the default def theme @theme || 'plastik' end protected # Sets up colors with a list of images that will be used. # Images should be 340px tall def init_photo_bar_graphics color_list = Array.new theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme Dir.open(theme_dir).each do |file| next unless /\.png$/.match(file) color_list << Image.read("#{theme_dir}/#{file}").first end @colors = color_list end end gruff-0.6.0/lib/gruff/accumulator_bar.rb0000644000004100000410000000104612540054077020232 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' ## # A special bar graph that shows a single dataset as a set of # stacked bars. The bottom bar shows the running total and # the top bar shows the new value being added to the array. class Gruff::AccumulatorBar < Gruff::StackedBar def draw raise(Gruff::IncorrectNumberOfDatasetsException) unless @data.length == 1 accum_array = @data.first[DATA_VALUES_INDEX][0..-2].inject([0]) { |a, v| a << a.last + v} data 'Accumulator', accum_array set_colors @data.reverse! super end end gruff-0.6.0/lib/gruff/pie.rb0000644000004100000410000001052312540054077015644 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' ## # Here's how to make a Pie graph: # # g = Gruff::Pie.new # g.title = "Visual Pie Graph Test" # g.data 'Fries', 20 # g.data 'Hamburgers', 50 # g.write("test/output/pie_keynote.png") # # To control where the pie chart starts creating slices, use #zero_degree. class Gruff::Pie < Gruff::Base DEFAULT_TEXT_OFFSET_PERCENTAGE = 0.15 # Can be used to make the pie start cutting slices at the top (-90.0) # or at another angle. Default is 0.0, which starts at 3 o'clock. attr_accessor :zero_degree # Do not show labels for slices that are less than this percent. Use 0 to always show all labels. # Defaults to 0 attr_accessor :hide_labels_less_than # Affect the distance between the percentages and the pie chart # Defaults to 0.15 attr_accessor :text_offset_percentage ## Use values instead of percentages attr_accessor :show_values_as_labels def initialize_ivars super @zero_degree = 0.0 @hide_labels_less_than = 0.0 @text_offset_percentage = DEFAULT_TEXT_OFFSET_PERCENTAGE @show_values_as_labels = false end def draw @hide_line_markers = true super return unless @has_data diameter = @graph_height radius = ([@graph_width, @graph_height].min / 2.0) * 0.8 center_x = @graph_left + (@graph_width / 2.0) center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit total_sum = sums_for_pie() prev_degrees = @zero_degree # Use full data since we can easily calculate percentages data = (@sort ? @data.sort{ |a, b| a[DATA_VALUES_INDEX].first <=> b[DATA_VALUES_INDEX].first } : @data) data.each do |data_row| if data_row[DATA_VALUES_INDEX].first > 0 @d = @d.stroke data_row[DATA_COLOR_INDEX] @d = @d.fill 'transparent' @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2) current_degrees = (data_row[DATA_VALUES_INDEX].first / total_sum) * 360.0 # 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(center_x, center_y, radius / 2.0, radius / 2.0, prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2 label_val = ((data_row[DATA_VALUES_INDEX].first / total_sum) * 100.0).round unless label_val < @hide_labels_less_than # RMagick must use sprintf with the string and % has special significance. label_string = @show_values_as_labels ? data_row[DATA_VALUES_INDEX].first.to_s : label_val.to_s + '%' @d = draw_label(center_x,center_y, half_angle, radius + (radius * @text_offset_percentage), label_string) end prev_degrees += current_degrees end end # TODO debug a circle where the text is drawn... @d.draw(@base_image) end private ## # Labels are drawn around a slightly wider ellipse to give room for # labels on the left and right. def draw_label(center_x, center_y, angle, radius, amount) # TODO Don't use so many hard-coded numbers r_offset = 20.0 # The distance out from the center of the pie to get point x_offset = center_x # + 15.0 # The label points need to be tweaked slightly y_offset = center_y # This one doesn't though radius_offset = (radius + r_offset) ellipse_factor = radius_offset * @text_offset_percentage x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle))) y = y_offset + (radius_offset * Math.sin(deg2rad(angle))) # Draw label @d.fill = @font_color @d.font = @font if @font @d.pointsize = scale_fontsize(@marker_font_size) @d.stroke = 'transparent' @d.font_weight = BoldWeight @d.gravity = CenterGravity @d.annotate_scaled( @base_image, 0, 0, x, y, amount, @scale) end def sums_for_pie total_sum = 0.0 @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX].first } total_sum end end gruff-0.6.0/lib/gruff/bar_conversion.rb0000644000004100000410000000261312540054077020101 0ustar www-datawww-data## # Original Author: David Stokar # # This class perfoms the y coordinats conversion for the bar class. # # There are three cases: # # 1. Bars all go from zero in positive direction # 2. Bars all go from zero to negative direction # 3. Bars either go from zero to positive or from zero to negative # class Gruff::BarConversion attr_writer :mode attr_writer :zero attr_writer :graph_top attr_writer :graph_height attr_writer :minimum_value attr_writer :spread def get_left_y_right_y_scaled(data_point, result) case @mode when 1 then # Case one # minimum value >= 0 ( only positiv values ) result[0] = @graph_top + @graph_height*(1 - data_point) + 1 result[1] = @graph_top + @graph_height - 1 when 2 then # Case two # only negativ values result[0] = @graph_top + 1 result[1] = @graph_top + @graph_height*(1 - data_point) - 1 when 3 then # Case three # positiv and negativ values val = data_point-@minimum_value/@spread if data_point >= @zero result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1 result[1] = @graph_top + @graph_height*(1 - @zero) - 1 else result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1 result[1] = @graph_top + @graph_height*(1 - @zero) - 1 end else result[0] = 0.0 result[1] = 0.0 end end end gruff-0.6.0/lib/gruff/deprecated.rb0000644000004100000410000000117712540054077017174 0ustar www-datawww-data ## # A mixin for methods that need to be deleted or have been # replaced by cleaner code. module Gruff module Deprecated def scale_measurements setup_graph_measurements end def total_height @rows + 10 end def graph_top @graph_top * @scale end def graph_height @graph_height * @scale end def graph_left @graph_left * @scale end def graph_width @graph_width * @scale end # TODO Should be calculate_graph_height # def setup_graph_height # @graph_height = @graph_bottom - @graph_top # end end end gruff-0.6.0/lib/gruff/side_stacked_bar.rb0000644000004100000410000000634212540054077020341 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' require File.dirname(__FILE__) + '/side_bar' require File.dirname(__FILE__) + '/stacked_mixin' ## # New gruff graph type added to enable sideways stacking bar charts # (basically looks like a x/y flip of a standard stacking bar chart) # # alun.eyre@googlemail.com class Gruff::SideStackedBar < Gruff::SideBar include StackedMixin # Spacing factor applied between bars attr_accessor :bar_spacing def draw @has_left_labels = true get_maximum_by_stack super end protected def draw_bars # Setup spacing. # # Columns sit stacked. @bar_spacing ||= 0.9 @bar_width = @graph_height / @column_count.to_f @d = @d.stroke_opacity 0.0 height = Array.new(@column_count, 0) length = Array.new(@column_count, @graph_left) padding = (@bar_width * (1 - @bar_spacing)) / 2 if @show_labels_for_bar_values label_values = Array.new 0.upto(@column_count-1) {|i| label_values[i] = {:value => 0, :right_x => 0}} end @norm_data.each_with_index do |data_row, row_index| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| ## using the original calcs from the stacked bar chart to get the difference between ## part of the bart chart we wish to stack. temp1 = @graph_left + (@graph_width - data_point * @graph_width - height[point_index]) + 1 temp2 = @graph_left + @graph_width - height[point_index] - 1 difference = temp2 - temp1 @d = @d.fill data_row[DATA_COLOR_INDEX] left_x = length[point_index] #+ 1 left_y = @graph_top + (@bar_width * point_index) + padding right_x = left_x + difference right_y = left_y + @bar_width * @bar_spacing length[point_index] += difference height[point_index] += (data_point * @graph_width - 2) if @show_labels_for_bar_values label_values[point_index][:value] += @norm_data[row_index][3][point_index] label_values[point_index][:right_x] = right_x end # if a data point is 0 it can result in weird really thing lines # that shouldn't even be there being drawn on top of the existing # bar - this is bad if data_point != 0 @d = @d.rectangle(left_x, left_y, right_x, right_y) # Calculate center based on bar_width and current row end # we still need to draw the labels # Calculate center based on bar_width and current row label_center = @graph_top + (@bar_width * point_index) + (@bar_width * @bar_spacing / 2.0) draw_label(label_center, point_index) end end if @show_labels_for_bar_values label_values.each_with_index do |data, i| val = (@label_formatting || "%.2f") % data[:value] draw_value_label(data[:right_x]+40, (@graph_top + (((i+1) * @bar_width) - (@bar_width / 2)))-12, val.commify, true) end end @d.draw(@base_image) end def larger_than_max?(data_point, index=0) max(data_point, index) > @maximum_value end def max(data_point, index) @data.inject(0) {|sum, item| sum + item[DATA_VALUES_INDEX][index]} end end gruff-0.6.0/lib/gruff/side_bar.rb0000644000004100000410000001104512540054077016637 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' ## # Graph with individual horizontal bars instead of vertical bars. class Gruff::SideBar < Gruff::Base # Spacing factor applied between bars attr_accessor :bar_spacing def draw @has_left_labels = true super return unless @has_data draw_bars end protected def draw_bars # Setup spacing. # @bar_spacing ||= 0.9 @bars_width = @graph_height / @column_count.to_f @bar_width = @bars_width / @norm_data.size @d = @d.stroke_opacity 0.0 height = Array.new(@column_count, 0) length = Array.new(@column_count, @graph_left) padding = (@bar_width * (1 - @bar_spacing)) / 2 # if we're a side stacked bar then we don't need to draw ourself at all # because sometimes (due to different heights/min/max) you can actually # see both graphs and it looks like crap return if self.is_a?(Gruff::SideStackedBar) @norm_data.each_with_index do |data_row, row_index| @d = @d.fill data_row[DATA_COLOR_INDEX] data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| # Using the original calcs from the stacked bar chart # to get the difference between # part of the bart chart we wish to stack. temp1 = @graph_left + (@graph_width - data_point * @graph_width - height[point_index]) temp2 = @graph_left + @graph_width - height[point_index] difference = temp2 - temp1 left_x = length[point_index] - 1 left_y = @graph_top + (@bars_width * point_index) + (@bar_width * row_index) + padding right_x = left_x + difference right_y = left_y + @bar_width * @bar_spacing height[point_index] += (data_point * @graph_width) @d = @d.rectangle(left_x, left_y, right_x, right_y) # Calculate center based on bar_width and current row if @use_data_label label_center = @graph_top + (@bar_width * (row_index+point_index) + @bar_width / 2) draw_label(label_center, row_index, @norm_data[row_index][DATA_LABEL_INDEX]) else label_center = @graph_top + (@bars_width * point_index + @bars_width / 2) draw_label(label_center, point_index) end if @show_labels_for_bar_values val = (@label_formatting || '%.2f') % @norm_data[row_index][3][point_index] draw_value_label(right_x+40, (@graph_top + (((row_index+point_index+1) * @bar_width) - (@bar_width / 2)))-12, val.commify, true) end end end @d.draw(@base_image) end # Instead of base class version, draws vertical background lines and label def draw_line_markers return if @hide_line_markers @d = @d.stroke_antialias false # Draw horizontal line markers and annotate with numbers @d = @d.stroke(@marker_color) @d = @d.stroke_width 1 number_of_lines = @marker_count || 5 number_of_lines = 1 if number_of_lines == 0 # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc. increment = significant(@spread.to_f / number_of_lines) (0..number_of_lines).each do |index| line_diff = (@graph_right - @graph_left) / number_of_lines x = @graph_right - (line_diff * index) - 1 @d = @d.line(x, @graph_bottom, x, @graph_top) diff = index - number_of_lines marker_label = diff.abs * increment + @minimum_value unless @hide_line_numbers @d.fill = @font_color @d.font = @font if @font @d.stroke = 'transparent' @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = CenterGravity # TODO Center text over line @d = @d.annotate_scaled(@base_image, 0, 0, # Width of box to draw text in x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text marker_label.to_s, @scale) end # unless @d = @d.stroke_antialias true end end ## # Draw on the Y axis instead of the X def draw_label(y_offset, index, label=nil) if !@labels[index].nil? && @labels_seen[index].nil? lbl = (@use_data_label) ? label : @labels[index] @d.fill = @font_color @d.font = @font if @font @d.stroke = 'transparent' @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = EastGravity @d = @d.annotate_scaled(@base_image, 1, 1, -@graph_left + LABEL_MARGIN * 2.0, y_offset, lbl, @scale) @labels_seen[index] = 1 end end end gruff-0.6.0/lib/gruff/bezier.rb0000644000004100000410000000213312540054077016345 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' class Gruff::Bezier < Gruff::Base def draw super return unless @has_data @x_increment = @graph_width / (@column_count - 1).to_f @norm_data.each do |data_row| poly_points = Array.new @d = @d.fill data_row[DATA_COLOR_INDEX] data_row[1].each_with_index do |data_point, index| # Use incremented x and scaled y new_x = @graph_left + (@x_increment * index) new_y = @graph_top + (@graph_height - data_point * @graph_height) if index == 0 && RUBY_PLATFORM != 'java' poly_points << new_x poly_points << new_y end poly_points << new_x poly_points << new_y draw_label(new_x, index) end @d = @d.fill_opacity 0.0 @d = @d.stroke data_row[DATA_COLOR_INDEX] @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0) if RUBY_PLATFORM == 'java' @d = @d.polyline(*poly_points) else @d = @d.bezier(*poly_points) end end @d.draw(@base_image) end end gruff-0.6.0/lib/gruff/bullet.rb0000644000004100000410000000612412540054077016360 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' require 'gruff/themes' # http://en.wikipedia.org/wiki/Bullet_graph class Gruff::Bullet < Gruff::Base def initialize(target_width="400x40") if not Numeric === target_width geometric_width, geometric_height = target_width.split('x') @columns = geometric_width.to_f @rows = geometric_height.to_f else @columns = target_width.to_f @rows = target_width.to_f / 5.0 end initialize_ivars reset_themes self.theme = Gruff::Themes::GREYSCALE @title_font_size = 20 end def data(value, maximum_value, options={}) @value = value.to_f @maximum_value = maximum_value.to_f @options = options @options.map { |k, v| @options[k] = v.to_f if v === Numeric } end # def setup_drawing # # Maybe should be done in one of the following functions for more granularity. # unless @has_data # draw_no_data() # return # end # # normalize() # setup_graph_measurements() # sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display) # # draw_legend() # draw_line_markers() # draw_axis_labels() # draw_title # end def draw # TODO Left label # TODO Bottom labels and markers # @graph_bottom # Calculations are off 800x??? @colors.reverse! draw_title @margin = 30.0 @thickness = @raw_rows / 6.0 @right_margin = @margin @graph_left = @title_width * 1.3 rescue @margin # HACK Need to calculate real width @graph_width = @raw_columns - @graph_left - @right_margin @graph_height = @thickness * 3.0 # Background @d = @d.fill @colors[0] @d = @d.rectangle(@graph_left, 0, @graph_left + @graph_width, @graph_height) [:high, :low].each_with_index do |indicator, index| next unless @options.has_key?(indicator) @d = @d.fill @colors[index + 1] indicator_width_x = @graph_left + @graph_width * (@options[indicator] / @maximum_value) @d = @d.rectangle(@graph_left, 0, indicator_width_x, @graph_height) end if @options.has_key?(:target) @d = @d.fill @font_color target_x = @graph_left + @graph_width * (@options[:target] / @maximum_value) half_thickness = @thickness / 2.0 @d = @d.rectangle(target_x, half_thickness, target_x + half_thickness, @thickness * 2 + half_thickness) end # Value @d = @d.fill @font_color @d = @d.rectangle(@graph_left, @thickness, @graph_left + @graph_width * (@value / @maximum_value), @thickness * 2) @d.draw(@base_image) end def draw_title return unless @title @font_height = calculate_caps_height(scale_fontsize(@title_font_size)) @title_width = calculate_width(@title_font_size, @title) @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(@title_font_size) @d.gravity = NorthWestGravity @d = @d.annotate_scaled(*[ @base_image, 1.0, 1.0, @font_height/2, @font_height/2, @title, @scale ]) end end gruff-0.6.0/lib/gruff/spider.rb0000644000004100000410000000677312540054077016371 0ustar www-datawww-data require File.dirname(__FILE__) + '/base' # Experimental!!! See also the Net graph. # # Submitted by Kevin Clark http://glu.ttono.us/ class Gruff::Spider < Gruff::Base # Hide all text attr_reader :hide_text attr_accessor :hide_axes attr_reader :transparent_background attr_accessor :rotation def transparent_background=(value) @transparent_background = value @base_image = render_transparent_background if value end def hide_text=(value) @hide_title = @hide_text = value end def initialize(max_value, target_width = 800) super(target_width) @max_value = max_value @hide_legend = true; @rotation = 0 end def draw @hide_line_markers = true super return unless @has_data # Setup basic positioning diameter = @graph_height radius = @graph_height / 2.0 top_x = @graph_left + (@graph_width - diameter) / 2.0 center_x = @graph_left + (@graph_width / 2.0) center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit @unit_length = radius / @max_value total_sum = sums_for_spider prev_degrees = 0.0 additive_angle = (2 * Math::PI)/ @data.size current_angle = rotation * Math::PI / 180.0 # Draw axes draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes # Draw polygon draw_polygon(center_x, center_y, additive_angle) @d.draw(@base_image) end private def normalize_points(value) value * @unit_length end def draw_label(center_x, center_y, angle, radius, amount) r_offset = 50 # The distance out from the center of the pie to get point x_offset = center_x # The label points need to be tweaked slightly y_offset = center_y + 0 # This one doesn't though x = x_offset + ((radius + r_offset) * Math.cos(angle)) y = y_offset + ((radius + r_offset) * Math.sin(angle)) # Draw label @d.fill = @marker_color @d.font = @font if @font @d.pointsize = scale_fontsize(legend_font_size) @d.stroke = 'transparent' @d.font_weight = BoldWeight @d.gravity = CenterGravity @d.annotate_scaled( @base_image, 0, 0, x, y, amount, @scale) end def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil) return if hide_axes current_angle = rotation * Math::PI / 180.0 @data.each do |data_row| @d.stroke(line_color || data_row[DATA_COLOR_INDEX]) @d.stroke_width 5.0 x_offset = radius * Math.cos(current_angle) y_offset = radius * Math.sin(current_angle) @d.line(center_x, center_y, center_x + x_offset, center_y + y_offset) draw_label(center_x, center_y, current_angle, radius, data_row[DATA_LABEL_INDEX].to_s) unless hide_text current_angle += additive_angle end end def draw_polygon(center_x, center_y, additive_angle, color = nil) points = [] current_angle = rotation * Math::PI / 180.0 @data.each do |data_row| points << center_x + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.cos(current_angle) points << center_y + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.sin(current_angle) current_angle += additive_angle end @d.stroke_width 1.0 @d.stroke(color || @marker_color) @d.fill(color || @marker_color) @d.fill_opacity 0.4 @d.polygon(*points) end def sums_for_spider @data.inject(0.0) {|sum, data_row| sum += data_row[DATA_VALUES_INDEX].first} end end gruff-0.6.0/lib/gruff/line.rb0000644000004100000410000002624312540054077016024 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' ## # Here's how to make a Line graph: # # g = Gruff::Line.new # g.title = "A Line Graph" # g.data 'Fries', [20, 23, 19, 8] # g.data 'Hamburgers', [50, 19, 99, 29] # g.write("test/output/line.png") # # There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines. class Gruff::Line < Gruff::Base # Allow for reference lines ( which are like baseline ... just allowing for more & on both axes ) attr_accessor :reference_lines attr_accessor :reference_line_default_color attr_accessor :reference_line_default_width # Allow for vertical marker lines attr_accessor :show_vertical_markers # Dimensions of lines and dots; calculated based on dataset size if left unspecified attr_accessor :line_width attr_accessor :dot_radius # Hide parts of the graph to fit more datapoints, or for a different appearance. attr_accessor :hide_dots, :hide_lines #accessors for support of xy data attr_accessor :minimum_x_value attr_accessor :maximum_x_value # Get the value if somebody has defined it. def baseline_value if (@reference_lines.key?(:baseline)) @reference_lines[:baseline][:value] else nil end end # Set a value for a baseline reference line.. def baseline_value=(new_value) @reference_lines[:baseline] ||= Hash.new @reference_lines[:baseline][:value] = new_value end def baseline_color if (@reference_lines.key?(:baseline)) @reference_lines[:baseline][:color] else nil end end def baseline_color=(new_value) @reference_lines[:baseline] ||= Hash.new @reference_lines[:baseline][:color] = new_value end # Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only). # # g = Gruff::Line.new(400) # 400px wide with lines # # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility) # # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility) # # The preferred way is to call hide_dots or hide_lines instead. def initialize(*args) raise ArgumentError, 'Wrong number of arguments' if args.length > 2 if args.empty? || ((not Numeric === args.first) && (not String === args.first)) super() else super args.shift end @reference_lines = Hash.new @reference_line_default_color = 'red' @reference_line_default_width = 5 @hide_dots = @hide_lines = false @maximum_x_value = nil @minimum_x_value = nil end # This method allows one to plot a dataset with both X and Y data. # # Parameters are as follows: # name: string, the title of the dataset # x_data_points: an array containing the x data points for the graph # y_data_points: an array containing the y data points for the graph # color: hex number indicating the line color as an RGB triplet # # or # # name: string, the title of the dataset # xy_data_points: an array containing both x and y data points for the graph # color: hex number indicating the line color as an RGB triplet # # Notes: # -if (x_data_points.length != y_data_points.length) an error is # returned. # -if the color argument is nil, the next color from the default theme will # be used. # -if you want to use a preset theme, you must set it before calling # dataxy(). # # Example: # g = Gruff::Line.new # g.title = "X/Y Dataset" # g.dataxy("Apples", [1,3,4,5,6,10], [1, 2, 3, 4, 4, 3]) # g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3]) # g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]]) # #you can still use the old data method too if you want: # g.data("Capples", [1, 1, 2, 2, 3, 3]) # #labels will be drawn at the x locations of the keys passed in. # In this example the lables are drawn at x positions 2, 4, and 6: # g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'} # The 0 => '2003' label will be ignored since it is outside the chart range. def dataxy(name, x_data_points=[], y_data_points=[], color=nil) raise ArgumentError, 'x_data_points is nil!' if x_data_points.length == 0 if x_data_points.all? { |p| p.is_a?(Array) && p.size == 2 } x_data_points, y_data_points = x_data_points.map { |p| p[0] }, x_data_points.map { |p| p[1] } end raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length # call the existing data routine for the y data. self.data(name, y_data_points, color) x_data_points = Array(x_data_points) # make sure it's an array # append the x data to the last entry that was just added in the @data member @data.last[DATA_VALUES_X_INDEX] = x_data_points # Update the global min/max values for the x data x_data_points.each do |x_data_point| next if x_data_point.nil? # Setup max/min so spread starts at the low end of the data points if @maximum_x_value.nil? && @minimum_x_value.nil? @maximum_x_value = @minimum_x_value = x_data_point end @maximum_x_value = (x_data_point > @maximum_x_value) ? x_data_point : @maximum_x_value @minimum_x_value = (x_data_point < @minimum_x_value) ? x_data_point : @minimum_x_value end end def draw_reference_line(reference_line, left, right, top, bottom) @d = @d.push @d.stroke_color(reference_line[:color] || @reference_line_default_color) @d.fill_opacity 0.0 @d.stroke_dasharray(10, 20) @d.stroke_width(reference_line[:width] || @reference_line_default_width) @d.line(left, top, right, bottom) @d = @d.pop end def draw_horizontal_reference_line(reference_line) level = @graph_top + (@graph_height - reference_line[:norm_value] * @graph_height) draw_reference_line(reference_line, @graph_left, @graph_left + @graph_width, level, level) end def draw_vertical_reference_line(reference_line) index = @graph_left + (@x_increment * reference_line[:index]) draw_reference_line(reference_line, index, index, @graph_top, @graph_top + @graph_height) end def draw super return unless @has_data # Check to see if more than one datapoint was given. NaN can result otherwise. @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width @reference_lines.each_value do |curr_reference_line| draw_horizontal_reference_line(curr_reference_line) if curr_reference_line.key?(:norm_value) draw_vertical_reference_line(curr_reference_line) if curr_reference_line.key?(:index) end if (@show_vertical_markers) (0..@column_count).each do |column| x = @graph_left + @graph_width - column.to_f * @x_increment @d = @d.fill(@marker_color) # FIXME(uwe): Workaround for Issue #66 # https://github.com/topfunky/gruff/issues/66 # https://github.com/rmagick/rmagick/issues/82 # Remove if the issue gets fixed. x += 0.001 unless defined?(JRUBY_VERSION) # EMXIF @d = @d.line(x, @graph_bottom, x, @graph_top) #If the user specified a marker shadow color, draw a shadow just below it unless @marker_shadow_color.nil? @d = @d.fill(@marker_shadow_color) @d = @d.line(x + 1, @graph_bottom, x + 1, @graph_top) end end end @norm_data.each do |data_row| prev_x = prev_y = nil @one_point = contains_one_point_only?(data_row) data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| x_data = data_row[DATA_VALUES_X_INDEX] if x_data == nil #use the old method: equally spaced points along the x-axis new_x = @graph_left + (@x_increment * index) draw_label(new_x, index) else new_x = get_x_coord(x_data[index], @graph_width, @graph_left) @labels.each do |label_pos, _| draw_label(@graph_left + ((label_pos - @minimum_x_value) * @graph_width) / (@maximum_x_value - @minimum_x_value), label_pos) end end unless data_point # we can't draw a line for a null data point, we can still label the axis though prev_x = prev_y = nil next end new_y = @graph_top + (@graph_height - data_point * @graph_height) # Reset each time to avoid thin-line errors @d = @d.stroke data_row[DATA_COLOR_INDEX] @d = @d.fill data_row[DATA_COLOR_INDEX] @d = @d.stroke_opacity 1.0 @d = @d.stroke_width line_width || clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0) circle_radius = dot_radius || clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0) if !@hide_lines && !prev_x.nil? && !prev_y.nil? @d = @d.line(prev_x, prev_y, new_x, new_y) elsif @one_point # Show a circle if there's just one_point @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) end @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots prev_x, prev_y = new_x, new_y end end @d.draw(@base_image) end def setup_data # Deal with horizontal reference line values that exceed the existing minimum & maximum values. possible_maximums = [@maximum_value.to_f] possible_minimums = [@minimum_value.to_f] @reference_lines.each_value do |curr_reference_line| if (curr_reference_line.key?(:value)) possible_maximums << curr_reference_line[:value].to_f possible_minimums << curr_reference_line[:value].to_f end end @maximum_value = possible_maximums.max @minimum_value = possible_minimums.min super end def normalize(force=false) super(force) @reference_lines.each_value do |curr_reference_line| # We only care about horizontal markers ... for normalization. # Vertical markers won't have a :value, they will have an :index curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - @minimum_value) / @spread.to_f) if (curr_reference_line.key?(:value)) end #normalize the x data if it is specified @data.each_with_index do |data_row, index| norm_x_data_points = [] if data_row[DATA_VALUES_X_INDEX] != nil data_row[DATA_VALUES_X_INDEX].each do |x_data_point| norm_x_data_points << ((x_data_point.to_f - @minimum_x_value.to_f) / (@maximum_x_value.to_f - @minimum_x_value.to_f)) end @norm_data[index] << norm_x_data_points end end end def sort_norm_data super unless @data.any? { |d| d[DATA_VALUES_X_INDEX] } end def get_x_coord(x_data_point, width, offset) x_data_point * width + offset end def contains_one_point_only?(data_row) # Spin through data to determine if there is just one_value present. one_point = false data_row[DATA_VALUES_INDEX].each do |data_point| unless data_point.nil? if one_point # more than one point, bail return false end # there is at least one data point one_point = true end end one_point end end gruff-0.6.0/lib/gruff/scatter.rb0000644000004100000410000002146012540054077016536 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' # Here's how to set up an XY Scatter Chart # # g = Gruff::Scatter.new(800) # g.data(:apples, [1,2,3,4], [4,3,2,1]) # g.data('oranges', [5,7,8], [4,1,7]) # g.write('test/output/scatter.png') # # class Gruff::Scatter < Gruff::Base # Maximum X Value. The value will get overwritten by the max in the # datasets. attr_accessor :maximum_x_value # Minimum X Value. The value will get overwritten by the min in the # datasets. attr_accessor :minimum_x_value # The number of vertical lines shown for reference attr_accessor :marker_x_count #~ # Draw a dashed horizontal line at the given y value #~ attr_accessor :baseline_y_value #~ # Color of the horizontal baseline #~ attr_accessor :baseline_y_color #~ # Draw a dashed horizontal line at the given y value #~ attr_accessor :baseline_x_value #~ # Color of the horizontal baseline #~ attr_accessor :baseline_x_color # Gruff::Scatter takes the same parameters as the Gruff::Line graph # # ==== Example # # g = Gruff::Scatter.new # def initialize(*args) super(*args) @maximum_x_value = @minimum_x_value = nil @baseline_x_color = @baseline_y_color = 'red' @baseline_x_value = @baseline_y_value = nil @marker_x_count = nil end def setup_drawing # TODO Need to get x-axis labels working. Current behavior will be to not allow. @labels = {} super # Translate our values so that we can use the base methods for drawing # the standard chart stuff @column_count = @x_spread end def draw super return unless @has_data # Check to see if more than one datapoint was given. NaN can result otherwise. @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width #~ if (defined?(@norm_y_baseline)) then #~ level = @graph_top + (@graph_height - @norm_baseline * @graph_height) #~ @d = @d.push #~ @d.stroke_color @baseline_color #~ @d.fill_opacity 0.0 #~ @d.stroke_dasharray(10, 20) #~ @d.stroke_width 5 #~ @d.line(@graph_left, level, @graph_left + @graph_width, level) #~ @d = @d.pop #~ end #~ if (defined?(@norm_x_baseline)) then #~ end @norm_data.each do |data_row| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| x_value = data_row[DATA_VALUES_X_INDEX][index] next if data_point.nil? || x_value.nil? new_x = get_x_coord(x_value, @graph_width, @graph_left) new_y = @graph_top + (@graph_height - data_point * @graph_height) # Reset each time to avoid thin-line errors @d = @d.stroke data_row[DATA_COLOR_INDEX] @d = @d.fill data_row[DATA_COLOR_INDEX] @d = @d.stroke_opacity 1.0 @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0) circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0) @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) end end @d.draw(@base_image) end # The first parameter is the name of the dataset. The next two are the # x and y axis data points contain in their own array in that respective # order. The final parameter is the color. # # Can be called multiple times with different datasets for a multi-valued # graph. # # If the color argument is nil, the next color from the default theme will # be used. # # NOTE: If you want to use a preset theme, you must set it before calling # data(). # # ==== Parameters # name:: String or Symbol containing the name of the dataset. # x_data_points:: An Array of of x-axis data points. # y_data_points:: An Array of of y-axis data points. # color:: The hex string for the color of the dataset. Defaults to nil. # # ==== Exceptions # Data points contain nil values:: # This error will get raised if either the x or y axis data points array # contains a nil value. The graph will not make an assumption # as how to graph nil # x_data_points is empty:: # This error is raised when the array for the x-axis points are empty # y_data_points is empty:: # This error is raised when the array for the y-axis points are empty # x_data_points.length != y_data_points.length:: # Error means that the x and y axis point arrays do not match in length # # ==== Examples # g = Gruff::Scatter.new # g.data(:apples, [1,2,3], [3,2,1]) # g.data('oranges', [1,1,1], [2,3,4]) # g.data('bitter_melon', [3,5,6], [6,7,8], '#000000') # def data(name, x_data_points=[], y_data_points=[], color=nil) raise ArgumentError, 'Data Points contain nil Value!' if x_data_points.include?(nil) || y_data_points.include?(nil) raise ArgumentError, 'x_data_points is empty!' if x_data_points.empty? raise ArgumentError, 'y_data_points is empty!' if y_data_points.empty? raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length # Call the existing data routine for the y axis data super(name, y_data_points, color) #append the x data to the last entry that was just added in the @data member last_elem = @data.length()-1 @data[last_elem] << x_data_points if @maximum_x_value.nil? && @minimum_x_value.nil? @maximum_x_value = @minimum_x_value = x_data_points.first end @maximum_x_value = x_data_points.max > @maximum_x_value ? x_data_points.max : @maximum_x_value @minimum_x_value = x_data_points.min < @minimum_x_value ? x_data_points.min : @minimum_x_value end protected def calculate_spread #:nodoc: super @x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f @x_spread = @x_spread > 0 ? @x_spread : 1 end def normalize(force=@xy_normalize) if @norm_data.nil? || force @norm_data = [] return unless @has_data @data.each do |data_row| norm_data_points = [data_row[DATA_LABEL_INDEX]] norm_data_points << data_row[DATA_VALUES_INDEX].map do |r| (r.to_f - @minimum_value.to_f) / @spread end norm_data_points << data_row[DATA_COLOR_INDEX] norm_data_points << data_row[DATA_VALUES_X_INDEX].map do |r| (r.to_f - @minimum_x_value.to_f) / @x_spread end @norm_data << norm_data_points end end #~ @norm_y_baseline = (@baseline_y_value.to_f / @maximum_value.to_f) if @baseline_y_value #~ @norm_x_baseline = (@baseline_x_value.to_f / @maximum_x_value.to_f) if @baseline_x_value end def draw_line_markers # do all of the stuff for the horizontal lines on the y-axis super return if @hide_line_markers @d = @d.stroke_antialias false if @x_axis_increment.nil? # TODO Do the same for larger numbers...100, 75, 50, 25 if @marker_x_count.nil? (3..7).each do |lines| if @x_spread % lines == 0.0 @marker_x_count = lines break end end @marker_x_count ||= 4 end @x_increment = (@x_spread > 0) ? significant(@x_spread / @marker_x_count) : 1 else # TODO Make this work for negative values @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max @minimum_x_value = @minimum_x_value.floor calculate_spread normalize(true) @marker_count = (@x_spread / @x_axis_increment).to_i @x_increment = @x_axis_increment end @increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment) # Draw vertical line markers and annotate with numbers (0..@marker_x_count).each do |index| # TODO Fix the vertical lines. Not pretty when they don't match up with top y-axis line # x = @graph_left + @graph_width - index.to_f * @increment_x_scaled # @d = @d.stroke(@marker_color) # @d = @d.stroke_width 1 # @d = @d.line(x, @graph_top, x, @graph_bottom) unless @hide_line_numbers marker_label = index * @x_increment + @minimum_x_value.to_f y_offset = @graph_bottom + LABEL_MARGIN x_offset = get_x_coord(index.to_f, @increment_x_scaled, @graph_left) @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = NorthGravity @d = @d.annotate_scaled(@base_image, 1.0, 1.0, x_offset, y_offset, label(marker_label, @x_increment), @scale) end end @d = @d.stroke_antialias true end private def get_x_coord(x_data_point, width, offset) #:nodoc: x_data_point * width + offset end end # end Gruff::Scatter gruff-0.6.0/lib/gruff/themes.rb0000644000004100000410000000457312540054077016364 0ustar www-datawww-datamodule Gruff module Themes # A color scheme similar to the popular presentation software. KEYNOTE = { :colors => [ '#FDD84E', # yellow '#6886B4', # blue '#72AE6E', # green '#D1695E', # red '#8A6EAF', # purple '#EFAA43', # orange 'white' ], :marker_color => 'white', :font_color => 'white', :background_colors => %w(black #4a465a) } # A color scheme plucked from the colors on the popular usability blog. THIRTYSEVEN_SIGNALS = { :colors => [ '#FFF804', # yellow '#336699', # blue '#339933', # green '#ff0000', # red '#cc99cc', # purple '#cf5910', # orange 'black' ], :marker_color => 'black', :font_color => 'black', :background_colors => %w(#d1edf5 white) } # A color scheme from the colors used on the 2005 Rails keynote # presentation at RubyConf. RAILS_KEYNOTE = { :colors => [ '#00ff00', # green '#333333', # grey '#ff5d00', # orange '#f61100', # red 'white', '#999999', # light grey 'black' ], :marker_color => 'white', :font_color => 'white', :background_colors => %w(#0083a3 #0083a3) } # A color scheme similar to that used on the popular podcast site. ODEO = { :colors => [ '#202020', # grey 'white', '#3a5b87', # dark blue '#a21764', # dark pink '#8ab438', # green '#999999', # light grey 'black' ], :marker_color => 'white', :font_color => 'white', :background_colors => %w(#ff47a4 #ff1f81) } # A pastel theme PASTEL = { :colors => [ '#a9dada', # blue '#aedaa9', # green '#daaea9', # peach '#dadaa9', # yellow '#a9a9da', # dk purple '#daaeda', # purple '#dadada' # grey ], :marker_color => '#aea9a9', # Grey :font_color => 'black', :background_colors => 'white' } # A greyscale theme GREYSCALE = { :colors => [ '#282828', # '#383838', # '#686868', # '#989898', # '#c8c8c8', # '#e8e8e8', # ], :marker_color => '#aea9a9', # Grey :font_color => 'black', :background_colors => 'white' } end end gruff-0.6.0/lib/gruff/scene.rb0000644000004100000410000001301312540054077016161 0ustar www-datawww-data require "observer" require File.dirname(__FILE__) + '/base' ## # A scene is a non-linear graph that assembles layers together to tell a story. # Layers are folders with appropriately named files (see below). You can group # layers and control them together or just set their values individually. # # Examples: # # * A city scene that changes with the time of day and the weather conditions. # * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones. # # Usage: # # g = Gruff::Scene.new("500x100", "path/to/city_scene_directory") # # # Define order of layers, back to front # g.layers = %w(background haze sky clouds) # # # Define groups that will be controlled by the same input # g.weather_group = %w(clouds) # g.time_group = %w(background sky) # # # Set values for the layers or groups # g.weather = "cloudy" # g.time = Time.now # g.haze = true # # # Write the final graph to disk # g.write "hazy_daytime_city_scene.png" # # # There are several rules that will magically select a layer when possible. # # * Numbered files will be selected according to the closest value that is less than the input value. # * 'true.png' and 'false.png' will be used as booleans. # * Other named files will be used if the input matches the filename (without the filetype extension). # * If there is a file named 'default.png', it will be used unless other input values are set for the corresponding layer. class Gruff::Scene < Gruff::Base # An array listing the foldernames that will be rendered, from back to front. # # g.layers = %w(sky clouds buildings street people) # attr_reader :layers def initialize(target_width, base_dir) @base_dir = base_dir @groups = {} @layers = [] super target_width end def draw # Join all the custom paths and filter out the empty ones image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? } images = Magick::ImageList.new(*image_paths) @base_image = images.flatten_images end def layers=(ordered_list) ordered_list.each do |layer_name| @layers << Gruff::Layer.new(@base_dir, layer_name) end end # Group layers to input values # # g.weather_group = ["sky", "sea", "clouds"] # # Set input values # # g.weather = "cloudy" # def method_missing(method_name, *args) case method_name.to_s when /^(\w+)_group=$/ add_group $1, *args return when /^(\w+)=$/ set_input $1, args.first return end super end private def add_group(input_name, layer_names) @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) }) end def set_input(input_name, input_value) if not @groups[input_name].nil? @groups[input_name].send_updates(input_value) else if chosen_layer = @layers.detect { |layer| layer.name == input_name } chosen_layer.update input_value end end end end class Gruff::Group include Observable attr_reader :name def initialize(folder_name, layers) @name = folder_name layers.each do |layer| layer.observe self end end def send_updates(value) changed notify_observers value end end class Gruff::Layer attr_reader :name def initialize(base_dir, folder_name) @base_dir = base_dir.to_s @name = folder_name.to_s @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }.sort @selected_filename = select_default end # Register this layer so it receives updates from the group def observe(obj) obj.add_observer self end # Choose the appropriate filename for this layer, based on the input def update(value) @selected_filename = case value.to_s when /^(true|false)$/ select_boolean value when /^(\w|\s)+$/ select_string value when /^-?(\d+\.)?\d+$/ select_numeric value when /(\d\d):(\d\d):\d\d/ select_time "#{$1}#{$2}" else select_default end # Finally, try to use 'default' if we're still blank @selected_filename ||= select_default end # Returns the full path to the selected image, or a blank string def path unless @selected_filename.nil? || @selected_filename.empty? return File.join(@base_dir, @name, @selected_filename) end '' end private # Match "true.png" or "false.png" def select_boolean(value) file_exists_or_blank value.to_s end # Match -5 to _5.png def select_numeric(value) file_exists_or_blank value.to_s.gsub('-', '_') end def select_time(value) times = @filenames.map { |filename| filename.gsub('.png', '') } times.each_with_index do |time, index| if (time > value) && (index > 0) return "#{times[index - 1]}.png" end end return "#{times.last}.png" end # Match "partly cloudy" to "partly_cloudy.png" def select_string(value) file_exists_or_blank value.to_s.gsub(' ', '_') end def select_default @filenames.include?("default.png") ? "default.png" : '' end # Returns the string "#{filename}.png", if it exists. # # Failing that, it returns default.png, or '' if that doesn't exist. def file_exists_or_blank(filename) @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default end end gruff-0.6.0/lib/gruff/mini/0000755000004100000410000000000012540054077015475 5ustar www-datawww-datagruff-0.6.0/lib/gruff/mini/pie.rb0000644000004100000410000000113112540054077016573 0ustar www-datawww-data## # # Makes a small pie graph suitable for display at 200px or even smaller. # module Gruff module Mini class Pie < Gruff::Pie include Gruff::Mini::Legend def initialize_ivars super @hide_legend = true @hide_title = true @hide_line_numbers = true @marker_font_size = 60.0 @legend_font_size = 60.0 end def draw expand_canvas_for_vertical_legend super draw_vertical_legend @d.draw(@base_image) end # def draw end # class Pie end end gruff-0.6.0/lib/gruff/mini/side_bar.rb0000644000004100000410000000112112540054077017565 0ustar www-datawww-data## # # Makes a small pie graph suitable for display at 200px or even smaller. # module Gruff module Mini class SideBar < Gruff::SideBar include Gruff::Mini::Legend def initialize_ivars super @hide_legend = true @hide_title = true @hide_line_numbers = true @marker_font_size = 50.0 @legend_font_size = 50.0 end def draw expand_canvas_for_vertical_legend super draw_vertical_legend @d.draw(@base_image) end end end end gruff-0.6.0/lib/gruff/mini/legend.rb0000644000004100000410000000724112540054077017264 0ustar www-datawww-datamodule Gruff module Mini module Legend attr_accessor :hide_mini_legend, :legend_position ## # The canvas needs to be bigger so we can put the legend beneath it. def expand_canvas_for_vertical_legend return if @hide_mini_legend @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] } legend_height = scale_fontsize( @data.length * calculate_line_height + @top_margin + @bottom_margin) @original_rows = @raw_rows @original_columns = @raw_columns case @legend_position when :right then @rows = [@rows, legend_height].max @columns += calculate_legend_width + @left_margin else @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7 end render_background end def calculate_line_height calculate_caps_height(@legend_font_size) * 1.7 end def calculate_legend_width width = @legend_labels.map { |label| calculate_width(@legend_font_size, label) }.max scale_fontsize(width + 40*1.7) end ## # Draw the legend beneath the existing graph. def draw_vertical_legend return if @hide_mini_legend legend_square_width = 40.0 # small square with color of this item legend_square_margin = 10.0 @legend_left_margin = 100.0 legend_top_margin = 40.0 # May fix legend drawing problem at small sizes @d.font = @font if @font @d.pointsize = @legend_font_size case @legend_position when :right then current_x_offset = @original_columns + @left_margin current_y_offset = @top_margin + legend_top_margin else current_x_offset = @legend_left_margin current_y_offset = @original_rows + legend_top_margin end debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset } @legend_labels.each_with_index do |legend_label, index| # Draw label @d.fill = @font_color @d.font = @font if @font @d.pointsize = scale_fontsize(@legend_font_size) @d.stroke = 'transparent' @d.font_weight = Magick::NormalWeight @d.gravity = Magick::WestGravity @d = @d.annotate_scaled( @base_image, @raw_columns, 1.0, current_x_offset + (legend_square_width * 1.7), current_y_offset, truncate_legend_label(legend_label), @scale) # Now draw box with color of this dataset @d = @d.stroke 'transparent' @d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX] @d = @d.rectangle(current_x_offset, current_y_offset - legend_square_width / 2.0, current_x_offset + legend_square_width, current_y_offset + legend_square_width / 2.0) current_y_offset += calculate_line_height end @color_index = 0 end ## # Shorten long labels so they will fit on the canvas. # # Department of Hu... def truncate_legend_label(label) truncated_label = label.to_s while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - @right_margin) && (truncated_label.length > 1) truncated_label = truncated_label[0..truncated_label.length-2] end truncated_label + (truncated_label.length < label.to_s.length ? "..." : '') end end end end gruff-0.6.0/lib/gruff/mini/bar.rb0000644000004100000410000000114712540054077016571 0ustar www-datawww-data## # # Makes a small bar graph suitable for display at 200px or even smaller. # module Gruff module Mini class Bar < Gruff::Bar include Gruff::Mini::Legend def initialize_ivars super @hide_legend = true @hide_title = true @hide_line_numbers = true @marker_font_size = 50.0 @minimum_value = 0.0 @maximum_value = 0.0 @legend_font_size = 60.0 end def draw expand_canvas_for_vertical_legend super draw_vertical_legend @d.draw(@base_image) end end end end gruff-0.6.0/lib/gruff/stacked_bar.rb0000644000004100000410000000367712540054077017345 0ustar www-datawww-data require File.dirname(__FILE__) + '/base' require File.dirname(__FILE__) + '/stacked_mixin' class Gruff::StackedBar < Gruff::Base include StackedMixin # Spacing factor applied between bars attr_accessor :bar_spacing # Number of pixels between bar segments attr_accessor :segment_spacing # Draws a bar graph, but multiple sets are stacked on top of each other. def draw get_maximum_by_stack super return unless @has_data # Setup spacing. # # Columns sit stacked. @bar_spacing ||= 0.9 @segment_spacing ||= 1 @bar_width = @graph_width / @column_count.to_f padding = (@bar_width * (1 - @bar_spacing)) / 2 @d = @d.stroke_opacity 0.0 height = Array.new(@column_count, 0) @norm_data.each_with_index do |data_row, row_index| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| @d = @d.fill data_row[DATA_COLOR_INDEX] # Calculate center based on bar_width and current row label_center = @graph_left + (@bar_width * point_index) + (@bar_width * @bar_spacing / 2.0) draw_label(label_center, point_index) next if (data_point == 0) # Use incremented x and scaled y left_x = @graph_left + (@bar_width * point_index) + padding left_y = @graph_top + (@graph_height - data_point * @graph_height - height[point_index]) + @segment_spacing right_x = left_x + @bar_width * @bar_spacing right_y = @graph_top + @graph_height - height[point_index] - @segment_spacing # update the total height of the current stacked bar height[point_index] += (data_point * @graph_height ) @d = @d.rectangle(left_x, left_y, right_x, right_y) end end @d.draw(@base_image) end end gruff-0.6.0/lib/gruff/version.rb0000644000004100000410000000004512540054077016552 0ustar www-datawww-datamodule Gruff VERSION = '0.6.0' end gruff-0.6.0/lib/gruff/stacked_area.rb0000644000004100000410000000337512540054077017504 0ustar www-datawww-data require File.dirname(__FILE__) + '/base' require File.dirname(__FILE__) + '/stacked_mixin' class Gruff::StackedArea < Gruff::Base include StackedMixin attr_accessor :last_series_goes_on_bottom def draw get_maximum_by_stack super return unless @has_data @x_increment = @graph_width / (@column_count - 1).to_f @d = @d.stroke 'transparent' height = Array.new(@column_count, 0) data_points = nil iterator = last_series_goes_on_bottom ? :reverse_each : :each @norm_data.send(iterator) do |data_row| prev_data_points = data_points data_points = Array.new @d = @d.fill data_row[DATA_COLOR_INDEX] data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| # Use incremented x and scaled y new_x = @graph_left + (@x_increment * index) new_y = @graph_top + (@graph_height - data_point * @graph_height - height[index]) height[index] += (data_point * @graph_height) data_points << new_x data_points << new_y draw_label(new_x, index) end if prev_data_points poly_points = data_points.dup (prev_data_points.length/2 - 1).downto(0) do |i| poly_points << prev_data_points[2*i] poly_points << prev_data_points[2*i+1] end poly_points << data_points[0] poly_points << data_points[1] else poly_points = data_points.dup poly_points << @graph_right poly_points << @graph_bottom - 1 poly_points << @graph_left poly_points << @graph_bottom - 1 poly_points << data_points[0] poly_points << data_points[1] end @d = @d.polyline(*poly_points) end @d.draw(@base_image) end end gruff-0.6.0/lib/gruff/dot.rb0000644000004100000410000000752712540054077015667 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' ## # Graph with dots and labels along a vertical access # see: 'Creating More Effective Graphs' by Robbins class Gruff::Dot < Gruff::Base def draw @has_left_labels = true super return unless @has_data # Setup spacing. # spacing_factor = 1.0 @items_width = @graph_height / @column_count.to_f @item_width = @items_width * spacing_factor / @norm_data.size @d = @d.stroke_opacity 0.0 padding = (@items_width * (1 - spacing_factor)) / 2 @norm_data.each_with_index do |data_row, row_index| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| x_pos = @graph_left + (data_point * @graph_width) y_pos = @graph_top + (@items_width * point_index) + padding + (@items_width.to_f/2.0).round if row_index == 0 @d = @d.stroke(@marker_color) @d = @d.fill(@marker_color) @d = @d.stroke_width 1.0 @d = @d.stroke_opacity 0.1 @d = @d.fill_opacity 0.1 @d = @d.line(@graph_left, y_pos, @graph_left + @graph_width, y_pos) @d = @d.fill_opacity 1 end @d = @d.fill data_row[DATA_COLOR_INDEX] @d = @d.stroke('transparent') @d = @d.circle(x_pos, y_pos, x_pos + (@item_width.to_f/3.0).round, y_pos) draw_label(y_pos, point_index) end end @d.draw(@base_image) end protected # Instead of base class version, draws vertical background lines and label def draw_line_markers return if @hide_line_markers @d = @d.stroke_antialias false # Draw horizontal line markers and annotate with numbers @d = @d.stroke(@marker_color) @d = @d.stroke_width 1 if @y_axis_increment increment = @y_axis_increment number_of_lines = (@spread / @y_axis_increment).to_i else # Try to use a number of horizontal lines that will come out even. # # TODO Do the same for larger numbers...100, 75, 50, 25 if @marker_count.nil? (3..7).each do |lines| if @spread % lines == 0.0 @marker_count = lines break end end @marker_count ||= 5 end # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc. @increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1 number_of_lines = @marker_count increment = @increment end (0..number_of_lines).each do |index| marker_label = @minimum_value + index * increment x = @graph_left + (marker_label - @minimum_value) * @graph_width / @spread @d = @d.line(x, @graph_bottom, x, @graph_bottom + 0.5 * LABEL_MARGIN) unless @hide_line_numbers @d.fill = @font_color @d.font = @font if @font @d.stroke = 'transparent' @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = CenterGravity # TODO Center text over line @d = @d.annotate_scaled(@base_image, 0, 0, # Width of box to draw text in x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text label(marker_label, increment), @scale) end # unless @d = @d.stroke_antialias true end end ## # Draw on the Y axis instead of the X def draw_label(y_offset, index) if !@labels[index].nil? && @labels_seen[index].nil? @d.fill = @font_color @d.font = @font if @font @d.stroke = 'transparent' @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = EastGravity @d = @d.annotate_scaled(@base_image, 1, 1, -@graph_left + LABEL_MARGIN * 2.0, y_offset, @labels[index], @scale) @labels_seen[index] = 1 end end end gruff-0.6.0/lib/gruff/area.rb0000644000004100000410000000210112540054077015770 0ustar www-datawww-data require File.dirname(__FILE__) + '/base' class Gruff::Area < Gruff::Base def initialize(*) super @sorted_drawing = true end def draw super return unless @has_data @x_increment = @graph_width / (@column_count - 1).to_f @d = @d.stroke 'transparent' @norm_data.each do |data_row| poly_points = Array.new prev_x = prev_y = 0.0 @d = @d.fill data_row[DATA_COLOR_INDEX] data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| # Use incremented x and scaled y new_x = @graph_left + (@x_increment * index) new_y = @graph_top + (@graph_height - data_point * @graph_height) poly_points << new_x poly_points << new_y draw_label(new_x, index) prev_x = new_x prev_y = new_y end # Add closing points, draw polygon poly_points << @graph_right poly_points << @graph_bottom - 1 poly_points << @graph_left poly_points << @graph_bottom - 1 @d = @d.polyline(*poly_points) end @d.draw(@base_image) end end gruff-0.6.0/lib/gruff/stacked_mixin.rb0000644000004100000410000000115012540054077017705 0ustar www-datawww-data module Gruff::Base::StackedMixin # Used by StackedBar and child classes. # # tsal: moved from Base 03 FEB 2007 DATA_VALUES_INDEX = Gruff::Base::DATA_VALUES_INDEX def get_maximum_by_stack # Get sum of each stack max_hash = {} @data.each do |data_set| data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i| max_hash[i] = 0.0 unless max_hash[i] max_hash[i] += data_point.to_f end end # @maximum_value = 0 max_hash.keys.each do |key| @maximum_value = max_hash[key] if max_hash[key] > @maximum_value end @minimum_value = 0 end end gruff-0.6.0/lib/gruff/base.rb0000644000004100000410000011401112540054077015776 0ustar www-datawww-datarequire 'rubygems' require 'rmagick' require 'bigdecimal' require File.dirname(__FILE__) + '/deprecated' ## # = Gruff. Graphs. # # Author:: Geoffrey Grosenbach boss@topfunky.com # # Originally Created:: October 23, 2005 # # Extra thanks to Tim Hunter for writing RMagick, and also contributions by # Jarkko Laine, Mike Perham, Andreas Schwarz, Alun Eyre, Guillaume Theoret, # David Stokar, Paul Rogers, Dave Woodward, Frank Oxener, Kevin Clark, Cies # Breijs, Richard Cowin, and a cast of thousands. # # See Gruff::Base#theme= for setting themes. module Gruff class Base include Magick include Deprecated # Draw extra lines showing where the margins and text centers are DEBUG = false # Used for navigating the array of data to plot DATA_LABEL_INDEX = 0 DATA_VALUES_INDEX = 1 DATA_COLOR_INDEX = 2 DATA_VALUES_X_INDEX = 3 # Space around text elements. Mostly used for vertical spacing LEGEND_MARGIN = TITLE_MARGIN = 20.0 LABEL_MARGIN = 10.0 DEFAULT_MARGIN = 20.0 DEFAULT_TARGET_WIDTH = 800 THOUSAND_SEPARATOR = ',' # Blank space above the graph attr_accessor :top_margin # Blank space below the graph attr_accessor :bottom_margin # Blank space to the right of the graph attr_accessor :right_margin # Blank space to the left of the graph attr_accessor :left_margin # Blank space below the title attr_accessor :title_margin # Blank space below the legend attr_accessor :legend_margin # A hash of names for the individual columns, where the key is the array # index for the column this label represents. # # Not all columns need to be named. # # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008 attr_accessor :labels # Used internally for spacing. # # By default, labels are centered over the point they represent. attr_accessor :center_labels_over_point # Used internally for horizontal graph types. attr_accessor :has_left_labels # A label for the bottom of the graph attr_accessor :x_axis_label # A label for the left side of the graph attr_accessor :y_axis_label # attr_accessor :x_axis_increment # Manually set increment of the horizontal marking lines attr_accessor :y_axis_increment # Height of staggering between labels (Bar graph only) attr_accessor :label_stagger_height # Truncates labels if longer than max specified attr_accessor :label_max_size # How truncated labels visually appear if they exceed label_max_size # :absolute - does not show trailing dots to indicate truncation. This is # the default. # :trailing_dots - shows trailing dots to indicate truncation (note # that label_max_size must be greater than 3). attr_accessor :label_truncation_style # Get or set the list of colors that will be used to draw the bars or lines. attr_accessor :colors # The large title of the graph displayed at the top attr_accessor :title # Font used for titles, labels, etc. Works best if you provide the full # path to the TTF font file. RMagick must be built with the Freetype # libraries for this to work properly. # # Tries to find Bitstream Vera (Vera.ttf) in the location specified by # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise. # # The font= method below fulfills the role of the writer, so we only need # a reader here. attr_reader :font # Same as font but for the title. attr_reader :title_font # Specifies whether to draw the title bolded or not. attr_accessor :bold_title attr_accessor :font_color # Prevent drawing of line markers attr_accessor :hide_line_markers # Prevent drawing of the legend attr_accessor :hide_legend # Prevent drawing of the title attr_accessor :hide_title # Prevent drawing of line numbers attr_accessor :hide_line_numbers # Message shown when there is no data. Fits up to 20 characters. Defaults # to "No Data." attr_accessor :no_data_message # The font size of the large title at the top of the graph attr_accessor :title_font_size # Optionally set the size of the font. Based on an 800x600px graph. # Default is 20. # # Will be scaled down if the graph is smaller than 800px wide. attr_accessor :legend_font_size # Display the legend under the graph attr_accessor :legend_at_bottom # The font size of the labels around the graph attr_accessor :marker_font_size # The color of the auxiliary lines attr_accessor :marker_color attr_accessor :marker_shadow_color # The number of horizontal lines shown for reference attr_accessor :marker_count # You can manually set a minimum value instead of having the values # guessed for you. # # Set it after you have given all your data to the graph object. attr_accessor :minimum_value # You can manually set a maximum value, such as a percentage-based graph # that always goes to 100. # # If you use this, you must set it after you have given all your data to # the graph object. attr_accessor :maximum_value # Set to true if you want the data sets sorted with largest avg values drawn # first. attr_accessor :sort # Set to true if you want the data sets drawn with largest avg values drawn # first. This does not affect the legend. attr_accessor :sorted_drawing # Experimental attr_accessor :additional_line_values # Experimental attr_accessor :stacked # Optionally set the size of the colored box by each item in the legend. # Default is 20.0 # # Will be scaled down if graph is smaller than 800px wide. attr_accessor :legend_box_size # Output the values for the bars on a bar graph # Default is false attr_accessor :show_labels_for_bar_values # Set the number output format for labels using sprintf # Default is "%.2f" attr_accessor :label_formatting # With Side Bars use the data label for the marker value to the left of the bar # Default is false attr_accessor :use_data_label # If one numerical argument is given, the graph is drawn at 4/3 ratio # according to the given width (800 results in 800x600, 400 gives 400x300, # etc.). # # Or, send a geometry string for other ratios ('800x400', '400x225'). # # Looks for Bitstream Vera as the default font. Expects an environment var # of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.) def initialize(target_width=DEFAULT_TARGET_WIDTH) if Numeric === target_width @columns = target_width.to_f @rows = target_width.to_f * 0.75 else geometric_width, geometric_height = target_width.split('x') @columns = geometric_width.to_f @rows = geometric_height.to_f end initialize_ivars reset_themes self.theme = Themes::KEYNOTE end # Set instance variables for this object. # # Subclasses can override this, call super, then set values separately. # # This makes it possible to set defaults in a subclass but still allow # developers to change this values in their program. def initialize_ivars # Internal for calculations @raw_columns = 800.0 @raw_rows = 800.0 * (@rows/@columns) @column_count = 0 @marker_count = nil @maximum_value = @minimum_value = nil @has_data = false @data = Array.new @labels = Hash.new @labels_seen = Hash.new @sort = false @title = nil @scale = @columns / @raw_columns vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH']) @font = File.exists?(vera_font_path) ? vera_font_path : nil @bold_title = true @marker_font_size = 21.0 @legend_font_size = 20.0 @title_font_size = 36.0 @top_margin = @bottom_margin = @left_margin = @right_margin = DEFAULT_MARGIN @legend_margin = LEGEND_MARGIN @title_margin = TITLE_MARGIN @legend_box_size = 20.0 @no_data_message = 'No Data' @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = @legend_at_bottom = @show_labels_for_bar_values = false @center_labels_over_point = true @has_left_labels = false @label_stagger_height = 0 @label_max_size = 0 @label_truncation_style = :absolute @additional_line_values = [] @additional_line_colors = [] @theme_options = {} @x_axis_label = @y_axis_label = nil @y_axis_increment = nil @stacked = nil @norm_data = nil end # Sets the top, bottom, left and right margins to +margin+. def margins=(margin) @top_margin = @left_margin = @right_margin = @bottom_margin = margin end # Sets the font for graph text to the font at +font_path+. def font=(font_path) @font = font_path @d.font = @font end # Sets the title font to the font at +font_path+ def title_font=(font_path) @title_font = font_path end # Add a color to the list of available colors for lines. # # Example: # add_color('#c0e9d3') def add_color(colorname) @colors << colorname end # Replace the entire color list with a new array of colors. Also # aliased as the colors= setter method. # # If you specify fewer colors than the number of datasets you intend # to draw, 'increment_color' will cycle through the array, reusing # colors as needed. # # Note that (as with the 'theme' method), you should set up your color # list before you send your data (via the 'data' method). Calls to the # 'data' method made prior to this call will use whatever color scheme # was in place at the time data was called. # # Example: # replace_colors ['#cc99cc', '#d9e043', '#34d8a2'] def replace_colors(color_list=[]) @colors = color_list @color_index = 0 end # You can set a theme manually. Assign a hash to this method before you # send your data. # # graph.theme = { # :colors => %w(orange purple green white red), # :marker_color => 'blue', # :background_colors => ['black', 'grey', :top_bottom] # } # # :background_image => 'squirrel.png' is also possible. # # (Or hopefully something better looking than that.) # def theme=(options) reset_themes defaults = { :colors => %w(black white), :additional_line_colors => [], :marker_color => 'white', :marker_shadow_color => nil, :font_color => 'black', :background_colors => nil, :background_image => nil } @theme_options = defaults.merge options @colors = @theme_options[:colors] @marker_color = @theme_options[:marker_color] @marker_shadow_color = @theme_options[:marker_shadow_color] @font_color = @theme_options[:font_color] || @marker_color @additional_line_colors = @theme_options[:additional_line_colors] render_background end def theme_keynote self.theme = Themes::KEYNOTE end def theme_37signals self.theme = Themes::THIRTYSEVEN_SIGNALS end def theme_rails_keynote self.theme = Themes::RAILS_KEYNOTE end def theme_odeo self.theme = Themes::ODEO end def theme_pastel self.theme = Themes::PASTEL end def theme_greyscale self.theme = Themes::GREYSCALE end # Parameters are an array where the first element is the name of the dataset # and the value is an array of values to plot. # # Can be called multiple times with different datasets for a multi-valued # graph. # # If the color argument is nil, the next color from the default theme will # be used. # # NOTE: If you want to use a preset theme, you must set it before calling # data(). # # Example: # data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00') def data(name, data_points=[], color=nil) data_points = Array(data_points) # make sure it's an array @data << [name, data_points, color] # Set column count if this is larger than previous counts @column_count = (data_points.length > @column_count) ? data_points.length : @column_count # Pre-normalize data_points.each do |data_point| next if data_point.nil? # Setup max/min so spread starts at the low end of the data points if @maximum_value.nil? && @minimum_value.nil? @maximum_value = @minimum_value = data_point end # TODO Doesn't work with stacked bar graphs # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value @has_data = true if @maximum_value >= 0 @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value @has_data = true if @minimum_value < 0 end end # Writes the graph to a file. Defaults to 'graph.png' # # Example: # write('graphs/my_pretty_graph.png') def write(filename='graph.png') draw @base_image.write(filename) end # Return the graph as a rendered binary blob. def to_blob(fileformat='PNG') draw @base_image.to_blob do self.format = fileformat end end protected # Overridden by subclasses to do the actual plotting of the graph. # # Subclasses should start by calling super() for this method. def draw # Maybe should be done in one of the following functions for more granularity. unless @has_data draw_no_data return end setup_data setup_drawing debug { # Outer margin @d.rectangle(@left_margin, @top_margin, @raw_columns - @right_margin, @raw_rows - @bottom_margin) # Graph area box @d.rectangle(@graph_left, @graph_top, @graph_right, @graph_bottom) } draw_legend draw_line_markers draw_axis_labels draw_title end # Perform data manipulation before calculating chart measurements def setup_data # :nodoc: if @y_axis_increment && !@hide_line_markers @maximum_value = [@y_axis_increment, @maximum_value, (@maximum_value.to_f / @y_axis_increment).round * @y_axis_increment].max @minimum_value = [@minimum_value, (@minimum_value.to_f / @y_axis_increment).round * @y_axis_increment].min end make_stacked if @stacked end # Calculates size of drawable area and generates normalized data. # # * line markers # * legend # * title def setup_drawing calculate_spread sort_data if @sort # Sort data with avg largest values set first (for display) set_colors normalize setup_graph_measurements sort_norm_data if @sorted_drawing # Sort norm_data with avg largest values set first (for display) end # Make copy of data with values scaled between 0-100 def normalize(force=false) if @norm_data.nil? || force @norm_data = [] return unless @has_data @data.each do |data_row| norm_data_points = [] data_row[DATA_VALUES_INDEX].each do |data_point| if data_point.nil? norm_data_points << nil else norm_data_points << ((data_point.to_f - @minimum_value.to_f) / @spread) end end if @show_labels_for_bar_values @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX], data_row[DATA_VALUES_INDEX]] else @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]] end end end end def calculate_spread # :nodoc: @spread = @maximum_value.to_f - @minimum_value.to_f @spread = @spread > 0 ? @spread : 1 end ## # Calculates size of drawable area, general font dimensions, etc. def setup_graph_measurements @marker_caps_height = @hide_line_markers ? 0 : calculate_caps_height(@marker_font_size) @title_caps_height = (@hide_title || @title.nil?) ? 0 : calculate_caps_height(@title_font_size) * @title.lines.to_a.size @legend_caps_height = @hide_legend ? 0 : calculate_caps_height(@legend_font_size) if @hide_line_markers (@graph_left, @graph_right_margin, @graph_bottom_margin) = [@left_margin, @right_margin, @bottom_margin] else if @has_left_labels longest_left_label_width = calculate_width(@marker_font_size, labels.values.inject('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }) * 1.25 else longest_left_label_width = calculate_width(@marker_font_size, label(@maximum_value.to_f, @increment)) end # Shift graph if left line numbers are hidden line_number_width = @hide_line_numbers && !@has_left_labels ? 0.0 : (longest_left_label_width + LABEL_MARGIN * 2) @graph_left = @left_margin + line_number_width + (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2) # Make space for half the width of the rightmost column label. # Might be greater than the number of columns if between-style bar markers are used. last_label = @labels.keys.sort.last.to_i extra_room_for_long_label = (last_label >= (@column_count-1) && @center_labels_over_point) ? calculate_width(@marker_font_size, @labels[last_label]) / 2.0 : 0 @graph_right_margin = @right_margin + extra_room_for_long_label @graph_bottom_margin = @bottom_margin + @marker_caps_height + LABEL_MARGIN end @graph_right = @raw_columns - @graph_right_margin @graph_width = @raw_columns - @graph_left - @graph_right_margin # When @hide title, leave a title_margin space for aesthetics. # Same with @hide_legend @graph_top = @legend_at_bottom ? @top_margin : (@top_margin + (@hide_title ? title_margin : @title_caps_height + title_margin) + (@hide_legend ? legend_margin : @legend_caps_height + legend_margin)) x_axis_label_height = @x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN # FIXME: Consider chart types other than bar @graph_bottom = @raw_rows - @graph_bottom_margin - x_axis_label_height - @label_stagger_height @graph_height = @graph_bottom - @graph_top end # Draw the optional labels for the x axis and y axis. def draw_axis_labels unless @x_axis_label.nil? # X Axis # Centered vertically and horizontally by setting the # height to 1.0 and the width to the width of the graph. x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height # TODO Center between graph area @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = NorthGravity @d = @d.annotate_scaled(@base_image, @raw_columns, 1.0, 0.0, x_axis_label_y_coordinate, @x_axis_label, @scale) debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate } end unless @y_axis_label.nil? # Y Axis, rotated vertically @d.rotation = -90.0 @d.gravity = CenterGravity @d = @d.annotate_scaled(@base_image, 1.0, @raw_rows, @left_margin + @marker_caps_height / 2.0, 0.0, @y_axis_label, @scale) @d.rotation = 90.0 end end # Draws horizontal background lines and labels def draw_line_markers return if @hide_line_markers @d = @d.stroke_antialias false if @y_axis_increment.nil? # Try to use a number of horizontal lines that will come out even. # # TODO Do the same for larger numbers...100, 75, 50, 25 if @marker_count.nil? (3..7).each do |lines| if @spread % lines == 0.0 @marker_count = lines break end end @marker_count ||= 4 end @increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1 else # TODO Make this work for negative values @marker_count = (@spread / @y_axis_increment).to_i @increment = @y_axis_increment end @increment_scaled = @graph_height.to_f / (@spread / @increment) # Draw horizontal line markers and annotate with numbers (0..@marker_count).each do |index| y = @graph_top + @graph_height - index.to_f * @increment_scaled @d = @d.fill(@marker_color) # FIXME(uwe): Workaround for Issue #66 # https://github.com/topfunky/gruff/issues/66 # https://github.com/rmagick/rmagick/issues/82 # Remove if the issue gets fixed. y += 0.001 unless defined?(JRUBY_VERSION) # EMXIF @d = @d.line(@graph_left, y, @graph_right, y) #If the user specified a marker shadow color, draw a shadow just below it unless @marker_shadow_color.nil? @d = @d.fill(@marker_shadow_color) @d = @d.line(@graph_left, y + 1, @graph_right, y + 1) end marker_label = BigDecimal(index.to_s) * BigDecimal(@increment.to_s) + BigDecimal(@minimum_value.to_s) unless @hide_line_numbers @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = EastGravity # Vertically center with 1.0 for the height @d = @d.annotate_scaled(@base_image, @graph_left - LABEL_MARGIN, 1.0, 0.0, y, label(marker_label, @increment), @scale) end end # # Submitted by a contibutor...the utility escapes me # i = 0 # @additional_line_values.each do |value| # @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value) # # y = @graph_top + @graph_height - @increment_scaled # # @d = @d.stroke(@additional_line_colors[i]) # @d = @d.line(@graph_left, y, @graph_right, y) # # # @d.fill = @additional_line_colors[i] # @d.font = @font if @font # @d.stroke('transparent') # @d.pointsize = scale_fontsize(@marker_font_size) # @d.gravity = EastGravity # @d = @d.annotate_scaled( @base_image, # 100, 20, # -10, y - (@marker_font_size/2.0), # "", @scale) # i += 1 # end @d = @d.stroke_antialias true end ## # Return the sum of values in an array. # # Duplicated to not conflict with active_support in Rails. def sum(arr) arr.inject(0) { |i, m| m + i } end ## # Return a calculation of center def center(size) (@raw_columns - size) / 2 end ## # Draws a legend with the names of the datasets matched # to the colors used to draw them. def draw_legend return if @hide_legend @legend_labels = @data.collect { |item| item[DATA_LABEL_INDEX] } legend_square_width = @legend_box_size # small square with color of this item # May fix legend drawing problem at small sizes @d.font = @font if @font @d.pointsize = @legend_font_size label_widths = [[]] # Used to calculate line wrap @legend_labels.each do |label| metrics = @d.get_type_metrics(@base_image, label.to_s) label_width = metrics.width + legend_square_width * 2.7 label_widths.last.push label_width if sum(label_widths.last) > (@raw_columns * 0.9) label_widths.push [label_widths.last.pop] end end current_x_offset = center(sum(label_widths.first)) current_y_offset = @legend_at_bottom ? @graph_height + title_margin : (@hide_title ? @top_margin + title_margin : @top_margin + title_margin + @title_caps_height) @legend_labels.each_with_index do |legend_label, index| # Draw label @d.fill = @font_color @d.font = @font if @font @d.pointsize = scale_fontsize(@legend_font_size) @d.stroke('transparent') @d.font_weight = NormalWeight @d.gravity = WestGravity @d = @d.annotate_scaled(@base_image, @raw_columns, 1.0, current_x_offset + (legend_square_width * 1.7), current_y_offset, legend_label.to_s, @scale) # Now draw box with color of this dataset @d = @d.stroke('transparent') @d = @d.fill @data[index][DATA_COLOR_INDEX] @d = @d.rectangle(current_x_offset, current_y_offset - legend_square_width / 2.0, current_x_offset + legend_square_width, current_y_offset + legend_square_width / 2.0) @d.pointsize = @legend_font_size metrics = @d.get_type_metrics(@base_image, legend_label.to_s) current_string_offset = metrics.width + (legend_square_width * 2.7) # Handle wrapping label_widths.first.shift if label_widths.first.empty? debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset } label_widths.shift current_x_offset = center(sum(label_widths.first)) unless label_widths.empty? line_height = [@legend_caps_height, legend_square_width].max + legend_margin if label_widths.length > 0 # Wrap to next line and shrink available graph dimensions current_y_offset += line_height @graph_top += line_height @graph_height = @graph_bottom - @graph_top end else current_x_offset += current_string_offset end end @color_index = 0 end # Draws a title on the graph. def draw_title return if (@hide_title || @title.nil?) @d.fill = @font_color @d.font = @title_font || @font if @title_font || @font @d.stroke('transparent') @d.pointsize = scale_fontsize(@title_font_size) @d.font_weight = if @bold_title then BoldWeight else NormalWeight end @d.gravity = NorthGravity @d = @d.annotate_scaled(@base_image, @raw_columns, 1.0, 0, @top_margin, @title, @scale) end # Draws column labels below graph, centered over x_offset #-- # TODO Allow WestGravity as an option def draw_label(x_offset, index) return if @hide_line_markers if !@labels[index].nil? && @labels_seen[index].nil? y_offset = @graph_bottom + LABEL_MARGIN # TESTME # FIXME: Consider chart types other than bar # TODO: See if index.odd? is the best stragegy y_offset += @label_stagger_height if index.odd? label_text = @labels[index] # TESTME # FIXME: Consider chart types other than bar if label_text.size > @label_max_size if @label_truncation_style == :trailing_dots if @label_max_size > 3 # 4 because '...' takes up 3 chars label_text = "#{label_text[0 .. (@label_max_size - 4)]}..." end else # @label_truncation_style is :absolute (default) label_text = label_text[0 .. (@label_max_size - 1)] end end if x_offset >= @graph_left && x_offset <= @graph_right @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = NorthGravity @d = @d.annotate_scaled(@base_image, 1.0, 1.0, x_offset, y_offset, label_text, @scale) end @labels_seen[index] = 1 debug { @d.line 0.0, y_offset, @raw_columns, y_offset } end end # Draws the data value over the data point in bar graphs def draw_value_label(x_offset, y_offset, data_point, bar_value=false) return if @hide_line_markers && !bar_value #y_offset = @graph_bottom + LABEL_MARGIN @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(@marker_font_size) @d.gravity = NorthGravity @d = @d.annotate_scaled(@base_image, 1.0, 1.0, x_offset, y_offset, data_point.to_s, @scale) debug { @d.line 0.0, y_offset, @raw_columns, y_offset } end # Shows an error message because you have no data. def draw_no_data @d.fill = @font_color @d.font = @font if @font @d.stroke('transparent') @d.font_weight = NormalWeight @d.pointsize = scale_fontsize(80) @d.gravity = CenterGravity @d = @d.annotate_scaled(@base_image, @raw_columns, @raw_rows/2.0, 0, 10, @no_data_message, @scale) end # Finds the best background to render based on the provided theme options. # # Creates a @base_image to draw on. def render_background case @theme_options[:background_colors] when Array @base_image = render_gradiated_background(@theme_options[:background_colors][0], @theme_options[:background_colors][1], @theme_options[:background_direction]) when String @base_image = render_solid_background(@theme_options[:background_colors]) else @base_image = render_image_background(*@theme_options[:background_image]) end end # Make a new image at the current size with a solid +color+. def render_solid_background(color) Image.new(@columns, @rows) { self.background_color = color } end # Use with a theme definition method to draw a gradiated background. def render_gradiated_background(top_color, bottom_color, direct = :top_bottom) case direct when :bottom_top gradient_fill = GradientFill.new(0, 0, 100, 0, bottom_color, top_color) when :left_right gradient_fill = GradientFill.new(0, 0, 0, 100, top_color, bottom_color) when :right_left gradient_fill = GradientFill.new(0, 0, 0, 100, bottom_color, top_color) when :topleft_bottomright gradient_fill = GradientFill.new(0, 100, 100, 0, top_color, bottom_color) when :topright_bottomleft gradient_fill = GradientFill.new(0, 0, 100, 100, bottom_color, top_color) else gradient_fill = GradientFill.new(0, 0, 100, 0, top_color, bottom_color) end Image.new(@columns, @rows, gradient_fill) end # Use with a theme to use an image (800x600 original) background. def render_image_background(image_path) image = Image.read(image_path) if @scale != 1.0 image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph) end image[0] end # Use with a theme to make a transparent background def render_transparent_background Image.new(@columns, @rows) do self.background_color = 'transparent' end end # Resets everything to defaults (except data). def reset_themes @color_index = 0 @labels_seen = {} @theme_options = {} @d = Draw.new # Scale down from 800x600 used to calculate drawing. @d = @d.scale(@scale, @scale) end def scale(value) # :nodoc: value * @scale end # Return a comparable fontsize for the current graph. def scale_fontsize(value) value * @scale end def clip_value_if_greater_than(value, max_value) # :nodoc: (value > max_value) ? max_value : value end # Overridden by subclasses such as stacked bar. def larger_than_max?(data_point) # :nodoc: data_point > @maximum_value end def less_than_min?(data_point) # :nodoc: data_point < @minimum_value end def significant(i) # :nodoc: return 1.0 if i == 0 # Keep from going into infinite loop inc = BigDecimal(i.to_s) factor = BigDecimal('1.0') while inc < 10 inc *= 10 factor /= 10 end while inc > 100 inc /= 10 factor *= 10 end res = inc.floor * factor if res.to_i.to_f == res res.to_i else res end end # Sort with largest overall summed value at front of array. def sort_data @data = @data.sort_by { |a| -a[DATA_VALUES_INDEX].inject(0) { |sum, num| sum + num.to_f } } end # Set the color for each data set unless it was gived in the data(...) call. def set_colors @data.each { |a| a[DATA_COLOR_INDEX] ||= increment_color } end # Sort with largest overall summed value at front of array so it shows up # correctly in the drawn graph. def sort_norm_data @norm_data = @norm_data.sort_by { |a| -a[DATA_VALUES_INDEX].inject(0) { |sum, num| sum + num.to_f } } end # Used by StackedBar and child classes. # # May need to be moved to the StackedBar class. def get_maximum_by_stack # Get sum of each stack max_hash = {} @data.each do |data_set| data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i| max_hash[i] = 0.0 unless max_hash[i] max_hash[i] += data_point.to_f end end # @maximum_value = 0 max_hash.keys.each do |key| @maximum_value = max_hash[key] if max_hash[key] > @maximum_value end @minimum_value = 0 end def make_stacked # :nodoc: stacked_values = Array.new(@column_count, 0) @data.each do |value_set| value_set[DATA_VALUES_INDEX].each_with_index do |value, index| stacked_values[index] += value end value_set[DATA_VALUES_INDEX] = stacked_values.dup end end private # Takes a block and draws it if DEBUG is true. # # Example: # debug { @d.rectangle x1, y1, x2, y2 } def debug if DEBUG @d = @d.fill 'transparent' @d = @d.stroke 'turquoise' @d = yield end end # Returns the next color in your color list. def increment_color @color_index = (@color_index + 1) % @colors.length @colors[@color_index - 1] end # Return a formatted string representing a number value that should be # printed as a label. def label(value, increment) label = if increment if increment >= 10 || (increment * 1) == (increment * 1).to_i.to_f sprintf('%0i', value) elsif increment >= 1.0 || (increment * 10) == (increment * 10).to_i.to_f sprintf('%0.1f', value) elsif increment >= 0.1 || (increment * 100) == (increment * 100).to_i.to_f sprintf('%0.2f', value) elsif increment >= 0.01 || (increment * 1000) == (increment * 1000).to_i.to_f sprintf('%0.3f', value) elsif increment >= 0.001 || (increment * 10000) == (increment * 10000).to_i.to_f sprintf('%0.4f', value) else value.to_s end elsif (@spread.to_f % (@marker_count.to_f==0 ? 1 : @marker_count.to_f) == 0) || !@y_axis_increment.nil? value.to_i.to_s elsif @spread > 10.0 sprintf('%0i', value) elsif @spread >= 3.0 sprintf('%0.2f', value) else value.to_s end parts = label.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{THOUSAND_SEPARATOR}") parts.join('.') end # Returns the height of the capital letter 'X' for the current font and # size. # # Not scaled since it deals with dimensions that the regular scaling will # handle. def calculate_caps_height(font_size) @d.pointsize = font_size @d.font = @font if @font @d.get_type_metrics(@base_image, 'X').height end # Returns the width of a string at this pointsize. # # Not scaled since it deals with dimensions that the regular # scaling will handle. def calculate_width(font_size, text) return 0 if text.nil? @d.pointsize = font_size @d.font = @font if @font @d.get_type_metrics(@base_image, text.to_s).width end # Used for degree => radian conversions def deg2rad(angle) angle * (Math::PI/180.0) end end # Gruff::Base class IncorrectNumberOfDatasetsException < StandardError; end end # Gruff module Magick class Draw # Additional method to scale annotation text since Draw.scale doesn't. def annotate_scaled(img, width, height, x, y, text, scale) scaled_width = (width * scale) >= 1 ? (width * scale) : 1 scaled_height = (height * scale) >= 1 ? (height * scale) : 1 self.annotate(img, scaled_width, scaled_height, x * scale, y * scale, text.gsub('%', '%%')) end if defined? JRUBY_VERSION # FIXME(uwe): We should NOT need to implement this method. # Remove this method as soon as RMagick4J Issue #16 is fixed. # https://github.com/Serabe/RMagick4J/issues/16 def fill=(fill) fill = {:white => '#FFFFFF'}[fill.to_sym] || fill @draw.fill = Magick4J.ColorDatabase.query_default(fill) self end # EMXIF end end end # Magick class String #Taken from http://codesnippets.joyent.com/posts/show/330 def commify(delimiter=',') self.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") end end gruff-0.6.0/lib/gruff/net.rb0000644000004100000410000000777512540054077015674 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' # Experimental!!! See also the Spider graph. class Gruff::Net < Gruff::Base # Hide parts of the graph to fit more datapoints, or for a different appearance. attr_accessor :hide_dots # Dimensions of lines and dots; calculated based on dataset size if left unspecified attr_accessor :line_width attr_accessor :dot_radius def initialize(*args) super @hide_dots = false @hide_line_numbers = true @sorted_drawing = true end def draw super return unless @has_data @radius = @graph_height / 2.0 @center_x = @graph_left + (@graph_width / 2.0) @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit @x_increment = @graph_width / (@column_count - 1).to_f circle_radius = dot_radius || clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0) @d = @d.stroke_opacity 1.0 @d = @d.stroke_width line_width || clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0) if defined?(@norm_baseline) level = @graph_top + (@graph_height - @norm_baseline * @graph_height) @d = @d.push @d.stroke_color @baseline_color @d.fill_opacity 0.0 @d.stroke_dasharray(10, 20) @d.stroke_width 5 @d.line(@graph_left, level, @graph_left + @graph_width, level) @d = @d.pop end @norm_data.each do |data_row| @d = @d.stroke data_row[DATA_COLOR_INDEX] @d = @d.fill data_row[DATA_COLOR_INDEX] data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| next if data_point.nil? rad_pos = index * Math::PI * 2 / @column_count point_distance = data_point * @radius start_x = @center_x + Math::sin(rad_pos) * point_distance start_y = @center_y - Math::cos(rad_pos) * point_distance next_index = index + 1 < data_row[DATA_VALUES_INDEX].length ? index + 1 : 0 next_rad_pos = next_index * Math::PI * 2 / @column_count next_point_distance = data_row[DATA_VALUES_INDEX][next_index] * @radius end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance @d = @d.line(start_x, start_y, end_x, end_y) @d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots end end @d.draw(@base_image) end # the lines connecting in the center, with the first line vertical def draw_line_markers return if @hide_line_markers # have to do this here (AGAIN)... see draw() in this class # because this funtion is called before the @radius, @center_x and @center_y are set @radius = @graph_height / 2.0 @center_x = @graph_left + (@graph_width / 2.0) @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit # Draw horizontal line markers and annotate with numbers @d = @d.stroke(@marker_color) @d = @d.stroke_width 1 (0..@column_count-1).each do |index| rad_pos = index * Math::PI * 2 / @column_count @d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius) marker_label = labels[index] ? labels[index].to_s : '000' draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label) end end private def draw_label(center_x, center_y, angle, radius, amount) r_offset = 1.1 x_offset = center_x # + 15 # The label points need to be tweaked slightly y_offset = center_y # + 0 # This one doesn't though x = x_offset + (radius * r_offset * Math.sin(deg2rad(angle))) y = y_offset - (radius * r_offset * Math.cos(deg2rad(angle))) # Draw label @d.fill = @marker_color @d.font = @font if @font @d.pointsize = scale_fontsize(20) @d.stroke = 'transparent' @d.font_weight = BoldWeight @d.gravity = CenterGravity @d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale) end end gruff-0.6.0/lib/gruff/bar.rb0000644000004100000410000000664412540054077015644 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/base' require File.dirname(__FILE__) + '/bar_conversion' class Gruff::Bar < Gruff::Base # Spacing factor applied between bars attr_accessor :bar_spacing def initialize(*args) super @spacing_factor = 0.9 end def draw # Labels will be centered over the left of the bar if # there are more labels than columns. This is basically the same # as where it would be for a line graph. @center_labels_over_point = (@labels.keys.length > @column_count ? true : false) super return unless @has_data draw_bars end # Can be used to adjust the spaces between the bars. # Accepts values between 0.00 and 1.00 where 0.00 means no spacing at all # and 1 means that each bars' width is nearly 0 (so each bar is a simple # line with no x dimension). # # Default value is 0.9. def spacing_factor=(space_percent) raise ArgumentError, 'spacing_factor must be between 0.00 and 1.00' unless (space_percent >= 0 and space_percent <= 1) @spacing_factor = (1 - space_percent) end protected def draw_bars # Setup spacing. # # Columns sit side-by-side. @bar_spacing ||= @spacing_factor # space between the bars @bar_width = @graph_width / (@column_count * @data.length).to_f padding = (@bar_width * (1 - @bar_spacing)) / 2 @d = @d.stroke_opacity 0.0 # Setup the BarConversion Object conversion = Gruff::BarConversion.new() conversion.graph_height = @graph_height conversion.graph_top = @graph_top # Set up the right mode [1,2,3] see BarConversion for further explanation if @minimum_value >= 0 then # all bars go from zero to positiv conversion.mode = 1 else # all bars go from 0 to negativ if @maximum_value <= 0 then conversion.mode = 2 else # bars either go from zero to negativ or to positiv conversion.mode = 3 conversion.spread = @spread conversion.minimum_value = @minimum_value conversion.zero = -@minimum_value/@spread end end # iterate over all normalised data @norm_data.each_with_index do |data_row, row_index| data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index| # Use incremented x and scaled y # x left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index))) + padding right_x = left_x + @bar_width * @bar_spacing # y conv = [] conversion.get_left_y_right_y_scaled( data_point, conv ) # create new bar @d = @d.fill data_row[DATA_COLOR_INDEX] @d = @d.rectangle(left_x, conv[0], right_x, conv[1]) # Calculate center based on bar_width and current row label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0) # Subtract half a bar width to center left if requested draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index) if @show_labels_for_bar_values val = (@label_formatting || '%.2f') % @norm_data[row_index][3][point_index] draw_value_label(left_x + (right_x - left_x)/2, conv[0]-30, val.commify, true) end end end # Draw the last label if requested draw_label(@graph_right, @column_count) if @center_labels_over_point @d.draw(@base_image) end end gruff-0.6.0/lib/gruff.rb0000644000004100000410000000060012540054077015062 0ustar www-datawww-datarequire 'gruff/version' # Extra full path added to fix loading errors on some installations. %w( themes base area bar bezier bullet dot line net pie scatter spider stacked_area stacked_bar side_stacked_bar side_bar accumulator_bar scene mini/legend mini/bar mini/pie mini/side_bar ).each do |filename| require "gruff/#{filename}" end gruff-0.6.0/gruff.gemspec0000644000004100000410000000225012540054077015337 0ustar www-datawww-data# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'date' require 'gruff/version' Gem::Specification.new do |s| s.name = %q{gruff} s.version = Gruff::VERSION s.authors = ['Geoffrey Grosenbach', 'Uwe Kubosch'] s.date = Date.today.to_s s.description = %q{Beautiful graphs for one or multiple datasets. Can be used on websites or in documents.} s.email = %q{boss@topfunky.com} s.files = `git ls-files`.split($/).reject{|f| f =~ /^test#{File::ALT_SEPARATOR || File::SEPARATOR}output/} s.homepage = %q{https://github.com/topfunky/gruff} s.require_paths = %w(lib) s.summary = %q{Beautiful graphs for one or multiple datasets.} s.test_files = s.files.grep(%r{^(test|spec|features)/}) s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) } s.specification_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION if defined? JRUBY_VERSION s.platform = 'java' s.add_dependency 'rmagick4j', '>= 0.3.9' else s.add_dependency 'rmagick', '>= 2.13.4' end s.add_development_dependency('rake') s.add_development_dependency('test-unit') s.license = 'MIT' end gruff-0.6.0/rails_generators/0000755000004100000410000000000012540054077016225 5ustar www-datawww-datagruff-0.6.0/rails_generators/gruff/0000755000004100000410000000000012540054077017336 5ustar www-datawww-datagruff-0.6.0/rails_generators/gruff/templates/0000755000004100000410000000000012540054077021334 5ustar www-datawww-datagruff-0.6.0/rails_generators/gruff/templates/functional_test.rb0000644000004100000410000000140412540054077025061 0ustar www-datawww-datarequire File.dirname(__FILE__) + '<%= '/..' * controller_class_name.split("::").length %>/test_helper' require '<%= parent_folder_for_require %><%= controller_file_name %>_controller' # Re-raise errors caught by the controller. class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase #fixtures :data def setup @controller = <%= controller_class_name %>Controller.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end # TODO Replace this with your actual tests def test_show get :show assert_response :success assert_equal 'image/png', @response.headers['Content-Type'] end end gruff-0.6.0/rails_generators/gruff/templates/controller.rb0000644000004100000410000000213712540054077024047 0ustar www-datawww-dataclass <%= controller_class_name %>Controller < ApplicationController # To make caching easier, add a line like this to config/routes.rb: # map.graph "graph/:action/:id/image.png", :controller => "graph" # # Then reference it with the named route: # image_tag graph_url(:action => 'show', :id => 42) def show g = Gruff::Line.new # Uncomment to use your own theme or font # See http://colourlovers.com or http://www.firewheeldesign.com/widgets/ for color ideas # g.theme = { # :colors => ['#663366', '#cccc99', '#cc6633', '#cc9966', '#99cc99'], # :marker_color => 'white', # :background_colors => ['black', '#333333'] # } # g.font = File.expand_path('artwork/fonts/VeraBd.ttf', RAILS_ROOT) g.title = "Gruff-o-Rama" g.data("Apples", [1, 2, 3, 4, 4, 3]) g.data("Oranges", [4, 8, 7, 9, 8, 9]) g.data("Watermelon", [2, 3, 1, 5, 6, 8]) g.data("Peaches", [9, 9, 10, 8, 7, 9]) g.labels = {0 => '2004', 2 => '2005', 4 => '2006'} send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "gruff.png") end end gruff-0.6.0/rails_generators/gruff/gruff_generator.rb0000644000004100000410000000467612540054077023057 0ustar www-datawww-dataclass GruffGenerator < Rails::Generator::NamedBase attr_reader :controller_name, :controller_class_path, :controller_file_path, :controller_class_nesting, :controller_class_nesting_depth, :controller_class_name, :controller_singular_name, :controller_plural_name, :parent_folder_for_require alias_method :controller_file_name, :controller_singular_name alias_method :controller_table_name, :controller_plural_name def initialize(runtime_args, runtime_options = {}) super # Take controller name from the next argument. @controller_name = runtime_args.shift base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name) @controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name) if @controller_class_nesting.empty? @controller_class_name = @controller_class_name_without_nesting else @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}" end end def manifest record do |m| # Check for class naming collisions. m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}ControllerTest" # Controller, helper, views, and test directories. m.directory File.join('app/controllers', controller_class_path) m.directory File.join('test/functional', controller_class_path) m.template 'controller.rb', File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") # For some reason this doesn't take effect if done in initialize() @parent_folder_for_require = @controller_class_path.join('/').gsub(%r%app/controllers/?%, '') @parent_folder_for_require += @parent_folder_for_require.blank? ? '' : '/' m.template 'functional_test.rb', File.join('test/functional', controller_class_path, "#{controller_file_name}_controller_test.rb") end end protected # Override with your own usage banner. def banner "Usage: #{$0} gruff ControllerName" end end gruff-0.6.0/metadata.yml0000644000004100000410000001203512540054077015166 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: gruff version: !ruby/object:Gem::Version version: 0.6.0 platform: ruby authors: - Geoffrey Grosenbach - Uwe Kubosch autorequire: bindir: bin cert_chain: [] date: 2015-05-31 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rmagick requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 2.13.4 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 2.13.4 - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: test-unit requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: Beautiful graphs for one or multiple datasets. Can be used on websites or in documents. email: boss@topfunky.com executables: [] extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".travis.yml" - Gemfile - History.txt - MIT-LICENSE - Manifest.txt - README.md - RELEASE.md - Rakefile - assets/bubble.png - assets/city_scene/background/0000.png - assets/city_scene/background/0600.png - assets/city_scene/background/2000.png - assets/city_scene/clouds/cloudy.png - assets/city_scene/clouds/partly_cloudy.png - assets/city_scene/clouds/stormy.png - assets/city_scene/grass/default.png - assets/city_scene/haze/true.png - assets/city_scene/number_sample/1.png - assets/city_scene/number_sample/2.png - assets/city_scene/number_sample/default.png - assets/city_scene/sky/0000.png - assets/city_scene/sky/0200.png - assets/city_scene/sky/0400.png - assets/city_scene/sky/0600.png - assets/city_scene/sky/0800.png - assets/city_scene/sky/1000.png - assets/city_scene/sky/1200.png - assets/city_scene/sky/1400.png - assets/city_scene/sky/1500.png - assets/city_scene/sky/1700.png - assets/city_scene/sky/2000.png - assets/pc306715.jpg - assets/plastik/blue.png - assets/plastik/green.png - assets/plastik/red.png - gruff.gemspec - init.rb - lib/gruff.rb - lib/gruff/accumulator_bar.rb - lib/gruff/area.rb - lib/gruff/bar.rb - lib/gruff/bar_conversion.rb - lib/gruff/base.rb - lib/gruff/bezier.rb - lib/gruff/bullet.rb - lib/gruff/deprecated.rb - lib/gruff/dot.rb - lib/gruff/line.rb - lib/gruff/mini/bar.rb - lib/gruff/mini/legend.rb - lib/gruff/mini/pie.rb - lib/gruff/mini/side_bar.rb - lib/gruff/net.rb - lib/gruff/photo_bar.rb - lib/gruff/pie.rb - lib/gruff/scatter.rb - lib/gruff/scene.rb - lib/gruff/side_bar.rb - lib/gruff/side_stacked_bar.rb - lib/gruff/spider.rb - lib/gruff/stacked_area.rb - lib/gruff/stacked_bar.rb - lib/gruff/stacked_mixin.rb - lib/gruff/themes.rb - lib/gruff/version.rb - rails_generators/gruff/gruff_generator.rb - rails_generators/gruff/templates/controller.rb - rails_generators/gruff/templates/functional_test.rb - test/gruff_test_case.rb - test/image_compare.rb - test/test_accumulator_bar.rb - test/test_area.rb - test/test_bar.rb - test/test_base.rb - test/test_bezier.rb - test/test_bullet.rb - test/test_dot.rb - test/test_labels_for_null_data.rb - test/test_legend.rb - test/test_line.rb - test/test_mini_bar.rb - test/test_mini_pie.rb - test/test_mini_side_bar.rb - test/test_net.rb - test/test_photo.rb - test/test_pie.rb - test/test_scatter.rb - test/test_scene.rb - test/test_side_bar.rb - test/test_sidestacked_bar.rb - test/test_spider.rb - test/test_stacked_area.rb - test/test_stacked_bar.rb homepage: https://github.com/topfunky/gruff licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.6 signing_key: specification_version: 4 summary: Beautiful graphs for one or multiple datasets. test_files: - test/gruff_test_case.rb - test/image_compare.rb - test/test_accumulator_bar.rb - test/test_area.rb - test/test_bar.rb - test/test_base.rb - test/test_bezier.rb - test/test_bullet.rb - test/test_dot.rb - test/test_labels_for_null_data.rb - test/test_legend.rb - test/test_line.rb - test/test_mini_bar.rb - test/test_mini_pie.rb - test/test_mini_side_bar.rb - test/test_net.rb - test/test_photo.rb - test/test_pie.rb - test/test_scatter.rb - test/test_scene.rb - test/test_side_bar.rb - test/test_sidestacked_bar.rb - test/test_spider.rb - test/test_stacked_area.rb - test/test_stacked_bar.rb gruff-0.6.0/test/0000755000004100000410000000000012540054077013641 5ustar www-datawww-datagruff-0.6.0/test/test_legend.rb0000644000004100000410000000411412540054077016463 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/gruff_test_case' class TestGruffLegend < GruffTestCase def setup @datasets = [ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]], [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]], [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]], [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], [:Arthur, [5, 10, 13, 11, 6, 16, 22, 32]], [:Vincent, [5, 10, 13, 11, 6, 16, 22, 32]], [:Jake, [5, 10, 13, 11, 6, 16, 22, 32]], [:Stephen, [5, 10, 13, 11, 6, 16, 22, 32]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', 4 => '6/4', 5 => '6/12', 6 => '6/21', 7 => '6/28', } end def full_suite_for(name, type) [800, 400].each do |width| [nil, 4, 16, 30].each do |font_size| g = type.new(width) g.title = "Wrapped Legend Bar Test #{font_size}pts #{width}px" g.labels = @sample_labels 0xEFD250.step(0xFF0000, 60) do |num| g.colors << '#%x' % num end @datasets.each do |data| g.data(data[0], data[1]) end g.legend_font_size = font_size unless font_size.nil? g.write("test/output/#{name}_wrapped_legend_#{font_size}_#{width}.png") end end end def test_bar_legend_wrap full_suite_for(:bar, Gruff::Bar) end def test_pie_legend_wrap full_suite_for(:pie, Gruff::Pie) end def test_more_than_two_lines_of_legends @datasets = @datasets + [[:Julie2, [22, 29, 35, 38, 36, 40, 46, 57]], [:Jane2, [95, 95, 95, 90, 85, 80, 88, 100]], [:Philip2, [90, 34, 23, 12, 78, 89, 98, 88]], [:Arthur2, [5, 10, 13, 11, 6, 16, 22, 32]], [:Vincent2, [5, 10, 13, 11, 6, 16, 22, 32]], [:Jake2, [5, 10, 13, 11, 6, 16, 22, 32]], [:Stephen2, [5, 10, 13, 11, 6, 16, 22, 32]]] full_suite_for(:bar2, Gruff::Bar) end end gruff-0.6.0/test/test_bullet.rb0000644000004100000410000000076512540054077016524 0ustar www-datawww-datarequire File.dirname(__FILE__) + "/gruff_test_case" class TestGruffBullet < GruffTestCase def setup @data_args = [75, 100, { :target => 80, :low => 50, :high => 90 }] end def test_bullet_graph g = Gruff::Bullet.new g.title = "Monthly Revenue" g.data *@data_args g.write("test/output/bullet_greyscale.png") end def test_no_options g = Gruff::Bullet.new g.data *@data_args g.write("test/output/bullet_no_options.png") end end gruff-0.6.0/test/test_accumulator_bar.rb0000644000004100000410000000255512540054077020377 0ustar www-datawww-datarequire File.dirname(__FILE__) + '/gruff_test_case' class TestGruffAccumulatorBar < GruffTestCase # TODO Delete old output files once when starting tests def setup super @datasets = [ (1..20).to_a.map { rand(10) } ] end def test_accumulator g = Gruff::AccumulatorBar.new 500 g.title = 'Your Savings' g.hide_legend = true # g.font = File.expand_path(File.dirname(__FILE__) + "/../assets/fonts/ATMA____.TTF") g.marker_font_size = 18 g.theme = { :colors => ['#aedaa9', '#12a702'], :marker_color => '#dddddd', :font_color => 'black', :background_colors => 'white' # :background_image => File.expand_path(File.dirname(__FILE__) + "/../assets/backgrounds/43things.png") } # Attempt at negative numbers # g.data 'Savings', (1..20).to_a.map { rand(10) * (rand(2) > 0 ? 1 : -1) } g.data 'Savings', (1..12).to_a.map { rand(100) } g.labels = (0..11).to_a.inject({}) {|memo, index| {index => '12-26'}.merge(memo)} g.maximum_value = 1000 g.minimum_value = 0 g.write('test/output/accum_bar.png') end def test_too_many_args assert_raise(Gruff::IncorrectNumberOfDatasetsException) { g = Gruff::AccumulatorBar.new g.data 'First', [1,1,1] g.data 'Too Many', [1,1,1] g.write('test/output/_SHOULD_NOT_ACTUALLY_BE_WRITTEN.png') } end end gruff-0.6.0/test/test_line.rb0000644000004100000410000003776012540054077016171 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + '/gruff_test_case' class TestGruffLine < GruffTestCase def test_should_render_with_transparent_theme g = Gruff::Line.new(400) g.title = 'Transparent Background' g.theme = { :colors => %w(black grey), :marker_color => 'grey', :font_color => 'black', :background_colors => 'transparent' } g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [-1, 0, 4, -4]) g.data(:peaches, [10, 8, 6, 3]) g.write('test/output/line_transparent.png') end def test_very_small g = Gruff::Line.new(200) g.title = 'Very Small Line Chart 200px' g.labels = @labels @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_very_small.png') end def test_line_graph_with_themes line_graph_with_themes() line_graph_with_themes(400) end def test_one_value g = Gruff::Line.new g.title = 'One Value' g.labels = { 0 => '1', 1 => '2' } g.data('one', 1) g.write('test/output/line_one_value.png') end def test_one_value_array g = Gruff::Line.new g.title = 'One Value in an Array' g.labels = { 0 => '1', 1 => '2' } g.data('one', [1]) g.write('test/output/line_one_value_array.png') end def test_should_not_hang_with_0_0_100 g = Gruff::Line.new(320) g.title = 'Hang Value Graph Test' g.data('test', [0, 0, 100]) g.write('test/output/line_hang_value.png') end # TODO # def test_fix_crash # g = Gruff::Line.new(370) # g.title = "Crash Test" # g.data "ichi", [5] # g.data "ni", [0] # g.data "san", [0] # g.data "shi", [0] # g.write("test/output/line_crash_fix_test.png") # end def test_line_small_values @datasets = [ [:small, [0.1, 0.14356, 0.0, 0.5674839, 0.456]], [:small2, [0.2, 0.3, 0.1, 0.05, 0.9]] ] g = Gruff::Line.new g.title = 'Small Values Line Graph Test' @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_small_values.png') g = Gruff::Line.new(400) g.title = 'Small Values Line Graph Test 400px' @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_small_values_small.png') end def test_line_starts_with_zero @datasets = [ [:first0, [0, 5, 10, 8, 18]], [:normal, [1, 2, 3, 4, 5]] ] g = Gruff::Line.new g.title = 'Small Values Line Graph Test' @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_small_zero.png') g = Gruff::Line.new(400) g.title = 'Small Values Line Graph Test 400px' @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_small_small_zero.png') end def test_line_large_values @datasets = [ [:large, [100_005, 35_000, 28_000, 27_000]], [:large2, [35_000, 28_000, 27_000, 100_005]], [:large3, [28_000, 27_000, 100_005, 35_000]], [:large4, [1_238, 39_092, 27_938, 48_876]] ] g = Gruff::Line.new g.title = 'Very Large Values Line Graph Test' g.baseline_value = 50_000 g.baseline_color = 'green' g.dot_radius = 15 g.line_width = 3 @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_large.png') end # def test_long_title # # end # # def test_add_colors # # end # def test_request_too_many_colors g = Gruff::Line.new g.title = 'More Sets Than in Color Array' # g.theme = {} # Sets theme with only black and white @datasets.each do |data| g.data(data[0], data[1]) end @datasets.each do |data| g.data("#{data[0]}-B", data[1].map { |d| d + 20 }) end g.write('test/output/line_more_sets_than_colors.png') end # # def test_add_data # # end def test_many_datapoints g = Gruff::Line.new g.title = 'Many Multi-Line Graph Test' g.labels = { 0 => 'June', 10 => 'July', 30 => 'August', 50 => 'September', } g.data('many points', (0..50).collect { |i| rand(100) }) g.x_axis_label = 'Months' # Default theme g.write('test/output/line_many.png') end def test_similar_high_end_values @dataset = %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect { |i| i.to_f } g = Gruff::Line.new g.title = 'Similar High End Values Test' g.data('similar points', @dataset) g.write('test/output/line_similar_high_end_values.png') g = Gruff::Line.new g.title = 'Similar High End Values With Floor' g.data('similar points', @dataset) g.minimum_value = 0 g.y_axis_label = 'Barometric Pressure' g.write('test/output/line_similar_high_end_values_with_floor.png') end def test_many_lines_graph_small g = Gruff::Line.new(400) g.title = 'Many Values Line Test 400px' g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write('test/output/line_many_lines_small.png') end def test_graph_tiny g = Gruff::Line.new(300) g.title = 'Tiny Test 300px' g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write('test/output/line_tiny.png') end def test_no_data g = Gruff::Line.new(400) g.title = 'No Data' # Default theme g.write('test/output/line_no_data.png') g = Gruff::Line.new(400) g.title = 'No Data Title' g.no_data_message = 'There is no data' g.write('test/output/line_no_data_msg.png') end def test_all_zeros g = Gruff::Line.new(400) g.title = 'All Zeros' g.data(:gus, [0, 0, 0, 0]) # Default theme g.write('test/output/line_no_data_other.png') end def test_some_nil_points g = Gruff::Line.new g.title = 'Some Nil Points' @datasets = [ [:data1, [1, 2, 3, nil, 3, 5, 6]], [:data2, [5, nil, nil, 5, nil, nil, 5]], [:data3, [4, nil, 2, 1, 0]], [:data4, [nil, nil, 3, 1, 2]] ] @datasets.each do |data| g.data(*data) end # Default theme g.write('test/output/line_some_nil_points.png') end def test_no_title g = Gruff::Line.new(400) g.labels = @labels @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/line_no_title.png') end def test_no_line_markers g = setup_basic_graph(400) g.title = 'No Line Markers' g.hide_line_markers = true g.write('test/output/line_no_line_markers.png') end def test_no_legend g = setup_basic_graph(400) g.title = 'No Legend' g.hide_legend = true g.write('test/output/line_no_legend.png') end def test_nothing_but_the_graph g = setup_basic_graph(400) g.title = 'THIS TITLE SHOULD NOT DISPLAY!!!' g.hide_line_markers = true g.hide_legend = true g.hide_title = true g.write('test/output/line_nothing_but_the_graph.png') end def test_legend_below_the_chart g = setup_basic_graph(400) g.title = 'Legend below the chart' g.legend_at_bottom = true g.write('test/output/line_legend_at_bottom.png') end def test_baseline_larger_than_data g = setup_basic_graph(400) g.title = 'Baseline Larger Than Data' g.baseline_value = 150 g.write('test/output/line_large_baseline.png') end def test_hide_dots g = setup_basic_graph(400) g.title = 'Hide Dots' g.hide_dots = true g.write('test/output/line_hide_dots.png') end def test_hide_lines g = setup_basic_graph(400) g.title = 'Hide Lines' g.hide_lines = true g.write('test/output/line_hide_lines.png') end def test_wide_graph g = setup_basic_graph('800x400') g.title = 'Wide Graph' g.write('test/output/line_wide_graph.png') g = setup_basic_graph('400x200') g.title = 'Wide Graph Small' g.write('test/output/line_wide_graph_small.png') end def test_negative g = setup_pos_neg(800) g.write('test/output/line_pos_neg.png') g = setup_pos_neg(400) g.title = 'Pos/Neg Line Test Small' g.write('test/output/line_pos_neg_400.png') end def test_all_negative g = setup_all_neg(800) g.maximum_value = 0 g.write('test/output/line_all_neg.png') end def test_all_negative_no_max_value g = setup_all_neg(800) g.write('test/output/line_all_neg_no_max.png') end def test_all_negative_400 g = setup_all_neg(400) g.maximum_value = 0 g.title = 'All Neg Line Test Small' g.write('test/output/line_all_neg_400.png') end def test_many_numbers g = Gruff::Line.new('400x170') g.title = 'Line Test, Many Numbers' data = [ {:date => '01', :wpm => 0, :errors => 0, :accuracy => 0}, {:date => '02', :wpm => 10, :errors => 2, :accuracy => 80}, {:date => '03', :wpm => 15, :errors => 0, :accuracy => 100}, {:date => '04', :wpm => 16, :errors => 2, :accuracy => 87}, {:date => '05'}, {:date => '06', :wpm => 18, :errors => 1, :accuracy => 94}, {:date => '07'}, {:date => '08'}, {:date => '09', :wpm => 21, :errors => 1, :accuracy => 95}, {:date => '10'}, {:date => '11'}, {:date => '12'}, {:date => '13'}, {:date => '14'}, {:date => '15'}, {:date => '16'}, {:date => '17'}, {:date => '18'}, {:date => '19', :wpm => 28, :errors => 5, :accuracy => 82}, {:date => '20'}, {:date => '21'}, {:date => '22'}, {:date => '23'}, {:date => '24'}, {:date => '25'}, {:date => '26'}, {:date => '27', :wpm => 37, :errors => 3, :accuracy => 92}, ] [:wpm, :errors, :accuracy].each do |field| g.dataxy(field, data.each_with_index.map { |d, i| [i + 1, d[field]] if d[field] }.compact) end labels = Hash.new data.each_with_index do |d, i| labels[i + 1] = d[:date] if d.size > 1 end g.labels = labels g.write('test/output/line_many_numbers.png') end def test_no_hide_line_no_labels g = Gruff::Line.new g.title = 'No Hide Line No Labels' @datasets.each do |data| g.data(data[0], data[1]) end g.hide_line_markers = false g.write('test/output/line_no_hide.png') end def test_xy_data g = Gruff::Line.new g.title = 'X/Y Dataset' g.dataxy('Apples', [1, 3, 4, 5, 6, 10], [1, 2, 3, 4, 4, 3]) g.dataxy('Bapples', [1, 3, 4, 5, 7, 9], [1, 1, 2, 2, 3, 3]) g.data('Capples', [1, 1, 2, 2, 3, 3]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006', 8 => '2007', 10 => '2008'} g.write('test/output/line_xy.png') end def test_xy_data_pairs g = Gruff::Line.new g.title = 'X/Y Dataset Pairs' g.dataxy('Apples', [[1, 1], [3, 2], [4, 3], [5, 4], [6, 4], [10, 3]]) g.dataxy('Bapples', [[1, 1], [3, 1], [4, 2], [5, 2], [7, 3], [9, 3]]) g.data('Capples', [1, 1, 2, 2, 3, 3]) g.dataxy('Dapples', [[1, 1], [2, 2], [5, 6], [13, 13], [15, nil], [2, 17], [3, nil], [3, 17], [13, nil], [3, 18], [5, nil], [2, 18]]) g.dataxy('Eapples', [[1, 1], [2, 3], [5, 8], [13, 21], [13, 8], [5, 3], [2, 1], [1, 1]]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006', 8 => '2007', 10 => '2008', 12 => '2009'} g.write('test/output/line_xy_pairs.png') end def test_jruby_error g = Gruff::Line.new g.theme = { :colors => %w(#7F0099 #2F85ED #2FED09 #EC962F), :marker_color => '#aaa', :background_colors => %w(#E8E8E8 #B9FD6C) } g.hide_title = true g.legend_font_size = 12 g.marker_font_size = 16 g.hide_dots = false g.write('test/output/line_jruby_error.png') end def test_marker_label_accuracy g = Gruff::Line.new g.title = 'Marker label accuracy' g.labels = { 0 => '1', 1 => '2', 2 => '3', 3 => '4', 4 => '5' } g.data('first', [0.5, 0.51, 0.52, 0.53, 0.54]) g.data('second', [0.6, 0.61, 0.62, 0.63, 0.64]) g.data('third', [0.7, 0.71, 0.72, 0.73, 0.74]) g.write('test/output/line_marker_label_accuracy.png') end def test_y_axis_increment g = Gruff::Line.new g.title = 'y axis increment' g.data('data', [1, 2, 3]) g.y_axis_increment = 1 g.write('test/output/line_y_axis_increment.png') end def test_multiple_reference_lines g = Gruff::Line.new g.title = 'Line Chart with Multiple Reference Lines' g.data('Apples', [3, 2, 3, 4, 4, 3]) g.data('Oranges', [4, 8, 7, 9, 8, 9]) g.data('Watermelon', [2, 3, 4, 5, 6, 8]) g.data('Peaches', [9, 9, 10, 8, 7, 9]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005'} g.reference_line_default_width = 1 g.reference_lines[:baseline] = { :value => 5 } g.reference_lines[:lots] = { :value => 9 } g.reference_lines[:little] = { :value => 3 } g.reference_lines[:horiz_one] = { :index => 1, :color => 'green' } g.reference_lines[:horiz_two] = { :index => 3, :color => 'green' } g.write('line_reference_lines.png') end def test_baseline g = Gruff::Line.new g.title = 'Line Chart with Baseline = 5' g.data('Apples', [3, 2, 3, 4, 4, 3]) g.data('Oranges', [4, 8, 7, 9, 8, 9]) g.data('Watermelon', [2, 3, 4, 5, 6, 8]) g.data('Peaches', [9, 9, 10, 8, 7, 9]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005'} g.baseline_value = 5 g.write('line_baseline.png') end def test_webp g = setup_basic_graph('800x400') g.title = 'Line Chart WEBP' g.write('line_webp.webp') rescue Magick::ImageMagickError assert_match /no encode delegate for this image format .*\.webp/, $!.message end private # TODO Reset data after each theme def line_graph_with_themes(size=nil) g = Gruff::Line.new(size) g.title = "Multi-Line Graph Test #{size}" g.labels = @labels g.baseline_value = 90 @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/line_theme_keynote_#{size}.png") g = Gruff::Line.new(size) g.title = "Multi-Line Graph Test #{size}" g.labels = @labels g.baseline_value = 90 g.theme = Gruff::Themes::THIRTYSEVEN_SIGNALS @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/line_theme_37signals_#{size}.png") g = Gruff::Line.new(size) g.title = "Multi-Line Graph Test #{size}" g.labels = @labels g.baseline_value = 90 g.theme = Gruff::Themes::RAILS_KEYNOTE @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/line_theme_rails_keynote_#{size}.png") g = Gruff::Line.new(size) g.title = "Multi-Line Graph Test #{size}" g.labels = @labels g.baseline_value = 90 g.theme = Gruff::Themes::ODEO @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/line_theme_odeo_#{size}.png") end def setup_pos_neg(size=800) g = Gruff::Line.new(size) g.title = 'Pos/Neg Line Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [-1, 0, 4, -4]) g.data(:peaches, [10, 8, 6, 3]) g end def setup_all_neg(size=800) g = Gruff::Line.new(size) g.title = 'All Neg Line Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [-1, -5, -20, -4]) g.data(:peaches, [-10, -8, -6, -3]) g end end gruff-0.6.0/test/gruff_test_case.rb0000644000004100000410000000700712540054077017335 0ustar www-datawww-data$:.unshift(File.dirname(__FILE__) + '/../lib/') RMAGICK_BYPASS_VERSION_TEST = true require 'test/unit' require 'gruff' require 'fileutils' TEST_OUTPUT_DIR = File.dirname(__FILE__) + "/output#{'_java' if RUBY_PLATFORM == 'java'}" FileUtils.mkdir_p(TEST_OUTPUT_DIR) FileUtils.rm_f Dir[TEST_OUTPUT_DIR + '/*'] if ENV['RM_INFO'] && RUBY_VERSION =~ /^(1\.9|2\.0)\./ require 'minitest/reporters' MiniTest::Reporters.use! end class Gruff::Base alias :write_org :write def write(filename='graph.png') basefilename = File.basename(filename).split('.')[0..-2].join('.') extension = filename.slice(/\.[^\.]*$/) testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + extension counter = 0 while File.exists?(testfilename) counter += 1 testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + "-#{counter}#{extension}" end write_org(testfilename) end end class GruffTestCase < Test::Unit::TestCase def setup srand 42 @datasets = [ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]], [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]], [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]], [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], [:Arthur, [5, 10, 13, 11, 6, 16, 22, 32]], ] @labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', 4 => '6/4', 5 => '6/12', 6 => '6/21', 7 => '6/28', } end def setup_single_dataset @datasets = [ [:Jimmy, [25, 36, 86]] ] @labels = { 0 => 'You', 1 => 'Average', 2 => 'Lifetime' } end def setup_wide_dataset @datasets = [ ['Auto', 25], ['Food', 5], ['Entertainment', 15] ] @labels = {0 => 'This Month'} end def test_dummy assert true end protected # Generate graphs at several sizes. # # Also writes the graph to disk. # # graph_sized 'bar_basic' do |g| # g.data('students', [1, 2, 3, 4]) # end # def graph_sized(filename, sizes=['', 400]) class_name = self.class.name.gsub(/^TestGruff/, '') Array(sizes).each do |size| g = instance_eval("Gruff::#{class_name}.new #{size}") g.title = "#{class_name} Graph" yield g write_test_file g, "#{filename}_#{size}.png" end end def write_test_file(graph, filename) testfilename = [TEST_OUTPUT_DIR, filename].join('/') basefilename = filename.split('.')[0..-2].join('.') extension = filename.slice(/\..*$/) counter = 0 while File.exists? testfilename counter += 1 testfilename = [TEST_OUTPUT_DIR, basefilename].join('/') + "-#{counter}#{extension}" end graph.write(testfilename) end ## # Example: # # setup_basic_graph Gruff::Pie, 400 # def setup_basic_graph(*args) klass, size = Gruff::Bar, 400 # Allow args to be klass, size or just klass or just size. # # TODO Refactor case args.length when 1 case args[0] when Fixnum size = args[0] klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}") when String size = args[0] klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}") else klass = args[0] end when 2 klass, size = args[0], args[1] end g = klass.new(size) g.title = 'My Bar Graph' g.labels = @labels g.font = '/Library/Fonts/Verdana.ttf' @datasets.each do |data| g.data(data[0], data[1]) end g end end gruff-0.6.0/test/test_sidestacked_bar.rb0000644000004100000410000000443612540054077020343 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffSideStackedBar < GruffTestCase def setup @datasets = [ [:Jimmy, [25, 36, 86, 39]], [:Charles, [80, 54, 67, 54]], [:Julie, [22, 29, 35, 38]], #[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], #[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], #["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24' } end def test_bar_graph g = Gruff::SideStackedBar.new g.title = "Visual Stacked Bar Graph Test" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/side_stacked_bar_keynote.png" end def test_bar_graph_small g = Gruff::SideStackedBar.new(400) g.title = "Visual Stacked Bar Graph Test" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/side_stacked_bar_keynote_small.png" end def test_wide g = setup_basic_graph('800x400') g.title = "Wide SSBar" g.write "test/output/side_stacked_bar_wide.png" end def test_should_space_long_left_labels_appropriately g = Gruff::SideStackedBar.new g.title = "Stacked Bar Long Label" g.labels = { 0 => 'September', 1 => 'Oct', 2 => 'Nov', 3 => 'Dec', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/side_stacked_bar_long_label.png" end def test_bar_labels g = Gruff::SideStackedBar.new g.title = "Stacked Bar Long Label" g.labels = { 0 => 'September', 1 => 'Oct', 2 => 'Nov', 3 => 'Dec', } @datasets.each do |data| g.data(data[0], data[1]) end g.show_labels_for_bar_values = true g.write "test/output/side_stacked_bar_labels.png" end protected def setup_basic_graph(size=800) g = Gruff::SideStackedBar.new(size) g.title = "My Graph Title" g.labels = @sample_labels @datasets.each do |data| g.data(data[0], data[1]) end return g end end gruff-0.6.0/test/test_dot.rb0000644000004100000410000001411112540054077016011 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + '/gruff_test_case' class TestGruffDot < GruffTestCase # TODO Delete old output files once when starting tests def setup @datasets = [ [:Jimmy, [25, 36, 86, 39]], [:Charles, [80, 54, 67, 54]], [:Julie, [22, 29, 35, 38]], #[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], #[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], #["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]], ] end def test_dot_graph g = setup_basic_graph g.title = 'Dot Graph Test' g.write('test/output/dot.png') end def test_dot_graph_set_colors g = Gruff::Dot.new g.title = 'Dot Graph With Manual Colors' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:Art, [0, 5, 8, 15], '#990000') g.data(:Philosophy, [10, 3, 2, 8], '#009900') g.data(:Science, [2, 15, 8, 11], '#990099') g.minimum_value = 0 g.write('test/output/dot_manual_colors.png') end def test_dot_graph_small g = setup_basic_graph(400) g.title = 'Visual Multi-Line Dot Graph Test' g.write('test/output/dot_small.png') end # Somewhat worthless test. Should an error be thrown? # def test_nil_font # g = setup_basic_graph 400 # g.title = "Nil Font" # g.font = nil # g.write "test/output/dot_nil_font.png" # end def test_no_line_markers g = setup_basic_graph(400) g.title = 'No Line Markers' g.hide_line_markers = true g.write('test/output/dot_no_line_markers.png') end def test_no_legend g = setup_basic_graph(400) g.title = 'No Legend' g.hide_legend = true g.write('test/output/dot_no_legend.png') end def test_no_title g = setup_basic_graph(400) g.title = 'No Title' g.hide_title = true g.write('test/output/dot_no_title.png') end def test_no_title_or_legend g = setup_basic_graph(400) g.title = 'No Title or Legend' g.hide_legend = true g.hide_title = true g.write('test/output/dot_no_title_or_legend.png') end def test_set_marker_count g = setup_basic_graph(400) g.title = 'Set marker' g.marker_count = 10 g.write('test/output/dot_set_marker.png') end def test_set_legend_box_size g = setup_basic_graph(400) g.title = 'Set Small Legend Box Size' g.legend_box_size = 10.0 g.write('test/output/dot_set_legend_box_size_sm.png') g = setup_basic_graph(400) g.title = 'Set Large Legend Box Size' g.legend_box_size = 50.0 g.write('test/output/dot_set_legend_box_size_lg.png') end def test_x_y_labels g = setup_basic_graph(400) g.title = 'X Y Labels' g.x_axis_label = 'Score (%)' g.y_axis_label = 'Students' g.write('test/output/dot_x_y_labels.png') end def test_wide_graph g = setup_basic_graph('800x400') g.title = 'Wide Graph' g.write('test/output/dot_wide_graph.png') g = setup_basic_graph('400x200') g.title = 'Wide Graph Small' g.write('test/output/dot_wide_graph_small.png') end def test_tall_graph g = setup_basic_graph('400x600') g.title = 'Tall Graph' g.write('test/output/dot_tall_graph.png') g = setup_basic_graph('200x400') g.title = 'Tall Graph Small' g.write('test/output/dot_tall_graph_small.png') end def test_one_value g = Gruff::Dot.new g.title = 'One Value Graph Test' g.labels = { 0 => '1', 1 => '2' } g.data('one', [1,1]) g.write('test/output/dot_one_value.png') end def test_negative g = Gruff::Dot.new g.title = 'Pos/Neg Dot Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [-1, 0, 4, -4]) g.data(:peaches, [10, 8, 6, 3]) g.write('test/output/dot_pos_neg.png') end def test_nearly_zero g = Gruff::Dot.new g.title = 'Nearly Zero Graph' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [1, 2, 3, 4]) g.data(:peaches, [4, 3, 2, 1]) g.minimum_value = 0 g.maximum_value = 10 g.write('test/output/dot_nearly_zero_max_10.png') end def test_y_axis_increment generate_with_y_axis_increment 2.0 generate_with_y_axis_increment 1 generate_with_y_axis_increment 5 generate_with_y_axis_increment 20 end def generate_with_y_axis_increment(increment) g = Gruff::Dot.new g.title = "Y Axis Set to #{increment}" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.y_axis_increment = increment g.data(:apples, [1, 0.2, 0.5, 0.7]) g.data(:peaches, [2.5, 2.3, 2, 6.1]) g.write("test/output/dot_y_increment_#{increment}.png") end def test_custom_theme g = Gruff::Dot.new g.title = 'Custom Theme' g.font = File.expand_path('CREABBRG.TTF', ENV['MAGICK_FONT_PATH']) g.title_font_size = 60 g.legend_font_size = 32 g.marker_font_size = 32 g.theme = { :colors => %w(#efd250 #666699 #e5573f #9595e2), :marker_color => 'white', :font_color => 'blue', :background_image => 'assets/pc306715.jpg' } g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:vancouver, [1, 2, 3, 4]) g.data(:seattle, [2, 4, 6, 8]) g.data(:portland, [3, 1, 7, 3]) g.data(:victoria, [4, 3, 5, 7]) g.minimum_value = 0 g.write('test/output/dot_themed.png') end def test_july_enhancements g = Gruff::Dot.new(600) g.hide_legend = true g.title = 'Full speed ahead' g.labels = (0..10).inject({}) { |memo, i| memo.merge({ i => (i*10).to_s}) } g.data(:apples, (0..9).map { rand(20)/10.0 }) g.y_axis_increment = 1.0 g.x_axis_label = 'Score (%)' g.y_axis_label = 'Students' write_test_file g, 'enhancements.png' end protected def setup_basic_graph(size=800) g = Gruff::Dot.new(size) g.title = 'My Dot Graph' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g end end gruff-0.6.0/test/test_labels_for_null_data.rb0000644000004100000410000000075012540054077021362 0ustar www-datawww-datarequire File.dirname(__FILE__) + "/gruff_test_case" class TestLabelsForNullData < GruffTestCase def setup @dataset = [nil, 1, 2, 1, nil] @labels = { 0 => '1', 1 => '2', 2 => '3', 3 => '4', 4 => '5' } end def test_labels g = Gruff::Line.new g.title = 'Labels For Null Data' g.labels = @labels g.data('data', @dataset) g.minimum_value = 0 g.write("test/output/TestLabelsForNullData.png") end end gruff-0.6.0/test/test_mini_bar.rb0000644000004100000410000000151512540054077017007 0ustar www-datawww-data require File.expand_path('../gruff_test_case', __FILE__) class TestMiniBar < GruffTestCase def test_simple_bar setup_single_dataset g = setup_basic_graph(Gruff::Mini::Bar, 200) g.hide_mini_legend = true write_test_file g, 'mini_bar.png' end # def test_simple_bar_wide_dataset # setup_wide_dataset # g = setup_basic_graph(Gruff::Mini::Bar, 200) # write_test_file g, 'mini_bar_wide_data.png' # end # # def test_code_sample # g = Gruff::Mini::Bar.new(200) # g.data "Jim", [200, 500, 400] # g.labels = { 0 => 'This Month', 1 => 'Average', 2 => 'Overall'} # g.write "mini_bar_one_color.png" # # g = Gruff::Mini::Bar.new(200) # g.data "Car", 200 # g.data "Food", 500 # g.data "Art", 1000 # g.data "Music", 16 # g.write "mini_bar_many_colors.png" # end end gruff-0.6.0/test/test_bezier.rb0000644000004100000410000000120212540054077016500 0ustar www-datawww-datarequire File.dirname(__FILE__) + "/gruff_test_case" class TestBezier < GruffTestCase def setup @data_args = [75, 100, { :target => 80, :low => 50, :high => 90 }] end def test_bezier g = Gruff::Bezier.new g.title = "Bezier?" g.data 'Series 1', [0, 100] g.write("test/output/bezier.png") end def test_bezier_2 g = Gruff::Bezier.new g.data 'Series 2', [0, 127, 150] g.write("test/output/bezier_2.png") end def test_bezier_3 g = Gruff::Bezier.new g.data 'Series 3', [100,300,200,250] g.minimum_value = 0 g.write("test/output/bezier_3.png") end end gruff-0.6.0/test/test_net.rb0000644000004100000410000001265412540054077016023 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffNet < GruffTestCase def setup super @datasets = [ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]], [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]], [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]], [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', 4 => '6/4', 5 => '6/12', 6 => '6/21', 7 => '6/28', } end def test_net_small_values @datasets = [ [:small, [0.1, 0.14356, 0.0, 0.5674839, 0.456]], [:small2, [0.2, 0.3, 0.1, 0.05, 0.9]] ] g = Gruff::Net.new g.title = "Small Values Net Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_small.png") g = Gruff::Net.new(400) g.title = "Small Values Net Graph Test 400px" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_small_small.png") end def test_net_starts_with_zero @datasets = [ [:first0, [0, 5, 10, 8, 18]], [:normal, [1, 2, 3, 4, 5]] ] g = Gruff::Net.new g.title = "Small Values Net Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_small_zero.png") g = Gruff::Net.new(400) g.title = "Small Values Net Graph Test 400px" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_small_small_zero.png") end def test_net_large_values @datasets = [ [:large, [100_005, 35_000, 28_000, 27_000]], [:large2, [35_000, 28_000, 27_000, 100_005]], [:large3, [28_000, 27_000, 100_005, 35_000]], [:large4, [1_238, 39_092, 27_938, 48_876]] ] g = Gruff::Net.new g.title = "Very Large Values Net Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_large.png") end def test_many_datapoints g = Gruff::Net.new g.title = "Many Multi-Net Graph Test" g.labels = { 0 => 'June', 10 => 'July', 30 => 'August', 50 => 'September', } g.data('many points', (0..50).collect {|i| rand(100) }) # Default theme g.write("test/output/net_many.png") end def test_similar_high_end_values g = Gruff::Net.new g.title = "Similar High End Values Test" g.data('similar points', %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect {|i| i.to_f} ) # Default theme g.write("test/output/net_similar_high_end_values.png") end def test_many_nets_graph_small g = Gruff::Net.new(400) g.title = "Many Values Net Test 400px" g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write("test/output/net_many_nets_small.png") end def test_dots_graph_tiny g = Gruff::Net.new(300) g.title = "Dots Test 300px" g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write("test/output/net_dots_tiny.png") end def test_no_data g = Gruff::Net.new(400) g.title = "No Data" # Default theme g.write("test/output/net_no_data.png") g = Gruff::Net.new(400) g.title = "No Data Title" g.no_data_message = 'There is no data' g.write("test/output/net_no_data_msg.png") end def test_all_zeros g = Gruff::Net.new(400) g.title = "All Zeros" g.data(:gus, [0,0,0,0]) # Default theme g.write("test/output/net_no_data_other.png") end def test_no_title g = Gruff::Net.new(400) g.labels = @sample_labels @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/net_no_title.png") end def test_no_net_markers g = setup_basic_graph(400) g.title = "No Net Markers" g.hide_line_markers = true g.write("test/output/net_no_net_markers.png") end def test_no_legend g = setup_basic_graph(400) g.title = "No Legend" g.hide_legend = true g.write("test/output/net_no_legend.png") end def test_nothing_but_the_graph g = setup_basic_graph(400) g.title = "THIS TITLE SHOULD NOT DISPLAY!!!" g.hide_line_markers = true g.hide_legend = true g.hide_title = true g.write("test/output/net_nothing_but_the_graph.png") end def test_wide_graph g = setup_basic_graph('800x400') g.title = "Wide Graph" g.write("test/output/net_wide_graph.png") g = setup_basic_graph('400x200') g.title = "Wide Graph Small" g.write("test/output/net_wide_graph_small.png") end protected def setup_basic_graph(size=800) g = Gruff::Net.new(size) g.title = "My Graph Title" g.labels = @sample_labels @datasets.each do |data| g.data(data[0], data[1]) end return g end end gruff-0.6.0/test/test_stacked_area.rb0000644000004100000410000000202412540054077017631 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + '/gruff_test_case' class TestGruffStackedArea < GruffTestCase def setup @datasets = [ [:Jimmy, [25, 36, 86, 39]], [:Charles, [80, 54, 67, 54]], [:Julie, [22, 29, 35, 38]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24' } end def test_area_graph g = Gruff::StackedArea.new g.title = 'Visual Stacked Area Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write 'test/output/stacked_area_keynote.png' end def test_area_graph_small g = Gruff::StackedArea.new(400) g.title = 'Visual Stacked Area Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write 'test/output/stacked_area_keynote_small.png' end end gruff-0.6.0/test/test_pie.rb0000644000004100000410000000640012540054077016002 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffPie < GruffTestCase def setup @datasets = [ [:Darren, [25]], [:Chris, [80]], [:Egbert, [22]], [:Adam, [95]], [:Bill, [90]], ["Frank", [5]], ["Zero", [0]], ] end def test_pie_graph g = Gruff::Pie.new g.title = "Visual Pie Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/pie_keynote.png") end def test_pie_graph_greyscale g = Gruff::Pie.new g.title = "Greyscale Pie Graph Test" g.theme = Gruff::Themes::GREYSCALE @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/pie_grey.png") end def test_pie_graph_pastel g = Gruff::Pie.new g.theme = Gruff::Themes::PASTEL g.title = "Pastel Pie Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/pie_pastel.png") end def test_pie_graph_small g = Gruff::Pie.new(400) g.title = "Visual Pie Graph Test Small" @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/pie_keynote_small.png") end def test_pie_graph_nearly_equal g = Gruff::Pie.new g.title = "Pie Graph Nearly Equal" g.data(:Blake, [41]) g.data(:Aaron, [42]) # g.data(:Grouch, [40]) # g.data(:Snuffleupagus, [43]) g.write("test/output/pie_nearly_equal.png") end def test_pie_graph_equal g = Gruff::Pie.new g.title = "Pie Graph Equal" g.data(:Bert, [41]) g.data(:Adam, [41]) g.write("test/output/pie_equal.png") end def test_pie_graph_zero g = Gruff::Pie.new g.title = "Pie Graph One Zero" g.data(:Bert, [0]) g.data(:Adam, [1]) g.write("test/output/pie_zero.png") end def test_pie_graph_one_val g = Gruff::Pie.new g.title = "Pie Graph One Val" g.data(:Bert, 53) g.data(:Adam, 29) g.write("test/output/pie_one_val.png") end def test_wide g = setup_basic_graph('800x400') g.title = "Wide Pie" g.write("test/output/pie_wide.png") end def test_label_size g = setup_basic_graph() g.title = "Pie With Small Legend" g.legend_font_size = 10 g.write("test/output/pie_legend.png") g = setup_basic_graph(400) g.title = "Small Pie With Small Legend" g.legend_font_size = 10 g.write("test/output/pie_legend_small.png") end def test_tiny_simple_pie @datasets = (1..5).map {|n| ['Auto', [rand(100)]]} g = setup_basic_graph 200 g.hide_legend = true g.hide_title = true g.hide_line_numbers = true g.marker_font_size = 40.0 g.minimum_value = 0.0 write_test_file g, "pie_simple.png" end def test_pie_with_adjusted_text_offset_percentage g = setup_basic_graph g.title = "Adjusted Text Offset Percentage" g.text_offset_percentage = 0.03 g.write "test/output/pie_adjusted_text_offset_percentage.png" end protected def setup_basic_graph(size=800) g = Gruff::Pie.new(size) g.title = "My Graph Title" @datasets.each do |data| g.data(data[0], data[1]) end return g end end gruff-0.6.0/test/test_scene.rb0000644000004100000410000000635112540054077016327 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" require 'yaml' class LayerStub < Gruff::Layer; attr_reader :base_dir, :filenames, :selected_filename; end class TestGruffScene < GruffTestCase def test_hazy g = setup_scene g.weather = "cloudy" g.haze = true g.time = Time.mktime(2006, 7, 4, 4, 35) g.write "test/output/scene_hazy_night.png" end def test_stormy_night g = setup_scene g.weather = "stormy" g.time = Time.mktime(2006, 7, 4, 0, 0) g.write "test/output/scene_stormy_night.png" end def test_not_hazy g = setup_scene g.weather = "cloudy" g.haze = false g.time = Time.mktime(2006, 7, 4, 6, 00) g.write "test/output/scene_not_hazy_day.png" end def test_partly_cloudy g = setup_scene g.weather = "partly cloudy" g.haze = false g.time = Time.mktime(2006, 7, 4, 13, 00) g.write "test/output/scene_partly_cloudy_day.png" end def test_stormy_day g = setup_scene g.weather = "stormy" g.haze = false g.time = Time.mktime(2006, 7, 4, 8, 00) g.write "test/output/scene_stormy_day.png" end def test_layer l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "clouds") assert_equal %w(cloudy.png partly_cloudy.png stormy.png), l.filenames.sort l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "grass") assert_equal 'default.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 12, 35) # 12:35, July 4, 2006 assert_equal '1200.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 0, 0) # 00:00, July 4, 2006 assert_equal '0000.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 23, 35) # 23:35, July 4, 2006 assert_equal '2000.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 0, 1) # 00:01, July 4, 2006 assert_equal '0000.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 2, 0) # 02:00, July 4, 2006 assert_equal '0200.png', l.selected_filename l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky") l.update Time.mktime(2006, 7, 4, 4, 00) # 04:00, July 4, 2006 assert_equal '0400.png', l.selected_filename # TODO Need number_sample folder # l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "number_sample") # assert_equal %w(1.png 2.png default.png), l.filenames # l.update 3 # assert_equal 'default.png', l.selected_filename end private def setup_scene g = Gruff::Scene.new("500x100", File.expand_path("../assets/city_scene", File.dirname(__FILE__)) ) g.layers = %w(background haze sky clouds) g.weather_group = %w(clouds) g.time_group = %w(background sky) g end end gruff-0.6.0/test/test_bar.rb0000644000004100000410000003122112540054077015770 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + '/gruff_test_case' class TestGruffBar < GruffTestCase # TODO Delete old output files once when starting tests def setup super @datasets = [ [:Jimmy, [25, 36, 86, 39]], [:Charles, [80, 54, 67, 54]], [:Julie, [22, 29, 35, 38]], #[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], #[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], #["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]], ] end def test_bar_graph g = setup_basic_graph g.title = 'Bar Chart' g.write('test/output/bar_keynote.png') g = setup_basic_graph g.title = 'Visual Multi-Line Bar Graph Test' g.theme = Gruff::Themes::RAILS_KEYNOTE g.write('test/output/bar_rails_keynote.png') g = setup_basic_graph g.title = 'Visual Multi-Line Bar Graph Test' g.theme = Gruff::Themes::ODEO g.write('test/output/bar_odeo.png') end def test_title_margin g = setup_basic_graph g.title = 'Bar Graph with Title Margin = 100' g.title_margin = 100 g.write('test/output/bar_title_margin.png') end def test_thousand_separators g = Gruff::Bar.new(600) g.title = 'Formatted numbers' g.marker_count = 8 g.data('data', [4025, 1024, 50257, 703672, 1580456]) g.write('test/output/bar_formatted_numbers.png') end def test_bar_graph_set_colors g = Gruff::Bar.new g.title = 'Bar Graph With Manual Colors' g.legend_margin = 50 g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:Art, [0, 5, 8, 15], '#990000') g.data(:Philosophy, [10, 3, 2, 8], '#009900') g.data(:Science, [2, 15, 8, 11], '#990099') g.minimum_value = 0 g.write('test/output/bar_manual_colors.png') end def test_bar_graph_small g = Gruff::Bar.new(400) g.title = 'Visual Multi-Line Bar Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write('test/output/bar_keynote_small.png') end # Somewhat worthless test. Should an error be thrown? # def test_nil_font # g = setup_basic_graph 400 # g.title = "Nil Font" # g.font = nil # g.write "test/output/bar_nil_font.png" # end def test_no_line_markers g = setup_basic_graph(400) g.title = 'No Line Markers' g.hide_line_markers = true g.write('test/output/bar_no_line_markers.png') end def test_no_legend g = setup_basic_graph(400) g.title = 'No Legend' g.hide_legend = true g.write('test/output/bar_no_legend.png') end def test_no_title g = setup_basic_graph(400) g.title = 'No Title' g.hide_title = true g.write('test/output/bar_no_title.png') end def test_no_title_or_legend g = setup_basic_graph(400) g.title = 'No Title or Legend' g.hide_legend = true g.hide_title = true g.write('test/output/bar_no_title_or_legend.png') end def test_set_marker_count g = setup_basic_graph(400) g.title = 'Set marker' g.marker_count = 10 g.write('test/output/bar_set_marker.png') end def test_set_legend_box_size g = setup_basic_graph(400) g.title = 'Set Small Legend Box Size' g.legend_box_size = 10.0 g.write('test/output/bar_set_legend_box_size_sm.png') g = setup_basic_graph(400) g.title = 'Set Large Legend Box Size' g.legend_box_size = 50.0 g.write('test/output/bar_set_legend_box_size_lg.png') end def test_x_y_labels g = setup_basic_graph(400) g.title = 'X Y Labels' g.x_axis_label = 'Score (%)' g.y_axis_label = 'Students' g.write('test/output/bar_x_y_labels.png') end def test_wide_graph g = setup_basic_graph('800x400') g.title = 'Wide Graph' g.write('test/output/bar_wide_graph.png') g = setup_basic_graph('400x200') g.title = 'Wide Graph Small' g.write('test/output/bar_wide_graph_small.png') end def test_tall_graph g = setup_basic_graph('400x600') g.title = 'Tall Graph' g.write('test/output/bar_tall_graph.png') g = setup_basic_graph('200x400') g.title = 'Tall Graph Small' g.write('test/output/bar_tall_graph_small.png') end def test_one_value g = Gruff::Bar.new g.title = 'One Value Graph Test' g.labels = { 0 => '1', 1 => '2' } g.data('one', [1, 1]) g.write('test/output/bar_one_value.png') end def test_negative g = Gruff::Bar.new g.title = 'Pos/Neg Bar Graph Test' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [-1, 0, 4, -4]) g.data(:peaches, [10, 8, 6, 3]) g.write('test/output/bar_pos_neg.png') end def test_nearly_zero g = Gruff::Bar.new g.title = 'Nearly Zero Graph' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [1, 2, 3, 4]) g.data(:peaches, [4, 3, 2, 1]) g.minimum_value = 0 g.maximum_value = 10 g.write('test/output/bar_nearly_zero_max_10.png') end def test_y_axis_increment generate_with_y_axis_increment 2.0 generate_with_y_axis_increment 1 generate_with_y_axis_increment 5 generate_with_y_axis_increment 20 end def generate_with_y_axis_increment(increment) g = Gruff::Bar.new g.title = "Y Axis Set to #{increment}" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.y_axis_increment = increment g.data(:apples, [1, 0.2, 0.5, 0.7]) g.data(:peaches, [2.5, 2.3, 2, 6.1]) g.write("test/output/bar_y_increment_#{increment}.png") end def test_custom_spacing g = Gruff::Bar.new g.spacing_factor = 0 g.title = 'Zero spacing graff' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:apples, [1, 5, 8, 4]) g.data(:peaches, [4, 1, 2, 10]) g.minimum_value = 0 g.maximum_value = 10 g.write('test/output/bar_zero_spacing.png') end def test_spacing_factor_does_not_accept_values_lt_0_and_gt_1 g = Gruff::Bar.new assert_raise ArgumentError do g.spacing_factor = 1.01 end assert_raise ArgumentError do g.spacing_factor = -0.01 end end def test_custom_theme g = Gruff::Bar.new g.title = 'Custom Theme' g.font = File.expand_path('CREABBRG.TTF', ENV['MAGICK_FONT_PATH']) g.title_font_size = 60 g.legend_font_size = 32 g.marker_font_size = 32 g.theme = { :colors => %w(#efd250 #666699 #e5573f #9595e2), :marker_color => 'white', :font_color => 'blue', :background_image => 'assets/pc306715.jpg' } g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } g.data(:vancouver, [1, 2, 3, 4]) g.data(:seattle, [2, 4, 6, 8]) g.data(:portland, [3, 1, 7, 3]) g.data(:victoria, [4, 3, 5, 7]) g.minimum_value = 0 g.write('test/output/bar_themed.png') end def test_background_gradient_top_bottom g = Gruff::Bar.new g.title = 'Background gradient top to bottom' g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :top_bottom, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_top_bottom.png') end def test_background_gradient_bottom_top g = Gruff::Bar.new g.title = 'Background gradient top to bottom' g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :bottom_top, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_bottom_top.png') end def test_background_gradient_left_right g = Gruff::Bar.new g.title = 'Background gradient left to right' g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :left_right, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_left_right.png') end def test_background_gradient_right_left g = Gruff::Bar.new g.title = 'Background gradient right to left' g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :right_left, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_right_left.png') end def test_background_gradient_topleft_bottomright g = Gruff::Bar.new g.title = 'Background gradient top left to bottom right' g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :topleft_bottomright, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_topleft_bottomright.png') end def test_background_gradient_topright_bottomleft g = Gruff::Bar.new g.title = 'Background gradient top right to bottom left' g.title_font_size = 30 g.theme = { :background_colors => %w(#ff0000 #00ff00), :background_direction => :topright_bottomleft, } g.labels = @labels @datasets.each do |name, values| g.data(name, values) end g.minimum_value = 0 write_test_file(g, 'bar_background_gradient_topright_bottomleft.png') end def test_legend_should_not_overlap g = Gruff::Bar.new(400) g.theme_37signals() g.title = 'My Graph' g.data('Apples Oranges Watermelon Apples Oranges', [1, 2, 3, 4, 4, 3]) g.data('Oranges', [4, 8, 7, 9, 8, 9]) g.data('Watermelon', [2, 3, 1, 5, 6, 8]) g.data('Peaches', [9, 9, 10, 8, 7, 9]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005'} g.write('test/output/bar_long_legend_text.png') end def test_july_enhancements g = Gruff::Bar.new(600) g.hide_legend = true g.title = 'Full speed ahead' g.labels = (0..10).inject({}) { |memo, i| memo.merge({i => (i*10).to_s}) } g.data(:apples, (0..9).map { rand(20)/10.0 }) g.y_axis_increment = 1.0 g.x_axis_label = 'Score (%)' g.y_axis_label = 'Students' write_test_file g, 'enhancements.png' end def test_bar_spacing g = setup_basic_graph g.bar_spacing = 0 g.title = '100% spacing between bars' g.write('test/output/bar_spacing_full.png') g = setup_basic_graph g.bar_spacing = 0.5 g.title = '50% spacing between bars' g.write('test/output/bar_spacing_half.png') g = setup_basic_graph g.bar_spacing = 1 g.title = '0% spacing between bars' g.write('test/output/bar_spacing_none.png') end def test_set_label_stagger_height g = setup_long_labelled_graph g.title = 'Staggered labels' g.label_stagger_height = 30 g.write('test/output/bar_set_label_stagger_height.png') end def test_set_label_max_size_and_label_truncation_style # Absolute trunc g = setup_long_labelled_graph g.title = 'Absolute truncation (13 chars)' g.label_max_size = 13 g.label_truncation_style = :absolute g.write('test/output/bar_set_absolute_trunc.png') # Trailing Dots trunc g = setup_long_labelled_graph g.title = 'Trailing dots truncation (6 chars inc dots)' g.label_max_size = 6 g.label_truncation_style = :trailing_dots g.write('test/output/bar_set_trailing_dots_trunc.png') end def test_bar_value_labels g = setup_basic_graph g.show_labels_for_bar_values = true g.write('test/output/bar_value_labels.png') end def test_zero_marker_count g = setup_basic_graph g.marker_count = 0 g.write('test/output/bar_zero_marker_count.png') end def test_zero_marker_shadow g = setup_basic_graph g.title = 'Bar Chart with Marker Shadow' g.marker_shadow_color = '#888888' g.write('test/output/bar_marker_shadow.png') end protected def setup_basic_graph(size=800) g = Gruff::Bar.new(size) g.title = 'My Bar Graph' g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g end def setup_long_labelled_graph(size=500) g = Gruff::Bar.new(size) g.title = 'A Graph for All Seasons' g.labels = { 0 => 'January was a cold one', 1 => 'February is little better', 2 => 'March will bring me hares', 3 => 'April and I\'m a fool' } @datasets.each do |data| g.data(data[0], data[1]) end g end end gruff-0.6.0/test/test_scatter.rb0000644000004100000410000001244312540054077016676 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + '/gruff_test_case' class TestGruffScatter < Test::Unit::TestCase def setup @datasets = [ [:Chuck, [20, 10, 5, 12, 11, 6, 10, 7], [5, 10, 19, 6, 9, 1, 14, 8]], [:Brown, [5, 10, 20, 6, 9, 12, 14, 8], [20, 10, 5, 12, 11, 6, 10, 7]], [:Lucy, [19, 9, 6, 11, 12, 7, 15, 8], [6, 11, 18, 8, 12, 8, 10, 6]] ] end # Done def test_scatter_graph g = setup_basic_graph g.title = 'Basic Scatter Plot Test' g.write('test/output/scatter_basic.png') end #~ # Done def test_many_datapoints g = Gruff::Scatter.new g.title = 'Many Datapoint Graph Test' y_values = (0..50).map { rand(100) } x_values = (0..50).map { rand(100) } g.data('many points', x_values, y_values) # Default theme g.write('test/output/scatter_many.png') end # Done def test_no_data g = Gruff::Scatter.new(400) g.title = 'No Data' # Default theme g.write('test/output/scatter_no_data.png') g = Gruff::Scatter.new(400) g.title = 'No Data Title' g.no_data_message = 'There is no data' g.write('test/output/scatter_no_data_msg.png') end # Done def test_all_zeros g = Gruff::Scatter.new(400) g.title = 'All Zeros' g.data(:gus, [0, 0, 0, 0], [0, 0, 0, 0]) # Default theme g.write('test/output/scatter_no_data_other.png') end # Done def test_some_nil_points g = Gruff::Scatter.new g.title = 'Some Nil Points' @datasets = [ [:data1, [1, 2, 3, nil, 3, 5, 6], [5, nil, nil, nil, nil, 5, 7]] ] @datasets.each do |data| assert_raise ArgumentError do g.data(*data) end end end # Done def test_unequal_number_of_x_and_y_values g = Gruff::Scatter.new g.title = 'Unequal number of X and Y values' @datasets = [ [:data1, [1, 2, 3], [1, 2]], [:data2, [1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]], ] @datasets.each do |data| assert_raise ArgumentError do g.data(*data) end end end # Done def test_empty_set_of_axis_values g = Gruff::Scatter.new g.title = 'Missing Axis Values' @datasets = [ [:data1, [1, 2, 3, 4, 5]] ] @datasets.each do |data| assert_raise ArgumentError do g.data(*data) end end end # Done def test_no_title g = Gruff::Scatter.new(400) g.data(:data1, [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]) g.write('test/output/scatter_no_title.png') end # Done def test_no_line_markers g = setup_basic_graph(400) g.title = 'No Line Markers' g.hide_line_markers = true g.write('test/output/scatter_no_line_markers.png') end # Done def test_no_legend g = setup_basic_graph(400) g.title = 'No Legend' g.hide_legend = true g.write('test/output/scatter_no_legend.png') end # Done def test_nothing_but_the_graph g = setup_basic_graph(400) g.title = 'THIS TITLE SHOULD NOT DISPLAY!!!' g.hide_line_markers = true g.hide_legend = true g.hide_title = true g.write('test/output/scatter_nothing_but_the_graph.png') end # TODO Implement baselines on x and y axis #~ def test_baseline_larger_than_data #~ g = setup_basic_graph(400) #~ g.title = "Baseline Larger Than Data" #~ g.baseline_value = 150 #~ g.write("test/output/scatter_large_baseline.png") #~ end # Done def test_wide_graph g = setup_basic_graph('800x400') g.title = 'Wide Graph' g.write('test/output/scatter_wide_graph.png') g = setup_basic_graph('400x200') g.title = 'Wide Graph Small' g.write('test/output/scatter_wide_graph_small.png') end # Done def test_negative g = setup_pos_neg(800) g.write('test/output/scatter_pos_neg.png') g = setup_pos_neg(400) g.title = 'Pos/Neg Line Test Small' g.write('test/output/scatter_pos_neg_400.png') end # Done def test_all_negative g = setup_all_neg(800) g.write('test/output/scatter_all_neg.png') g = setup_all_neg(400) g.title = 'All Neg Line Test Small' g.write('test/output/scatter_all_neg_400.png') end # Done def test_no_hide_line_no_labels g = Gruff::Scatter.new g.title = 'No Hide Line No Labels' @datasets.each do |data| g.data(data[0], data[1], data[2]) end g.hide_line_markers = false g.write('test/output/scatter_no_hide.png') end def test_no_set_labels g = Gruff::Scatter.new g.title = 'Setting Labels Test' g.labels = { 0 => 'This', 1 => 'should', 2 => 'not', 3 => 'show', 4 => 'up' } @datasets.each do |data| g.data(data[0], data[1], data[2]) end g.write('test/output/scatter_no_labels.png') end protected def setup_basic_graph(size=800) g = Gruff::Scatter.new(size) g.title = 'Rad Graph' @datasets.each do |data| g.data(data[0], data[1], data[2]) end g end def setup_pos_neg(size=800) g = Gruff::Scatter.new(size) g.title = 'Pos/Neg Scatter Graph Test' g.data(:apples, [-1, 0, 4, -4], [-5, -1, 3, 4]) g.data(:peaches, [10, 8, 6, 3], [-1, 1, 3, 3]) g end def setup_all_neg(size=800) g = Gruff::Scatter.new(size) g.title = 'Neg Scatter Graph Test' g.data(:apples, [-1, -1, -4, -4], [-5, -1, -3, -4]) g.data(:peaches, [-10, -8, -6, -3], [-1, -1, -3, -3]) g end end # end GruffTestCase gruff-0.6.0/test/test_stacked_bar.rb0000644000004100000410000000257612540054077017501 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffStackedBar < GruffTestCase def setup @datasets = [ [:Jimmy, [25, 36, 86, 39]], [:Charles, [80, 54, 67, 54]], [:Julie, [22, 29, 35, 38]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24' } end def test_bar_graph g = Gruff::StackedBar.new g.title = "Visual Stacked Bar Graph Test" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/stacked_bar_keynote.png" end def test_bar_graph_small g = Gruff::StackedBar.new(400) g.title = "Visual Stacked Bar Graph Test" g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/stacked_bar_keynote_small.png" end def test_bar_graph_segment_spacing g = Gruff::StackedBar.new g.title = "Visual Stacked Bar Graph Test" g.segment_spacing = 0 g.labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end g.write "test/output/stacked_bar_keynote_no_space.png" end end gruff-0.6.0/test/test_spider.rb0000644000004100000410000001230412540054077016513 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffSpider < GruffTestCase def setup @datasets = [ [:Strength, [10]], [:Dexterity, [16]], [:Constitution, [12]], [:Intelligence, [12]], [:Wisdom, [10]], ["Charisma", [16]], ] # @datasets = [ # [:Darren, [25]], # [:Chris, [80]], # [:Egbert, [22]], # [:Adam, [95]], # [:Bill, [90]], # ["Frank", [5]], # ["Zero", [0]], # ] end def test_spider_graph g = Gruff::Spider.new(20) g.title = "Spider Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/spider_keynote.png") end def test_pie_graph_small g = Gruff::Spider.new(20, 400) g.title = "Visual Spider Graph Test Small" @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/spider_small.png") end def test_spider_graph_nearly_equal g = Gruff::Spider.new(50) g.title = "Spider Graph Nearly Equal" g.data(:Blake, [41]) g.data(:Aaron, [42]) g.data(:Grouch, [40]) # g.data(:Snuffleupagus, [43]) g.write("test/output/spider_nearly_equal.png") end def test_pie_graph_equal g = Gruff::Spider.new(50) g.title = "Spider Graph Equal" g.data(:Bert, [41]) g.data(:Adam, [41]) g.data(:Joe, [41]) g.write("test/output/spider_equal.png") end def test_pie_graph_zero g = Gruff::Spider.new(2) g.title = "Pie Graph Two One Zero" g.data(:Bert, [0]) g.data(:Adam, [1]) g.data(:Sam, [2]) g.write("test/output/spider_zero.png") end def test_wide g = setup_basic_graph('800x400') g.title = "Wide spider" g.write("test/output/spider_wide.png") end def test_label_size g = setup_basic_graph() g.title = "Spider With Small Legend" g.legend_font_size = 10 g.write("test/output/spider_legend.png") g = setup_basic_graph(400) g.title = "Small spider With Small Legend" g.legend_font_size = 10 g.write("test/output/spider_legend_small.png") end def test_theme_37signals g = Gruff::Spider.new(20) g.title = "Spider Graph Test" @datasets.each do |data| g.data(data[0], data[1]) end g.theme = Gruff::Themes::THIRTYSEVEN_SIGNALS # Default theme g.write("test/output/spider_37signals.png") end def test_no_axes g = Gruff::Spider.new(20) g.title = "Look ma, no axes" g.hide_axes = true @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/spider_no_axes.png") end def test_no_print g = Gruff::Spider.new(20) g.title = "Should not print" g.hide_text = true @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/spider_no_print.png") end def test_transparency g = Gruff::Spider.new(20) g.title = "Transparent background" g.hide_text = true g.transparent_background = true g.hide_axes = true @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/spider_no_background.png") end def test_overlay g = Gruff::Spider.new(20) g.title = "George (blue) vs Sarah (white)" @datasets.each do |data| g.data(data[0], data[1]) end g.write("test/output/spider_overlay_1.png") g = Gruff::Spider.new(20) g.title = "Transparent background" g.hide_text = true g.hide_axes = true g.transparent_background = true @datasets = [ [:Strength, [18]], [:Dexterity, [10]], [:Constitution, [18]], [:Intelligence, [8]], [:Wisdom, [14]], ["Charisma", [4]], ] @datasets.each do |data| g.data(data[0], data[1]) end g.marker_color = "#4F6EFF" g.write("test/output/spider_overlay_2.png") end def test_lots_of_data g = Gruff::Spider.new(10) @datasets = [[:a, [1]], [:b, [5]], [:c, [3]], [:d, [9]], [:e, [4]], [:f, [7]], [:g, [0]], [:h, [4]], [:i, [6]], [:j, [0]], [:k, [4]], [:l, [8]]] @datasets.each do |data| g.data(data[0], data[1]) end g.title = "Sample Data" g.write("test/output/spider_lots_of_data.png") end def test_lots_of_data_with_large_names g = Gruff::Spider.new(10) @datasets = [[:anteaters, [1]], [:bulls, [5]], [:cats, [3]], [:dogs, [9]], [:elephants, [4]], [:frogs, [7]], [:giraffes, [0]], [:hamsters, [4]], [:iguanas, [6]], [:jaguar, [0]], [:kangaroo, [4]], [:locust, [8]]] @datasets.each do |data| g.data(data[0], data[1]) end g.title = "Zoo Inventory" g.write("test/output/spider_lots_of_data_normal_names.png") end def test_rotation g = Gruff::Spider.new(20) g.title = "Rotation" @datasets.each do |data| g.data(data[0], data[1]) end g.rotation = 45 # degrees g.write("test/output/spider_rotation.png") end protected def setup_basic_graph(size=800, max = 20) g = Gruff::Spider.new(max, size) g.title = "My Graph Title" @datasets.each do |data| g.data(data[0], data[1]) end return g end end gruff-0.6.0/test/test_photo.rb0000644000004100000410000000141512540054077016357 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffPhotoBar < GruffTestCase # def setup # @datasets = [ # [:Jimmy, [25, 36, 86, 39]], # [:Charles, [80, 54, 67, 54]], # # [:Charity, [0, nil, 100, 90]], # ] # end # # def test_bar_graph # bar_graph_sized # bar_graph_sized(400) # end # # # protected # # def bar_graph_sized(size=800) # g = Gruff::PhotoBar.new(size) # g.title = "Photo Bar Graph Test #{size}px" # g.labels = { # 0 => '5/6', # 1 => '5/15', # 2 => '5/24', # 3 => '5/30', # } # @datasets.each do |data| # g.data(*data) # end # # g.theme = 'plastik' # # g.write("test/output/photo_plastik_#{size}.png") # end end gruff-0.6.0/test/test_area.rb0000644000004100000410000000561112540054077016140 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffArea < GruffTestCase def setup super @datasets = [ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]], [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]], [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]], [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]], [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]], ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]], ] @sample_labels = { 0 => '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', 4 => '6/4', 5 => '6/12', 6 => '6/21', 7 => '6/28', } end def test_area_graph g = Gruff::Area.new g.title = "Visual Multi-Area Graph Test" g.labels = { 0 => '5/6', 2 => '5/15', 4 => '5/24', 6 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/area_keynote.png") end def test_resize g = Gruff::Area.new(400) g.title = "Small Size Multi-Area Graph Test" g.labels = { 0 => '5/6', 2 => '5/15', 4 => '5/24', 6 => '5/30', } @datasets.each do |data| g.data(data[0], data[1]) end # Default theme g.write("test/output/area_keynote_small.png") end def test_many_datapoints g = Gruff::Area.new g.title = "Many Multi-Area Graph Test" g.labels = { 0 => 'June', 10 => 'July', 30 => 'August', 50 => 'September', } g.data('many points', (0..50).collect {|i| rand(100) }) # Default theme g.write("test/output/area_many.png") end def test_many_areas_graph_small g = Gruff::Area.new(400) g.title = "Many Values Area Test 400px" g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write("test/output/area_many_areas_small.png") end def test_area_graph_tiny g = Gruff::Area.new(300) g.title = "Area Test 300px" g.labels = { 0 => '5/6', 10 => '5/15', 20 => '5/24', 30 => '5/30', 40 => '6/4', 50 => '6/16' } %w{jimmy jane philip arthur julie bert}.each do |student_name| g.data(student_name, (0..50).collect { |i| rand 100 }) end # Default theme g.write("test/output/area_tiny.png") end def test_wide g = setup_basic_graph('800x400') g.title = "Area Wide" g.write("test/output/area_wide.png") end protected def setup_basic_graph(size=800) g = Gruff::Area.new(size) g.title = "My Graph Title" g.labels = @sample_labels @datasets.each do |data| g.data(data[0], data[1]) end return g end end gruff-0.6.0/test/test_mini_side_bar.rb0000644000004100000410000000141312540054077020010 0ustar www-datawww-datarequire File.expand_path('../gruff_test_case', __FILE__) class TestMiniSideBar < GruffTestCase def test_one_color # Use a single data set @datasets = [ [:Jimmy, [25, 36, 86, 39]] ] @labels = { 0 => 'Auto', 1 => 'Entertainment', 2 => 'Food', 3 => 'Bus' } g = setup_basic_graph(Gruff::Mini::SideBar, 200) write_test_file g, 'mini_side_bar.png' end def test_multi_color # @datasets = [ # [:Jimmy, [25, 36, 86, 39]] # ] # @labels = { # 0 => 'Auto', # 1 => 'Entertainment', # 2 => 'Food', # 3 => 'Bus' # } g = setup_basic_graph(Gruff::Mini::SideBar, 200) write_test_file g, 'mini_side_bar_multi_color.png' end end gruff-0.6.0/test/test_base.rb0000644000004100000410000000015712540054077016142 0ustar www-datawww-data#!/usr/bin/ruby require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffBase < GruffTestCase endgruff-0.6.0/test/test_side_bar.rb0000644000004100000410000000301312540054077016772 0ustar www-datawww-data require File.dirname(__FILE__) + "/gruff_test_case" class TestGruffSideBar < GruffTestCase def test_bar_graph g = setup_basic_graph(Gruff::SideBar, 800) write_test_file g, 'side_bar.png' end def test_bar_spacing g = setup_basic_graph(Gruff::SideBar, 800) g.bar_spacing = 0 g.title = "100% spacing between bars" g.write("test/output/side_bar_spacing_full.png") g = setup_basic_graph(Gruff::SideBar, 800) g.bar_spacing = 0.5 g.title = "50% spacing between bars" g.write("test/output/side_bar_spacing_half.png") g = setup_basic_graph(Gruff::SideBar, 800) g.bar_spacing = 1 g.title = "0% spacing between bars" g.write("test/output/side_bar_spacing_none.png") end def test_x_axis_range g = Gruff::SideBar.new('400x300') g.title = 'Should run from 8 to 32' g.hide_line_numbers = false g.theme_37signals g.data("Grapes", [8]) g.data("Apples", [24]) g.data("Oranges", [32]) g.data("Watermelon", [8]) g.data("Peaches", [12]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005'} g.write("test/output/side_bar_data_range.png") end def test_bar_labels g = Gruff::SideBar.new('400x300') g.title = 'Should show labels for each bar' g.data("Grapes", [8]) g.data("Apples", [24]) g.data("Oranges", [32]) g.data("Watermelon", [8]) g.data("Peaches", [12]) g.labels = {0 => '2003', 2 => '2004', 4 => '2005'} g.show_labels_for_bar_values = true g.write("test/output/side_bar_labels.png") end end gruff-0.6.0/test/image_compare.rb0000744000004100000410000000261612540054077016764 0ustar www-datawww-data#!ruby require 'chunky_png' class ImageCompare include ChunkyPNG::Color def self.compare(file_name, old_file_name) name = file_name.chomp('.png') org_file_name = "#{name}_0.png~" new_file_name = "#{name}_1.png~" return nil unless File.exists? old_file_name images = [ ChunkyPNG::Image.from_file(old_file_name), ChunkyPNG::Image.from_file(file_name), ] sizes = images.map(&:width).uniq + images.map(&:height).uniq if sizes.size != 2 logger.warn "Image size has changed for #{name}: #{sizes}" return nil end diff = [] images.first.height.times do |y| images.first.row(y).each_with_index do |pixel, x| unless pixel == images.last[x, y] diff << [x, y] end end end if diff.empty? File.delete(org_file_name) if File.exists? org_file_name File.delete(new_file_name) if File.exists? new_file_name return false end x, y = diff.map { |xy| xy[0] }, diff.map { |xy| xy[1] } (1..2).each do |i| images.each do |image| image.rect(x.min - i, y.min - i, x.max + i, y.max + i, ChunkyPNG::Color.rgb(255, 0, 0)) end end images.first.save(org_file_name) images.last.save(new_file_name) true end end if $0 == __FILE__ unless ARGV.size == 2 puts "Usage: #$0 " exit 1 end ImageCompare.compare(ARGV[0], ARGV[1]) end gruff-0.6.0/test/test_mini_pie.rb0000644000004100000410000000112212540054077017012 0ustar www-datawww-datarequire File.expand_path('../gruff_test_case', __FILE__) class TestMiniPie < GruffTestCase def test_simple_pie g = setup_basic_graph(Gruff::Mini::Pie, 200) write_test_file g, 'mini_pie.png' end def test_pie_with_legend_right g = setup_basic_graph(Gruff::Mini::Pie, 200) g.legend_position = :right write_test_file g, 'mini_pie_right_legend.png' end # def test_code_sample # g = Gruff::Mini::Pie.new(200) # g.data "Car", 200 # g.data "Food", 500 # g.data "Art", 1000 # g.data "Music", 16 # g.write "mini_pie.png" # end end gruff-0.6.0/init.rb0000644000004100000410000000003412540054077014147 0ustar www-datawww-data# For Rails require 'gruff' gruff-0.6.0/assets/0000755000004100000410000000000012540054077014164 5ustar www-datawww-datagruff-0.6.0/assets/plastik/0000755000004100000410000000000012540054077015633 5ustar www-datawww-datagruff-0.6.0/assets/plastik/green.png0000644000004100000410000001173012540054077017443 0ustar www-datawww-dataPNG  IHDR=^6u pHYs   MiCCPPhotoshop ICC profilexڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/%ҟ3gAMA|Q cHRMz%u0`:o_FIDATx{o93^{Y 7i%$iLR!}wPU+ U) ?(jn19c0׀kh~4;|sgf[ FѦ_(Cerf|N?ttPX(mw?>Zt_5?wC:Azm~"gt+ lE냎蠣fōˣew{_YrvkogLXP8񝣣(TOT8/J)бx8Q(I`tOYvcoTywCm>ITzmDqP(, :hGK/M9;T˙64cr B __xޏ<.1[^;KS(f #~E.Gٍ3ƨc N6ל:Ÿ&şyM/giP^XΙc6V^}]>D_x2,6j4 I*,{'L.}B2Û^/|DOGh1!AVܤV/Q,io[)r~}/ϖt{7Ylϲ`I }qq]Rm:\׾5NX )9ؖZ j{rx=iAcJik곔3K5ޯ?k;w6\m"pyzL`x)7f~C޻ïC<R=җ9@&s L&NozM]> gUb1EvRmjsyn/kFUܺ@}H?O_Ev'K1:m3ߞܜ`9K]@S%`> EQ - `8;;wjͻLܴ ̮_Y2[Y0%3tUFȖc$g `:<V( a q X0`4MOvo:koBsǞv mcpyc\H03sq7e` Ok Ei z}O>Ut_ o* {p;hv[9'0HW9nf,~@L\jQw7B߂>2'ЙtwxY޲+Y6+9ՙ؁]NgYA;%}sTrJqE /- '[e`d+޳p$7*;e;xj>:cR~9n;_!yܳqU-:K%Y`htUxw׀C)n >OLx6ovS3+NΎ`aWmUC:ӳIsN[].9`x-2M:`hA>n6 G|PM%AwjH z>@dۿ mt,T?KP"53ަSI]iHgА낾 ~t,mlZ+ ʪt.4G} x I i^I`a- B‡f`,%YмIENDB`gruff-0.6.0/assets/plastik/red.png0000644000004100000410000001172612540054077017122 0ustar www-datawww-dataPNG  IHDR=^6u pHYs   MiCCPPhotoshop ICC profilexڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/%ҟ3gAMA|Q cHRMz%u0`:o_FIDATx_oډ$kx] Kh^qћ} }hQm U$DUP $MB cljٵ޿9+qJ!)ssvgvΞșWŗ̬w Z_OI7~IN {AG9 l z 3amfm ׀"t㗏;w) .dRZ#ٓ ١Lϑo -/~;_̖fn`k38L?E_ɯ-@l3>?Z̕g=wE2ShD4;uy[斜%u{t?6Ε*֍j p7jfMvzjԱIbg0Ws%L#,N$F$EnӬi4E}zPSk?8Cb8M$=)|oW0<(AF}ѻ , ͝{Kxo|H/Hst+OҺxHw%N" |EfɣQ,M?eM}QO:ֿ]a:14KEEFѻk஽e|f'hOߠ}fmF86źQveBBFѣ]aX&WKE֦Ko~Bt>=?e"n kUh͖h{0ާvι%<5C Rz6b}˿\,{<;؄ZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZhZ;G79ٟ|enP_B{:jZIWF#+=C*nGuGRTfy*s۠|Xy \&VxiɁOV"90| /z>Ě8nOݹ_q`e?hwցrk|> 2p8f]S}竎flkf8`M͹{tDYKu4{@X.=6* qYE`2fv ^\%Nqq8у؂6Xq 5`0`0Xk+,6겚:oWZ8PfBi`B[|sD;pn%1UlV:k 61v:K5׮ڳfb*P x6~F 4|pıw!><L }|#>xM:8܌28Dz#k:Aut-`)V'~2xa,쯇k@tG-`0.gj09ߌsMsݏ6kח&tśLQD-\ׁ !|/U=7?L/ǀJwl3:pͿ}|xK1!t; Wa+,!} r3@?S}"}G36 R$)MtɦuXQ7Uv[AoG$0 <.{R>oG<4IyVƒmsa(Hg)H;{|70QxHzwF6 MB)Hz > a6*KAKyMuIENDB`gruff-0.6.0/assets/plastik/blue.png0000644000004100000410000001172312540054077017274 0ustar www-datawww-dataPNG  IHDR=^6u pHYs   MiCCPPhotoshop ICC profilexڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/%ҟ3gAMA|Q cHRMz%u0`:o_FIDATxkO\,,f.,J$$VFh_@EAV[r5MRUnb텽9LpbSp7tfֱmkLT[vb~-8y`R pOȤOSi ܝB;+vl6; \L̖|MwKW3)tr l6t6}$yyL̗R2}>#)X=r8:N͖̩R01[2fK{ x:Ϥ3)u`2˵fŊyr!89W2sk-Xڟ̤:}g2Uvy&pbj%C#fSXɩ++>~?K]s\l|~͜l5l)ںwT5_3)w)\.|K'u1[ =8ZKʤӿͯo/>{bNK>>/+, A`9ԚrDv~2.ǎ$yXGvr`+jcݻ.XnVP5X)Y)Y*xЭC Syޞ*pxn4jbٲP2z`xܣ,jbFB7]}8˩|zNᒯtBԛ~p1+|Pc_D'X' Jumׁk- +J.pϾe!o]Z5JHKM&v1uak@bBBJ BhsUf f M~?`v3sJI"aePj3[l֤ A50{K;7{ |U{;xj$g4!Ֆ vw߉)4 W*|}wlq| B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -B -Bto=S{$%4kWoސ_?S:|ݿbs-S~w;OFbfbvBs{7*];Z&*v~xKl6Vr\ vXyx$R@|*t[C?;s:@6;7y醌&*&}PE ,Er9c3 m N]\p#㬏Ywvx&ߛSk! +cc]%h.#@wބw#}7/ggo(g[l^B8RN?u)n?ແ$pdVu>畍49!^Q`冃-EM;+nHzh?,:ĎxF34EZQ -o  p]Aoǀp#qV &inS ]mv@ohڏt=yPI UMxH!Uty7i4&IENDB`gruff-0.6.0/assets/city_scene/0000755000004100000410000000000012540054077016311 5ustar www-datawww-datagruff-0.6.0/assets/city_scene/number_sample/0000755000004100000410000000000012540054077021142 5ustar www-datawww-datagruff-0.6.0/assets/city_scene/number_sample/default.png0000644000004100000410000000404312540054077023275 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx}l@)0t.W%K机dY\$.l6uM.4ƙ) nnNh"Z:KR }=Ou{`O]k{yk\.6 :: : : :: : : q M*xVF1y|_rF >|y%?xymZ G̳[;S\_]37z-'o[W_xr˪:mžޏEm*1# y&r¡cd7|W*y֧}&zXj[5OrɴI5 }w/ilyW,O~! {1܊\;ԉ7f2RЉϔDŽ]CƷ~u N׏ zM]ݽku4e2ӧNzr?67# CτzA?+Q=Nd:W#sǍv`#s1`5t^z:_5j qףQ}*(+y{G2|_g4-^u'*G}΅&ܾ!Zm]=} M}E?vG#ޟ?x AHh̼~W~t m*7r{,{~_O/Nze wOɂysNpokY񿷻smK8';T=~9L`z{w#F -{ g\| vo+^\vdc=hݸm L_sR%@*cF12\x1 /z㗯PW}|ҙlȏrCvD!m'~Ms͘ZsU4j'E!nV74}慆])';_we>f|?޳o_~<6^jWhrIw?O8𼳫^W>ĊK-NWFnɭg_Y7*uq{gkt:_o iehAύ+VmټV޶їy4v%'!O+~2s?wٵ'ևtUiT*{ll>zF4J"7a`c-zrPb>)k볇NzҴm8>!j<>0@}Z=ohӾW7SOj.YgԎYZx]ߌFs*/8~}Gmں#ķ/YYw%Kc+n?%q^M05b'FM Ͼ,bNVP'*21uaz8ޅ$'4o_3}CvܙϹkK zeī]xtr/m?WrS>հxW{|+`7;~L ^8x`AU/=ҽass˽wvܺ MkA?z Y9u|-y|YZC{kŪ-;M[qc8NЇ:Q>OGhL) }[<>=|H7ta$A?>l2IoztGU: :: : : :: : GJ nqQIENDB`gruff-0.6.0/assets/city_scene/number_sample/2.png0000644000004100000410000000404712540054077022016 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx \eofv3nۥ--R)XkG `bB$H< jT@PؤB6B ZR@@RZ7lWmnh^/23dKkRr9't@AAt@@AttT#RT#~wuA^3tɗXK FkR>AĈy6Lػrrcp/eg=<_=Tyf:steV=s'U(vݽmg͞Bbܲ'naodҵb+F<* ltA(6?{gȝp\ż~t\Wl-l])[rĺƍ/J{sS>L{;ΑWU}TAC}U.]?<;\c5<>9 Chx:R/VKOo_gu.u֨ɧ]F>t#|OmygPU]??o5+/{d4 ;q7aȉ[v߻wξAgw4G|: z<{3os5sommsGXno޶_wϷ;K#A2A7'Dͳ.9k7Bo{[u[~~cOEvDQRt?M|LDM4L=}¥ݭo6|LhQ7;o֣ݖ㴵%ާuܰt5Tdr!?(B& 6_ @ЩsO_t&#'Vs=jSYgKfM=rb|RYs/5 " TL}fN:my'pgjƆsv.z՗זvuPC8 ABr7N;)RI{wvvτ>mݽ{ͨkV%/:s)d$ii]奮e+6Ÿo5pp},ȁOqr4|I }st?>=>!k=tmc$u#At[@9>l2" :GJG: : :: : : :: : : : ::  %IENDB`gruff-0.6.0/assets/city_scene/number_sample/1.png0000644000004100000410000000405412540054077022013 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx lezݺn+Xd,&hPDĄH &f`P FA&*1HtE _dlccUJu]~|boK&LR [&At@@Att@AxO d2Z`?,~s)}< _ V{tq1oK}`[kxm墻=@{Esc@B o&/>/Y3?yq7/+yGg,yXjZՏ^u7ڵwYWO_sWOh?Ӱҟ/,3\.[_. ;1*xAA|?xzwaӦ=捓CڟRKջZ59Glbl|\~ˇM\›gzήbM0ʥ}'lh)d L\zmUc9F '@5d|嶾bCӨ+fŮ'vU: AV֚@Ё?=s#.B$䳆ΪiUp9tjAq?'ӱM ';v\s'_5KMaR6t\2+N9+Q~a{w=l͋؊Z6ymmQXr)7LȜ<=W[7X?ؖ<>LCOAЁ(O5)O lMl[o|ҧ~x=dt]#_ZAЁSf'mwRJVKF{ d*p }Wqx{zyaC[6)?ʶ-⼳OwBc.Kk螯\_oZ2ZUyYAЁo_wԄW6K;{fE׭|sN8aN B{C f'~${U9pn }"d_'! _=ycqɧ?;嶺/ʅ.~Fi7lYrSB0A7:PqO^۶W+iG{Ȧ+ҷݯ]xb0#w[AЁCjd޳y= dtt}6~¯&M/Yײ: h/!גz=Wn|?^UkׇЁO$_߈'qx2|Y }s<==dc (F=P71 D5yD=8)[2M: : :: : : :: : : : ::  yN{x⹼}u[cOG?7%i6=ܔՏbkoo$vlF&>Ar9@۰g\ZP0ZZb]^Hnms0t%z?xPQ#F};@>FѨXVټu]{Ƴg_Ys3qp7c:VСہ:p-'D..VW/* wbzpF +vZ3;+~]/#r?jAPz:╝myc6LΩnkr4q>"^9g&,-#AW+jH˘C:̼U͗7~qixVpIHjp =rǞg;`A-.3-XwqU4&ǵbl.r?{w]L?֥Oa \`tYr8*牥 :Ժ\f8ͳ|V%mw۽g_fJCnG3{7bފ7|qu?~/ʥp]{A0q/4u[Njw\s۸_.@$;VM%hstlv>IJO2II$R׶9Ȧ:)w⟵R%һSL=MJt{IO>rF8a$SئGuLv'ÍzCz_@$?{5ص5a s?kR]}Mz!w^ٛI:#Hp@|eԺZy(R*|82p$'+@>p«_:.+!1ĒJfP\#28{^[ |S nDkz+RS]&NqCg29K2U&A'6خQc tj[ǾEsm51a|e|,">)2S^S}l%Ѫf,xy_ZdGO#k(܁ ij?VF- ʅZ}OwPϭVw렫ע}^{oU K&?6)n#W3kxʧ䟏*\Wox O94pT@Ȧ՘t7y+gH(~D&/CEoK_/ 4CK/m;C}{L rx JȲ㪣ٵCG;czoun-YJSQ7%PR״⚕+A:Pb:s]kz]cOJ2x U*X870ʶg~u3IW7>qp=W40CpfAcݚ_}O]?w{cֆmo'*^>a:tÎyjׂj/)C}(Ƙ@vCח:(&: @At: At@ :t@A: t@A: @At: :t@A: |[% IENDB`gruff-0.6.0/assets/city_scene/sky/0600.png0000644000004100000410000000457612540054077020226 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe< IDATx{lgkiϡ(tAmYB70EbBtSYD%e@43,uYD̦Q1 6.m)zC'&;Ʒ=.vߧ3y3KLwvdSuJ&rZґ3;5+|\FЯ?j$XP|:emze6T֮.+izHElwKvnSAIPO_Pc@\+Z<}Flњǯo6d$s׹zZ v =a;%=$ -X6Jj\9Z>fw\L?ҥYaik0tНLf++3Sa䖡0ie3k.n5?Sͪg(/MvoDܝ8ղ" L}P^ɟ激ƮF|nANϔvAW [,Qg]P +ޔd8ٹaڊ6;ˤ%nsUO9⛹B%ԽSL=M9_/?;}-$>pP#H?6];Oc>n^{(r'y@zD݋0_פRV.w?eݩd9< I8@-Ku!#5O5OuC\xS#CAb=׿t@c g+2p% dwc3` m{z^[xC8nXc|+!DRYQ"vq]Ox,$L>p]Ooر]]=F?k#Ѳǭ}} 0sjmm-3v4xc}3nG=I.=gO,?p6 ?VFM`w- [ohu=9b{BݷJ]= P]ٲ O`\*Ȧ'X w۴[ GFC#\_ZbWZ.[S-OD}N/|]v26 _6ng/~{Ţ9;k6j[rROحYyڊyn:Rj\fjn}{ݹcMޅiW煾gVOZ_WϽǷw~ O۳7i a ;֮^Wz=羹U_Xo~zw'rps>ދUxfA+7n{>TAcVدyz5A7뙺<]׿Ve=6FnMn a0rfLVt@A: t@AnK`%(IENDB`gruff-0.6.0/assets/city_scene/sky/0000.png0000644000004100000410000000405412540054077020207 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx lezݺn+Xd,&hPDĄH &f`P FA&*1HtE _dlccUJu]~|boK&LR [&At@@Att@AxO d2Z`?,~s)}< _ V{tq1oK}`[kxm墻=@{Esc@B o&/>/Y3?yq7/+yGg,yXjZՏ^u7ڵwYWO_sWOh?Ӱҟ/,3\.[_. ;1*xAA|?xzwaӦ=捓CڟRKջZ59Glbl|\~ˇM\›gzήbM0ʥ}'lh)d L\zmUc9F '@5d|嶾bCӨ+fŮ'vU: AV֚@Ё?=s#.B$䳆ΪiUp9tjAq?'ӱM ';v\s'_5KMaR6t\2+N9+Q~a{w=l͋؊Z6ymmQXr)7LȜ<=W[7X?ؖ<>LCOAЁ(O5)O lMl[o|ҧ~x=dt]#_ZAЁSf'mwRJVKF{ d*p }Wqx{zyaC[6)?ʶ-⼳OwBc.Kk螯\_oZ2ZUyYAЁo_wԄW6K;{fE׭|sN8aN B{C f'~${U9pn }"d_'! _=ycqɧ?;嶺/ʅ.~Fi7lYrSB0A7:PqO^۶W+iG{Ȧ+ҷݯ]xb0#w[AЁCjd޳y= dtt}6~¯&M/Yײ: h/!גz=Wn|?^UkׇЁO$_߈'qx2|Y }s<==dc (F=P71 D5yD=8)[2M: : :: : : :: : : : ::  `{}ݱvh UG3;A~ OnmjTUuUF1܂Yݶ~f˜g^kzL}P崪ݗ[ݫۭm@}ue5\*G>7i7]lGgwokW36v# ,,{\۰FUw\Lg@(=yjcu_>w1Y!.xǷnUqUY20)(}Ui;HϼнTGк~dG]>?>C(]뒟wXZt pm]/?l* Xd"g$9+]zÿRJ@Kse.IeLe:a2}uJ2rE]%xW|~߄p@ zǿ ЁѕKxꗏIjP HFj#49{{_k _]<`WBDLvSD%KbG}dxƝ;۳t4]Zhݍ;2#緣\1Sfmp@ HTUdnK19M7wA<xls0#X?VnprOyKrH pF}'ղ 誟r/:F@ Xe~ߨ/T NbgoU ^9:{ճ;%<ͻ.6<_YٞO5km|kK2Og 7i[da֭YpKz{q>7? +_|Q.[{KbO\/tXzK?UƘ6/ }9'.@: t@@:: tt@@:  t@@@@: t@@:  t@@@ο%iIENDB`gruff-0.6.0/assets/city_scene/sky/2000.png0000644000004100000410000000405112540054077020206 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx \eofv3nAKR#BcE,4i$ 5* `lRD!-AE+VRzliەen=-mAvڥKٙL2|T\-#At@@Att@At@AAt@@Att@At@AAt@@Att@Att@At@A*JR#~r4=e3t@29߽иhGt6/tg1jòo}ьB8W?vVt_N F8^6w)-) ChOzg^x:_ͺhGY:QV[#l>TUd4w,a~|+t !^Rvh|_/߽`w4DvWO_CSsoΙsh6@EϞGG]qӱ3tm{~[OjxG|I=3|7ξ{_ Ί߮%.켇FvD(/:GxTiOxɪp~wzadž/N[xiDuӖ75=a\ɕ|LU.GOb>>dFkc">^ +/_W};p3ِ9)Fևڃ-T뀠PYg疋gORsMtd1NB>n44[\Ú*r@;ч}nSf{Ϗtf>f\?ڽwϒ_|~⅍룻l:ZfJ'l~۝]==OXʽ<'Vnj9dt-F.<{Y-ә(ң_vwӪѺ?=72\e_XuG_'ؙ<ݞ<>t*#?SW^7aB}HWUN Ue>e{g9񯣛G${6kG/TN6>3h&>*ܺz4Ҵo]?1l<1_@'};[oն}_W/onIR'^_>gՎ;Oxk[KdV^qA+u_ώxm[$}̺+]JUܺ/=šiaG MkbOȍ?d->ޝ9+PS/QG]H|>xM;cMsa__X!dǞ۾ݵ: lnϠ:vrރ̽[/wt|W]3t^|#>.1OfۢQu Yt6ta ]8>l2z|ICtA>t@At@AAt@ β 8+PIENDB`gruff-0.6.0/assets/city_scene/sky/1700.png0000644000004100000410000000457112540054077020223 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe< IDATxil̞޵wc8M6pBBmMO ڤ-AQ>T Ej|RMTETIPh "!>`{;/Oz{+=\N't@A: t@A: @At: At@ :t@A: t@At: At0ƂnFuPü6,~ zo=G輻mY\O\erg>4~un=~Ҷi5j܄%gxuV>Z={KˆFϞ \>ko6wUy3\0s׻Nv̛\g':?k:Kd{| {PV#FYtA7F֊-}#}U':|;&:),A'b!T?/}SLL1b#K$ldew~W|~_H[e.I5HsvuZOgC*ÏG.YaO$ٷ]ݥg8zJ)j;}s=3vT-ld+ɚ'o6jT$;׹zF6@G =af$,F -X?6IJ\>j:S3#;pղ"r=uKoDǙO@UU㊩7$)wne߲+w 9⛱\b}$ܳCL=Mϖ8_/??u=,'NqF".mDwIgF3C/wpxʗ~:w@K&ϯ=+-=.6`e .%_ڦcy ƃ=<'9/~)e陹T7% 襑ZNIE/I"%DP{j@P-~|PCx^2z8$z ~H.ŐTnl 2C.9־wo[ׅc;G<h4&Uebt:'1I3j%a>w2p2p=h/VrifB2aYM1ZwrWL`L'UW/On^WZ ؃_eWwzW筕s@K(kUG7sϑMtGOvG~_솉bvAzOeY_bm^Vi@Xx(5Zߕ>iK*J}h,ػDǒ7ߥ/qaַ:sξޙtO9O^Sa N sEf7i a ;֬ZŁ+O~o.}b}[=h=k\&B @oNЯA~cʹCVu^RCzǷEwX[ai@oM +zOX^KM@oMn\h |q?@`_At@ :t@A1? .%EIENDB`gruff-0.6.0/assets/city_scene/sky/0800.png0000644000004100000410000000456512540054077020226 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe< IDATx{lkOs RbBuE26$?wQm$Ȗ8LYFl AP 7~U|a 3<ojޔՏֺɷv);F\.^ކNbʅ?ׂ-W>w1y!._k]9c1t(}EUy;g^Ys3qh7nm:V_o('FwٟxhVQ:|-A g`TLPDX3߭}rߨU tWv2٬2HgrNYtOw*+GL 9rf’ߩV t5_VT1Cyzq/eս75paIJj@Q=sgh `⁞[;]f[qU4Mam.Եzؑ?{t]Jէ>I7[:Yr8*牥]q?S T ՖLyϪ>m{\/4pw?,ݻ+gz+fVm?M_—Rx=t+;p:[njw|Ʒqj+j_.@$; ] VM)hڹa:J;bٷY$+[b׃@)w⟵R$һKLMJv_IH9rG pSTp~j< G=I %3\)+*/kՐbI|=p(ڈ O'rcoٱaoϠ@'~`DbTiLNLdId1#/p2aYUmP?i.-Y4YSsx,+bL*dž:HX~yRn@nR=?r.跭[Kע/At~k\o}w޾>1$!c1t9>}vO;l7% :o|;].{}sf*#=3w9<9w# l3t@@:  t@@@@: t@@:: ttbƍf?Q:T֙Vci6Շf{w-z BӪ^ oszo@ҶkkUU5}iK3 S\nj٤UҲѳf+mt#$#\.G/\>mX#g` ׮B\7|Mg':h:%~zۭ**gUz KFQ/nhڲ7s/u:?۵w1QWO~௬a ƖRwX}>[6ϝbzf@.lKS߯czߩ?}t9@/=o~e6 7R]V:!{d#L YCۮ6t\RPQSyja'w_&k[6jT$׹zF z}tI&ڕ_,FjXK$y].~=zsr:/$3褏[5q@wfYbdd/N a>ʷrIOuNqv_KCO{KR=}STmE>.R=W ǯҶFbڝI]-Ig.^UU+FߖTp0m%Ze Ǔ6>@ڜ~X.!gK_N:r]H M3̸/]Õ/ba@Lk+_X{=W[W늅.KPvYMm'SZѓNvۜ?@yJT[zdoxTo\AV||P^d5p3It^#mCRỹz(Sm>]r}w޶n[o ['@XјTWnit:'1I3 au;Y~6z@Fuւ ̾qzg,[vv,sѰxǼ)n=carf~UjU ыv_._)oD@j{jZR=]}?܋`^=hYF*>0b dtO|xW_jEku`pA Sɫ9@5>'=H;d^6E-[9/|;YkW=@ߞ@7P#u}>]N[X/kN胡n h ⠿~:'L@:: tt@@:  t@@@@: t@|WR%vXIENDB`gruff-0.6.0/assets/city_scene/sky/1400.png0000644000004100000410000000460012540054077020211 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe< IDATxilTY=gcpm"ᤅnPST I[|%AjFj4JV- DB0-,1Lϙj ?Xws1٬d@@@: t@a4-Mա5ϴʸ:nA[,xn[2{כWU9*y{>vkOyߺ.oZVQU&(YW?͵aߦMYhլ-[={'ٟf ҃fl=mX=/3ـ?Z嘿lmg/ķ8?o:%xzۭ**kUzt?Ѝ*[Z޴u]{ng_^}#vpjcgVС_Z!`| n\zg3k)g:1 dS~ĺ$9-Y?ӧ*&ŧCϗ&F2תlF4qV?B|xLlR?;WTP};"=Á+2j@cs3rT.j+ǯAS\5H2pD z蹋\V=-Cc\=a$pNjGY 5ͦ.Wμ9eo}tǬ`|ʸmݙdlу8ɇ*׆ja&<\fֺ]:yk~MQz4,MlDJùbHC=SE_k @ؕ_޾`݈O:xQ3cpVe5I:qp ϝhAL*.X"=@xJmNxghA Sw3Et%No WEqOMө/]**^;(WW=G{;p ^t[: i'C_hzyO*ٷmsR))?Pm鞹T5r@Gq$~橖JP$#%D< +yxPmLJ z0 :F_g8@'7خޞQ5őh]ӶǾsm9>ۖ>|$$)SnSWD"wUX"nޤ}{ij`|0YNNhTCQU;/]>q%~pگ=j*t^t"wz5\1 T NbgF ^>2bKU:%7=Ҫy~U}V~); qk}0ԍQmX_r?@"1@:  t@@@@: t@@:: tt@@:  t@@@:: tt@v+#% IENDB`gruff-0.6.0/assets/city_scene/sky/0200.png0000644000004100000410000000404712540054077020213 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx \eofv3nۥ--R)XkG `bB$H< jT@PؤB6B ZR@@RZ7lWmnh^/23dKkRr9't@AAt@@AttT#RT#~wuA^3tɗXK FkR>AĈy6Lػrrcp/eg=<_=Tyf:steV=s'U(vݽmg͞Bbܲ'naodҵb+F<* ltA(6?{gȝp\ż~t\Wl-l])[rĺƍ/J{sS>L{;ΑWU}TAC}U.]?<;\c5<>9 Chx:R/VKOo_gu.u֨ɧ]F>t#|OmygPU]??o5+/{d4 ;q7aȉ[v߻wξAgw4G|: z<{3os5sommsGXno޶_wϷ;K#A2A7'Dͳ.9k7Bo{[u[~~cOEvDQRt?M|LDM4L=}¥ݭo6|LhQ7;o֣ݖ㴵%ާuܰt5Tdr!?(B& 6_ @ЩsO_t&#'Vs=jSYgKfM=rb|RYs/5 " TL}fN:my'pgjƆsv.z՗זvuPC8 ABr7N;)RI{wvvτ>mݽ{ͨkV%/:s)d$ii]奮e+6Ÿo5pp},ȁOqr4|I }st?>=>!k=tmc$u#At[@9>l2" :GJG: : :: : : :: : : : ::  %IENDB`gruff-0.6.0/assets/city_scene/sky/0400.png0000644000004100000410000000404312540054077020211 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx}l@)0t.W%K机dY\$.l6uM.4ƙ) nnNh"Z:KR }=Ou{`O]k{yk\.6 :: : : :: : : q M*xVF1y|_rF >|y%?xymZ G̳[;S\_]37z-'o[W_xr˪:mžޏEm*1# y&r¡cd7|W*y֧}&zXj[5OrɴI5 }w/ilyW,O~! {1܊\;ԉ7f2RЉϔDŽ]CƷ~u N׏ zM]ݽku4e2ӧNzr?67# CτzA?+Q=Nd:W#sǍv`#s1`5t^z:_5j qףQ}*(+y{G2|_g4-^u'*G}΅&ܾ!Zm]=} M}E?vG#ޟ?x AHh̼~W~t m*7r{,{~_O/Nze wOɂysNpokY񿷻smK8';T=~9L`z{w#F -{ g\| vo+^\vdc=hݸm L_sR%@*cF12\x1 /z㗯PW}|ҙlȏrCvD!m'~Ms͘ZsU4j'E!nV74}慆])';_we>f|?޳o_~<6^jWhrIw?O8𼳫^W>ĊK-NWFnɭg_Y7*uq{gkt:_o iehAύ+VmټV޶їy4v%'!O+~2s?wٵ'ևtUiT*{ll>zF4J"7a`c-zrPb>)k볇NzҴm8>!j<>0@}Z=ohӾW7SOj.YgԎYZx]ߌFs*/8~}Gmں#ķ/YYw%Kc+n?%q^M05b'FM Ͼ,bNVP'*21uaz8ޅ$'4o_3}CvܙϹkK zeī]xtr/m?WrS>հxW{|+`7;~L ^8x`AU/=ҽass˽wvܺ MkA?z Y9u|-y|YZC{kŪ-;M[qc8NЇ:Q>OGhL) }[<>=|H7ta$A?>l2IoztGU: :: : : :: : GJ nqQIENDB`gruff-0.6.0/assets/city_scene/grass/0000755000004100000410000000000012540054077017430 5ustar www-datawww-datagruff-0.6.0/assets/city_scene/grass/default.png0000644000004100000410000001415212540054077021565 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx'qǫy=XͮY?:%+Q? ~ ;(vb.5Kz"Eiߜ==UB:@ PB(t@PB:(t@ P:(t PB:(t@ B:@ PB(t@ B:@ PB(t@PB:(t@ P:(t PB:(t@ B:@ \?$bYEw(yL˞zl̿絬Gzڔwz[bio>aJt;&vK^f} KNeoQ3:VM555)[ 77=]>}:<:e:KQhpRm,5MPInOzݾה#z.R֗u}YϹoI/Kzz=LyJu9u=a=q~hJĥ0|_ͪz}=|_׳c_걲BןiMߺ^kK|3ֿ}rO=}.}Rg22hky~ 䣫]-~F qk9h뙽w)52(.@/ g˯=2>ٵYӶteޛ~~.NA$Dz>CA"fύ Be_P{ڝXM 4fOQ0y {\!~XhOIR`{IR\[Qv4/׊VcGAS)Z更v}*YqMwXŜKkHkEG}ܦپOM,7d#Ban XǠZ[k:_ȼbfͥsl5Jy| e%A(wlZϻi?+v&FZY^qH;L eVc *|M#ʬR=zOzʻ~,ռ|g>/,؟\}wvw(Ŭ:c=Θ!_cMEBX#GhXV4>} )oI{YFŽF݆.! ge羦7]mߞR|zF a-΃8y$^YʍFa3k ofh .9b*+w Zے/Qݣظ7T=Mԧz§Dן&#).*]k}b{[BdFx>׏x̫Ysxŏ>n:K;oF[9vl{FwިCezkFOE=-k1(=GzP쵓w~w7e Oz0z솟/IY/ldTGŅߛMyCׯuc/m3Idd6̨^>su阗h<۷&r9qW7䅢3{>(fZ ^.wdqCr"KTrvi, %1;'F;T4 `-LL!2H^enٔ;KBZ*L3*3Z# 6yS$wkq09yNO՝XپǮBWƮ#u}aÞSU4X0; &o|/;Jc:ߒBG_;C?H2הkƯ鷺_Ӄw][DI/DlS{]:㽧HBjНm(Ԑ<wbKQBЍӚ'w-=,RįdQmn[gd+zw_%}ᾧDߟ<=G4K_Sw8haʠMc9q7X$H-iOuy*kϵO !u4BLJTQľy^Q"lyFDr$iŎ%o_,$ebl#0DvwM7(=m5c'2߯+\}|pZ^ұD{+膤_Froݚ֧ 0vx3z~6DE(u@Loo_WX^Z:k'ܸ7똳~~ȢXKGmlᴓRĜa~EitM¾uZDwɪ(C#iQ_j2Ǡ$ʑE['} W2%Zm'U'AQJq 9ITm}i.CJ2:pM\$ʮבqnIu>! 5-nh'0h~V*D,)pO$N+0{__莟o]ӿ;m5-eo4e) )Xj󥖣~#2\9=+G/uw*[\cʻ;F1u:~zQidaj3I Jʫ[fx&ݫnb/ԋ:2[w ʌ(|m]HmhFX>:ckX`6›LJͳa#oܷ)t-Q}x˼cy3"^vYxY/2~Y/ %Dct=O5zM O㘹a1<9(7gL@{PZ}c_2\.㥥+㯯J'DPKʘЦ Gef7ԭ47XY U9Xg$UM6FL[|3  )K֖&^0K1MAo}u :saFvhd0$}oܹqZK?ٯ;_wCiփqoq&<=[}!I3:߈1r%7J\YGO eN3[pkK_ԍi,s?Zy rr$+Yeg>UV"/DDa]?"eއ-ON9DKfZ_^ӡ!.L@,.[I)w^,SbiSz_ [*8fCC4p&'pB |804i._Ee,㘹lOo/ۺzi鬉p_ڢC k Tm~mqqR=9¦?~SVT'(=co6&d?^IQ7ElCo8kmzŗ-+C$䕎2;|m̼[ޤ4=N(Fx5|9$z6qDz 31"U#(زMj*Z`~鱗z;fȢ3`Z\dd/Lxá!b3h/*vl]8v_ܨF[U,i_Z^u5;Kt]czA%qm艉r7uvhnM4z6cw 2)U8.MUT ?]"aT^-P7/*GMZ\,S~E_)җ +f?N7] #>]k Jqh-rFD|PU^2MV%)ƻĶK'ӱ놈 SM+0j{~(uduV\d~DwDu76*'M9r8؃ Hp6D٤{Y8ah5/RyӶLZ^8,kzDzz%ʣJAZmUxק5ƶiɤ{ޛb t870[]\&Mc[3Z:qQyBk9!f'e-P[՝z6LY b&]fD8㵓TL\͋saNW6Y.?ik51_N~K< ߒ=ryX}˞4[4tjH:n7,2~Np^jݒ݄`xa(1THsXlNKd&hU=8fg6yY}n < He nNgsV@T߰1+7/<\CAFqWK#"Eb}- POEA2=j?7QNJQB|:FuI#COYɧ-sati?&>`mݘ&-י6(DҧZzʏܨ!=Nu[~VQ1O0#̽ 8ᤤ_RqvjyO+.*)m GF[P 3S!%`鱄|߭rlzlv6x-3c,;e+%L h]];~ޭECA/>vM)gG2dHb( #/h^_XSϊi;2Sii4-W7nK0Z;b,1(G S23;ʹ|xNcE!uV7mP.4h냭ӯi1}͏7%'m83uU4Y 04AR\>#Z0,k~uoȦo;JHٴLV+Kfsj DHn'N) cLLw-+6hmMhBT3[XMO5uI{[\$L&$.=830B`PLtw BZo"DPq@ASDU=0PDdC~Yj1ָsm ?; +m9ޗaq?c??굿'&`-q<~PeY^y>fmܷ 'BrǮ#{uS°Jq8XJ?Zhi5#j#-Dw IENDB`gruff-0.6.0/assets/city_scene/haze/0000755000004100000410000000000012540054077017240 5ustar www-datawww-datagruff-0.6.0/assets/city_scene/haze/true.png0000644000004100000410000005266712540054077020745 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<UIIDATxڬ} -ǎ1{ ^E`d1 oSL om+ϥ?g??S|>~?N~~N .WFngS{\jϿr3"}07w9VXNԶY{Lb1?V-daZ/wԾ /(V|>P/|\Ng}Pl/?b3R~c <.r=w\?kcx8"}D'IR~~ t 3\nX98&o(WTђEEnscw> NB?ڥ \k$&vwE}glq o{{lYCw1\yn||DL} BQݳR6R%.T}@_Lx ŭ@$=+/1^J\{ ¡)\lzп?]/{ l=S +AdHKUq`* {袱3ST:wٚ|s nS_܈&w~7֦pIik>l8&QÑ =pa4*;< $HqO^b~}J%7P"0ϏGz$;H#ŚvEʾ\hіP=xJ"DW,FNkCKP1>{VņMA~Ou N]Ե)98ȉb|6,˥7Sԇj0e=TQ N_@|M$Yb~l+r 3Gٯ>BLI+m^ť_p1+ ksDHcWjuRtJ>T[tSGF@rrW R_b$h9 ^GN e^@IbZ 8P3V=!|:PF6^P@kHt&\#I{d#CPwx2Wң2Ltko8}a\' u'|4u 7TW!٦kn58` ?<cj)^ aJ٤J[=!uWҭP3|HS&W9ɪBǶ~ UALlfǟ%vV=Z@gCm, )'d]d~]gb'*0OHD.!p7={2raܹ\"6X D7*#/t0<7\w/dYl&Dv8/.z*E|KB FWghPܸLuO 43|g~v2*hӊ2 0"Z?\8j\%Z;BbGPa:/+ T dl{AW;7e=4 z#cd?rv2zixgx @@{nRdP;&2\o.|$^z*fɉ9\^jIcYM@&.M_[AtZP+k},5kX4KU.B3xW=h7u^{%}Wr&+=L#A]a<^0bccu(WpE2JJ7cGm;:¥^ HD@z(R@WRAY_! 앂v-D2b*6n#s₆>A%Y/88xReFa ԗ h",j LtckjE\J IuC  P{>.J| V\`0ły( bxʙ8OȌ.8a#pA'uK}w0u,*pť@@?iKKPu 8#}z3m)0u4UR, ܃L$SݨI*,d( #Q5䳽.x j٧ Rr7?pڈԔ?|x| ' TP(:Sf;Mq 2p}= L ?ʋWNDA5~/f׋nRT*B`I1ʱB\ ]|=/8sE*laIa!TDxp0Mp={8Эm$Yk6c0+ho802)82__,U#Cf3q`a#Iwy 0:M2TI͗= ń(]T,?xP59``YL@t0.% \!t`/(U[K,0B`0qK`Hl.=Aj|-ݝer^xL#ILje$]f Y\cSryа*66Nqa0u* KJY@O"9R JCM]2nuh(LF^uMfm5#ZJx]o@1ŝUy_hx1Cddjc*~+w噒}.iFd¹EՔ6g'!%DƳ͒Rc{Ƿ9 4y6te~yHD:jL3}-њF4E[\PḾ bzeY;%Ps?ܐIɅNRQ''([Ϙ꺋Xn}^ira(XvcJUiKtQAJH5:T;1,hAt] Cfj׉+j_p=* MBXNs!9t0hw =F Mh%E مݚH3Op@?,ZuEQuNVozL|ϲ*(~*Juݬن?K]ͼ1@B/O{#%mڦ-.G',y8Cj@-a!}m9$ |@wTZ*1}bU ձ{ǣkU@=ȁz"M`иS;iIU,ƸF1TTN#u͝n>%`ՋuCc6iv%L0AX `gMd:8ٍuM||prL=!b!yeDǜao &쨔>9ܼ2X$4E*FhNp_C/&S_B-h! ; q9_n" 8@?:$Cͥgnx񺹭T(IVuʄmgDs>gt~tկ ĽL U5j QBFH ]TlGujL3\EBy<! C6IgE=Ұ>%Uy5-5X_V.gXv']%. r@ cuSřQ42tV"w;-n5_S% :VܗLĊ%7r&.4lՀ,Ueá:H.xÙ3hvQPY)T낌+06fMM1*:X|Ek1P  S@ uMz[%CJhRyA sCt" vB8 o\T`nwZ )QOf>7 WIT1%,Iǝ&9NrGVrQyQdBlE$T;BԡV 6`)BlJn ET$^l8rS2XL*Zdm:u+m9[Xew|NRUGzmJ>\f-Y.xc͢S`ť ݩ1!IH_BN]pZU߉zN&d/a`)c )&s0)bLҨHu@>/:T}9GH64IɡMRU?nkum$ 짂^ 6NFpR'b ;tTץ_̺;K_KgkW]P:7&YlmWA$O!ұm!rb7l$/b.J)aGk>| 6rz|zV0_L%DtwƉ͙_áV +P´L2pgVPذP&Ѕ ]yL%el١ t 7 Ry.Q0)p#9O6qT[vc7);m ws'Lӥ~AP NVch(UA0?%Wҧﰪ;~WP%0hB:Ӆ-A{Ɍ>$ن`A1i8=Z٧:EV6R s@Ar *t=^{צ^H"V& %uq6V\la@IDʞGv]UCJy<T}E$wԙa*XTrMh;I O6=A)zr)*t3$vߊ[臃3[5Õ/bibf,A\L` ΁~z)Bg!7LU- nW%"r^Pj'I#lPmJ]W1F J~Ua,nS}2`y__csL|sǴWpL&!5$ ge%%^B4L4D]&/~ 1an4JXg}ѹ9©nUR O]XPgwu^e/2u }rfU jigszJ `>gTCõ,U NKIW?mp3s/UzW{PsⰂJ]>+IK`fYS ,)R_9h[,Xln&QTp}Z,|Fs{~o$) &IS\.B˦ N"b2:v#ήRБCu^|*T2L ێtlM}/.L}&]|=/8sE.ljq5֮TY9{ %|BG`T-~$:IN [dשׁ*$Dr 5r%SxMc~8?@̓U.^OiN:nX6 - xP595`ē Q@qIi:Hzϸ]r%'w3B.V_P)ූX`<ĕJ$!s>!;ڃvVC0e!Őj~?-v.׏cLU<:+y̐w]-(l1}jPUJظYn)rbIE.q G`83J !q& ɶTV>ZA ّ+ɾ8`0tN*/4}6昂I+˕<1,uekc* +w噲}.yFdCBrn Gs@-H4M\Uiv]CS'h>P{;$cR@͕sC"#x:Xo[GPrNImnqņ nzSfӪ>ka^a&_BНd/':UUʳ! ! VhY9dfvJ ߣ$씈8H›Ľ6 æCvngxX':CORɂ} MriXů (g8$ |@w񰗉,pLXUBu(Z=MyCssuS1D.,%UWPRW2ӗ`MݳHI8$h>UEMҗ1>F Fmc]PAљj!h 46 |d7E9Xt.U&n+1As؜ <7p杇:%)HWq}4B3>sS=P3 P| ̀MC_nUhRL,Ń/zuL I0KВuS[/. -8cRôW h|#ީ)@/ueԜhd@_uZX@@@ ,8d͝PEq&~T45pX(NoFkŠfbpHO55P--l%_/iŅFγX`e{[ee!H]T6yΤ\%.eyga+J3&YJT\sL(::zVWs6%VD(y,vTrZ 4c)QrRL7?Nj@e ׭xmuAd啌DPY3B妪 Ie]XJ  S@$%nq08?T@5P ֋9UqRQd8 *,%dkQ:OxQKXƚu T{)hʕzCt_6\&6%>89m@tLm Zq~$|PDC%ǟ=YG8ֿl 9 AgTR+dp6tz>Z^Ҧ jШ,4SzPh`3Q՞3y~%l4#ă8o CŶ8W9Wb@.eu˰?nX0|kxDBRrh-cQȫUqt6"De}泊7_ tE<>>`0g4W&_no Dr[<ot Kgl2 _1]In ]q5=bse~_^TPpju'2b&X Uĺǰ|G4; \'Ó%ԭ %7>UIFϱyUU%tX%|9pp)BCt ]$USt^JcoM.DChT6ڕ"E%[eu*37sYJDtrRxR c<_|͞i_ ΎK"1>T}^PnibD*F!E_qBElvz\Q\Zq莻S.mH_vY􌉇HЉܰ|#@*)S䫼p2㔸b/.F5ċǭ#7<=Qqjujww=F5ҌsOc:w|xU)1^h ԅAFX$,ee LB{zw(8!rgnk ,9r'zgM1;9 /7ƪ YB.&3m3Й6$nHK-&RW~ݦ*(p!PQ .Z6Lb[foe]mԴ6<,"u([r`DGqj-ثGhf'F0Bl)0#9N6QT[r?TW> e.=~8VTl8sV/(j-@$Fi&zWgW_/=oʽ `85JXkC_LmFeolOKtLgvU%*N?F cGT2s4AhGQ`⦓h :)QX("~Zi Йk%uʹ#[W%=×ZS-^a ;Uawi7|ŃNBxf]F͠a@FIkxu\ϝJ=3?8؍+*xx%%S>=褗1!P Wq[g.)8})o+)R.)W`̴;u3+--&G͖[qcŻY:dל"X`S)w7`oh s"acZIYWm~?Hͅa4DG~HXE*/xk 1$+,Ve]rd T s%d.]> ,;Mo$QIjuޘм_C' P|I Q5g>x>Owjp n/VvmTF)q'rCm\g3$T%bH/c>}+p0b nSaVp8B˷9^-LW3`_Zf ʏz:m`>raVB}Hdhh:[G4))W%ן {B\E8~8ZLLbLv\&R*앚r jM2JJ"z "9Mi$/F1t+iHC/Ww4 WJQAFkqIt;TV- , \$L u!kZ@u+QQΎ%Nm%CfmR0Ǡ^u1^EW YBc,۽W~A!ɺ̉ek(G Q(unBCSxU|7470SYz^7 6~t0</UYR^$$^ dˡ=Z;IqedfS꒼}12VH&#/wU[Q cq V\ Zn-]gչN0YpL) 3/'*"'RixC@^XW]ZZDRM 4%yڧ܆j4;vv3vvJV&H-\ {1ܐHɅQ'2Uadt*$NZn-4IGxiKt䵠TC΂T/JP-X!<)9Lզ}v 0E5 e!;&""&so Ȱ)z吺޽-։">9ДTG`yBS_:Xk ;vⶪ]:r e׮ֻfi2ݺ ~ I}yI9Pĉް(Ped䣯[ MIgO FsN~Vh=h|y(UGXt,/ 9֪ls!"_E ra?'-(SpmGJx"Ŭ7_o.PuCc6iv%61y;lY9I\$adk/3)圱 * ܕ蘳q9$r4:w^딄h]E\̉Nycβr NG^oH*(*pgr+x;aיGq41Rb! 37x֋KBlc 0֘T0U&l=$349݈wC_@~e gO:QSL\' t2BNʿ8et?S7,['7^waEW08}T=Q'^4s[@8lp-12bf2`k*|фB6ՌF*qǡ^NHU]Q7nTMz "Yq%e4%SL( <BTj:~GR4&] 7맔=)W1NS/a>fX1m̔M bVqbI$Fg -5VWH՟:H0A0lp|"Tu _#֒M|AL~<;̚o(\i^֞oDS’t<h1q}+[T9((bz_!y6Nzm!]Pp@D!w}|4.ӐQ o y1FofD'ާC.?%>!ŤA`Ɂ?^7ݖ%<{%N smT6oyJvulC S@x*W$$ίW@Z?j9awi[jJW˿"|'v9В zKML!&C7̡15+"%rGkpq~LS]r-R 6}oQqL\q2ѣStlh 7  lNf>]Ee6quԵh1{F'*Nm:|CWo\v%R⟝?HrJ :`2CjGCu$+KN̉s[] ɐ풳))phƑQ_  ~Mw;]!YM7^nO.GhiDO!+ Z4͑f=ꪶ^γ!B'#Pt"& 5>|T}g>=+9{b8@ HUɵl2oL|u('B 0$ _L0B.REF4ml,9ǶQ #Jf'F2BĢp#IwMvV)]8N|k,`)a'Ý !TcUQM'ez+14*4?%Wҧﰎ\ uXAį(Й.l BKfD?z6,9~&/WT\ >!Fx#y*LG9T﯒2#hRwc`0qI̅Z(L:m9.|Z`I]n8E͸#[7E03']aVƙQޭPnPgvP.#bP67h;I O6=A)zr)*t3$vߊwpLz#h!8d1&4d.z'V`5}"=މM3MճX7TD!;wErpfZ흺!|f٭Ye[ Yyf|'dLM؈3u{c){l@e1vq֜Sf :z?0C)J}NQ! 1$fT_*oO~"\sx9&.!ď¨nmDp7n_8?s P5Up}GW}a!CjL{?l2H=ed+lۉn.Z^*漰ka6F}#|ܺaةiZ^%1rPtH۠b}DyP9)Bim+G!M luoKLaaij8φVNAnVs0CF~)_vq2\zTɡ8mJ#ya5kM\'~SRxl+^gK.>,$ :Wǎnn.]Ȟ hAq$QoÕqj+nXA0S,X `Wң^Zr;CYEM|bj ~PnZ6gLd6srǙDHD́ي&73V*Df: cPMYb~ @\E8.۬UhCFsU$7Jv *z~&XNȡHq)2S*WurxZ5fGsC#$[>+,qIt;TV) ܈wqArUu e6*$fیt:<wKxÙtTUuD1A@J|4ƃcA.ƙLY Ęw⡶%|566: }.ɫPIg 3HQ ^O6)q@cP",1mTs+; d]DurrtYL$4 $BV>{臓3[5Õbif0Ar03B. 3Ð*KXӋq7«alm/EWF66.ɫ#cd?rW%Uժ0 Roo01칥ӛ̗:{L{wL\Ρo[0203\ADj0+f iT/]i* DCTei*z3F㨄DqBl֍Ntkߨjx  T ;?s+.x{W9]*3R :w]C̣.+mݪ`wRЃQ ֒T58-%]i ͌ZTOTU@͉ ^tS+Bt{,44a3Ы0x#oePtRB,57 (P*8>@->y%n9N؛$7^?ZcV#ك3[ʘ&u~iKIJIan@a`jDžp5S91~M4SnSΜAG ]A Yg!诣\iE8-ACeO~gj4;vA},eΫDS~ǃ9Ω%w0UK OA֯JNtWf]`W9So-x++HC R|BvT2zaB!~6[\&1 EǖxBuV2ט!ԯ9Z4 QX;E Ԡ(7UOuqbIE.q~֬ ˚230% SҐ8|hR4lb_ JrPΠUD*/4}6昂I+˕<1,u%kc* +w噲}.yFd¹.]A9$C4@)_h$^`iwKy8)4ʽQϳ|:~<֭H ھh=My.tӧO{ S|N| %0I%֭>:Py\W 4J \m!q&{*.4S TIrɵF[0`/|pڗH kA NdnjBre/JP/X!f*琙u"*W.~ HS""qo '(Ft)Jz6%E u:2ֱ_Py9r 5̡?"]Akwjoeu g;>lV[C+Oɸ"P-a8#;%%<g?VM%,6$$?721<?\PꏰX;X3H]Eqa?,(S pmGJx!AŬ7_oEҗ1>F FO+L 2#(>S O ggMe:8ٍuMq4+ oɆ[JtapfFWp:ܼ2X4E*FhgNtvf8C2ASZ?1zCRQpT?ts |UM F:wx(G8!P@ܫd@_uZ@@@,8d͝PEq&~T4oX(NoFkŠfapL zL}QskXsVšጇ {o!i _f=5#͙ĝ6L<83&YJTnG>\. MOG+]CabUɂ9j@b2FIlD{͏6Pj5+Bc~>dz@d啌DPY3sB妪 Ieo]XJ  S@ $%nq08?up bBy?FNUT#"g+K n>dԤ)^sVf]4C mF)MRLU+?l>a-{9eb_⃓VjtMǺԖ zGv.1U8__:)pge)ϡTB?Z$C<ӓv6MPFfϞΠI׃BqCDT+d ]%yS$$*ιrm/[9*tPva/ ^ nUHY\fR=dWK'&loumEHωJ goFT yPE}<|2hWa<\i+mӵ>YL' Dyd*ʱb \;ӥPjzİg(HTPpiu'2Z&X Uĺǰ|G4; \'/|c m $.jq.7.J{9Jf" )TY:tiKG@ER8EW85d=dB>8LAesA]I)XTU^7Rk?s9=MIW ''0ƩcJ%2}$?L;;J,8,>Ry! s#t vB8l^:UǃuZ"oR@D˷pWѠgx֩JlU ݀hjj   vH4{S[>:`= SŤiLÝoAM[G~ehd 놤5ϝ2 wQᩚ˵?;9r{g?k*ZHBui$ѝ; w e5wGEC*}ϡ[*M(N |b$As2hLɮywj'qsL`+@jc5 ;qo,*]/B`ةdBK6aҊ.qvD|@ƕ7*C;ȠfiT$T'@)XJTELJmRΔY&5M*RyHiVhK c[^CoPQk|7 F]d-T%3ܴ{>ȗ+#Ո@?$i*"]F ; gL4DNDzA,Qa}/ 녓ĥmG;xq ,5oumm'Z F=ne鉊S+--3ഽ{ij;t:Kr 5%R4b\AFXx$9 e+0 IYzѡ%E7 Ҵ"#^{r)o??<k7|Op?]~V뛀ް\Oc~^zS} ~r3~.y6}Am`Vq+~-z;o'Yɳ{n=W/B+n 3z>#˺O` %}&[փ|FO sjv5l>ycd~YAX_d"fo(eO[e;HI[{uOX"kn .W~_:0Ty/{2¶BgdS/[<0 c⩟>HdC >&Bbel>Pn/u@w"luO&j%wy{q03Uupm2suK\9ԧ8 YhmY?ּkfSAܟbnه=pGkECzKW?)4οZRzN};O{P(٤z>юY$jEV՗Á׭Xv3}̎_2fQ\ө;&zzb!YۣIZ%lCy џ"7}ڄ}v_kk"n[Ξ;\9&}9C}WOxJx_ο҃Q2s1c3٘\+1NG{=ߓӴe{L^.u ]q1 gٝg#Xi~FF5v3K`[zكa#y3һ3MlB/LgK'irV~ uc3ċ8(P>q0wBVr#)w RvwBq<b}@d 3ACِ>?CYR<6ݴP/(CF [aF@UPѾJ@z;Cw_d!!lSGT5nxo=GӑmֆF| LDЕVfqz "YL @vr"#}=,qQ%5maz6*BylT% $e0BVT~Z !w9jD+ KiKG򮼣?Kڻa& 1Iz}1!1hT:BǞeF~5w=wSz0q] lOmgzAJƗnWthZѱrC{;iw vؒN x4Ɔ\pd !8?m ۆ)BFHcˤmc=܇/€=0)^bo6Ԗmj&ZPTNX[Z<2/r}WKCC\ V [$:l8n (b,\ P{W<-*`}۩kfPĪJdӊĩYFn-2Aqjۂ)u\+< D_ ]&5ſv+:6Bmtnm⵳eO}~WA+cIYm^EqGe 4NRV{QĴ-}e-ǰ˾+4AJ{"g|1*>'McEۏ/I7 V0zʧg'bGc(Q[RqZN 騈.1 t&hw2zv11a>#j'M~e;aKV0M/dl]; H\\"cZhYƈ-¡aEj0 ʎb;qe2[ (״%YWDI%yM/E8#V86oBؽ HRYv;s2QRW{ّ򜛦pxv\grnK9*{fn1؛5e`)B[x0ZʅϠ c ~ zgMw4ԕu+ط#t:L$B ܝOeIQXa(<>Pzϯ?AP*Xms56skEY:Yl!i߶_㉕~oNd.W<3L~jl]TOߧzMXԚ(ٌu(mKb2y@<~m(2kviVF`Sh0' ?Y]R=lhڴ%hS6,{Zəna]N4b5Z'VTlUCQ\J(#KQNf;Ы׈ PS@Ec  98Ep ~ STӖ2r֮҆2Z)NcY<(,4ٺ!aŸfwW 6b,:5A}238*l%?X% >eOs?1Dj5N5HU$$VIomKj1S b| jhS65N[s"] pfF~hEdh:{/hϵ}ee m3=7?pMt}G5De]Q^}CQzdjrRrȆ鞓 d03t#u$eVVAi$TW}RnA1tDqRIp/:,"㫶,EUuCt7ȹkTʆRtzW gq{e[3 h7D|B9W)9%^uUjAn\Z`b :{UI"P(2OQ>ʲr횶lӗ 3JokL_nM9g]!T"E60.?eIqup'QL1Eؽݴ1;2MOQz`RA/ GLy<";%t<#*c13֎/CNl0<321NjvR rۍ)C~s>L#l ahŘOӆ0{J:َDh*~2 xAg^>^La'Ĵ&4\'a7!ޝ|/#y:.qazȵ*p0r;ZT/GݏoښPys2ZEb`qifzdKH1<^<,M պ0DKL;FVStEUQ<t5W5\_qkmчm2!.(VsSA<&e7J5Sa&hEb@S4*E)JG9@vtN4GS3˄`Pm;R"ȸMB6!esVO{jԕE7ETR OdeݡTMU!I{HĕEzNVN!F<07)KUr@ej%mZ nk\&vBIӳm r3 %YL8m@z{P3쁌СL u_qҟQq*K{vʝ0@F@~0 6#*2RЋYM؏~ iv Mg,7}ʶ8#}̑rl,b*Nх]ToJzJD2%R#"JyIaI"Jx fEV øN,\S:"FX'©31뾦S\y߼>]J5?8#'/16)ҠҋG9 6qF؀flLl"}!x,ƹbiW갇 Ԗ\T'?';"EPNRmPfUme9SrTD]pI'zcexV7VW(5򣜹BoW#1ؾ2|]kq?Y|y, 7#Bho, XيE)a⮤F>_¨mq! 4xIGMUS[tVJA 3+YcXο$ 53E3?WE |>Û_)f XK"֜[M@ݡƣOJmÆ.Hcb"'<6g55#,җ^M(z@uUHb.dz%=);qh NKE"tX2CcaC4 *s|𭐲ICI-<051`j`4>EDs}a/ CG(&>A #Eɐ"TKtxZMuEk1L3WV& N1_'_^:)+M U,1Bt.}@K6S&N <<ڄ[;Eh?vV@1ta[ VIR럵6?IL2鏷:,*%@W/a3Iتs'İkb' D4*8V;0QU/4T@ "p2E6.ȩ~ $P aa.x쿖c6CtF\UEn0M"F|;+֝wz, M6k!Wtt0bI8vRQ5h1mVD T*}nZ0EFi p5qY%$œ!)s"&VE6RerPT~#nꬬ.O-͙T;iukɶ=fމ"„$? s8!njq6M;h^ "mIqd@􌤔'펴?6 e9 ܌$EvmV6\BKxNǺV U􋦙Y_]m 5i3anـ5Q|@d HE*bMfMMk~WͲ}xv,Jg˛NS2n- .m\r].Ճ6?$E#?bf1R>6Tzۤ ξ)#֥t{ b,J ZJ]./?BOVN'TRő, BQn(<#u 7F ]ICT ?heQ@Y7a^ZC+Ek6G+7 7s6P:P˧P&H7"Z\6qK-g SĦ9!yqޱn #B ՜qP;uO`9Gt;,;~=l q]L;Z;@=ָ[. mR%x~c+sك$C-782S噽fe:6(v8H5ARY=u'Rwy^ݮ-li'ͦQ'ҏ{pMVڙVfe܍zXneψEG??9͈ En߸WZ%Qp=TFEsU"*e8T-o6ZhAڝa+r):R`LIa yMS4Ø_6w%*BMkeը4qrJk^<27\|:MU@0lL#6`?,9o ,UP S35^x9pЅLM8lT5djE Q諰8Iga rPduBc**8JhC] m6jQPpbc.bʠ++PZ;a~7O8٦rp9?(|tICn50GQ]$|h "jDԻ?csbDmҲ:Y!h~%O#4):3U"!؂8FZ7?]M+>NUXc`wy\ M)/h3NVPZjt*oT17\~nj\:zѹヽWZb1QCQK`8#Oz0tPi3R//{64` KhY8A1~^",Zf`PRTc%}la0WcIgx4ч~7^6sJD+#(xfL{lq ,CD#sZhKn^"@yYHn o1l7=3<7|hǬWCx#췼:_,QE b,S_fx{-_|ER7wj}ђd.p*=WkiAN=BԺ/հم6I*fTpuqG˼l )LNo#3R5r*~!H`mFf ηJip{U!X1 lS>ѪZɛ'v+,в*{`[-0ڗoQ3X#_!gm/Uo{J1WaX-bUnB%wAäGM8=F#li"I&՝P|4T*;gۺWӽ>ߕf>[rfm[/#7r9}Ɠ@΢6p?Xj*Bx^eG5\0e?g1R@#b9l䚚g>S F^+d`2`OQϡʞv@+$_bm.B`Ԋ ΂g=҅pp%gJo|u5c1#8Oj%E]OI=Y _3L5Eؾԑ>3{`mwT5 ([Y?8rߒ8-xK3U b*É{]|F=ٞV;-+Nj?id~R wTgeFS'cH5c"pʀ4wDg_7Bm"A2iNt#K \yo/yqk#Jg`bch1Uu:-k[8dzMU씗{-{!"2׺}Rߏ)`T XUj׉ yaV F=YYuC@ވ`rǐ;kA .Vz [*[WhKt~W> $4І|c݇+y!/dŻ$"p`6p^N|'5_g-`ƅymQ_@~^L)"6w5$&hR@(upsy0{dF*e)YqTfo&?Qh@'be5]E(T^igy=("6eVcxݰ!Ӏ>^@zdXRb֟1uZ!G,jVMw6#듚Yc0[yK6Ync$1d5n ];\N)Ur^H~ S@$'D-Vª+ITs fOmft!ZYCTjܙtQ>wݫȼ8$|\Uؼ%"SN}zAčWpH9imZfZIbYI}cs^:2CczE[HĚOVquN ԋU"myF&)RYʺr?[5GɢK84rBek2eg G xSjh.Sl&pxXǨ_6zkk纊Bڬih{;YnY hKGT֭9E?.wqۃ∪Vs'.NA=PΒ],D4ŚƊ{يO%0>FV)@` i!Vע0gT ]F=dh^ VqVڥt+͉!4PKgF5Tje6ŒάFmƫS#΁f`P׷҆>ߗ^`фO{A"&˰t)N YX'N%I' X)\\":/FɿIK63ΟEOI,z]KIdv#eذc M+Zd c TBpg`q4J4VZ9FM(exlɢ ) сT#t pWD$pMX|`\PStLI;ݢFYUhD\F٩DTBFK质mâ͘Ƥ'' 41ǛЪl ؾ\AvwdqvVkO4E'B+̆vѯVP @$c03Jt!|7  6'dlB󷏹q/-Z5\%;YlErPJ=L02!R~|ԂtᗁP_|"i35Fu{.&+& .T1ۨZ/2lqyZ:Qa2Nw |(kf_ir=k"VjaYI>FUXBה `7qeoTxHֈj*2.^~Xe}&Has1rKpQ)upY8jg6Q ٸ2="KUB eE =Pw|#'?Af>*TΈc2Χϼǘ^e8ۻQǺ1[4EZSƥ@Dz %x4&Tmj#~(9xbL8*w/bQܤ,C݆ݟ03"q'Mϸw>+ƴD|_ˉu䑒 ] g{ w1%oW *`{ d:\&=Ӊ^TTyx {(6#7)pbLP'J[ݼcL!{xX /Dfܞln TT nx+IZZ-'].^h}<޿ 0-PS/T-[Hxlw_WY86^b97BRdJ.].`kk7a\ v2ڟѫm%~'5J[%TGFϥq)yכNv6:9nˡg4y{f,R<ЖX!|4  d83[(5q\d.dgTw{-eH 8}ʅ8bAKcFOWG.z kuFp.$~M(Ra0 <,={0D-PmO['_+b>.,3ìVimԱ9]X# 50](jVg#)EbEC w6}4HC>EnVf{K'گb`pHP!=k@!IQyQSmqT 3#ڼy.@gGE۬Ù7 I=?DҸ, 4?t{jޗ=Zybs$75N;?{|[-ݞES|ޡ'Vl44N39Iu4Ψjʝ۩讀Y2Zj`^D@zAWw5kݵ;+=FK߶uFTdn;q܄VHnfƒNy\RGi}'s=V dT݂Z3 &[wZ}et{[ ePyXŴbHI[|mW'.w&w*淘Qyw{T$__8zG;\J+M+gZBK )wMd&j&j5|&j7tpɓ{'DKO+_:\R2ɥC^t702xwlr۪࿛WJrtjnfP= ^h=Ot6nx$DԄ]T6|vm_bycpwƬthz#pǽ[뗢 iY*]u@6r$ͮ fފPv Jꂕ3]jZc1y5Za u6Ijss0x(` kVyIb؈MytƄ ,N^SlIZ71-=-C 26o_H%hCB$B԰Xq>4(UQT WÀ]۷tWKo[|dYZǤ`(؂g.:#~لsxU9VTG z.P$ro,hC&hw=K>e3Q:?.k2ujViJ)A\Fuk{֝G% c =vm>8uC7lpR߂xpHIZA,Ë;1r3JBܚ|uKp] vZ>T^e"_ 5g7S,ݯ鹒fbLdu:{ M8qohšLQoI `pہ<ŝl2}"SF-rn:q"%ŪAȰh.Bنo{rgDMTTRHC䋶,9}6>b`/@L*Aѿtg*ov 60t/'i+cѪw03^ JZ>ëR)4r>{/^teᎁT3'^tQ_i&!(>z+il,x9lDKw1 ,JT@l<1>6 y5hD͐^(I,M\YHt:Aݐ." sa%Ұǂ&JLG;Zln,n%uM!ЌގQKV5?'-xJW'`LiFх2A -I7LbeM?gV6ֺW J>%1w(=xh`&܍tZn7p 2gL}JPy;#W髠pֳc"'ĉxsn!vxLL?!/ )ɈEwP ij$hV;g6;k]PX=a1/ ~Qx\"& r]`D?SQrZ計vj7?K."m!SC;yB~A[E/O !jnv6tykWcb{f& (_ƿj(GV@&&;ƌlމ5i7ڂm؞L3 =p ǦjL0 70-l]8-+r5nXX{ؚ"s)L'.]{J,V-A~1Ksot ;p0$ۓQ#]'O #|wSM|%GyhJn䦢s1<׍rxw; ZK{ n-5( 7_VHej~8";jm:( AsIl\`4oMFy*X\UX഍x{| 3fV+{Ld%ś EY0 kVE,y"ż$0KۆĠj pPmb,"_RhE~tʪ{F(WvkwЮ3 ϕ\ 5֬GV|Q͌$t{zgCm`^V 粊ĿЃw:Xgnga?܈+i8g)Ꞔ? xI 57rhOX= ;oV !6A6$gܣH?e"$!4[,T=)~0>E-QC5Ӿdc!ISh񮉫j"Ԋ(K(#1bWw_$͵2,uuL~`z$y7|(8c tFm:nz4d _;5qLJ'L]< 홗k7"4yWUK f9 'gv`hAhXF 1EZ|B`T*'2xcyŲ;/;:)O ԭ"<[~D]DIݢZiU)a73"`ӁPw2g;.!P3clPkڛPyUSg ? ]!6*gq?#f7%Vi|,% -t G:'"Us76S MW<$~nһ=HEW٨ "~3gգ F&y4q2[xbhI4S=9Ro13wLF*Y ,gq1[xNbF4m4&jѠ}9r Bǃn|)ޥ>aJt5%Λ}%-xjD5,FR:=NDVw?pNnk'pp|b'0{IOFؤH<׋Trxly|)J-/ƓH@#>QXwjŒjyy˃W|2⺁xc5@Kt?Lٚj鬷8 ŎE/%.mGhBݝ笔<Е˸hڅh2." Mwx~;᭍9FrlFlWV㚓1:WնQi>pt=\^"Y@Co4@Fqr _Nˎq``]d*g(WUtֶ/~d2MyD} ü-=ŤmYZryJt2nĥ@KNa "}"F1&at3s> ֟gfsp\\L2Ut9ax R\ĜK !'|fӍ+=>":~pfHQUq,kZ*J%~{}d f8]N*F[Q+vCvM1j-6hmH&0",/;BBRgsd3[T LhR Go[3V tb;=#h t5(WJ>iK{A ^|Dq( qHq,A<=ˠHsе_&@BGHݕMTђ,uM Tӫ jE9m}ٷtY_zDEߌˎ2Ŕ! y;yd }ᣵjGN"5?;`JEډ~~kҭڥ'g-`h'Yzߤe*{D#:P>E_1&_ yRR K8#6i?I滷D %/t@G@,#uqA-5=aF;a]^\ )| >Q(*BK€:"@'ģߩl~SrJ ukQNN(i@q|X[2/K[ճQjSE?^8#e0K3M3;vƶ3ˡt$AU^L$@{ j{BαvEP\:lK '7!zm#Nuڹ n.⸣HK ]喔TKiǠݟuiŲWrۡ-'.V:SPw5MA!,|1'y,i,ݳ6Bprō s?7S(M-IENDB`gruff-0.6.0/assets/city_scene/background/0000.png0000644000004100000410000007312712540054077021527 0ustar www-datawww-dataPNG  IHDRdU*gAMAOX2tEXtSoftwareAdobe ImageReadyqe<uIDATxڄ}Y$I'WK_FEuկh0W ߿ ϟ¿ﯜ}o~>O/9kv/:~;?쿈?o87\{/O>O g /}(ާ|.%{k3ǹw'/ka|ol^G]3~x|oϢyT~kB1VY>_k`ύ/˦W(_s8RC}AeQ@>_~n}]gRy~IhsyS-?oĂ.hϹzzcTPO3mw_=YdEoAA|.0MX%*~-zC}?i>~;x_8'Ygz޿}Pw9^E+  <@\e혧{p quU_>HKs9so*9gKt(.i?k6{[ӘyKd8+$nF|<7=';|-'{e}k5!B"mqbĉ9z}&`}l_Grnz>$0!$x~f}~sİC6 kT/F1ηr¿jTYe^5 "⭿KSw냽0f۰"$O}| f+k#q'|Wl.y%@Mof˚ZJYw ]]ў}OM K ]ubO6x7:5}gNL"⥔%Q#@Mj4i@o ῬFZ{t(yoLl[@Oy}M@.=@\ĉC=9@?ZxZ,GVy"nk>[`o͉%RR)xB{XlBNM)%lL[IOň CwsYkm1z>dzURYU>FJyfkM‚s;0.ia Zuϳ~(:͂nտ|}A^I4͕6 \YrEЙ=7Y?%Jj“DZRI^23w$ъz}zFuM͍vNygwL?.WŎbX%$Yo y[~Wߙnߔz{!aQUh/F MQyBB"sV¹˹@$ICfro hARbx:2MEzys:=k5q$H>Au  <c[f--l0K ~Y(gEkHA=^8AyیdJ9У$W˛vo^ 8pV6օ/h-wg|m9l4}0*Nmr@vA$@m'TL|eϳQO9Ũ"dϼ85ml3 E 3pf~ˊzS~9D=ɺ1dʭ<'8~Hu+dJlD6r9a;r)oi[]G>\*,"hLL`O>͞?27T=ttl:c } t8:DA5(1e5W!}\ yѤAX  <NNK!h(RYT<9nUl/)`ғ[xnٗ6Ec~X0/#ˈiNe"B_GyNY>MK:oDp&6C?YB#h( RWH; ML"keJ"gG^)9Rav@0c<٥zlexBBm'jVE2:VQ;mKuf#O^3'WE|M um]5@>`OeL_tg SS(fCɌ_&#f}8 ֋Q|ܲg(iY/ѡKn2g}Kkj M%kFHBs8HEثz6/xy,46(~g@80TmW4)&Wa2{݄Y|adP"4(Fh$ muPMs`a o%!*e?'i]`y u0`E@>1k&cbj\0b"ȷYL8Zi9?e,{ڮW x8Oˠ7$s`0!!w CkfIALRf"j5A9u cx%qٛPf2&!/zgH#D&†W`+ /&|2͵v|gfT:6R()XL}1諛p2#@@WA::*9 פJVѣW.{C T(`% G3ju@ ~|I4RO&y,>?D-9w*qhx,!.d=03JoMy[ת%{- 7y3i4y( >A2G}tF7[WaҖX,o)fT(sysIZ=@T.O",%rCqu'z"6cb%GKT2M9_G*8B,ɚ$R0PȜI a#|'G9(u~aY8B>`W29'DV y~ Wy~wu_ɲt;d)ORq%0D˿}YI22xa(v$6rhfɻEO9#qtBdGkӆCU0)$P) H?B~X7$ԃ>ypD'm*T*_Lrn9ĒG6 6zrS`\  ݉ 7IfB+.O!m>> R$&R7|A AT>@6 3Nx#/RKi De:](,M㬦#JCT CTޢ5T {J/ Db!aaCDE'P!d!9>(T-Jh{J7@:UFp2uvDCJU 7/+$9NFJ ߙf B>(z883z9tUz*̚zȐy~l:=R-1,u fBQzz> c0\iM?]I2m8K.osfyPCug'F~Ԕu@dsR8Ml+ĩ Rt+l 6 őR0`Ȇ7eZvGO{ .l:v>KHO7ڀ~.mwfKoɣ3sc"v[\D,D*:b%O/~tqF%!ꢆ pN ԞOʮqT\"9x=c7P7 M7+mƝ4I$ iV̵7PB| ,XHu%tB7NJi!M8Iܠ.x&V4D07P>腛bQJRÚ`M*<,pDHٷL!&ʚ[ތCto[AQ` |1qy2cFX+o) !SGjaAi92 Q =dEy|F-:M(w0&0da Rc+~cCEeF%CKc_I>9?ink5󱔸àO L!uڍΫ/F0\U E2‚i98Z(Iz/'Q{mu臫RL32i˖zcƘ)mU;[ &&%4V'A mJ %l*_kqfAyJL9E։a7-ŶFF32T+Iґ(D(h Qe^'8 lW5.OE_8Ht'4e|?"rN>8K)s7lFx7*')TcQOTR?bg \'tzyTn rr EZ\i|GXzXQęe6#^h^nHM,( ~[3r+M|va^HCӑ6}>yuf+ 85Lw,YAt䓀kf>s9R%K\K)6Vm6Tol g0,2Ђ$sR-ڜi¨ZCxUN2$R0l7kYLT1XW9NTi-R[r5,Hm\TŴnA&gwK b.^~#7*Vjfa$r7fI*?R4=3fF9oXe8؂ot5JF@>5a'l3j52Sw_h `Rܾ1Uv #Ģm+bip|O]G8Mhn^R̝% zHk.+s8N=n2&0)iJ7&*Ұ).\lRic'( fŦ ×d?*vrFˊlthԩxhx{2Jϩ&T6K/KG݇']⿑v˸[]rW>H 6jxAk˺nl\6)7֫no2G 2 l@Vš T6F03hWRk>J5s@Xm2 Nx (L89gSb EGz< 1ߎZXNO69Tt.`-(*xV^Q'_Z3HAHfI2YE~P"c u9*l..n5斠א|1Yk4pEq dns5mZWe~l48KKd?}>a3gU/mé؛-{5fh⨃g<3@Uͱt`݃wJ0ʤqf'# :}CÅYQ"-5jyө.@u )^{O;vAB545t)Ze ;F=A+!EQ QeB?S0=@Pb8[ b/Mw,FT ^P)P(#h"leGwnl=ދGu992s :U*[f("Wdײx1)Jխ`Oө=~Tx'Ntre6:؀ C)!mؗו)fpܭᘽ;yU|ydBAT!nTLFeK#C<8q86ͯ~X+flKޗ>8|0=dfu@ٖHK+ YN>̀ 7NR6?)$\w{՛B)j]S.FbR!X4߅l-BqT[+F._R-ČwKv:9d$|1 Mx*נqw3P;*>8S V(rWySaRfн4F2b =c}RKBA7Y1W6[x&uU=#q9rX!#) H|{?BUwMHA1?DaDةFW5B~|D.aK[Id7Yꝁ4g)ctDmYU~ 3hnX;s6x͋i^ͦ+@0 Йl';A>~/%4NzrR S2@dbir:6&0n=mq`ʪ8zH2Cz+9E*]=|2X2Ug(%*z|tcsR>01aJ*E>[ntB" #[ w;2ЁOGҒ9q3ͺBC@T;XtQs|~f!"'ȥ峩UiFdvL@fJIr] M O/F F#Je5Bۘ>.*1zIwLBSD?df/jV* uXJ)'s8BH5i7j(¨5//#JdJm+.\AHv6 :Ҏӗj ~ͯnSOS6 n Ç&_bgs%Sȍ\cn}>"A9airsޚv FBX=U3LNSv9N)=9 SCi2ۧ qqVU7rXJ5eaInDc<040QԶə,rQaFVL7;b/━a)&7h噮wP [$cK S¬[F38(I~+B](FsWq$kѶDYQ_{@AX{ghk7{8q@&4Ӹxm$83 q/7zǮM! Be ˴@QE' S+`kbI)SP&k*f|p<`uQA$ӓrQm3c H&Ys+/f`-)3dKs~ov7׊)Ò¤2 aa2)}Y" O6|YB+L46iY~K*H`混 gͫLpkH2"mUEQh]m}^>s!v5rja)@`poBi9b q+)ds22?Wh֙5j_e#h~!b^M֜!u m)5V? ?a5g齒2'mV S.Rk_OI09&R0\ڡ+n=Xkp%M&2CdH֕u{ܖF4uueVc$0M@ar^Q :\J[1R4Ω8[&*֯И<W%igQkǐI܋LXxڟݟ37 ~o9m c[I/+,4P)fO_$>\ȆhP09qD? ;%w^Eΰ v-f聞 k֫O|ÖsYu^u!E`52$f2Z]Uqf ب a?)%VppbLlN*77PL ]<Ҁ#94 .̂X7EH-H ݵ 4j;{(KvLN%P\Kd(GHGt-v}ue0R֑1ڗCT[gSwkEU !~~"(8gX%D$(mR ݞ{'p]}eww\_ qAƀDLp5ߠ#a  e* h)28ɦURQZu`@^9Gqg+1 & 3bûs8,bs;apF{nG$ a0$C*S R^UKx^mt1fB@{TYv+Оpr5(yn[*z~ZO> =BHH*!j֞j 4F-X>p;8tEK1 :tat#.,HtO9ILfO`t6`ACM Fh`p.ASd_su#+t A e:+0+ D;z= /L:)ҦVLB.=V ^o+K K/ Vҵ} rˈ "fPŲ!:Hae yW"1aRHa 4"з>QMɻZ[al_6")+䂹evEpgy(`uy:KfK )QSXyX ]c9@ŗPq#NvimSDӑ')U uc.f$Ӈ:lt-<.E5 Uqb?,dP6}[hiHo2#9h3zvN#L'!U4u<RyCԒHIĂ30mtFMf&f2&!6ou~po{BN!Ga- ~0*9?vFVKKW<1#R^ TZ1) )滛Mπ[~'mS%Qt ڣ~!7t#A05hgFSQgLYihPh?Hvz1[JOߐǫ1ϰp@`ϧ]vגhќ⍚JQ/ 5z~9ʫEi AU0 /)$!^2_. AA7*EC.Jy!pYJOh4o?:Pۢ͡Mbn0@66mc9Ёz\iKʂثRkAzNzu_4H@=XI @_q"]UORqڕ Sz;|ڷ}%!}g 2ü2Tr2%q)R9rG]]'XXrĖX+AxHyQ躡#^%fS!M&CUm'י Zƅ^ƣ( !&2fU&_]Ү} >l3:߆Ζn̺&zNaE+O/˴ U\ONwTGi0)_4{󟀪r޼vlN)U͞\$5 Y-QC>1h<F§уi5I Z(S3)a'K"Z.h%D=3I eǵ(Qt_R]1)2HyBf\')(m7MdZ#Vn voEA7+5Q4vsH&AqJ(#tJ!H _\#pEu%LَDSbe%nl` /P'8kiD̪b_xWf D8n&6=_F-W>-~LE2{kKzxv)V.W{a y+:DZX >=H f$#8]>o'۟%d9x-Spj^}NհX.vU*z^}.2|r ?Icib8+=ͬޘWm :HN\ҿ838"m.[!Z\?#ô̦Q ~L|$77 ԖP2Rot j5hr#^Rh:NQkc}\Uf"k8@՗ï-6] G#G0i=T0A M]P UbJVIgBki5 F:2!1p쥨q}xF0 LE="P1kD'Q g2qcLQtO8a]ۨ^_d܋*sP[9t{L@C{ali!Ӝ`@2GJԫ1V?T#XgHq*7!fP~9 %mź,zy H9f2,=I|R1Z"$HQs4pADx'ityCk9 ,ޱQ G{Gmj}}sNj ;B>PAEHͶVPU-FH7fyTjy0D6i ^Gմ8梄q 5#WQ obRciׅEEU. ZcBAPiA,"T*P:/zI9"fP5L@p=2_> %tȫRIȇqj,V I/8zNI#=XZfȠxi5!yPɞh% e)h(7CYQqYȳ<ܒöLđh'K伻o\EYpUߴh]pG(S?c9c<`jUPۗ9%jД Ϣ ڷĈe+^d|n1P٫P6;ʍX|>f}/Ŋ1Ft$=ˤ,;C=iАS.D!F,/xFRä"ŬUZ<[:Luީi:o=}nZ'k.:Ъ!畯琠(=>i[28fHf/O[3aHkPWj5*bOJarVs E95* |eq T'hc{$hUcZLv 1o$v-[rj=dR̰n>[#P9j!Wl bkKO+߉Ě)N.̱óa0 MJㄍ\nBa&_Nޕ]Yw*PaRc3  ˙T ,ܠz ^HT͚e90=`F"Ss%L[PzL& g*R(5 rh#fMNX?PL읬%iSC xNqR9֦t@s{C֨Ja)tݡ1~&4Q Ru\1Bh4X>DFU5vZtI3~ɕp 9rĀy^vE2&^osK's6)uZgqXM8'KJ[dg? KT,j )Bq%]V"03]R Qa P^ Jsg$:8 51T5clւJ%?#D 9ӎєF^eQ mLFUr!P-KYY3SS4Kʪɍ)ё@$[gFPQ QR '1SWLt1MiIf06-e+o.dǷjWnU !eS0VR+*95V]@ ,\I~M_đia]c@$]y.6m}l*ڦ>){GIáN!zcӣ,! c%|!#Ǘggx^RS|{|AjNEJ?UB)00M/`Ai8sr*$nJa]+.D5ߔ#p!K(jK% 6z Aj7ѿv,حrRiEAf 3Ye-|KPm>U<"kB88"z67S~T r`@,o- QŢCr BZ u aMbiGibtvV9nr2}S^'.c’d<-g^Dͅhu6SxY~SJ@GLt!VT9hEV1b}@ c Q!kM c$FMN1IL' oieCq}`VSS>lJYW>G&"+->& pUgW(B1>waBq4(S2"(HQ7Ʃ7L %^ =;Ӻ,GG逷f1l|W& P܎ƞ@u>]Ӌ+iJS .|h Xn"].E%_)ylUFbIK3P>ɨH |M`qҨ)4ϵ|ǠS 8Q^M+sK0R )ʮ ^% @?`VЉ}dV`vq_"tS*lv tEm*QmeD+sKئA̜oGe+L7^¡ da[: u ҙI5C0SGBBSfMv[$dCIJ&5d]xTAcbJn[i;u Bqzm;Y?LvbI$76ȏ6: |ss aʴ$ G"W{ FZ1?x?6BD&qC z.m46Uཤ\<)S~[K9-WUrc~q%<%][3G.멛^'3OJ|߳vu6.t槷7%![uPL@\oŠwu{ ǧ|S*E:O{]䟸 [LR cjetH\TQAUJbj 1)XGJ@ +tn@,@%h퍨R\ ٝo0t.Eb=xQ,].1^FF&PP 8`n,C' P NHYS֒HzND%§ks A,rcZAAq%f" DN$'8 R^֦]V"l&\{]IFqYԒFrHYnBשGVAKӺ 8P+7_ _$cJ< hG%rUg=rmb6ZAZQ= \~!BlՃaylM;Th՞a&PF;ulk9+jHb>M[zG^jqf-8`~t(0P[JC=<e|RawV0ϙV|H RrF=HU+I Tiz@,P,Z/ܔ͋L=eu[Y%cNČs *[g`$ Lq,8~fYm (LLSO.M@ۨdX pB9/g4e7-RWPպ;uEL!)h#wsb CiK`IEQ}AjI[k,G)wrx.57S>@,hy>5ARjC%9χ,æѓF/Z۾Jj5oMRXݾz.NAn,&NEBڟ!(~J,F{c鴑P{qSeѻTyYLToǃ"04[FJWSv3L4iY'O 熸 wD:0zp`BkRǺ8il[^cZA&[!1X1{'Wg/IH?_E =H, ~ׁAU޲~ 蕞-vuϺ'Lyr (Ε,Kԗ4USKY%EJC'ӝ,u#_o::6dG@RuE osM[F9ԧ& x3dӬ>׃48 ڝ r!)M?."^/hS dh T[.8n0jAzjt(<3xVV ӳwE5oWh,\Wa'ޤs=&u4 $=teR}oߑRv& Ik|0Sq]y[-3d@VYECpSt;們ҍn=Kӳ| )+gH7V !Ś9ցݒb +iJJFyn.^q+(WeG ݍHUq;KgꁄvQi INF0 1lq˨KK2MQ飡ҽN|r]Ni (2T?(=:$D9Ll1Og>kY[ kS/`MLz 4I68c4٦dl4"=rvAZ a!(^.W9srG~}Cb-^f.y d2ŬXsA6!Zy4}ިp#0aH Oxޔ4/3O9AU2TS? z} >I֝ӳ4BVB!#@>RrsJRj-B|oDZ3 2lDK7+@C$dQwҍ+8dFQd}Ѡna*l.O 7q9*¬OqH@;iB Rbh LR#UV\]: [d׹3Pvy-\iT'V1 @iA@4 ׌' h:NS:Jᄋƕ,{d}f˛r!&穳FuXEPYKvver ck&VTj){2t2UWa!jhD:s8jP/LiJ{.f"Qu֠X-0HgRy$\qD4'Mo 80щ(StZi9ACR _75=\X3ryuQYU"zν%9ԧl1(9vF\=L4sho7M}QB]s L_P-=GW ~g]؛z|rhT[?6 {i{UUQ>ʕW"qK`M/d7?HT5XY XluSV^LX)9/e`J|EiRaCJ0H:^H;~vX+"ۧp=1ی/Mn!6`ROU]eЃ[k)G>Rl,rEHzq1 !N\yg/v`ܒ".ꞵ[%kAs$X(xnثMw2j?mY`jU"Cxz5rʟYyMB`;},_ D MCyYJBI!qg%s5Ő}`F4ᰕxXK=@РY[pRz& ADз+*V&c02/myG1Z294<`8N9P >K@ۄll-EZs+傽Q22!E=) a6{pLq蚎kMOC?KYS+4#i\Ӽ: t\ #鍊$KKM1Ar3XfU 䪑ޤ beI;ip!2cTo(M @.EtR,}+ ,XX0`b iM9$LmzK*nkCP n{[sԐo&-wF"7GE%WA1@;G2ARtkO.1M=*Gs4+Q:=c5TȄT*fȌQQv},k\+S 8utU h}$" lptͱS"yKExȧk#o"#Pz/. A\-NFTIi= \ &T?F~ïnhKdMsVCD)+0a$uD2HMcS?ۓum^ c..Ƶ)iӬ~b %Q[S)0ff\Oŕ7 -\-cKE0OZ8;M>}>!HUlS /i(?eOck#+ P-t(jע(JPa^,|ask\ʅZN޳BەԃʡXO@ :2A 2hXjGX 7 >H!X *wܟ:,L>%;m5WpW ؒz:~!ãBÝrH]r ݩq0aVwX"swl,K;6(=Li#rU^KƳL@@(Qc ՄZ|BeĴo,B90F; dB#7iQ ۻ+0[Jl;yAe2/ ꖞ3]ްGNƊzlsHeby4Da*Y 1,eUo]sHCP3 tOv5E?v쐫1[֪ldhK>:ᜩo?cTf`Nc@(|Y#BNv4=UGDlZ}^9lgv=EWԅaA|l Ti[E ofg]s@p=*_l[~̓R5ɳ)}:0ӭόrԸ. 6h?gBn c@>NLcByLܽ%jqb_d &ӜSzbгSQ+ç4E'!#caJ-2`t*iʔm )iCgS8 ^ 9LRrJ0;E_2E!e P3?uGIF.Iif ݷ04>Jtt(oɖQc?!Uv"d 6Eg^m|'E1S8k9(to׳M~p#-ZKL1$-(bC7weC K5} [#F*Tђ<38}T,smLG-Dչlb;aM%'fM41`NC Jo!&# a=Au1&܇]J-+|byrOluyI}wKkAH *J1CqS:M^ RMvg]m>"=+a}vy윁Ka{hޮn2y"%3!VMIKT~` }Xe!oFo.X(Sv3BCQFYwawF/!Ң^r-Og ߕT y ԏYn(z&Q&^~[zE2aptfXq8}f)Qn`&sЌUj7 pPO-}k#79 43^vpm (bj%Aږp&qxt'7Z*sBSY=F)˧\U"zA{T#PUrxƄpB4:7k*{IcE|CJ1un43:S!E:¸`kRb9}ŷT+!h1?aR4iH、4,Ali >B2Q#VLJ%'QZrsa$LGt bpj-6-5e,I j {#TFBB)gX5RU! :€͋Gx|H#8H CTRBVΉp7Sg_KK\A*#-K]IlO^m''9]kJY0n%ơX "˞=Oym4gD;@i I.E3\f3}RW5Kxeȇe Zw@$s:yvJBJX.,^z1&װP,~K70pckC,2r5scir6,PDdW`p>X1TsTcW5l`/(FȰuEi8 o<)/$(( (C?..©^i\A71ΔAhbihYo ê9#KxHpiGM1!4 ݇!joqPjTz: - a,—!UCk8Sqll%-&-`%T-SSH! F0|hצJ*J^ eς7Io9ZXd_ÚLzf]ZZNP1̍dM) ʮ7'T( lW@L⋸ +~g$zeCL%-Ѝ 7= W~]` HVrRQ N {Rf׫)EpJׇwHx"ի^%zx ˶_Ur( .%xmV7KZXYӄxN5 =4y͡*c<WgߩWæ*6 >!lH;o,cGVE^FG.{ :Xѭ8l2MךR2 $U:MGD+SYԿ%J>TT5e"qL(*&W >?LƊ w/aG8eBuVҿl㏖4@RﹺlOeXag%3 FB_{ir\~ټ?0ɾjFk^n;sNq<Z˔:?ZfS6ť.k'm7<k>Y)0RU:y~ cYkk#R?HW<?27ԤBi |t^^~BQ Ē~.v>^NJ:Vo%Z!JdH;E]'%*9٘*or|2q c*6oZvDʈOm_/WǠ8 ʇO:?TLUdy Gx$Xmu%)s{@.ocr~ـmtP)JᴐArbeǨ݂RI[&IsSި{5щBߟ𬠆:㟡 ׁuz'11PH*jM7=ƗXOï9nA_GZ~poyza)Rs? mݫCкZVAmD#Z"v]1USxeDbf/i.<4_[єƧQi)ZSՕWaR+hXI·a?,)P RnpJ˦5)(|S0 | 򚓈 *EESkpಠY"iSA{(9\pM=- h<䓄Di$ 9ڬ-tUCG9,DE4E>`kH+wH3K\A w">o Bk޺=Ԩ h89ǘ_M:HͭYl+/A۲B -u\!6C}Z:v"5oMq.Pdk4FbMdL?iM|,(\->­ w+ 8Uzhh&-LY*TKݚr]˟Ht]:eȷ̕ڽ%-40>5l^0 _cP| ʤOࡱj @JDo1:^Yl%d,{-# kݒK{ܭU>XZ 2MxJd $5y#ܨ؇hkf10ЩܞDͼ3T]Ƽ'TX8qWpxf#1]{Vl?@he3.=I)Ol#lT oX4O+;좃YTޠpz3]niMX&PE0wA_[JeX‰ 4@`z%CBu-yC@_*9YVJӨώeyqG8hrf˵o8rWcl,]BC[,Ť`].8_O?ZفTwT֡Ks)"F-A SG; K;V0霴(]4)*19hJ "Hde^bֲgP l5+rW|'F0DU+N9^VNN -sz}~P˲w q>Uq-Cueܧz._-8:T;5v8> \fl/-u7 2m\>7*O@ohOGG:pOJvZRrC>$Doː=z}tgm+RrP[BuQ5n>|؟G̾5{0pb?ĔV!HA6;twcBnmN0O(5YjN/8ʙe3p<IJ}u2/;qq&Hެe-.-16;J UI1 a4gV!E3xMpي԰mޚ+(]e(#nrEJR$txԎ\(1La2i2sYm?ҩYw#+j-spR>P<`b6IZk!F6&xlD`veu:_iYӟQ]1`t949=gmgh7j:{9DoN3Vjtߦr ӿsou Z_,t+3u/0NpM4)]j<0ݔ0f9^CNڃEpǦ\$*Uo<$v/] } nslj H"35Z-0Z)H@ Iij>,sG`wJE۸cIENDB`gruff-0.6.0/assets/city_scene/background/2000.png0000644000004100000410000007312712540054077021531 0ustar www-datawww-dataPNG  IHDRdU*gAMAOX2tEXtSoftwareAdobe ImageReadyqe<uIDATxڄ}Y$I'WK_FEuկh0W ߿ ϟ¿ﯜ}o~>O/9kv/:~;?쿈?o87\{/O>O g /}(ާ|.%{k3ǹw'/ka|ol^G]3~x|oϢyT~kB1VY>_k`ύ/˦W(_s8RC}AeQ@>_~n}]gRy~IhsyS-?oĂ.hϹzzcTPO3mw_=YdEoAA|.0MX%*~-zC}?i>~;x_8'Ygz޿}Pw9^E+  <@\e혧{p quU_>HKs9so*9gKt(.i?k6{[ӘyKd8+$nF|<7=';|-'{e}k5!B"mqbĉ9z}&`}l_Grnz>$0!$x~f}~sİC6 kT/F1ηr¿jTYe^5 "⭿KSw냽0f۰"$O}| f+k#q'|Wl.y%@Mof˚ZJYw ]]ў}OM K ]ubO6x7:5}gNL"⥔%Q#@Mj4i@o ῬFZ{t(yoLl[@Oy}M@.=@\ĉC=9@?ZxZ,GVy"nk>[`o͉%RR)xB{XlBNM)%lL[IOň CwsYkm1z>dzURYU>FJyfkM‚s;0.ia Zuϳ~(:͂nտ|}A^I4͕6 \YrEЙ=7Y?%Jj“DZRI^23w$ъz}zFuM͍vNygwL?.WŎbX%$Yo y[~Wߙnߔz{!aQUh/F MQyBB"sV¹˹@$ICfro hARbx:2MEzys:=k5q$H>Au  <c[f--l0K ~Y(gEkHA=^8AyیdJ9У$W˛vo^ 8pV6օ/h-wg|m9l4}0*Nmr@vA$@m'TL|eϳQO9Ũ"dϼ85ml3 E 3pf~ˊzS~9D=ɺ1dʭ<'8~Hu+dJlD6r9a;r)oi[]G>\*,"hLL`O>͞?27T=ttl:c } t8:DA5(1e5W!}\ yѤAX  <NNK!h(RYT<9nUl/)`ғ[xnٗ6Ec~X0/#ˈiNe"B_GyNY>MK:oDp&6C?YB#h( RWH; ML"keJ"gG^)9Rav@0c<٥zlexBBm'jVE2:VQ;mKuf#O^3'WE|M um]5@>`OeL_tg SS(fCɌ_&#f}8 ֋Q|ܲg(iY/ѡKn2g}Kkj M%kFHBs8HEثz6/xy,46(~g@80TmW4)&Wa2{݄Y|adP"4(Fh$ muPMs`a o%!*e?'i]`y u0`E@>1k&cbj\0b"ȷYL8Zi9?e,{ڮW x8Oˠ7$s`0!!w CkfIALRf"j5A9u cx%qٛPf2&!/zgH#D&†W`+ /&|2͵v|gfT:6R()XL}1諛p2#@@WA::*9 פJVѣW.{C T(`% G3ju@ ~|I4RO&y,>?D-9w*qhx,!.d=03JoMy[ת%{- 7y3i4y( >A2G}tF7[WaҖX,o)fT(sysIZ=@T.O",%rCqu'z"6cb%GKT2M9_G*8B,ɚ$R0PȜI a#|'G9(u~aY8B>`W29'DV y~ Wy~wu_ɲt;d)ORq%0D˿}YI22xa(v$6rhfɻEO9#qtBdGkӆCU0)$P) H?B~X7$ԃ>ypD'm*T*_Lrn9ĒG6 6zrS`\  ݉ 7IfB+.O!m>> R$&R7|A AT>@6 3Nx#/RKi De:](,M㬦#JCT CTޢ5T {J/ Db!aaCDE'P!d!9>(T-Jh{J7@:UFp2uvDCJU 7/+$9NFJ ߙf B>(z883z9tUz*̚zȐy~l:=R-1,u fBQzz> c0\iM?]I2m8K.osfyPCug'F~Ԕu@dsR8Ml+ĩ Rt+l 6 őR0`Ȇ7eZvGO{ .l:v>KHO7ڀ~.mwfKoɣ3sc"v[\D,D*:b%O/~tqF%!ꢆ pN ԞOʮqT\"9x=c7P7 M7+mƝ4I$ iV̵7PB| ,XHu%tB7NJi!M8Iܠ.x&V4D07P>腛bQJRÚ`M*<,pDHٷL!&ʚ[ތCto[AQ` |1qy2cFX+o) !SGjaAi92 Q =dEy|F-:M(w0&0da Rc+~cCEeF%CKc_I>9?ink5󱔸àO L!uڍΫ/F0\U E2‚i98Z(Iz/'Q{mu臫RL32i˖zcƘ)mU;[ &&%4V'A mJ %l*_kqfAyJL9E։a7-ŶFF32T+Iґ(D(h Qe^'8 lW5.OE_8Ht'4e|?"rN>8K)s7lFx7*')TcQOTR?bg \'tzyTn rr EZ\i|GXzXQęe6#^h^nHM,( ~[3r+M|va^HCӑ6}>yuf+ 85Lw,YAt䓀kf>s9R%K\K)6Vm6Tol g0,2Ђ$sR-ڜi¨ZCxUN2$R0l7kYLT1XW9NTi-R[r5,Hm\TŴnA&gwK b.^~#7*Vjfa$r7fI*?R4=3fF9oXe8؂ot5JF@>5a'l3j52Sw_h `Rܾ1Uv #Ģm+bip|O]G8Mhn^R̝% zHk.+s8N=n2&0)iJ7&*Ұ).\lRic'( fŦ ×d?*vrFˊlthԩxhx{2Jϩ&T6K/KG݇']⿑v˸[]rW>H 6jxAk˺nl\6)7֫no2G 2 l@Vš T6F03hWRk>J5s@Xm2 Nx (L89gSb EGz< 1ߎZXNO69Tt.`-(*xV^Q'_Z3HAHfI2YE~P"c u9*l..n5斠א|1Yk4pEq dns5mZWe~l48KKd?}>a3gU/mé؛-{5fh⨃g<3@Uͱt`݃wJ0ʤqf'# :}CÅYQ"-5jyө.@u )^{O;vAB545t)Ze ;F=A+!EQ QeB?S0=@Pb8[ b/Mw,FT ^P)P(#h"leGwnl=ދGu992s :U*[f("Wdײx1)Jխ`Oө=~Tx'Ntre6:؀ C)!mؗו)fpܭᘽ;yU|ydBAT!nTLFeK#C<8q86ͯ~X+flKޗ>8|0=dfu@ٖHK+ YN>̀ 7NR6?)$\w{՛B)j]S.FbR!X4߅l-BqT[+F._R-ČwKv:9d$|1 Mx*נqw3P;*>8S V(rWySaRfн4F2b =c}RKBA7Y1W6[x&uU=#q9rX!#) H|{?BUwMHA1?DaDةFW5B~|D.aK[Id7Yꝁ4g)ctDmYU~ 3hnX;s6x͋i^ͦ+@0 Йl';A>~/%4NzrR S2@dbir:6&0n=mq`ʪ8zH2Cz+9E*]=|2X2Ug(%*z|tcsR>01aJ*E>[ntB" #[ w;2ЁOGҒ9q3ͺBC@T;XtQs|~f!"'ȥ峩UiFdvL@fJIr] M O/F F#Je5Bۘ>.*1zIwLBSD?df/jV* uXJ)'s8BH5i7j(¨5//#JdJm+.\AHv6 :Ҏӗj ~ͯnSOS6 n Ç&_bgs%Sȍ\cn}>"A9airsޚv FBX=U3LNSv9N)=9 SCi2ۧ qqVU7rXJ5eaInDc<040QԶə,rQaFVL7;b/━a)&7h噮wP [$cK S¬[F38(I~+B](FsWq$kѶDYQ_{@AX{ghk7{8q@&4Ӹxm$83 q/7zǮM! Be ˴@QE' S+`kbI)SP&k*f|p<`uQA$ӓrQm3c H&Ys+/f`-)3dKs~ov7׊)Ò¤2 aa2)}Y" O6|YB+L46iY~K*H`混 gͫLpkH2"mUEQh]m}^>s!v5rja)@`poBi9b q+)ds22?Wh֙5j_e#h~!b^M֜!u m)5V? ?a5g齒2'mV S.Rk_OI09&R0\ڡ+n=Xkp%M&2CdH֕u{ܖF4uueVc$0M@ar^Q :\J[1R4Ω8[&*֯И<W%igQkǐI܋LXxڟݟ37 ~o9m c[I/+,4P)fO_$>\ȆhP09qD? ;%w^Eΰ v-f聞 k֫O|ÖsYu^u!E`52$f2Z]Uqf ب a?)%VppbLlN*77PL ]<Ҁ#94 .̂X7EH-H ݵ 4j;{(KvLN%P\Kd(GHGt-v}ue0R֑1ڗCT[gSwkEU !~~"(8gX%D$(mR ݞ{'p]}eww\_ qAƀDLp5ߠ#a  e* h)28ɦURQZu`@^9Gqg+1 & 3bûs8,bs;apF{nG$ a0$C*S R^UKx^mt1fB@{TYv+Оpr5(yn[*z~ZO> =BHH*!j֞j 4F-X>p;8tEK1 :tat#.,HtO9ILfO`t6`ACM Fh`p.ASd_su#+t A e:+0+ D;z= /L:)ҦVLB.=V ^o+K K/ Vҵ} rˈ "fPŲ!:Hae yW"1aRHa 4"з>QMɻZ[al_6")+䂹evEpgy(`uy:KfK )QSXyX ]c9@ŗPq#NvimSDӑ')U uc.f$Ӈ:lt-<.E5 Uqb?,dP6}[hiHo2#9h3zvN#L'!U4u<RyCԒHIĂ30mtFMf&f2&!6ou~po{BN!Ga- ~0*9?vFVKKW<1#R^ TZ1) )滛Mπ[~'mS%Qt ڣ~!7t#A05hgFSQgLYihPh?Hvz1[JOߐǫ1ϰp@`ϧ]vגhќ⍚JQ/ 5z~9ʫEi AU0 /)$!^2_. AA7*EC.Jy!pYJOh4o?:Pۢ͡Mbn0@66mc9Ёz\iKʂثRkAzNzu_4H@=XI @_q"]UORqڕ Sz;|ڷ}%!}g 2ü2Tr2%q)R9rG]]'XXrĖX+AxHyQ躡#^%fS!M&CUm'י Zƅ^ƣ( !&2fU&_]Ү} >l3:߆Ζn̺&zNaE+O/˴ U\ONwTGi0)_4{󟀪r޼vlN)U͞\$5 Y-QC>1h<F§уi5I Z(S3)a'K"Z.h%D=3I eǵ(Qt_R]1)2HyBf\')(m7MdZ#Vn voEA7+5Q4vsH&AqJ(#tJ!H _\#pEu%LَDSbe%nl` /P'8kiD̪b_xWf D8n&6=_F-W>-~LE2{kKzxv)V.W{a y+:DZX >=H f$#8]>o'۟%d9x-Spj^}NհX.vU*z^}.2|r ?Icib8+=ͬޘWm :HN\ҿ838"m.[!Z\?#ô̦Q ~L|$77 ԖP2Rot j5hr#^Rh:NQkc}\Uf"k8@՗ï-6] G#G0i=T0A M]P UbJVIgBki5 F:2!1p쥨q}xF0 LE="P1kD'Q g2qcLQtO8a]ۨ^_d܋*sP[9t{L@C{ali!Ӝ`@2GJԫ1V?T#XgHq*7!fP~9 %mź,zy H9f2,=I|R1Z"$HQs4pADx'ityCk9 ,ޱQ G{Gmj}}sNj ;B>PAEHͶVPU-FH7fyTjy0D6i ^Gմ8梄q 5#WQ obRciׅEEU. ZcBAPiA,"T*P:/zI9"fP5L@p=2_> %tȫRIȇqj,V I/8zNI#=XZfȠxi5!yPɞh% e)h(7CYQqYȳ<ܒöLđh'K伻o\EYpUߴh]pG(S?c9c<`jUPۗ9%jД Ϣ ڷĈe+^d|n1P٫P6;ʍX|>f}/Ŋ1Ft$=ˤ,;C=iАS.D!F,/xFRä"ŬUZ<[:Luީi:o=}nZ'k.:Ъ!畯琠(=>i[28fHf/O[3aHkPWj5*bOJarVs E95* |eq T'hc{$hUcZLv 1o$v-[rj=dR̰n>[#P9j!Wl bkKO+߉Ě)N.̱óa0 MJㄍ\nBa&_Nޕ]Yw*PaRc3  ˙T ,ܠz ^HT͚e90=`F"Ss%L[PzL& g*R(5 rh#fMNX?PL읬%iSC xNqR9֦t@s{C֨Ja)tݡ1~&4Q Ru\1Bh4X>DFU5vZtI3~ɕp 9rĀy^vE2&^osK's6)uZgqXM8'KJ[dg? KT,j )Bq%]V"03]R Qa P^ Jsg$:8 51T5clւJ%?#D 9ӎєF^eQ mLFUr!P-KYY3SS4Kʪɍ)ё@$[gFPQ QR '1SWLt1MiIf06-e+o.dǷjWnU !eS0VR+*95V]@ ,\I~M_đia]c@$]y.6m}l*ڦ>){GIáN!zcӣ,! c%|!#Ǘggx^RS|{|AjNEJ?UB)00M/`Ai8sr*$nJa]+.D5ߔ#p!K(jK% 6z Aj7ѿv,حrRiEAf 3Ye-|KPm>U<"kB88"z67S~T r`@,o- QŢCr BZ u aMbiGibtvV9nr2}S^'.c’d<-g^Dͅhu6SxY~SJ@GLt!VT9hEV1b}@ c Q!kM c$FMN1IL' oieCq}`VSS>lJYW>G&"+->& pUgW(B1>waBq4(S2"(HQ7Ʃ7L %^ =;Ӻ,GG逷f1l|W& P܎ƞ@u>]Ӌ+iJS .|h Xn"].E%_)ylUFbIK3P>ɨH |M`qҨ)4ϵ|ǠS 8Q^M+sK0R )ʮ ^% @?`VЉ}dV`vq_"tS*lv tEm*QmeD+sKئA̜oGe+L7^¡ da[: u ҙI5C0SGBBSfMv[$dCIJ&5d]xTAcbJn[i;u Bqzm;Y?LvbI$76ȏ6: |ss aʴ$ G"W{ FZ1?x?6BD&qC z.m46Uཤ\<)S~[K9-WUrc~q%<%][3G.멛^'3OJ|߳vu6.t槷7%![uPL@\oŠwu{ ǧ|S*E:O{]䟸 [LR cjetH\TQAUJbj 1)XGJ@ +tn@,@%h퍨R\ ٝo0t.Eb=xQ,].1^FF&PP 8`n,C' P NHYS֒HzND%§ks A,rcZAAq%f" DN$'8 R^֦]V"l&\{]IFqYԒFrHYnBשGVAKӺ 8P+7_ _$cJ< hG%rUg=rmb6ZAZQ= \~!BlՃaylM;Th՞a&PF;ulk9+jHb>M[zG^jqf-8`~t(0P[JC=<e|RawV0ϙV|H RrF=HU+I Tiz@,P,Z/ܔ͋L=eu[Y%cNČs *[g`$ Lq,8~fYm (LLSO.M@ۨdX pB9/g4e7-RWPպ;uEL!)h#wsb CiK`IEQ}AjI[k,G)wrx.57S>@,hy>5ARjC%9χ,æѓF/Z۾Jj5oMRXݾz.NAn,&NEBڟ!(~J,F{c鴑P{qSeѻTyYLToǃ"04[FJWSv3L4iY'O 熸 wD:0zp`BkRǺ8il[^cZA&[!1X1{'Wg/IH?_E =H, ~ׁAU޲~ 蕞-vuϺ'Lyr (Ε,Kԗ4USKY%EJC'ӝ,u#_o::6dG@RuE osM[F9ԧ& x3dӬ>׃48 ڝ r!)M?."^/hS dh T[.8n0jAzjt(<3xVV ӳwE5oWh,\Wa'ޤs=&u4 $=teR}oߑRv& Ik|0Sq]y[-3d@VYECpSt;們ҍn=Kӳ| )+gH7V !Ś9ցݒb +iJJFyn.^q+(WeG ݍHUq;KgꁄvQi INF0 1lq˨KK2MQ飡ҽN|r]Ni (2T?(=:$D9Ll1Og>kY[ kS/`MLz 4I68c4٦dl4"=rvAZ a!(^.W9srG~}Cb-^f.y d2ŬXsA6!Zy4}ިp#0aH Oxޔ4/3O9AU2TS? z} >I֝ӳ4BVB!#@>RrsJRj-B|oDZ3 2lDK7+@C$dQwҍ+8dFQd}Ѡna*l.O 7q9*¬OqH@;iB Rbh LR#UV\]: [d׹3Pvy-\iT'V1 @iA@4 ׌' h:NS:Jᄋƕ,{d}f˛r!&穳FuXEPYKvver ck&VTj){2t2UWa!jhD:s8jP/LiJ{.f"Qu֠X-0HgRy$\qD4'Mo 80щ(StZi9ACR _75=\X3ryuQYU"zν%9ԧl1(9vF\=L4sho7M}QB]s L_P-=GW ~g]؛z|rhT[?6 {i{UUQ>ʕW"qK`M/d7?HT5XY XluSV^LX)9/e`J|EiRaCJ0H:^H;~vX+"ۧp=1ی/Mn!6`ROU]eЃ[k)G>Rl,rEHzq1 !N\yg/v`ܒ".ꞵ[%kAs$X(xnثMw2j?mY`jU"Cxz5rʟYyMB`;},_ D MCyYJBI!qg%s5Ő}`F4ᰕxXK=@РY[pRz& ADз+*V&c02/myG1Z294<`8N9P >K@ۄll-EZs+傽Q22!E=) a6{pLq蚎kMOC?KYS+4#i\Ӽ: t\ #鍊$KKM1Ar3XfU 䪑ޤ beI;ip!2cTo(M @.EtR,}+ ,XX0`b iM9$LmzK*nkCP n{[sԐo&-wF"7GE%WA1@;G2ARtkO.1M=*Gs4+Q:=c5TȄT*fȌQQv},k\+S 8utU h}$" lptͱS"yKExȧk#o"#Pz/. A\-NFTIi= \ &T?F~ïnhKdMsVCD)+0a$uD2HMcS?ۓum^ c..Ƶ)iӬ~b %Q[S)0ff\Oŕ7 -\-cKE0OZ8;M>}>!HUlS /i(?eOck#+ P-t(jע(JPa^,|ask\ʅZN޳BەԃʡXO@ :2A 2hXjGX 7 >H!X *wܟ:,L>%;m5WpW ؒz:~!ãBÝrH]r ݩq0aVwX"swl,K;6(=Li#rU^KƳL@@(Qc ՄZ|BeĴo,B90F; dB#7iQ ۻ+0[Jl;yAe2/ ꖞ3]ްGNƊzlsHeby4Da*Y 1,eUo]sHCP3 tOv5E?v쐫1[֪ldhK>:ᜩo?cTf`Nc@(|Y#BNv4=UGDlZ}^9lgv=EWԅaA|l Ti[E ofg]s@p=*_l[~̓R5ɳ)}:0ӭόrԸ. 6h?gBn c@>NLcByLܽ%jqb_d &ӜSzbгSQ+ç4E'!#caJ-2`t*iʔm )iCgS8 ^ 9LRrJ0;E_2E!e P3?uGIF.Iif ݷ04>Jtt(oɖQc?!Uv"d 6Eg^m|'E1S8k9(to׳M~p#-ZKL1$-(bC7weC K5} [#F*Tђ<38}T,smLG-Dչlb;aM%'fM41`NC Jo!&# a=Au1&܇]J-+|byrOluyI}wKkAH *J1CqS:M^ RMvg]m>"=+a}vy윁Ka{hޮn2y"%3!VMIKT~` }Xe!oFo.X(Sv3BCQFYwawF/!Ң^r-Og ߕT y ԏYn(z&Q&^~[zE2aptfXq8}f)Qn`&sЌUj7 pPO-}k#79 43^vpm (bj%Aږp&qxt'7Z*sBSY=F)˧\U"zA{T#PUrxƄpB4:7k*{IcE|CJ1un43:S!E:¸`kRb9}ŷT+!h1?aR4iH、4,Ali >B2Q#VLJ%'QZrsa$LGt bpj-6-5e,I j {#TFBB)gX5RU! :€͋Gx|H#8H CTRBVΉp7Sg_KK\A*#-K]IlO^m''9]kJY0n%ơX "˞=Oym4gD;@i I.E3\f3}RW5Kxeȇe Zw@$s:yvJBJX.,^z1&װP,~K70pckC,2r5scir6,PDdW`p>X1TsTcW5l`/(FȰuEi8 o<)/$(( (C?..©^i\A71ΔAhbihYo ê9#KxHpiGM1!4 ݇!joqPjTz: - a,—!UCk8Sqll%-&-`%T-SSH! F0|hצJ*J^ eς7Io9ZXd_ÚLzf]ZZNP1̍dM) ʮ7'T( lW@L⋸ +~g$zeCL%-Ѝ 7= W~]` HVrRQ N {Rf׫)EpJׇwHx"ի^%zx ˶_Ur( .%xmV7KZXYӄxN5 =4y͡*c<WgߩWæ*6 >!lH;o,cGVE^FG.{ :Xѭ8l2MךR2 $U:MGD+SYԿ%J>TT5e"qL(*&W >?LƊ w/aG8eBuVҿl㏖4@RﹺlOeXag%3 FB_{ir\~ټ?0ɾjFk^n;sNq<Z˔:?ZfS6ť.k'm7<k>Y)0RU:y~ cYkk#R?HW<?27ԤBi |t^^~BQ Ē~.v>^NJ:Vo%Z!JdH;E]'%*9٘*or|2q c*6oZvDʈOm_/WǠ8 ʇO:?TLUdy Gx$Xmu%)s{@.ocr~ـmtP)JᴐArbeǨ݂RI[&IsSި{5щBߟ𬠆:㟡 ׁuz'11PH*jM7=ƗXOï9nA_GZ~poyza)Rs? mݫCкZVAmD#Z"v]1USxeDbf/i.<4_[єƧQi)ZSՕWaR+hXI·a?,)P RnpJ˦5)(|S0 | 򚓈 *EESkpಠY"iSA{(9\pM=- h<䓄Di$ 9ڬ-tUCG9,DE4E>`kH+wH3K\A w">o Bk޺=Ԩ h89ǘ_M:HͭYl+/A۲B -u\!6C}Z:v"5oMq.Pdk4FbMdL?iM|,(\->­ w+ 8Uzhh&-LY*TKݚr]˟Ht]:eȷ̕ڽ%-40>5l^0 _cP| ʤOࡱj @JDo1:^Yl%d,{-# kݒK{ܭU>XZ 2MxJd $5y#ܨ؇hkf10ЩܞDͼ3T]Ƽ'TX8qWpxf#1]{Vl?@he3.=I)Ol#lT oX4O+;좃YTޠpz3]niMX&PE0wA_[JeX‰ 4@`z%CBu-yC@_*9YVJӨώeyqG8hrf˵o8rWcl,]BC[,Ť`].8_O?ZفTwT֡Ks)"F-A SG; K;V0霴(]4)*19hJ "Hde^bֲgP l5+rW|'F0DU+N9^VNN -sz}~P˲w q>Uq-Cueܧz._-8:T;5v8> \fl/-u7 2m\>7*O@ohOGG:pOJvZRrC>$Doː=z}tgm+RrP[BuQ5n>|؟G̾5{0pb?ĔV!HA6;twcBnmN0O(5YjN/8ʙe3p<IJ}u2/;qq&Hެe-.-16;J UI1 a4gV!E3xMpي԰mޚ+(]e(#nrEJR$txԎ\(1La2i2sYm?ҩYw#+j-spR>P<`b6IZk!F6&xlD`veu:_iYӟQ]1`t949=gmgh7j:{9DoN3Vjtߦr ӿsou Z_,t+3u/0NpM4)]j<0ݔ0f9^CNڃEpǦ\$*Uo<$v/] } nslj H"35Z-0Z)H@ Iij>,sG`wJE۸cIENDB`gruff-0.6.0/assets/city_scene/clouds/0000755000004100000410000000000012540054077017602 5ustar www-datawww-datagruff-0.6.0/assets/city_scene/clouds/partly_cloudy.png0000644000004100000410000001614512540054077023211 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx{lTվ{ϞLGK鋾hJ@sz0\%&>&7j Mㆄx^s Wo9,U I P״ 3}kf׎uft(3ڏ{K4$I3o.ojQ06?L[UVL&[ZZjiu~%t!g`{p4 zUl(0(,SAu|,Zk7љ3gu~raᖎrM~ܪPF>eEik6..v@,_t#Gy͚}$|Q[h?M)I7ҤEmBu73qr:Mc)!2|.*:g$6=3;%3Id2L ^ON25kr~w:A}c@/:\PU[(:Zf†f fn?p&ޫi)p ɋuܜ1m6*./Njn +ph˔ ; &=-.˄P%}oqHE Fpd*J"ff!6J0r}NN%p HE@ _]ʞ L SJ½{$ew;3Ǹu@& ~B Q.H~?CD@:HG  @b#_qKҍAhThȺ@r>h|ud_tu~,]."A"v f 4Q}Ag{H"E@ Q/<^uGbJIt]) I 9߁Uq|֮O`YHh}HQ2ߤ 躈eWI⢈^4tM\rpY!>m,J[[m{Q);| {T u }ws~:G>ӳ+++/,:100Ç?kkk.z#bv$Cu 4vUdNgAqvSOxuvYUq?2+QULm{W_1}yot]RX9#jnooMQh| =:)C IM`2AuQMVhuVI|'?rWEspCP_$59uڊM!~?M6OLQwv ϯb"uk۰a{,yH*tGJeQV˰ ,\҄SuspϼT[Xh#}.q.^̴~T$oj}0ؾf@56ɖmgZ<.c-t2멊 `咮gN=s_Znc=7_\ףѾo>\0-ec^zuioBt]ֱG g#~\oϬ]a4PYTChT@Gآ.luDgΜikll|Ff`9)9ikOdp E.WLet.,mUOo!ٚNNk7a_~rH˭ ձP5PQ"45Ctqv;H=%),OŐ.ZpK ]ZҵD.Gy͚}$뙷nU +惭I7ҤE=u7X^uVNNmo46:*NBW)9s v'H30pj?D=3\#ڄ 3N<6@\ IWf[f͟\.ב8Civ6PVߌ_>50K+K~!U+9f fn?p&+i)E,D]h %S; [loVqy^صkWFB`9:"R%q`p#/-MzKNVZR7tH. E xUJc\?_-MMMuww?|N?a˔9 >$iyxL 6 V.˄EI߽{) I414TyvWp2ڧa :6>''g U ǡŝOl.͌ߥ402H, ݻ?~@gz`$IٝC'Όx77E1\i1 o2.H~?CD@:DIX>,._qKҍAhThȺ_&}N> Ǜ`].ՠQ ;Lx&:=*Ӱ/(v܃ qB~x)%uҭJt(D&)$앩y&{Z7  )~Ri4^|֮O $@ռWժm݀;љ9Z 88 kɝ:V}@}BآILg_Etg=͏WgU^8{-[ń^+.~qp߉߶wq%.'!j}/q_>s466)RmC!>  qy_Sܮ <啛(]ꬒO=|op)&'@ĥ֏tx/RP$@Addd[),Ģ,8wܿ׿o'!7#p1{ڌW38{3/֮/+4-Hߊ.q.^e,35rԩ #;՟p@W3-AGj0z@56ɖmgûs6M4{=K3W!n+t=suڭM5u+]~W_3S9Sq,ϟ^UU(ߌ_ӀXvDu~zīg0(,S!4^OlQqn3gδ566U#w4'{Uuk*MUz!.\"08ull=4ȭJ.'w;mӿetQL>a_~rH˭ ձP5nx /+MS^^^.&k Pe*Goiһ).T\o͠]wX:::viӣSR%1ܵ>nU ο+դiҢF,^:U'7i3TŶ0ЈS1@e,,eel *8k?7#Kc=:[ ?Ib'; ` j3ڜ{:9xtMLLt~w:$؝];4UzW@dݹ'P'w;=tv7|"Kf fbDşi)E<3S qsvjga͆v~߱c^صkWnS]5/vh@`(FN* Ot;uo&W)J^1/8ͷf4455}dss9 ,czݒ,4m:G@,I>$*zbϴFG fs]eB芢޽ ~#j qjlTtW9YkY9,Hs+Z999[ca^HCLEZV?o3$"iZa eסlp޽8˻ŝOl.͌ߥD@D^ohcnLcܣb Aަi e,0I:iwfsBW;؈hkOhUbh삋iLYkۯn $T(߬QNF tuY4Q}AL0&JjЂ5?п$2U;^k%J4wBR[wT(ԙJ7kMQq@z4Y%,<ѱ._#'4Ma^$2.?g5j6T:2ѶYLI^rF *<4vUy/7I~։ӳ+++/,ۅ݋}:100Ç?kkkNt C`q_jtugT?rӣZURr>;dG~7 qbrdN ˊ1yS8{!3illlS0~c| =SWBem.@!p 4! [7;7>RmaJb#1/?r]L\i-ivi6@Uz\\\HN:b`~LFFFMzV';>J,xַmÆ SȨOPhlLG,{ fd=rI3W9{-TS\Ҟ/Kh7.}g>i ̣DRanWUUewl_jdKK˶ֳ\]$TBa 2B.9]StMS[;E.WLeUCe{p4 r>Wɥ?Y<{@fR QQ].zbMgk&:sL[cc[\5.؄ 8w9GoXVkJd%kʪ ̷ѱ._#':q"pSme\\~sW+ͿomeMF-URc&#w|0%z_/i>zp"p LX$$W;uO=reUű,ˬDV1CCtgǮ_w]\}K?$_t/zqb9®;ǶWnztCJ*V{> g.b!7`Ps_'IL2; 4.o\!38{3/֮/+4-H_`9G.n3E fQ믲^=%]zo쿴vkSMqJ{nG}W|U~q"8!œfDo wz00_r$Z~!sŠ;SX;ܥ;`t~H 0nЎd(a-B~H`9̴" :2t @@: @@ch͛IENDB`gruff-0.6.0/assets/city_scene/clouds/cloudy.png0000644000004100000410000001665612540054077021625 0ustar www-datawww-dataPNG  IHDRdp}gAMAOX2tEXtSoftwareAdobe ImageReadyqe<@IDATx]mlTU;w3:QR-ZBh*1%*M6jJu?lH˺$խeT%$C7͔a[;ԙ۴MO#&|j0_OLc448(\L!psC1CzY=$6 د_ gR t>'Mv v:#T 0 !;/~^¬PϨn 8*`F)MHOCGk,btd ?lO655CQ0{t9%Yؤa _a}1>/Nh5#Ǯ1(iw~8`PQ9M~;nLwEuk7s9`kz,YCruxd*JUz_ (S<0=p,|p޽Q0k/|bK=ʆ@bگd))2݈J :y'P0K^ۻnfd2]/$*45JѮtOE\|FWaUrh~u % 1|)I7RI%Z/$*}nL[3FtzP~O 3 sG@{VEujnsoA_&LxeJy)DipuwK4}wP0'/<^uGbHIt]-~_mol^n9U}qjaGtWb~bןm UU烥mh^єC)/h.p#{?PvceQƊH,Ҳ_tϺH'dJ8|UX.yC ^o}y霧B>%ALkAE,Z1pVlڔ!CMu۟/j}R_`--?ڧ^ShC{f2).m[G_|{qb]YYYe*x~GooְCBo)kA ىmڧӌx_`--?ZeY͏lxpM֪ӹO=|_r'+6w͢iP 5=Ъ(JatDV__ ףkѠ 9T $*ID\/xK~E&˴ L@ڧ5#8{3/l(-Mp ~-U sptPoԩS7tzcdff=vG=n?u'tʤ3XWW>uh:t`Q1L_/74qss4!c<Jj6Vn4}C%4?~l?TmpѼ^׬6b 7rm8Z,1X,.9 $_ٯڠM{X /kFf3yKOOoci'*Xv^GFRuYvugS:Ξ=G LARKZ,?Lj~i_໨-CLKP܀]K4ILgΜ!/4 ,k7$Z$eUQ֣cE1˫9w 9L®Yb/_e 9/g C!LiU~C,yc34=Ti Odm  r2"L ?y @/NWa~+k 5p?Rٹ5=& JQݻwA@?T~v{`3eG\x9\ a/M999Y" JR?C.-%c(b7F+ A-ne{]~:%KŞ*IRv{͓֌X"Ӎ(4Ƞ?:%G/zE;gDpF r:q@+n~?A:%VBFrRT*4d㝉qa$|u 5 K Eu*q)+xƈN A? _<4m'lQRR#~.  feJy)Dik8事ansڃm\'Hn@)'ŅlvKtX#ý@ڍE+R3#]/8mJ]M_#'gN_@մO+8u ~c_]Kd|V)i" 7yJl.{pX!/TP֗߳"Y.|jX/yC2^o}of\6/Ai7U>ƌ?)XCjgg箬ɲl׉|~ÇzMNf[0w"?Q0bDlSg>QP]k*<^]ZYbLTf \DGF}'}ї9m 7[ z԰OA`1O҈B2=Ъ(Ja V__ ׳͡PB7v?F2ZeV=b%\|tdG}7*9yܰu4i o רOR+$n^̲~nD>_IGLJ`[}E}xh֌MϼTSXBo4å[烖W1D2y6he2՛>uԺ:.=Y7ӝɬ?Qq~cş++ڬj|>ȁ577h'76Cc搦y^WIM|\|U4ʣ= 4 V3]pa{eehg2oKF+mС?\NUf r2ꂳ:rEOuV={-O/h2mxEp5♇?H桡 rlg%&cY@o*{G?9:*3+T'ܑe=>>Vz ;M h=;p2b (U+@V(w&{ sΜ9C^fi<=8XVq2/GӘ~?eN?VWiĤ큸[|?q3Z_{.!N@d 덺ozǎ{a׮]g(8U'w~ێtsOѳ3RaVgO7c["5,^7!w? q|H.FIG#dSSymHbt9%Yh*v͝?ȍ'~Sq0>&2R%moPp PQ9? ܧYMy'2D?''7KDAKjHLEjX/L >,F|c8A<{GԐ_Ė{di'n{g5 37EQޑ[iґA'oSUڴ$KRv{͓֌X,(8|*E<MZCh'_B?77<\D>ڵ_BK _uJҭ>*夨ThRrIv7iWz?߳"YN8w* rG|8G_d^:le#>GNhbÛ1mk;qV,bo+ŵ ?w_~,+t`IcGYkk dB=Z~BbIlg>2S-] nď2jdV=b%\|tn}9׫~ ):sz b#.xw3=Ъ(Jat_) {L/L~_?E#p|cw{"MϼTSXBo4%h  H9yӧNZW^^Y=k#33lvG=QvOxOU&Elٸq{,̺<!{=5Ρ?~?JYڰߥcҤ 9ĹSOi琡yVJj6Vn4}C%4ÿa M6L.\^YYy CWmeYG|lknn>ѮXk#E_~5? >}{8O ܤEvƇi&ŋwp5#/Jcnz/==ڪbzeJgUt=^LE쭮ٳ oq|!.4IfƄОy"7Obh@xw%dhe݀]Kθg ,0̙3;eӃe'㣟DXefjY*()"kb ]TK-W YWF~K1A`(ă?S%2H/^w~iz1qXnDek{?FCB\}M ^ ?ָָOk<'BA`Qz{^M/ b!*pv|)wp8lß{!Gb R/9w Lm j5LŌJ `jŇ\PYSx2B"K΢x6 rLYk-4%!qwjea-j/$^G}jXaA\=dyh!-R-|p޽_Rxn0 su5#6t#;+M:2m/y/z@ 399O8Dӕ0ME!_uJҭ>*夨ThR<ǒ6z9~ѠF้^;YR ;HX3FtzP~O@~ Nٺ\1G:]-A {eJy)Di(u8麋ansڃm\g%7`R$IbR>gEk7VUoHMLtr gG|Dz%N'>|O``hЁ Zlx/{pX!/TP֗߳"Yv"U,}!-~Nq~ѠuG[ 9mڧR32 kzzMŖǫK+yY콖8d\DGF}'}ї9~F__4@lE-ƂD77e3^V ZUr:y|#>z0h#e:`4$2(:W Ih) &ې&@wsǯI[}^{ٯ/,)?pzqFfx<|^ *J_6i6?h4P~.Bj߿j5۰ac:y|){+**2};YCf?}ܐgffؓ'O_`dGÂf2Xaaizz:A"> KЏ}`j .75Eǎ{~vX헟GiiZڨ\%7E"*y饗D9M$98u₎||w78u~UjYuu53aO8::*~C~P/?\pa-5p_ _m6] av}~UDtC͛7RRRm||\D AU|׮]{_񏳸) gϞB Wܳzvmv޽'?۾};[nL'J:q^ooo_j zdKa~~ p]F K:܇؍7J eyĉ]|KT!֜I Ok4Ro߾VfX!?>d k dۯ/P?eb{__ÇOϞ=9of$AFzUIMT($5"s`T)祈//gVTmwk/Hde1پ#KM&SΝ;+kfsY:_zzz{:tSN)2sE(Mn[[F[VIǓ'ܹscy8vv'_эiQz%;ݘ RGB9L-HN`NtV8Kmmmeqqqi$YWWW7K [JEE\1P=zcFcu>li=jiD7u R~A_0ITɴ_:It {qPXٽ{bWTTXsrr;ɓ'R/O*ӧ*#G8qJD+>ozhhG/v4S.xOrt_X{8w1nN /MK(E?ɩgCOߑhUVz^ RQ>666UUUY _@ݿ{7RdO .FasoƛfpԔH^/'iooO,2A͊q_If|_$#ϠtZ)zKI#y 5So5 #Uӟra6S %wDkhWkM:B۶msڵ+\hZV]]-%zPtWWЗ\prSP -+rywKM d SEP+-[@BM)p=^YҥKb#~wEQv?`K#A?.d OY& dH=RDZ:tիWRXiijqe]F.G>QFJC%Jaͯ_㞒@٥C__ %hcQ PǤ4̙3=n) @ ]*8tT00h+W.2 }$dK&:4ڐF%>z((k%heR%ij-*3٭[v"Ww^k΃].GYN+oITMRQ~?_<Ǣ֙V#:y@D->ozhhG1v4ߎS.|OףXNNXȞ{8wq2+# 9w[, +;:::[j&)2]%fQ9r ʕJUOM$*N#/JljjJ$x^ϴxz'qnRE E_Jƺ*kAAQFu ⺤7fTнTӿt/u-F0%?q;%?7!1)ܝbbbB &|?M?%*R#7IM,$m7m&eOkr3՟>}S͔kp)eL1:YNI_Szzw$S~`P} 2wvm(|I($dtL1ʏ!EsRsVѼ*)Wib#@f ǎ{+n bZ_&[I~,;-?m(2yw"7h(usi~Cс~`/?\pr>[mE # Im6])s^/KF*HTPjohhƒ]XxޝyfQJJJȁ_^Mwڵk?~+4} {y-o{A~OKk"Sͽ{tddDp}v1l>P?-O^r< IϔS0@ ?b:ީ)6~#K=jŋ4vF ,ZlT{A~V@d#ellLs8AG]z*^Fֲf:&OՐA~G~HJ ևڿ*577?~ y ŋ,= C(]i;͡;sCFg= 7=4tʕ+a'KiӲT02L ?"_LO+AA֣S\.WD#5iDg {A~O [nм{WX7d-bVAv A~_'Go޼ܹ#2v`Gj {wբr却 t ?/~ >:݇؍7JP%,XC8q˗/}kbC8Ok Ro߾VfX!?>{_7~YkGtnRGK~Y^^kfgg'>b8ӳg~z3圸'X#$%殘3GSgKi )f+ijjMG݃J!?/j]ɱGГ*?E'0L;w1e"ؿ~:vСg÷O:u~pppobєym(FѭVǓ'ܹscyhbSN]k~* ei{=?W8Q:&Yp8,ťn kRJ}f\ZѦʏ59qH^bpJk"9 eB~#D/"[]姒,^ݻw-KyEE5''g}r䈃+G"|CCC=<9 2aNc~Eݻ 'Q֐'"PbbbB D~JdZۅRG~L1<&oSz&M~ M#}R4'59hz$ELK b@›7;vm^7쒊4d2B;Z39d|II'5= ~#_6>~pQ^HEw ay$BS(n8(*"Ԭ( 4577G$E $ۅRD~Q8ˤoי?E IBo۶͹k׮pʬjYuuX?7*Jegg'.\֪i1v!ԐbZ/[堽d :aسg[m~CG ":}ܼ ϻ6o,JIIqRK˫8_vǏŖ M325^dw^GFFD o.lgR~{\K\.z{{{ejCC,[l< 5CԿ{eK.ѡԿŋQh4-\~~4999kD]y )#m1p-zURhVZZZxZ~~AZYx RhZ蟦m577?~ e¡8JE)B3g1SLB#=+J5z/XB>qAm]N39oF6dueu 7&nK^+~$;B^(G$? %,O=[n;AW@_m^G9f7F\s6ZdYaBcWH~4U[3=DPtO5lkjio:#/r]T2_K`GbVNI38A-SLRCDMQ0?=,ljio:8]ee`.bGXVI3TBSLQs @fмKn<@fA߸ݠW~ @fn=ᓑIh4 kvL@doܻm7$#@dpt[x|,R 2ޮ18"* c7t{7{ F3@X=m7ú@d4wPk :ydh4c 2+d@f7p-l BǦսkv#VM- tW߶1B Bp˨@f:T7nۍ@poXFh4|jd?&O޴wuh4P=7SD揌g nu ?FcSؐY7 F37;C;ddlrBY4AЇH9k4?22No}֝hA _ rɦ&2+t B@U!45Y} F3û&)WiH >#;;,Nh!+ȨJGvv!4AӃ'Ayn5s@fҧ}GUGvnny !4AHfƣZw Bj`T;|s BZհ @V3^9 F3IcW5Ch4?y5ӻT}k B|հO@!c4&~!4Aƣvp !43K~zZ]Tc 1#$6|9 FǠ^8rǮٳg+<#B43ȕk~c$ٳ[A_+5??JKKY 1#-jh$g*h?|>??f'y0* S @ |4 Qw^sREƠ_]2bǔ6^ 1\B˄!@ r4 AF`/@ Bhp!9}$B^hc$N?IZAn握?CY431XcŋjP@ rhc1u-- WGP$mmwhec參.<QA握?/'1P@#,1>?S 9ܤrA/bcy?@Ψ Gǐmm$C$@#*<C0ِCR5H ; c9$vCh$e3 ce#!Ԝ Aπ?::Γt5 F/H:9 @ Bx W8ttpzhd{{p4sH))hc@ O=z9s,` FHy<xG1caR{ FX 0z?sΟwwbm` h 6>OPbm` h :>L^LhNf4 lY2 r5@16|G?2c2#:d q}4?H@ r@(2握? ] hgۃw}4?sM@ B`;hZc"SzA"C-,ZDB743Ae7@<<A~E'43]RxFh LJ@~4 q`643hczE(43ȽA:;?(әOAnAq̧p xFP4(9 q18׸䏊7(9H q=(XJc\z  ek4P-%EP/@ @`叹odA4@1 4 {7ӅNG0"+1G;g hf{p hr(*l!@ r5??vaV fc䏢"SZW!0+c䏢 4BaU c䏢DɬBaU C4;fҶ  WHa?v̜)C* FZb6?WHgi@1\dshY0 x'GpN V-+pE@ rmP t.-An !A.I* _=*AA|BBvah*AJlBH/@ r{T * i) W9X*AعA d4;.]*I^V @FZbҶа x辣cx珥ڴа *,z {4@G-42ȭA>[:¤Zl:@ ro0l$GHfEܣ;tN HH5{thXd M0 x + %,$>FeBÑ+p"}>@'\I@8G~էBhd$&,9++HVT  ¢ #x+H|[D'+b-,~A? ó/ 'Od7! F@ KJc4IH\TncoaP)h cm|_a%#6Vm,b-,*uхp5oD?~ #=ĚR@ þE._"#<ƊR@ þ%O%C Fzn } ˞*D'=ַ{Ft%r*Mhd'4E'к02 Gmg d4]Gj#?VhSoE/@ rN-,Ƣr?Vԣ^' s'VB L*zb]8*s2B-vCnj Mz~A8Ӱ`%z,1v:!4H #=462Q@ \40 Sfn3ȈVQ@ \9$/Gh?'9ZGVk0Eh6~Ejc%#c P,ERQZAFzhi%*\%hgӄf 4OR1D΄N^43CvADHIu4$+̈́> ~J[sH ?'hgvAH hg42=*MА h!1lfRcGGY+z43 .u'DiR rG|.hg=r Bw`N/ |JLH 499LȞe3@C>ܦn[Ai?rHX39Z![̥А |.8Dm=GA?bKAntcQ2Gt11D ^@C=ܠ4!yi? җGa,:-7K@ Dvة°ͥ: zyLHKL] vr4$UP Wi:N80 돂Y"ķFRe zqa0 55/_|D(5?j¨uhy'tM@ rƗR!Vų.3Ѿ#= %` >a:-6ODFSe xIA,y֤I}}t:@>_t28'w 1? *X43u口T)t緓? `SɓFx1F!Ar ({jBt?1:?1 @C<< $gxNC/ףF!An343hUyhg;4l@q^D}4E @C;ܧ(h# hhg cXj fc4Q! iAF0)TA`w4 A @d4 qܖ  vGc䏔ԘJAR{p4 C @d4 q|#52@ c4揍 U GӨ vDhn F3h e1#: F3h# h4a?Ҩ F3h F3h eqAhXf1? F3h F|7P#H cjdd4 A @#;a?AhDg152@ 2?y责< cjdd4 A @#6Majdd4`bvvsIFAnXq4`2%4S!аa?AhX^`6?.z!X%%T 4<DUWq FZa$(a?JJ43 R3h參6 xR̜a?Q!x4ʚ!S0%% 4<3h?pv<g Mb432Ȉ.FD$9$A_ @C;ZhO 01J\Nf3 4<3C 썫FH0W#HǞ@jd*}/-dUA_;0HǿVQ g$PGV*/2l@{0R# \k?G$.(kqّLIgBwoOa43tBW*&( ڑL1i+Mhg'DMc$C>%Dzdyv 43?@3f.Õ8H!A)6?@ @d U^&c^ U2@ k"DObR ( a)kG s`xyDb=hg!XG?WW ʍ"I(q<@ DW$_3M? ܨUq 1u*qXvW'0[Inx(Q8aV&c*s@@ 䔻?A0Aa7/qL H\H!hgxַ=#Kc W AG v4@ "gyʆBu:]󇍡U 1u*XFyhg8b<)1huqSRO>p *a?\E`?@ cWA{j FkJilztӎaE(Lcj(UFyhg\X&Pyp7BPACѤ0#B:gWGyGXABB43劄FDhjjjhh"*؀* M5Ы_}݆GQF\\#hg[8|N|3;:0PDN ;|78FyG8Fd @ s5K0>^g,*x]?뉜GH(y 8zNGr>o L {>dX#N0u0 8C O1{w)oGFAt`] AyIe5DUG`켁gWfp+V!GaAplbO~NWop{ A,?V4 2S7脼&.i%֠JրXP>, p 8 eDN+כ߾+|| *bGyE'@"42GߏcڄD>Ү5XgP;gc 9h8d#cWwrG523`xZX"AMDApuB ܯi0|;/|GK uG'$j}'o^K0 |2 Z5bcK uW'D%jc = <0ķhXd\G=%XC` /P;GlBDaApuB'῟˗ѵEHl 1I Gˎdy kp1]מ>;cfddáG.@ kO<х622ɢ"=ETD Ic(1XGD]{8䏉D0 zD;N ^{d8[X׉JY4L2^'+1tDvqtAC z X@@$ຊ-vt]?ºF\Ajc]kښ>"fAC1uX.>6nj+,pE 7KX4\2#xǚnC -GB?Khgwt@&lcݾ#nl#~9 I W5"O ԤbV!B]tۡ?&?IHJw0Ak55v#bk!3|fM|JJApv55އl!.$2!v/!0 顉G 7ԋ}NuP H@]!%E0 Wq55(k`q c)=@ k6]PV ~:1^ꡞ?H@HnD6` ?jR1 upn1)m@ kcB|: Df$L4 42^; BF#It42{'"d k*{$&'~H:epQevheUH/9Y8*;!?hhf8B#Omd@Ćz@QԎF @0,!?RS HIt;?0lh8f|Uq wa UXvC>XsAh8fU Ys>}Xcm H%yhXf\M4͛ީOpq!^R@ sY e ɐWC~3Rz$픂*PHrc5)=t' A gY[r` \=t`2z$O@ 1$? duC@xHJIBaApdYG(n[\)Frդ̡{1:Ap΅h99FlG\4@hfU:B#Hݡgth|@hfUq^B43~z#okTȐ!xdPd hgK씈[F7 %&mE|c CY:$4JQ? s01s!9R; @ s딝`c4샽v 3z@@# IL+ uY5tDž@;Ё FBhWg3wCw =t A4BIk|;!?HP44"2F1pO=P=W(N:422F/:dp]@aI!@ gYNO2x|&^$krA,@ FH3]LTH+$ \>Gr*$)t`Qc2jPtD"^Dr8 C FLsqw{=ͬ l?hd< ̉6!=#By:d(; ?hd?6C^}^L0zC9\,0AQ'*ddd!v=Wb he[tԉW_ma'3?; TK442n :d@*DTv<rH -QO7#$B7/C}Hr: @ bCd8ل'=DץWI.\C9\0!5\bAuC|uM'5K^<#. kbhe'5$!ϻfj8/Q|B@#-Y!䏦l9nDDheǸl+#;o$}48ǐ?.EAu'״"/>ٍ"3nQ7 :vJD ,݈?H!t@#0٢ng'F(^S} 㧗HH,?I޼xIV~8 1: @#7!>IQWk;; ^ޢM*A!M]$Gg_3 |}5eYҼ2%7hHh$g!Vc.}Z~X>30@#:!'g&NK 4 s$w, xs/RكQpt `@ ^P!%v1;h?hg+sItIa?-&@Q%%reQ~MZ@$-ݻh Y$pM@f9D,1e7.:g.ɥ%#yuڦ rHHc7faSAG@p1?IZp CH?u B8?+p)XUqtN@@f"rݯ+kӡq:gi?h4CI/^F'0?Α|9?h4|e7Q?0_1رc& C/=h! 4wQ??aSUu =#{tL4AH26?6n 222ȈIJ2ƎE 0?h4`!:-Z4 *`.޽ } u ]@f2{B̩?-s B`hRqNt0 fى;h!T*ss BvWNe4P?p!@7xN(B JNA?h47 uD\Fz uD=Aa$4A(DFe4A͈lf C F3,;AI$<rc F3^,;7A~ C F3 f]h ;p!@ѬDSnNS!?h4sœh /<C F3}ۄsh 'pr>p ۅr#>' BվXh =+H BJ%mGr`$ h4P$ y>1@h - Br%r,z~4?pwBnlthpA| 2 @ pqȢAdŽd`{4AׯY" 8  B fu^v\Axu@# F3m;AA8< S4A_#h'b ELG<( h4P6u,£Z1?fb\Ax c F35bh?xyqpm  F3"=`hBWYDTb4`c' F3U"WHFGy`f0;!4A *X qy<03 N@fjI"v<3E`).1X4A*؉ ^sYS?.YX4A0piCy"%\w.Jh4 h呡?)2[Q?.3=!4A:ÐX&-P?8 Oڑ ,P@z?6mL 2q-֡?\V|skzh42hlY LvEJeA7 @fY52(G28G #1w )?T4hLuxkaP"3$R?2h4ж3؎Lg5(+ Pd_Mn5P+zh4 ΖO9D w3?.6@f:Qx% Xifr~@.vQI[+:@?>ʜ 쪓?6@f:'(|ڌLL465W YLW F3wxd%?9ÏZg샫@M-2/Q2ɵb|spzG@Q0*02ZX_P-u|-z$'\vTm, ~AԷ(uTh4 ;C1sܾ,!h_!@fo<Z;&a;F^ݻ΃h_!@fA05T*`ApAtv@fA ɵǘ}r~!\l$ 0y^qth4 ֓Wrx5o`Y<!e6 F3`%ݒkWߠBg5,{q,΃t uߤZ+mlqoK)~U@f!n=~f߸I,q NOp 2S-\rvDŃd F3WnsëW}|AYXO"&|`1#4AC-@0Xcu2zqo`&:_@d4b!\* 2 h:N-gI(|`)4AF=/ d!@fQ@Bh4鈀YBh4:tDpF;ŗ?E@d;s@fQ0*-hCz;Pu F3(LB4AFW@ͭ{XRo"@fQ0U5݌@d WsU*m 2 U]r*Uh4aW|~իWW 2 F@d< F3(x@fQ0  `4AF(h4Qh 2 F@d< F3(x@fQ0  `4AF(h4Qh 2 F@d< ) З*IENDB`gruff-0.6.0/assets/pc306715.jpg0000644000004100000410000023363312540054077015770 0ustar www-datawww-dataJFIFHH ExifII*  (1 2 i  OLYMPUS DIGITAL CAMERA OLYMPUS IMAGING CORP. E-300 ::Version 1.3 2005:12:30 10:29:55"' 0221  P |0100      # 2005:12:30 10:29:552005:12:30 10:29:55 OLYMP,8@HT  Z z@@)*+,-. /   0 @@ 8P p ddpdD4041OLYMPUS DIGITAL CAMERA ` 0100  0B H2  C   !0100@4 8HPV     hr0100     0110 $ `1&$$ $   $%$%Q40100$     o p%%%% 1  ^1   n10|1 18 2$2 ddR980100  ( HHJFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Gw(1'[H =+vt*%)ݩbNU1Kgk̷k{Xg=(pI#yێdQE}K>i|rHNOjb1yUzmgQʢkU嵺9)~Z}jM<&$qgM;V-늮4􋞿Zr/< ST]I2KJk㹺l3M 1²J{)nբIlrr,1a!Df h kR" z3)'9Je߻ȨXf4#H-4Q`іi?:@ATHsNaSK*  muQE;eNN#P}*3qL2sMXf 6{Ot<]W {\jC3`9R4ɀӚTiKv=l^*] ;!=[4a@rƂyAϘ?:Q$] E'$ԩclz4ͶQ̠ҭշL@(0#+oZl"rm9]VS3Ph:ލ+Y`A `5+Ɩ_Y{RI2hAKu,W|E:wzn%\ ՉVSRf)Pp:~)5׈.lvWq=~T`p@ GOЊXI%.֨pQ!'皍$+}1`*Nwf.[ʘrd!I5elP# =jTh7CQ}cZa"n8\ve"3E[7;Ci ڊWV83bGI_΀]b|&,8;sSĀl\vC aONЖ1Zy(Z觷#x(>$+XB&rq  ڒ?2iD*.y<p!aD*6)nH; ~βnc>Tl4&t,]a{]/SKx!'G\JQ7wΏm+u9 CG8+mr {\|xP.2 *gmRNr?fͼh3.8&m{s$13;O&m&8l=(7+%֬\r9)YIo=Gj+&IRҸܳ!~HB'g橑[qU AI'!m08$ M+Fn@_Tehƶ2Ͳx$x< ذ ,sM(s={SDhѥm! < (Ґ\[N٧p`9*O$dX7V+I|!v| NYiQgպ ͓U3pI<(5ִFF McWRynJkBf\+z{Qb+?IiD* BwJ 鞘Ƌ?bx=dqi~X"* ;s`2fJ'f,a&y3[ڔZm%Ia@ޠuC>J! u^j umvl[)sۂW7< z%ƥ{q#[G1dUBO;zG(fw(r8=suZkU^<7A\|6.W 6@sn !'#5h{,xiaʒTߺ M5btdv] TGu0 6JRL81~ 4d<*E&H Y*}UtM&HhFVɚ5iֱyϽ4ePh}M ̗,%F=i۰EU 2sS; m#>էګߢvN{U{=2; $TulX>͚q\ݍ_V&v̈́A5q>"9<`[W'vg퓫(ʨ>!O/>m.~oCgXˤ+u`l,N**b÷JbE@*]JcԚV[d@ J'>5L27^OvRh'+#+A·i9 *mcM$3" P ?2qJ'' #bk<ʞn9B aLt1ܒr{ jVu[ԫdGsT@]ܚcV\r*cW;3gd*upe{i2) ɠ_%ݾ# JHA!p3sM[]ڠm*z{V\G7 $d"r#fY2TzQ< ȏIP]@_5A8^>ԴfnT׵_(U`F1Z+;ۯF;Br[9Wkg3zg[ W*~zu=JI#ުDjM4JECɖ㎕ s#pAF+FLJ;9R9*GHH8YNj%]%9!qxl穤3HppNiCp*5hCQߚ`C=Ȧ,;2=(j8f(1Ҋ5 l@@f n`?WKcr}bkemg.v&̕R>^= 9;9Q -,ü*0/nEJHO-1rČ; o>2Of$5ga HZMCrT95Idv uKc&b[vh6t9:tsnUvzkoq ]i-㍠H辆F̚!@}(QJ I?*_h*2k>ob] QGIϐ~\n*DPD3W '@\z St&y~LdG6vYB^X(3,B9>l1L$4K[I 8jGIJnggTP.'J 5 8R,4btm㩡@Jƀd̞%"[ $*mԾ[weN}isShPdR'jAB6(V`Thbku9֊h|0\1tV-X ~9U` >Xg5fvHݟaJT"8IBW.嗩џHVuCtV)9Zqb[1ҘŞ7L;3'F?Of}8v~ ʠc bI#>hD,FծOO4 m5r[+ӱ#5X٬D63pqUX䳚ZLjۣ si1`Etib,p[*^ n6 'QERh9tVH֊)ZHc, QҊ(gC  !"$"$CX "@!1A"Qaq2#BR$3bCr4%S%!1AQ"aq# ?PoEIE-g'`jyi(;c40qCe}G:GJF34 2*gTqVBdgsU*'xYe;;_{]ϟduY\cS~!wb{W{sS21#zv+1ڟs#;{"O6#Hmhk:AmaDXT|SGڇ Gp738` w84e\ N=2sTS x40rx#vjgA'5o.j =O[O1F#pE6FwӞ҈z hʞnO58QWq@;g#jwmCEvˤvޫT4x98zj 6NdgoSKs^s§upj4"@^OAp3qO^KɍϥW>NJ IBɾ=hc2T|މN}'Ta"aH(BbFqw0zڄOs2NsWVKGHx'OBkǂ3^l Z47'|18$wn9P1 9}U99i`oSh@UnVOϥ |{Qj꬘9gM aB,iP8*#88 ~*b6ek_8ctqW~-_'8oޱo:Ďvo޲%s-]WW?1|w$J+9ܝ\yMhCiV<ϓNd" 7$}E-@ U*ҜoVKPa}=n1ڮ pHZz,x WF=^BA@_By4$jűF+VnP$j?_ ۟J TF{(jXz/mU#=Ef1'/nUm-Hm^6-*}RF*F1U,;zTz&6wiEpx`8zQ@6(jF1DZlc 6cjiLsgC @yp%fήz*9jBr?KhSUsFj3$z T)b{V/ƨ@ۑA$ɟS4}v8,o[=6at;n#t6 ;I+nmS7(D||nBGɤs[F69*k h2Q 01r?کqS1t9_u InNi:[dv>57:xw?j8tt_)zhY%Vr1 jf.#"P6[eeϋC !4v!|V~GU|2.]QOhw  NV78?QhQGoz!٫6!T}5ñ2ăKb&9]\g'j:~.ZI~ypzW7ToIs,V'ox^Bb^usk-g8 4HleO9?G/JMw4f?z%fӊň^(iEo8|wenųMg~8tG(4׻~Gfތ P9;PϵJ@QT a7ކǸ QyI0})Q0I#jY[\UjxFU W{UI6ۊ)(ˌ G9Ϯ)z'Hc$jG5xؖ\>$jTQ K~=~O8bhŹ$k>j q^$iX8f;j^#f7sP68 ,O.uP8[Q ;TձjlT'ږfHj53ղ~*xq^W ܓJƢ@iiL01PH3gb9 fjXlJ>ެ҉>v91{ P~I$ݳ j9Uo?-9JgH?jǥ?忧6INjc9ks?(g'rqH ?z\x;`((EV=~^:~ܽI`FEmZ)}*tq?0H"*xx ALd޲=?Irth_SUkT7Oߟ;Pxp/h2upA#ޏ.I|\VF D~gK NWˮCsUkSWq)Y}D?>S._HŸ?ZRDzB14ZV8/~-s?Pϟ?$ұ>bGBI,?5lq|ӮsI\uYd1NJ';o2N1ۚӟǎ/n_HMU#y,?QGKLkqw'_jA#Zj$inHf_uMΝ\M'K\1ZQiӵ# q_LjȱbQا\`ުqwK ~^EAO\w=x朢Bj#gO:@ R eP  㚩> @7sSB>p>?c>W} L9aQ M˩/8mi99uQ3:*2>Ԭj߽|^38uYjO}>/_JM 篜}GV1}O?jZ_?#ĐdRx;*OTP>JڰSފm;w'-?'X5An+9$jV-b} #_#Rw+d9<]QlcEr QSH>Nl&Y+ D,Tr712Kf瑚2رPA8TޯpqMhj}sKn(zR1$5'⑧>9Q޵HRH%<@} ]&989?';zSNc(ȫۑIX)8U3{88 ALH8vN4xfvx d"8GĶsvXFwӫ8q^vH6|DXw8`Q/};|sF$=2!ߌ22k\.lbΜ(dg'zRa5\s^@s}jVe4=d -g4#xҤ۱ T9{rkؚ?YmqFClNG db6ce cqmi"ۜX; ڠ[ {L#l wZK"]*<9ο/U~e|wVv eQ22/wŽ4v9\e!QjT;l4U涂1ޖQ2?ڴ *RBYweǵ?Pj4z*msxaĕcOq_Q4㊐8ڬdx ;zЗ\W# I8P|WzTSU*Ugn֫㮬Z:pvވK-`Ej3ڕT*ÐA<oڎڗE^ i (o9ϾO)nx?ս5B6d~4czd?;Ȏ>_Σ^=Ep|NukRa?Qo@~FOjS^:>+=@y5]Ns@NW4/$fLAӼ7mYIuZby)8wtV6qMNӜOl!a.7sGMh$'@?J[c2ܤq~\}Y,O.Wm_OۦOi3#D(JTcmJa%Ȕq[Fw󗱽K!r[eu 2Ir}K^ޱi ) QuJs.-fJS~ BR5b 9|~kZ'4JsVåkUۃDYy|6~43! 6};8]Kmm>t*+8澍оY{`UVszлF-M|?Wڽ3e탎Rqk ǨRrNުH\8<לjb*R 7;~%獿 6ka#l5瑏q^7aO8b1|푷c柊Nm :NnNj/*B#_4*D5 lï':z֓IQǁqkm* %,c;)gԑu 2qiO:H$~>/ֺe$l`Oϵty̎ Y:ꯢ7gvctǾph$ ?cs]K3 H-Ne\u>||/(GK``5<|3}sؒKiRj:pI h tBFIշb0~4Z$\{N#0bF5y'Ql=im%]HʝϨZ xNJuq `]MJiV|*^:O\-M#.#u1}-] DXkNQx޿(u kwk⭬"A'־}TcoE%0veRSWOKq>4== I{A2\2\ٯ_SĽ-\랠_œQY|ڹ'ӹ#mcY(Ic+BrH޲~fhwrnnzp|z Lqp=*XX sѧ?Nwjs~(oJ&6;Ts ^?E  5B~jGn j$ z4@uPskڡlЩvqP_#&v;PGF9]^׶'КFO{p״z 5cڼj9>Lc|#ǁmḳc֣O yߵWYoN6x y~"ڻUmrQj0lb#j4`x٩NyGdVAhӐMQ8n8߽-Tc#޽`d)x*7";8GU(rW?hJ1ȪNr{4^YGci$j2H4j|Hj珵4J M9Nr}ֵj3'9i[WRm7~kHg|vM}  ~Q[:˷ OlZU-/޼,bٓ `c~F O|jo@j?hQdNH\zzQt3n*ʩPICr1NR!'49aZK)%8V+lpwm+F,i* Ks"gHb@OSzInmcDo-'k[0c?jXAUѿ&nBI0GoB5-ĞX>LvK{O5.4 *AHZtnk# Żj dH wgmno A`ķ]N98x? HM{0ER]I>K\/O t˙&)6ھs #!u1C}2~#rOuz[Buc8u_/[e%2@}(@=[7VX˜A#|Z\=}2j!4ŢpNխlmKcA18 ԋ[H#ں;f+hx2g;i5-k0ҴQڌ,rm;ւc |ڳe5S*v`ϥFDߜb#m\;86 X#LņOpyW9sz9;PImUA$K0Pm01 ExPr/׊9j"P 1ϭH*w탚Zr!p{Q2GxdFh=sڂqZ0I'u4y$B'2Z^dAVG%w{Qi^횢W;R~(aI$aʏL֊֑ s5J]-Y׶6 1FR\Zl꒿pN$)`06pNG>d{(O!$ps 9SIIAć-e#qU8sWj1F*$PTl;BOjh;^bFEyFT0R/Pܜ#I,3L9=5 G|wcӑH%N3YV*@G7ެv#2N3an{Gx'V1RT>(npxJb8U|\mj,%0Ϟj0<\zW 58`ءr:<5B>,I늪1jwc y;,M}[* *Z)Vhr뿥\ވOFII$lW<.[ +oOzq^*lW`E]l&6vQ)/5|LF)nsV.X{Ԡ^wUɉ<z>;wڄ4–pT8i/Z\!> V 公ل2U[j@!y ]x MXˀu@h]jAm%ʀ:D*69q=_8O2L9\$c?j?^C=@ O =#@^J)$aQ-3Sctqޔ-I ^KIF>EOhto溽ߍ !8ʸu9_V%MlЭL2~=.դILpB|@Q}?,j̰fbGs8ŶGKH% Li~-#YEͪB iFl+F;4m7]d?&wƬo!݉2f@<8S2Z SG)A6ޅ*idBGJ/!ZwZRZidi :{ U;hY,E $Y3&IsnB/}CZ\ !78C(n/l`g1:O|٠i)US Js38O gD7?G#I2‘?`r޺8dʞXlNc_Ķfi#E9l?U5i,tFeιf+-z O}#coР6ErMpVq_OY~;r6r+Z `&g#SXw[iy4tE1X~0 `7S>X[ߎc4nuֱݟqz UqavވdX OS9QCc4PsWcYھa C5ns튫xhq*niAQWTdр1l1R7#zXQچ)ƜU_QJ"=QI8B{ۊ=Uh]D;ڎO}YWQr*]5FjgpEa'|` f'`OJFS0mU~QVew@>Vߓz T8uISzLqV]94YH8E 'W" ,?!Dn^f jp? PH{F 49ǵ`Aԡj F#>F;ҪdjBB\qڼuqBNjx$jll5u`w8#(M-ИI8! 0楈Ƕs0'WA'=TV᝾s4=;Ք7&v^RToAHT0v &歂6ޢ0s&Tc|ԌHcjZ6 hF i9o:F<>=F1¼#ёQq^*@ 8[_$ElNn0v9111̌sV;{|><Pgfo rxAlk ' љ/t61g'|wZ˫읭т]0]*I>}u;դɍd]:U^$bvNwj7ӝn[cX5"Bߒ}+kY\~2yDic+@Hڍ ]_]1-%A4Seݱ(Po#aֱnND֮JYJS,OaGoQLA}r?$4^9o*"?~+հC5֓1;d =pN=ϭW,"1 qj>SH$nf/jC#DTHrF|Ӱ&Rti*QmLG>\oO @̗WOf9O sң4;[@!|tlf-FI< bpucum 1bHgQ\\X<6Yuw-x#OlU`ig?1[7]"8(zJϋ4ztEqɂB֧׽(OF3ohO[ǩBԤ[7P1yU񥍭09bwlڍ_M˨E&Y]~ՔɌ\'Ϟ12x4lT9ʇ g? 8;~~(-NVw8 jĖ8;{U1a,XެJv8j34@=2}ib9,@q^B}hȪ6EUzēU$Q`qUXU'r)Z`OU,3^:qT;F \vw?և;js?Jڻ1Q9|TlyBިw'EҖ%BNH׈8[9Pu >ƣGAaRA=q@APE;Tzk߭UԃQsTlf sڨˀ>r=V zlcm|~TNqR?M=*Ϯœ;T8oEp )S֭>W n*03ށU21F1`6⫭A=Uם!TC~4| \GnMypyvPF8D\.S; bMXڡ{h3zU3VS4RHȢ m}*F2(4Lcr8>Hʊ;~ 2g? ,mڞgzFzJ8#'7Nh Ձc,*sOJ\M4SOJHh60|^y_# [%O0DƐvcOUNZ rer?pGޱ?GAjse;@5 2.!Q_Y׌}kF ]OMv uW2[Fud ח60xnl]IrbVp0< o5{qӗOE}7RnAǡG-tcIC H6ԭS٠F봅ɏQ# ͜"$ `=aȎ $܍>Qh 9ϵeޘRyRFc9Ulo$klReQj t\k.T]>!֯aYϨ,vjтxZD5;qnnbߏsaʑ5 |8[!6񡌐B1OomU{Q/,VE!,$Ռ>(Mtxᄚ4Ȧ8ʬ:sރvVL.xqlǷzQ ͌Ɲ\?}:~.6.em,Q(akέ^+!,pnIv;~ԏ:t*O}fq|P^~p\ʶh'O_>j5TA]?MhnGww\$fEXMnMt-h%'3p9r_\v)LgrJdX>X/ v$OI1 ytz*H#'sEs^N# d~sV$/װ9ՈlTF;mj(3ެ7cڥP8:% A!d=i"TjƙuP8U,d(d3w%]h_Zf7#96GA՚,jEJ> 4},{ռ9iiO{Փ,s{ <?_Zz9 8?;Tz񊂣K{K@NAO`>wx 2Lqexx<{R}*HBhn3Ca(5N3AB;[74׆"0zwxGެ!cx\z'4[n1H`=j pi֤E\1 FNjmNp>jj;ן}MxR- 'J!Qxj(cIX&=@;}('b9lA޽jXj\|bWI:>WqQ/Ӿ|~o7 vP[[=KƞL1$1?1ǿ M%Y!X<4%#0~җe,04 s'nv\tcALϨjd8nK[i@#J^@ =*gr]dAg $Xp{SÚ2OVTa؊QF7E!jd\W8KhʪE#퓂Qq]< [8@E0x8yWl߄;1"[9ڔ>9SޓRF#'+W^Ŀ_b9/o{qP"1I;g!v , b$rdǙr|;b=+SL@!w۾9hzO IаYem\L-KFW "6'>HZ%p 3C.x4b%FO#ϢB=TmjnYĵX!^tHFs|n7MEdXD0 ٧z=cGyf6Č)SG1"IPb0ԛZj-1(B3jˋĎQ..iC3ap5j= w+?Hfsitxb}(R Ě $1hs* 8vsDX!#VߵgX׈5GvPhB23[h!ui 1Ju HNXȥb T|euZޤfLG@v؟{;]ro.?ڗ;@t:gAv?|RJzxzWcֺ9ݮϧ h)dD@}톹;ho\gH$l l :oDfYG>4F^&7V򺁟$Q.e iQpL  iAG9ѝǾ)| \tĈсs@HyEAOVr0>*3J p x`A"]ϯxT7ޭj`鈰= I4;6p+S,2I Rhw| l֫Qy ;clqR3qSK!p#ڋrv55\G{q~r;-?AF7ҌyMh2#Ujz# ]0'8,H_Nr T);oU|i^ 3FU#C3gJ)Œm CڍKEPWb懯m qA3sUi VyRi'j[U%*O,Tr1j1Ꚙ/cʅBsO\UxqUA4s}yp3*l,Wh';;wګwzQcEjp{'|׷t0?QU0@S/X=-Tޒ!*"z2qx&;HĤƧNyh Q1&2vV#crVA@Qv㚷23E qSXh ɣEhz$0K&~1[3;P5_<-|8'FtG(Cm^^Sb=iFcvH[UցcKFCIQ2ae,fy!4AV]P//_jG'cړ .%HƜя1a0;iV͖R&Ӻ 5(❸X@q{zϮ^x wMR9ޚe K Оqؿ,e8="3+&F Ҭ.I5uiLrpyڳA5d e2dVWL8w +T’Ţi` ^(@{XWA6O!{ڳi=8i'F e#Ҹ7Y@vٕ3_Uv6*v]?\?}"[̍4?cڣ[{=ͤpĘBIm)>d4=M 1un-HYB9 s`AWoZiVO-q,DkOf NF VNq)' ѕ;mdX(Ju`ǯ?+-ආi䷕ȫ02td;4CY"G- iбFIsOZ,"I|NIWmӥYdKp0E[we&) Ǯ+WZ<I®h@i 0wǯK2 #c jʰZY2 9aӨ?ڕ =F8Ɛ޻bόLs .Y#>@z^%Rq8iϴɾhGaĤsP;֟D[ ܰQ+ѭZ|2~=+LPǰ`H=89T8X v:*"a:D_`T~juru[+5tez$Mf\J?W"TIkQ?]U娶A HwbkfQm@rS57t[OPMl/S\x$gqеb-ksצڲ0E_AÁ29ϛnب[/MInDg&L 䚇pK8@.i)n1jdvIT? dgޣsOv n!/֡X^B5##zJĝ6 NE$gJp6wby8ޠjW9&9>+WI3mCb@2F h3ߊ|4a٠ 񊺌z{P%i4@=qJ;)Aڨ@ѤamJJ'2,ބ.9,Dz(Ndȯ*p(PG`UDz{bH5$ ,[qD>(2N*0r@iccTc=\C'iOrj<ڴR1>vTy(_mۑe;v4g"(=,m`+- cfz cm@VQm\Rc`oްGlnqڧJal֒-JHswngƹ;많)WԄ<D[9$3d/9kTd w: 8 g>K5TQBD~# U1Q5uFI15mi-Y]qOoyz)>9k>KZn{b"[IA曆d-TqF#%h ($ k;c3B˥6E<1l"'H+?{lwXZO *~4~ݜ]IJ~(&+soHRCݿu=1~ڦHk :j;e Y+^#)i073.?zLRk ۜcOvG /13|?^icuK+eezwމg-j/^+^B`40njT޾kK{;eUp‡@kkou;{EWJ#$REqkHuo#%ju)J.HT졳VF ȃVZƼVv|BWG' 7f: dxJu}Ȯq[ۂ/^w1X5}# 1$ZDަ']"8b ,,a'?ڻΙҭ(VzH9U}EI_rު MW (8UC&Ē6?qNm`.8㚱?jNB<68sLHA⎩2KTpybB6H{KUjV=pI!h&aGoZUr(ER<o4ʅ$ ! 6ji4BU;U-T'$sEE81(NqlPjoMʇn .fT(zm/#=j5ԯܷWF-M!ojFGfe'J3J'' zv4ՁPW#==sU5nH֤{PcPZ58?=5X9T,rf Is/#P|NA;w5~i#FvF-/#)hBFsRxg5*f}*5ՍTd˝^F40[; R˸Ir ϧ}X|R~)aZr+v{oOE88 m)mCLN*ŷ celqD ?\{j+89۸3)N,R頊0Yw5.~do*9L(|oj׻TɸrI+D.|m"6jk.$5ЕboI Lڳ.8>+*4օ 麽!]\+G5Ԧv?seu{b7<оIr+xbX5m>ԸLcbˠ|Ȥ\ok)mzj2O/cI*oޞڎI" &dT^$_6:QZG' }7tҺo'?/Rݿ2On-;Bp㌚t;pc{^RǏ6S֧0ٴ$D\Z~T>IJ6 Z!}EsUħO;֟3y L.Oj:Ig78'sE܉0 U6P}X =*K|cFcA5hyev#>rٷp휶H}ԤNn"h F=Bnm>g7U{B-z\QćKK`Oy&LW#O袸GiU-ѐ#RGmW[_Sa zs[|}y1fe»[[)ҟ]c\oNIm _*LD\}룹[PCi@( ;|g;y:Fr>Yϻ~)!Jۧ L\kywB/+i$6t1_+m5@ƮW7\WG͉K6.$&Ee}AkqlU!hʴXg6Ҷ,㶅RR~4P=,^; tљbAFNw\JEgRw=we nH?j)53 TEczjKt Q짌czR䐺'HB6 IjJ@G֪3}+K\so[_FoMa9يY߰,c6>j鐂5?'m;Vm#g~sȮA5;LX;ɚdwRtuz{u83"I 5P|<+Yg/Ih r6ڹxZnj 7֍V6<#θ% ?,q+ YTx}/ԢFc_s}"dic.JI?ֺ>q<:Dsu t}{-4;+/s|ɘL2͒ :F8uַOXguS+dt{Ocsn[wScދ Fi%':t 3-iHb6IiDڦ `W.w G*bsSzUPxG(( yM $Ur^`đOBTW};R%TdwA Xskc}{|P$)xrYqNqRfJ$Ue) 6QsWY9,EyƢh$5C.ZbO& "F\xb[$*@҂c~(~3I5ڏÂʮI8faޖAՂoE9xl8=Z>`x[&yvi_'5tRKoWxZ*ߑF\4)W=ʮ )HHmϽD zQ0F4Fq.jh0sZہZxB8A9 =lIՌv€{7ER) 1<tqOL#c)Uވ4^|'j~7FqW'asBA#I,+kvjY}x !qhhj}5 :&g`k|f}kol;eE~+EޑrcZ+s\+rgrGk$FyҙoAY\r7TG``ޞ'f:sysYV{R04o-f$;VΡM'+#7q`V+z r#=BVbԹ(d_R?ާX D5 Ds]=rNT 0ɤ޼l+}HF\\Fvן:v|3VYG, n6X4>z= i*fzʹ6OO#EP3@ͽ_PuA ҵ (LJzR퓹5կO>Ncݏ5KFHݴ@sw{sNu QmrNcI9aa?QK#N4 4F.0?սuRT`qkqϘTz5VD 'h2Q:/U('D4)9$n+ z)i(D6$Sݒ+m|'>,vT8?m\=I-ar5c$0ѫnndfo( #9o7ogTkeҤ*uuO܃pMg~Szi}W:FeB(Qk+t/Kukx㽶.YdEfQϙ?4_YZtiN,\c%pGSr\ߤ줍o:Ԋj[HU>7?jB{'舶*ZnDg'_,z|NLai]d>WUҾӦtnt C$311W9ޭW?t%n"EZ%lHd|6%4Jj @8ܰ9?q_Nu]yY@#liT'zd!q19:j2\\Ln-lTHQmq=A=:ܿ +X>K{ޛ{c7ᐦ;# ^Ľ+RyJ5ƙ7}(뉞KOՂH }T։prܴ"6Bh]JHUSC2o<{ҝ>{1{:Nmdo/ʓW|>amI<p8կRhM6tMSjSFsp3;eNߔO;!K=jx뿒\#"SA6m ߂cY`P0ޱ0DA-O*ѿX[tHYteڻxH $M*h$8هv?1kp|HH޾uj+Xέwn:i!q"~jޘV𐁝DxO<2[.:EP&5_M\ǽ;?iCf}85!7b9402i5cv\z{ъN(O"JJKF3@i$Q\63S" ]ǛaD,,:(&P=fE#ڜ!ՙqd^8c$7-VbQB5<犻ʙU;|Q$y"nHt\0 9 ։k-5cDBmj #mw]si~ӡz1mssN#`>)M1GrN6AO5B/ޮ}}AQ$ gr_b2dnёݜ[>J{b:y=z;ёӚ?41SN&QBIVU'sC5.j(R[#H4a4cC+8Ȍ[r}71H˪J#;h,NL:r8d?^/&~yL>2sV c#esjW^=ݤZ mm#:F<+qL6W3.6U~Dl |Mcu!V q^>_jK3m\?V{E[@2IY?mjtUyHL mD(i@8 z}ċ&EhtKY NAUlX uwgVn`kG o`=ߵ|6\IgW0rW8}GlV&z]rͦMd {TrwkoOfVl)'$stV:ΑzS,!T}Lkr y!#Ov2ӹI|5uE ~c]q}$ Yeҥt0?.#Ҳor͏ Ov:K0Y"rs';Ict1( n>v 99Wt/5ҥY#'eJ8b {m d2{ՠљ#[J{|ڗ<}먖sKk(p$n+0J39"ΌcQWeAQ$ls\H.o$iCٽk1qk!McWOoig#M a:19; z7X{2*sڱx%q\/Xf[ۚKކVdZ;{JLEspP}nj7%Pi@`M$=6qF۳Sb[goZqZ2Sk) V=icr)9OЕ~WvrAɨxN2?ZUp8ȫ*w9<#,=&OBƑI})L (MOM=-AzLG0k ևDt7Fo/Rܙ$cKu[+ x}^Ryb)X['sUzrisDV@0ޢ`e8IՒvƹ֧ȴ[?Q1GqjZ5M w$gq#~*F8ޠdU9'W.QI5fm=-A﷭[*`ICTJ}^>?O>EeSVcs}W?ntKXx1ڗAaݵZ&p7pH޹b(/%K(.9 zާEwYI|Wq rsrvqy^hx _l OS?!VXYX[:!̪X>#W{ruOoK{l~{qRb\Ico[4Xst-*:X (qLHX0-/kv[:9rJMXicIHxz̈'3co*4%PɩړMVܣ4e>m+\Ėd T~sۭo߼V~h;gHy1#|~%Gr`9$ @e)߿GںO跐dXVUL5>o$ˑ =鋋B`'ֲ1:97XJ$vMt} t(<+7ΐفVDҨ #cIM]Jv9.H#l\?Qhep yڧQeJڡc8QTl01jK)S~* ޚ'}2Fqh, %9}%m\cԨ8`[Iȫ8֢1($;Tz19zo.3K㚀r6FF1CPܑE AM9jsWcDpb1UŒp}ꅎNsTf$hZGoj$y8O5>TD@#H#cޕ=$dmNBrF{W?}]u|rY -R3 mbW:x:aHex|e`+`bGH4W0Hڅ gVTuw&p1RgV2+qh RXPcs63`oOG-sڀc:ˁ;R|R,T&EJ ?z w]HJmCP5/n@;@vQ3NvVazՃȫt{ѯSnd;(!r?Zb6v"A6$E&sޏ9Q0w`*FJ"j'=͌oB6 Pޗa$}*!lք[}aR6#Q l@dF5a5o# $RIWQ&:GO}J02MVWQ}HMtKO;"2{W:d̶|3ӛE-Vc+H▐,kZP;ۣY&Fڇ}9ޖKQ |MeE i$va\ld&z}@X,j|;O5w&qb;%(!ڪ]"Wi;?"Q4%dv*+zI17%=Ϳ"֪R|+Y%3hAO79u+Fd+(#=c[Y%kbygiˢF30RVf-r'jǓs֣,HT+R,#t)Q5v4Z5ecG4Ąi;P;ҷZ# 3mڴa pg7m[ v.+lcɤqS [ƪYPSpAcL*v/%|4Go({V@܁ܚ:ݱaU1kYb̪w<#mjIbR9k #:jtn'y*QCA2w۳wdGThV/;yMX55S.ޝ^ , 6эK"#!sU![[ea .۵e])v w$?] Z!:fP r@޲/ p&S{ ޝf'K+ϛ#-é, 0)ɺyL6cwan8/F3.>0i|LNPZq=*&V҄9bjNoU.ğSxc|IMS96A޵*.{ђ0p asa:0`2 Tfx2(m8Vbrh8Eawr5BIK}\Pl֌QE9N Tn ޜ5u l(VBԝ>H&98N{+"ݱڼЕQivqڞ0)G^3`B-#e$Akcɮd%UT#NչҒst{zVrXߟ\L%uz-W>lQA )׺Q ?Z1GU9=k]TB[3;JJ;mޱ$I$/W?Ku~0_!}{"ڈĸRD<}~涬1˷|&Ꙁ1nkoR=XWa:aHuH\?N6.j\]m{v58b!R6!?ɻ1CMD=7e=RLmȅXli~Bq19wXL!=ZtWJ;M\X~f0Ԛid>A ,Br}-B[*/[Ϋu,piʼnԼhY*]/Gy'Ӌ vr (J‚{/_KƸ4huV ~Ee$v;8b"=ORt6?Z[AG@Eg޺- >@F d/z|}]V%Q*yqrUo>a{R k4ÚFUyqofn#HmnsکU*I` X{ii,q/$*A:%9$nGj-[c_n)z5?AKb Epkԝ1-z?Pr$V~qz'ӎ0Ib}1Ut@ޘR)n3qUNpF޵9=x'wSGm)zo+ħ#$kIY[,8=h2HSlc6Ю\sIN2NjBlI@Z+#a,(FǿqT:{(v푽\wjc)>R" ==ڈ7;vZImhQhpn3ju6{P5 lОRwJ˨+JX{qE%g(O8e擼xh'|c޼Qd{S @V1@z-8Ppbz3j;c_%qf]Pt cxn'ϭnZeMsՎd`Y2OCӠAŸ1+G%{)<mm>9 5kvZBTSxVDJZہ( 7筌:%\pi)aFsL(ُ.+`d$oJ[:ņK };hNWRNloH9wȡv48mcLGk$);IK]*Ta''v 4mBgQ?ުJӫ 󳜚m[mFNgڞ MR&lREnr=sBwBsұ2|T[ˏ6+0 dmz&N e(B ;J!Gj6[2AJc5qyt@EEi7ϩ?N T j΄ՅLS> 1[\ `3ڻ+ƪp6d#%~\$Zێ$G]%NN=.Xn9A j庄okyatE F+PJ= rk7N~BNܚOE + \c8A'*q}ap"ڸSlOv_QAHKmHQeV͖aHkə zXZfl9A2 &֧AXJT)8#ڲHraF}_079!e׾F0}*Mx`7u1ӥԲW~՛ 4JҔBڗ#*bmpMRwS=i]f>_FJi|74J>6%]863 P~ƖBH lqmb̋ A3ށ2:Gl]8+7>q6tW`O4E:pE\K JI\:6Nvǥz6ڴ, 6fZH[6-O])' `5ū 3F9j~Z=2kIJ{va!`޵_!rzc ^Dώ1(:2y}h9aJr0EڈSt4!AQbX*MzR^aU}3 ?B.t)X ^*]\o$LF;qjjcL̄OoqVN2 z;Gڤ1,[YQ<J$+?/4 >krǦޕz>h |kѭopJԵN=ZqNmJ"ڵG1.Frɝed5d.H_PߵT#mAn!NIk̈mU@5X4IA'ͺܮ[}/"KPtH޴1:1(Qp6Nrlꅟ'`EZmI޳Uyv>~(=RqzOֳ݇`ēa!aJ2Iv:@+mڰRK]'WLjvGX͟L9YYB \f:fFȬBj1PŀUe`Wr2+W95zȥzj:]̌:d׺7TGb[XJ+LGڃ]m|IQWQc+kim8af8w>`& f)߬1C(ƒF'On"HKt8]TPqVv-zz7 vP^HssFD`;VuK}77sn_p3Sޭu3E ܝ¡Bu^ ؖc=i;\9: u7x.UY5T4vMa b\/шc qUx녍KǓC'(LϨ%I"d$֕X?'reqk*u.SKǰ+rʱ8iLWfY?PNMS]Ktva=N2cV9&3*3)9#=@i}k^aT[KsZH*+Vm18!u(OtˀW Vv"Q N6e- hX?18NԯX:.PțN*XwD !ÎV%$~i;lp#26qe;Jr9uc$g^3@Xq۸n đ뙃1mK9[L+h0q"F3ϵ50PlĚB)5Ti& ye̠lL㓿J۲;U}N ;/d0Vh8ڀbj?z0j- !l7hпJ\S^X J1eOތΎ;^g~إ7jF4vp6 dj3`V)|г*RpSqB2Ā1UoԁL[ː{3P dmU\lG 75FrlSeQ< Q/zN?j;) ޽,2$Qb!W~ r}O9;g`= 15~6wF*{. ryF{U=j@ӝ' sW3MKAޢ(E~\`p7惈mN1Y{ӊ<;Rs8cxVB<׼`w.-YE#Gj2#(~sC/bL#h$ё>И՞ئb*vVDqZѲ:* +_|$Яw5g|nVZ7 Rkyt05zqE,(pxS=tVMMvc\uQ1fQs}:t##桺mԅNEԡH?(z\q7SۡN `@w%铕V֟2L55X_($f; #j~ k*_8('Ouswʊc]trFin[$$s j* oJ{;%rzrKAa>R(AUZNզ#\SwS$}*yGZu;os/ٮuM\hoZ͔͐3dRW3do^kki1YD鲮s&1tTdR#V?ҽodwtim6 5uHˮc|?w:rt?Wξac)l+]$YEm+{~D tŸ[8Ti4G ʹKbrNx河6s,3E,Fd=;,p BwjSu4Pr=3Y}4yf6ϥsWI%lQ.Com YrK,TCY? B:M̬ ҒYh)BNN 1DiL /Hj  ?!ѹ5e^ǜx5QQLc_޳ՂFw54#u^ J889Ⳬ-ȶ.uen+Q+sI=gUblVPK79?z{U,pqJ[ ^'Ñ Ua GfM#v^ _l=(XyCGn|%edSK2v.B`>f$\͹l6`Ԇ0oCVþ3ކ֥FBEeY &6HV 6ҧg#'ҕM zb+$9yx Oh HFV( i7>&gs(SIcߊCfԘF9[ZA.@5@@l㷭$ :JGH M@.cֱ_ErɩmrY~jfF\xmL $Ε$,ة@&sޥ$'8 5a8SsF51csL3Bp@9p_j'HϨr T9 cՏzr`gz7NaEYH^ݭ+F m qW=6`?nh p1qn @$ ZԶGTS÷b5c'-\EH( J٧l1Za&QF۝j2V,}^iH R/zQ Ͻn!E7-ǾjK,dIlb0+* s *_ƑONP)@C1ȸI#lG2yG VwPB9"ȋx܏;Kݲ[rqsU[6򄌟j_EAԼn?`SqqO3pdլjn"r\\Sv9;k~W8eb0++}u6z mvE$`╾d;PN ڦ( os-'i яھg<319ϝ>{w9׊3w?hɋ P wӣ]dFc_4DO|q] "t[𕏆cMoG&X-=q_H\ {&\[5t>2}_ ucF%ǰ!խ %\PKRB33X3L'b!$6 I$[|7&V 7$& bCx\VDǟ;kܛ8$xdvO$|$0&[ =ن~R.d%ᔖo8Tۋn@\PD0 <)kp_wkY rHs76'pz^3٬l ;dZ :cH W?z˨Kd:vVeʘBnxX2g;QBB3ޣ(:0Crr zѯgb7 @OH*Lh1פRPP I f֐=ab# 5hQὨF%m iP̌@74k+DU^F)ͥBd2c:n.D|]f[P򫮭,HӽCUn =Wrt"$ Z]kv@x>O A֢DЕ-?| +)V*G$g*Z#<Y-ޘ8W1m$ PjuXOmS9Q7\XHuwd\^hl`goXYYbɅߏJC +Y]V4T+_azTnoIWXd{m8*T)kH(QA-nP.uc5cbc4|DTƥy[VmI˘DAUn|48Iqɡ΢N3hW+M!#q*iSAV .HZDKwVЈ;jMؚr:ph<9Hw 8ڼ6{JFDh4p7FY6(w搑1A8MV`w(hvac\}CkHaQ:O_^C)lqIɛ X;$gq($VI~G~;Q41zP pvڗ؃.3L~8\2an+ǥ2c1((lNkN~L lY"F||$r$_I Q t#4˫c;AfWvxR~lQ/1s)piRC!՜N N)5 @}.da'PeFj^h$=+.5FZk!1A$Y2lyM,F7#҉ a8+@IRFVcs4r=* ;MX@ߵ9u6:o]ձjDȊp)ںy&0>>@c,Ha]Die`ʾΝF6~1 ]UFkNj,ou$#skjmHMg'ڶfF\E ĚSZS6R^y_Tdm@OƐ@ؑq7 izQHuiZ[EO& ZD.nM $j'j;VKFy_o6ă1LKP}r*@ֹہN[J֝J_v5nqGޏ%) Iits:s7TIs=txɰ'ֹn{0唁.7яޢ4Q߸f]V}Z_4Ư4ۅc N& wĊ?iKiȽ3b3N@r1ʤsA'&]#męhSR HʷؚW Y|($҃gY6W/w,xDoHsp9Z} kt;vdRU$RSeaHV6] jƲ1M s53~3[ӽ/Ԣ !"[IT<`\"5E{dZU|NnˍCcGCmڒ EpA]E/r 8L>dfx0w DHck.)ϭstFjtZ6TYid!V,GBctJ}sF˖c%\=8V{@pcj;ݒ.m;%,ђRWg=GyWeaDO,M c!$v'8sxzԘ'}j@l;ivTͱA iH켫:Heޗ"@Hҟ3i>CvzÝV@1;l gҝaڦ&6lcjgq@;":j>Efʇ b,u JY) ˚b;ƞKdHs@h7$ZII0QKhs,@'h`QɣƲS9A.rJc#}d2>S>bCd\=qGYdSQsijKOކ#u:@?ֈ0cSPE#6P;|QjuSF$qk#kW wG-C'#4F=5kl?ҟ% >X4Hs֋'C}dFP?zUoqk,I=fcdPi!PxG9*jdwFq͆8d`Q*Ғ`{W4CA]BzUx_õ+谔oWt'ҏ/?)= BB&Q\T< eB4>K?YQ0I"3eY|x>բ9Ҧ #1޼#ӳ.׽i=qii` 5Z~ՀSі>csqx'u+Kʈ*2WR{N^qG:qaM$!'ҡ-W=(.ϭhʰln(Ƿz<5hoJ^\zVIcmAUd},g*qB@֜\(Jb7?4Ec>N}H ZE; 搚A)Nm0>cIcH|ht*No;~{$QqK}7 \,eWS]pv#Һkg~q&v8f4 Hģ ;-uF6撼]\wMJ{x^٬NCXkBy63BCNhafF`Tp#>V8WNsL$cE9#9YϥK\ӗ3:RgKn1h5cu#*Μ|!Q~(3 FE:#NXbmS, Q5DF& 0HazX6}^(,t Хn)1lF8+\J09ڬd/#j~$l]8lꨫhR[Ix]@{Wܺy9Dg\Sk2"N=%C)NIȩ\9eމ3$j h19ܩǵ]ҝj-Ͳ?֕V&:v#5; U.D#H2=88ρcA'cPm݁mJZieYaME;qV j#c1@0Xcu'`p29U$(Xǵ-3:F98 1ҢI76Hc12zN8f'^l6=kKv|H4[ZwpѮLPT$p }jLJ#,R8]IlsF=1Y04ߛV;dǧ\yzoĸZjZ1Al's` RqOP$oj|t1Ҋ6BwO:öxC{D"ʬ;tќx+-(TqKQvGBT~*d>#J<#i`K+|`=MCqaO *z 9d ޏMֽ#gV2}.܏*DK=yTvKªSؚxQFZX܎8ąg>:֏`ނCm#zVx!yIE2J7%F9ڳoe.VxN.\[Pr>iqdcjƄ|Й35ww@NET 3|֞~"@7Ȭi&rvYC0aUϲR!*;/gH`7t֞fWPMtϤ(}'z/:[Ka85Z ;4k #ޤ}<~*R][Sg"wPXw6 9n:NdHeAhc1).䀾Ҵc~wcEXГ6=-f9-kҢ:t 4J5v9W=t41ozVOT ,|{Ҋ l{7Rc´fm6ԍ?rc.v#$ ۶&n,Y5e rn62\nkccx+- "=ǥ -:C[#Q>b7SPĺ>:o"bVa'N ͺU0R`ZOY"b@v/6+VDZǔFZlr걀ډ KG\/5%dޛ1I'œ-au/UItGh hmǝnlo7D9|4nƵQqn EiuVG9sC;R G3LŊ)^0ly=/I6G*6r(#j47ی?ԁY q\TpsS<)ໟߵJǡ #Hı&*iI#֜qy!Ҧ+eՒ Б9}%w+:4T, NeD8#,Hg>a:g'z/Bǝv}cL(2}[]GJf1MMwB%X%pdB; 6x#ڛ#WMM8ł@?)ǥڐG޵n QH-Ǡx3FKvQ&li4jy߭8m+,>ׅ+F*"*⌗'3g>sN64O1,1H,iyb@?JZ>nM-y W:Nq %kN  Lj\PLo2W`ϵ?IgHw Vqr:kjת[~<8TttIIYή"q=ڕI|5vXሷ$0+6pc%U"E ހӰb_~Ⓛ܌l5/0ShF\95)F6ۚD[xr>)=9#'<^Iv.qE{Lx3[q^2ʸ J Hmꅑ Dڮ[pFyT^QBMξH=O0GJoLRY{;BXLP6jӰSRag񍸗]m(̸'L߶z݄\us:iT־=A1ɮcky2>>G? b{WU%հՁ A$v0"~)վܽ48.;xhuv&\o1Ʉ>jbAldmeaFⰦiXcq޺ȮqSLV1 NT]u'ʇqBՔ<13J⣧ʖ0FŤnS#\X@zDĻIsj^X^Ұ<қ12BHs*)z'AKcf%8de ;ՂU3 d-f{Ź,'PK#3aNՓu9 <{R[)'zxq(inxCșؒFkB XlG;"K(Z7*D>y4ݝkPrC`鮠ݿ8"` KLdb!m`UOib'Y~)ɥ=+ISUb?5_\~yqO|S]2%yU!SҨC YR/|m>]6 buި$mW#HVݴ0sH Nw.uFT/.wZUKF 20'#QN8K)2]}wVl[% ^-P`=uwMWQsSd[5p3ֳ.:;(&Y~hx@#(&zV©# I|׏T(bӴ>¢^f+ҝdS Nh ԥOʧ5%@c颀3)ΊsW,A&׷\VFsz_L$AN{qdCz*]ޢ]ZG ⑒5S0?K̺MR;j˨˃;Q0D һJ7~=sPt44UY:2 5Comd DŽ8_'pDŽvQFTy6'$ˏ.L M}#R`3&'Bw-f+/Ϲ$.l=JQ˨<8+ Ē_K@9.ALQ-[0|o"8@*RgqQ$lNsh5$RWr1XP^=C>gPy3d)Km}m%aʻYe8ht01\)Dm'EnXM ,0Xgڈxv?4" V3HOy4.7;"R[~!rO->IڝP+DiWƳ^-曲 ֮v1Ei{E7o?s_!+lPmg"L']^BP("gdFP3ݔG?RYh;͸EsO}G:|<-#W-U"[ =˼I(+%DCWU$sj\bZLWɁzQ@搶ӰA)_-]'o -3%Nk$1,d-ՄhڧOX 9<>hqxu"ˀ~*G$JJB O}0ڽpVwqЅ(Ɠc(wj`R*]ANOj2Z."u:>rq ҙ s,&K}ԪX'3ؒtnv8z%94v@'Q#v߽=g%Z'ڥCqVlm<9(H):4d}j Ϧ)y.si);qb8(x,ن64h|=KR.Gއ4 z}7ڡQe97D$;}()+Ygm >տeX%r)|s&z#OMO+Ⴒ|JI!1?s+ק|$_Lդr/"G)RzZS 뚓iT7<ҲR KJYԍ(6Te#@^%˂[.&%wxe>y>֖6mŸy1g5}r.w?5'X!5{-EV2\+;K޷(+3I} dn;og$-"<ϩ\ʤߚdM,Eޕ[O!SꦄQG|E'̷ 7E.E5KglͪA71”Ij65?W?MUx0B^{&}jr/-UHkcNLXv9$־ۘY>,W=>rU]~MPV~uVf$ɿPl$9Ge6[((0ڮbGjY _?IV.Euam$k;ĵsᗥ_Ih;w/F-;o޻{*n">E6*zrO/qM=9xYć К.ҥ[~ӠȎ)nCsǽR{g_&Uݎ7>|]Mj[ͰykqLsTr9'˱fAHo ޴Xb!s#4h%Rl}2}-_SS݁{FF cw+l^dFHhLin<A&_˶a+\;gN2a$zVtqI V$\uSǑ=ءu]bp5Q=*S1\]Ra:K5|M>f|]c zVk9i&AHp޴QDlA;VUQ)u[0wŲܝ'!H5[-O'z}UM /{mlHXNA5ROy3f4TX^8>kId 9͍jVem{ar 9'{UDoڝ`s]VwO8޲gտСyu2yaǠg 65sap[1!oj:k! 75gbV ;baw"]NIڝΓH!/]wQk I";n8Ӌzg]2ڲA:64oG8Юzdb Q1dFeھߦc'H+oH>%n@yK\*AM; l&342:JzA I"uCv[8sFxvTS!ֻ1iRUrii7VZ'®sO/ qc7^*0+G%"A dF^L>Y`dA|42*)7 L4J ;׌#eUgcS,H XVҔ!UW3ښO4jm8xG0b7s"AK$gAj$O"6yy$ySN )smnf*XU}e zf4qEo&l²LK&œ+뚬(%:ο}ĸ,'d# (pɧhPq~V"sY$o/F&*ړCrr0ieHc߾YLSp荠JNy@-kpI qI} 7j9ϵysXp= YEס؏޼cF='E4Ut,d:byƫ.eUfTq_T:GZ*F;T$L*'# J&>R?)>3a<[[XlI!(u+*wirȅ|f]<`▓]k8+ɨdjűeܷ&<%Jކ#Ϝ;)QHҴ?^rh8CHS^KmQ,T1_ 8=5kV4h93;Zq(䮴`?y&A9R[}MUzJVѧlqLe,#% <~AFG lQvq9C;F sYɷڟ;ᮚBO^P^#%̑<š] vs PPFIgfݤ;Ie 4KGC*WR ̠y 4'0J*LGmsWK=JW"sZqbhIe;d(Zs\Ƭ gl̓49ZwY3ntq\G(m]S?My_<Ư댁X2LlkK5̉*6T`dWLé1+s_;~}E$ pF9O8duY1Ik:\׀=AQ),4 ]` x5NS,/oB3ӳAK)-w#ðVI$R&3Ti8E?Tu(Q"sv?JOq5ΛR;_ L Vӽ@կ->l%:|,W_'ҝƗ!I4SnPS/MVX'cZNaiޣk]$# ،+{@c;vi׭7gu#}{R_OA[ΕSx@U^g֗Uybea(w^@#H9}[`11;S_E[Ԓkaa֧pؤdv7*`=jXW~pc\ԝWW߽;|2HzaPx}QcxbMk_B{E%ZE>>2J`.PѨ(Bn٫5v2\$3PV _9e--cV۝k)WYf02#֪[EdbRc`#Yy⊱\K_jMΉH: y-߆)ȫcזpGt o49Re8>1 Qr[X:w:i 7zi. VDR5[1qn 翭K|oJ,hq +0Q<ۏ)Lm\ 6*CǬ)Hyf8r2$,Q(noӀLpO5TkG_ZIwrଌ@7~ݭWNRE'%,CG4qB`3s!% Ž^829RUwcYuM2:e[)4¹8C霅˟;浥"XJp23+޶z/R Dxm$uY-R̐5V;R}W^F_[&\l7: H~EbpjK"|QEYqG3X_StB{N2;_Q0}LD(|zʶ#XpƓ\;#;VHINC$4u26EHQ-Bvz0qSOHjچ=:xܚÆE/#@yC`1Oh"zj<8!ӔESuo[In*NŪZ9PN7犵4J`fHΟ;}).Ih-و%I/QRFF KCl L-&r֯)xh.7N[DѮ29p{2* *N+a25=5)R*ѿvp=NiЂˠgq\3 w,8$$Džq!_j~l!VcڱZc'lsBKR];mEk(b.cSE%~w9ޛ0H:3nld+=0jdF Ly)inD ֗w%#ker$J]H'UqqOm~8 J\^J v"Tp#*4Q9hy]gb擸?ĕF1ړ;ReV'aUU'P:tG1-=GP&N7ڛb4'֙fdwvpUAύg\ɜܞzi469zF$O Of;+JA^) pFpyictr(@wT{e,y^g֞$*:lLvf[{mW`jEx3Y4Bu(2F*YLF 7[Lta 4p"-EID8O"'xdBI9!X]C"H<UܔڏEԦ"Gv*CݥbnjўHm9qhOS͏EY؃ź[(}}jk%mJ1I3CdR]Ib3zB;ԁD+9sWkbDn5f'0i0[=Q$=]ܲ,3,6:BsF8ϡ$_0Ju{'X$3m_Bx&Hˍnb $`s5i#޵/K"f wqYoqڍ&,O@_6ZN'Ƙ62SEv5+!e:Ȱ@VX_DF,v}9%VH;zѿo:w]ydGTTL:) rsOSd}/K~gg- ;_V-B[KUWB_P%L>0 3]|ŏ\ק> G2לZDMu`6|ϧu刜H6U1thYIXvɩ6T2 fl+"X+Rk3N7ڛ`u:k+mfG+ڣQڜv񆗌lQBroM7 ]Zsڪ[톧śXԝ^1g1Yؓډ0*&]BKW5<K4xŀX0$5Z,pũl.;w?j$7?NɟlS+u~>5[x- oBH5/!@i0O>#?sYOdkx"rx(:͇wbLqoW13AEU KзwмۤsܗڔNui?ʻU-s;;"gNee cv9Ӣ2ǥk19|pڡB3@NAɴx;qpG2/A3Fl 1" TvN[gWncjaQf嗹:C4@%¨9Q#0_?-\9g@><c)C$X#BrE]G+lB & 'ْe-+P@csT~]@P;%쵛n@V*&.]!ŔYuq܇U8988Ϸ*ǝTy!JGj xCsC= =<_j$;JBp) ̈u HS(yXRq"\צ؅O'mTDY3 ږQ36 $mw\դ' "clz (NPmԬR$`.|VVpu"`pp9,R30v'lQݚ <;agp4_) #l<0?(8Y`mDdS'yFNH>`j 1G+ Og9tEΰ㊥ĥ#%U{WaC:;; t~q:grFWk#wgǢ d)d..cBrt_/*$2tS^vK!sr~'/sC)˹Qen7İ |7^M8( 7{E +hZi2{ڑeUi$4$qL wɭhb01$S'? /[\eW@ vfH,mmLHfni[8VIں$hѓQ3:(Z^o{xݵjhZ]\ʲ l l<HI>u;V扬k-F; ևN1C[iqC!>:D>Gr z@B֖*Gvoz%͗!?՚?:=D4$j1»#%&wEy>Oyp; ׭z,D>,6q( dUv[2}C ڤ+hˈciId9xA?%W"!'~;֣%̅.I9䰺$%f4E$oc6:`ʦx%s S%t)i#A@&7Üzz|ҝ+pȼ,$h Hkl2lbTH1ȥZy=MVXF o++x,̓i^1;olcNآ~nC\(i+q#)mӽOM6y$#neuE fkh`y#c@Y<$  cVoP*lIZߤPGlq7oQ 0)+o<f=38=iU AZ$>h/q*8P+=)# p1Wpڿq#X8҇["/3FhL(ޔW*';U3!<‰*J\x Xa'|VxELPpvf[g2;3K Ǜ|FY`R<v.bp)=#T< wH٘R ܌sAZ;#;995g.vSDwQ}#vC\\ HuJA$ Zx?+`94NXNj(ɧ[LK)„Filj!uTE,3qMۆ2J]D̬xoh&ci$FI伖uF3N;i<0i֚X̞`W;mU>w]FĐm?ځ=: p "DU 1iy326@ޝM&0У0~ŜitFc[X'&!64٘Zr#O2HS,C5zRNFq^[Zt#cN`zKF|Jw5Agq^'X a&AZ-Lp p*I[ 9?ltVHm[6Q)s3P<~"C~Գg/WdrT4 Zu(ZμTȉ,ʢMDlN3^dH ٮǐDVƋ&-$j.g$Z9cIaX ObNĀ*İʒHڣo-K-8&2 R{kKtM WJJFq Zm|;(狿bJZX?dAF8cS46[#F|ghm!OvUYKh$hRj"F: Ni$ ZهR)FXR!(V|x!S@&6R@%ͬ`isڕ}IpLcrzJnJi݂K›O,^ihIlҩmL$y)ʁaڈѠtafvѾ^V DF8 r>uPdMmlug:~h-SaK9$wȞ'QʵhO mj+7NUJ4yc>Æә$D}'IQg")% [t i՝}i IЮ! Sh銈8ܱ yX:C8K rFt4M;gTDӵ~r2)Pt#?zQ6 aӯ I.K,XTSn1,. ScT\!22L1 7~fZJC'5c.8p(3YʊIOڍee1*2ei7\OH.a{ xӂ}gȱlaC*V=AHf#HgAҖ8J&qZԌTJѤJ-M/Ȗb9 {fYTn_ ]@<֍2WDArYv"W/s^+t'!\jf]#-m~@58*H83҃B֩"O0?˝xLLFYd}(Ar]y跑4q |ks?QCP2\<0?Z~IO HNCeWPFs<eVTk4RϮeDVf;lQ"Xac#URqVLxKc#7a!rr|[ڔӎ.{sDmw aacqsd#g3@OI0,ƪ];RB0-ƈdPe5P`;RhdǐrBǍJQfnqm94uff1"hHg89eމ q$lʪP3^/qo$MC h[VF ~s4 L[Pl>PX3gR8a\ڵ,z|pFLJyL-;{L,QtT}gfmTYP(3He]z@!9qc)+t2%<˧ӫ\clmV 8,ZHuNv9(/k#)&CH{`~cP>_j4lU'cq*N$Tb9qfeG>Q"IJ*͒*t)0yj@vx ,[i?sZv,\D9Xx=]cO4,eTa9FK]!W Sr#7NNJzf-fRV HP=ro"BrӧNK;ş@T&_oxl|CT49ts+ 5O ګ$e\3T,{q"J(JY$ lJ㤐²ͫ U־np'A oP/w ir@T_U$uiMژKIH ZmeMFZYZ2Cb[1-'OuKREpƺr?zu/47j8Nby",ECx dԡmɐV|xv- =)-rb4oI d|ئgm at)eS} Ͽ4i+]s\0S\K!ϛ#8fHʥ)O RμZ :zw&^m}$%2N(BG)ьU^R/+,hxF տ[SW[0aٱz.P,IQ?ԷNrG׫_Mzm2[/(LJҬѠ10J׫IUR&)S< zfX+FpH^ bua":Iޮֈ.f ugNF~_YQ1]d~ }U ^0|2% ͑Gұ~(17β?zEI- d'" ϥNN5{t+%̚?LwR;|g;ö"\3d*E}Yү告WuKZ9A 1R_xG"y珰W|_fl˂?J@6ozN{RU.Uʴs1ɦ踊0NڰAWK5y'ߚ,3'$ꖘ,+*~,BC5pNʬvS)#>o18-ك6Y׫ԭoĚ**j->' L2;$]&^s_F<-?̚2 UvϽzNzJoͨBPܱ \^oв ` tj#Yp;)"^}.iAڨLC!YW(<73M w[ 0NMzS>obi]؅ 1M,3rzC۸ǀpʱj,0,-"7W׷4h@DZ>zs54"sRjװYc`3+ b(g޽^WdmATHu,L[QR"N.I4ibYy!˰(N׫զḆx1)i`ћWrR#"],Ҹ" 2aTEE!?kH.$G/oM3m88Z!31zU'ݎp(坘]W +f %H;CBӀ|׫s.2 '5/6', 1 => '5/15', 2 => '5/24', 3 => '5/30', 4 => '6/4', 5 => '6/12', 6 => '6/21', 7 => '6/28' } g.data :Jimmy, [25, 36, 86, 39, 25, 31, 79, 88] g.data :Charles, [80, 54, 67, 54, 68, 70, 90, 95] g.data :Julie, [22, 29, 35, 38, 36, 40, 46, 57] g.data :Jane, [95, 95, 95, 90, 85, 80, 88, 100] g.data :Philip, [90, 34, 23, 12, 78, 89, 98, 88] g.data :Arthur, [5, 10, 13, 11, 6, 16, 22, 32] g.write('exciting.png') ``` ## Examples You can find many examples in the [test](https://github.com/topfunky/gruff/tree/master/test) directory along with their resulting charts in the [output](https://github.com/topfunky/gruff/tree/master/test/output) directory. You can find older examples here: http://nubyonrails.com/pages/gruff ### Accumulator bar chart ![Accumulator bar chart](https://raw.github.com/topfunky/gruff/master/test/output/accum_bar.png) ### Area chart ![Area chart](https://raw.github.com/topfunky/gruff/master/test/output/area_keynote.png) ### Bar chart ![Bar chart](https://raw.github.com/topfunky/gruff/master/test/output/bar_rails_keynote.png) ### Bezier chart In progress! ![Bezier chart](https://raw.github.com/topfunky/gruff/master/test/output/bezier_3.png) ### Bullet chart In progress! ![Bullet chart](https://raw.github.com/topfunky/gruff/master/test/output/bullet_greyscale.png) ### Dot chart ![Dot chart](https://raw.github.com/topfunky/gruff/master/test/output/dot.png) ### Line chart ![Line chart](https://raw.github.com/topfunky/gruff/master/test/output/line_theme_rails_keynote_.png) ### LineXY chart ![LineXY chart](https://raw.github.com/topfunky/gruff/master/test/output/line_xy.png) ### Net chart ![Net chart](https://raw.github.com/topfunky/gruff/master/test/output/net_wide_graph.png) ### Pie chart ![Pie chart](https://raw.github.com/topfunky/gruff/master/test/output/pie_pastel.png) ### Scatter chart ![Scatter chart](https://raw.github.com/topfunky/gruff/master/test/output/scatter_basic.png) ### Side bar chart ![Side bar chart](https://raw.github.com/topfunky/gruff/master/test/output/side_bar.png) ### Side stacked bar chart ![Side stacked bar chart](https://raw.github.com/topfunky/gruff/master/test/output/side_stacked_bar_keynote.png) ### Spider chart ![Spider chart](https://raw.github.com/topfunky/gruff/master/test/output/spider_37signals.png) ### Stacked area chart ![Stacked area chart](https://raw.github.com/topfunky/gruff/master/test/output/stacked_area_keynote.png) ### Stacked bar chart ![Stacked bar chart](https://raw.github.com/topfunky/gruff/master/test/output/stacked_bar_keynote.png) ## Documentation http://www.rubydoc.info/github/topfunky/gruff/frames ## Contributing ### Source The source for this project is now kept at GitHub: http://github.com/topfunky/gruff 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request