squid 0.0.0 → 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +11 -17
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +11 -0
- data/MIT-LICENSE +21 -0
- data/README.md +146 -2
- data/Rakefile +26 -1
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/manual.rb +7 -0
- data/examples/readme/01-basic.rb +5 -0
- data/examples/readme/02-type-point.rb +5 -0
- data/examples/readme/03-type-line.rb +5 -0
- data/examples/readme/04-color.rb +5 -0
- data/examples/readme/05-gridlines.rb +5 -0
- data/examples/readme/06-ticks.rb +5 -0
- data/examples/readme/07-baseline.rb +5 -0
- data/examples/readme/08-every.rb +5 -0
- data/examples/readme/09-legend.rb +5 -0
- data/examples/readme/10-legend-offset.rb +5 -0
- data/examples/readme/11-format.rb +5 -0
- data/examples/readme/12-labels.rb +5 -0
- data/examples/readme/13-border.rb +5 -0
- data/examples/readme/14-height.rb +5 -0
- data/examples/readme/15-multiple-columns.rb +5 -0
- data/examples/readme/16-multiple-stacks.rb +5 -0
- data/examples/readme/17-two-axis.rb +7 -0
- data/examples/readme/readme.rb +28 -0
- data/examples/readme.rb +7 -0
- data/examples/screenshots/readme_00.png +0 -0
- data/examples/screenshots/readme_01.png +0 -0
- data/examples/screenshots/readme_02.png +0 -0
- data/examples/screenshots/readme_03.png +0 -0
- data/examples/screenshots/readme_04.png +0 -0
- data/examples/screenshots/readme_05.png +0 -0
- data/examples/screenshots/readme_06.png +0 -0
- data/examples/screenshots/readme_07.png +0 -0
- data/examples/screenshots/readme_08.png +0 -0
- data/examples/screenshots/readme_09.png +0 -0
- data/examples/screenshots/readme_10.png +0 -0
- data/examples/screenshots/readme_11.png +0 -0
- data/examples/screenshots/readme_12.png +0 -0
- data/examples/screenshots/readme_13.png +0 -0
- data/examples/screenshots/readme_14.png +0 -0
- data/examples/screenshots/readme_15.png +0 -0
- data/examples/screenshots/readme_16.png +0 -0
- data/examples/screenshots/readme_17.png +0 -0
- data/examples/squid/baseline.rb +9 -0
- data/examples/squid/basic.rb +9 -0
- data/examples/squid/border.rb +9 -0
- data/examples/squid/columns.rb +9 -0
- data/examples/squid/every.rb +11 -0
- data/examples/squid/format.rb +9 -0
- data/examples/squid/gridlines.rb +9 -0
- data/examples/squid/height.rb +9 -0
- data/examples/squid/labels.rb +9 -0
- data/examples/squid/legend.rb +9 -0
- data/examples/squid/legend_offset.rb +9 -0
- data/examples/squid/line.rb +9 -0
- data/examples/squid/line_width.rb +9 -0
- data/examples/squid/lines.rb +11 -0
- data/examples/squid/point.rb +9 -0
- data/examples/squid/points.rb +9 -0
- data/examples/squid/squid.rb +48 -0
- data/examples/squid/stacks.rb +10 -0
- data/examples/squid/ticks.rb +9 -0
- data/examples/squid/two_axis.rb +10 -0
- data/lib/squid/axis.rb +61 -0
- data/lib/squid/axis_label.rb +15 -0
- data/lib/squid/config.rb +46 -0
- data/lib/squid/configuration.rb +80 -0
- data/lib/squid/format.rb +22 -0
- data/lib/squid/graph.rb +113 -0
- data/lib/squid/gridline.rb +16 -0
- data/lib/squid/plotter.rb +187 -0
- data/lib/squid/point.rb +40 -0
- data/lib/squid/settings.rb +15 -0
- data/lib/squid/version.rb +1 -1
- data/lib/squid.rb +35 -3
- data/squid.gemspec +22 -14
- metadata +176 -23
- data/HISTORY.md +0 -1
- data/LICENSE.txt +0 -22
- data/bin/squid +0 -8
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> uses a line width of 3 for line charts.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:line_width</code> option to customize this value.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 6 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 7 | 
            +
              data = {views: {2013 => 182, 2014 => 46, 2015 => 802000000000000000000}}
         | 
| 8 | 
            +
              chart data, type: :line, line_width: 10
         | 
| 9 | 
            +
            end
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> plots a column for each series.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:type</code> option to plot a line chart instead.
         | 
| 4 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 5 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 6 | 
            +
              data = {views: {2013 => 182, 2014 => -46, 2015 => 88},
         | 
| 7 | 
            +
                    uniques: {2013 => 104, 2014 => 27, 2015 => 14}}
         | 
| 8 | 
            +
              chart data, type: :line, labels: true
         | 
| 9 | 
            +
            end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> plots a column chart.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:type</code> option to plot a point chart instead.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 6 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 7 | 
            +
              data = {views: {Safari: 45.20001, Firefox: 63.3999, Chrome: 21.4}}
         | 
| 8 | 
            +
              chart data, type: :point
         | 
| 9 | 
            +
            end
         | 
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> plots a column for each series.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:type</code> option to plot a point chart instead.
         | 
| 4 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 5 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 6 | 
            +
              data = {views: {2013 => 182, 2014 => 46, 2015 => 88},
         | 
| 7 | 
            +
                    uniques: {2013 => -104, 2014 => 27, 2015 => 14}}
         | 
| 8 | 
            +
              chart data, type: :point, labels: true
         | 
| 9 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            Prawn::ManualBuilder::Example.generate 'squid.pdf' do
         | 
| 2 | 
            +
              package 'squid' do |p|
         | 
| 3 | 
            +
                p.name = 'Squid'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                p.intro do
         | 
| 6 | 
            +
                  prose('
         | 
| 7 | 
            +
                    Prawn is a great library to generate PDF files from Ruby, but lacks high-level components to generate graphs.
         | 
| 8 | 
            +
                    Squid integrates Prawn by providing methods to plot charts in PDF files with few lines of code.
         | 
| 9 | 
            +
                    This manual shows:
         | 
| 10 | 
            +
                  ')
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  list(
         | 
| 13 | 
            +
                    'How to create graphs',
         | 
| 14 | 
            +
                  )
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                p.section 'Basics' do |s|
         | 
| 18 | 
            +
                  s.example 'basic'
         | 
| 19 | 
            +
                  s.example 'legend'
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                p.section 'Chart types' do |s|
         | 
| 23 | 
            +
                  s.example 'point'
         | 
| 24 | 
            +
                  s.example 'line'
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                p.section 'Styling' do |s|
         | 
| 28 | 
            +
                  s.example 'height'
         | 
| 29 | 
            +
                  s.example 'baseline'
         | 
| 30 | 
            +
                  s.example 'ticks'
         | 
| 31 | 
            +
                  s.example 'every'
         | 
| 32 | 
            +
                  s.example 'gridlines'
         | 
| 33 | 
            +
                  s.example 'format'
         | 
| 34 | 
            +
                  s.example 'border'
         | 
| 35 | 
            +
                  s.example 'labels'
         | 
| 36 | 
            +
                  s.example 'line_width'
         | 
| 37 | 
            +
                  s.example 'legend_offset'
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                p.section 'Multiple series' do |s|
         | 
| 41 | 
            +
                  s.example 'columns'
         | 
| 42 | 
            +
                  s.example 'lines'
         | 
| 43 | 
            +
                  s.example 'points'
         | 
| 44 | 
            +
                  s.example 'stacks'
         | 
| 45 | 
            +
                  s.example 'two_axis'
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> plots multiple columns next to each other.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:type</code> option to plot stacked columns instead.
         | 
| 4 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 5 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 6 | 
            +
              data = {views: {2013 => 196, 2014 => 66, 2015 => -282},
         | 
| 7 | 
            +
                       hits: {2013 => 100, 2014 => -12, 2015 => 82},
         | 
| 8 | 
            +
                    uniques: {2013 => -114, 2014 => 47, 2015 => -14}}
         | 
| 9 | 
            +
              chart data, type: :stack, labels: true
         | 
| 10 | 
            +
            end
         | 
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            # By default, <code>chart</code> plots ticks above the categories.
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use the <code>:ticks</code> option to disable this behavior.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 6 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 7 | 
            +
              data = {views: {2013 => 182, 2014 => 46, 2015 => 802000000000000000000}}
         | 
| 8 | 
            +
              chart data, ticks: false
         | 
| 9 | 
            +
            end
         | 
| @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            # By default, ..
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # You can use ..
         | 
| 4 | 
            +
            filename = File.basename(__FILE__).gsub('.rb', '.pdf')
         | 
| 5 | 
            +
            Prawn::ManualBuilder::Example.generate(filename) do
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              data = {views: {2013 => 182, 2014 => 46, 2015 => 88},
         | 
| 8 | 
            +
                   earnings: {2013 => 104_323, 2014 => 27_234, 2015 => 14_123}}
         | 
| 9 | 
            +
              chart data, type: :two_axis, border: true
         | 
| 10 | 
            +
            end
         | 
    
        data/lib/squid/axis.rb
    ADDED
    
    | @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            require 'squid/format'
         | 
| 2 | 
            +
            require 'active_support/core_ext/enumerable' # for Array#sum
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Squid
         | 
| 5 | 
            +
              # @private
         | 
| 6 | 
            +
              class Axis
         | 
| 7 | 
            +
                include Format
         | 
| 8 | 
            +
                attr_reader :data
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def initialize(data, steps:, stack:, format:, &block)
         | 
| 11 | 
            +
                  @data, @steps, @stack, @format = data, steps, stack, format
         | 
| 12 | 
            +
                  @width_proc = block if block_given?
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def minmax
         | 
| 16 | 
            +
                  @minmax ||= [min, max].compact.map do |number|
         | 
| 17 | 
            +
                    approximate number
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def labels
         | 
| 22 | 
            +
                  min, max = minmax
         | 
| 23 | 
            +
                  values = if @data.empty? || @steps.zero?
         | 
| 24 | 
            +
                    []
         | 
| 25 | 
            +
                  else
         | 
| 26 | 
            +
                    max.step(by: (min - max)/@steps.to_f, to: min)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                  @labels ||= values.map{|value| format_for value, @format}
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def width
         | 
| 32 | 
            +
                  @width ||= labels.map{|label| label_width label}.max || 0
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              private
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def label_width(label)
         | 
| 38 | 
            +
                  @width_proc.call label if @width_proc
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def min
         | 
| 42 | 
            +
                  [values.first.min, 0].min if @data.any?
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def max
         | 
| 46 | 
            +
                  [values.last.max, @steps].max if @data.any?
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def values
         | 
| 50 | 
            +
                  @values ||= if @stack
         | 
| 51 | 
            +
                    @data.transpose.map{|a| a.compact.partition{|n| n < 0}.map(&:sum)}.transpose
         | 
| 52 | 
            +
                  else
         | 
| 53 | 
            +
                    [@data.flatten.compact]
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def approximate(number)
         | 
| 58 | 
            +
                  number_to_rounded(number, significant: true, precision: 2).to_f
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            module Squid
         | 
| 2 | 
            +
              class AxisLabel
         | 
| 3 | 
            +
                def self.for(axis, height:, align:)
         | 
| 4 | 
            +
                  height.step(0, -height/(axis.labels.size-1).to_f).map.with_index do |y, i|
         | 
| 5 | 
            +
                    new y: y, label: axis.labels[i], align: align, width: axis.width
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_reader :label, :y, :align, :width
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(label:, y:, align:, width:)
         | 
| 12 | 
            +
                  @label, @y, @align, @width = label, y, align, width
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
    
        data/lib/squid/config.rb
    ADDED
    
    | @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            require 'squid/configuration'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Squid
         | 
| 4 | 
            +
              # Provides methods to read and write global configuration settings.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # A typical usage is to set the default dimensions and colors for charts.
         | 
| 7 | 
            +
              #
         | 
| 8 | 
            +
              # @example Set the default height for Squid graphs:
         | 
| 9 | 
            +
              #   Squid.configure do |config|
         | 
| 10 | 
            +
              #     config.height = 150
         | 
| 11 | 
            +
              #   end
         | 
| 12 | 
            +
              #
         | 
| 13 | 
            +
              # Note that Squid.configure has precedence over values through with
         | 
| 14 | 
            +
              # environment variables (see {Squid::Configuration}).
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              module Config
         | 
| 17 | 
            +
                # Yields the global configuration to the given block.
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @example
         | 
| 20 | 
            +
                #   Squid.configure do |config|
         | 
| 21 | 
            +
                #     config.height = 150
         | 
| 22 | 
            +
                #   end
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                # @yield [Squid::Configuration] The global configuration.
         | 
| 25 | 
            +
                def configure
         | 
| 26 | 
            +
                  yield configuration if block_given?
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # Returns the global {Squid::Configuration} object.
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # While this method _can_ be used to read and write configuration settings,
         | 
| 32 | 
            +
                # it is easier to use {Squid::Config#configure} Squid.configure}.
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                # @example
         | 
| 35 | 
            +
                #     Squid.configuration.height = 150
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @return [Squid::Configuration] The global configuration.
         | 
| 38 | 
            +
                def configuration
         | 
| 39 | 
            +
                  @configuration ||= Squid::Configuration.new
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              # @note Config is the only module auto-loaded in the Squid module,
         | 
| 44 | 
            +
              #       in order to have a syntax as easy as Squid.configure
         | 
| 45 | 
            +
              extend Config
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,80 @@ | |
| 1 | 
            +
            require 'ostruct'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Squid
         | 
| 4 | 
            +
              # Provides an object to store global configuration settings.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # This class is typically not used directly, but by calling
         | 
| 7 | 
            +
              # {Squid::Config#configure Squid.configure}, which creates and updates a
         | 
| 8 | 
            +
              # single instance of {Squid::Configuration}.
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # @example Set the default height for Squid graphs:
         | 
| 11 | 
            +
              #   Squid.configure do |config|
         | 
| 12 | 
            +
              #     config.height = 150
         | 
| 13 | 
            +
              #     config.steps = 4
         | 
| 14 | 
            +
              #   end
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              # @see Squid::Config for more examples.
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              # An alternative way to set global configuration settings is by storing
         | 
| 19 | 
            +
              # them in the following environment variables:
         | 
| 20 | 
            +
              #
         | 
| 21 | 
            +
              # * +SQUID_HEIGHT+ to store the default graph height
         | 
| 22 | 
            +
              #
         | 
| 23 | 
            +
              # In case both methods are used together,
         | 
| 24 | 
            +
              # {Squid::Config#configure Squid.configure} takes precedence.
         | 
| 25 | 
            +
              #
         | 
| 26 | 
            +
              # @example Set the default graph height:
         | 
| 27 | 
            +
              #   ENV['SQUID_HEIGHT'] =  '150'
         | 
| 28 | 
            +
              #   ENV['SQUID_GRIDLINES'] =  '4'
         | 
| 29 | 
            +
              #
         | 
| 30 | 
            +
              class Configuration < OpenStruct
         | 
| 31 | 
            +
                COLORS = '2e578c 5d9648 e7a13d bc2d30 6f3d79 7d807f'
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def self.boolean
         | 
| 34 | 
            +
                  -> (value) { %w(1 t T true TRUE).include? value }
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
                
         | 
| 37 | 
            +
                def self.integer
         | 
| 38 | 
            +
                  -> (value) { value.to_i }
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def self.symbol
         | 
| 42 | 
            +
                  -> (value) { value.to_sym }
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def self.float
         | 
| 46 | 
            +
                  -> (value) { value.to_f }
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                def self.array
         | 
| 50 | 
            +
                  -> (value) { value.split }
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                ATTRIBUTES = {
         | 
| 54 | 
            +
                  baseline:     {default: 'true',      as: boolean},
         | 
| 55 | 
            +
                  border:       {default: 'false',     as: boolean},
         | 
| 56 | 
            +
                  chart:        {default: 'true',      as: boolean},
         | 
| 57 | 
            +
                  colors:       {default: COLORS,      as: array},
         | 
| 58 | 
            +
                  every:        {default: '1',         as: integer},
         | 
| 59 | 
            +
                  format:       {default: 'integer',   as: symbol},
         | 
| 60 | 
            +
                  height:       {default: '250',       as: float},
         | 
| 61 | 
            +
                  labels:       {default: 'false',     as: boolean},
         | 
| 62 | 
            +
                  legend:       {default: 'true',      as: boolean},
         | 
| 63 | 
            +
                  line_widths:  {default: '3',         as: integer},
         | 
| 64 | 
            +
                  steps:        {default: '4',         as: integer},
         | 
| 65 | 
            +
                  ticks:        {default: 'true',      as: boolean},
         | 
| 66 | 
            +
                  type:         {default: 'column',    as: symbol},
         | 
| 67 | 
            +
                }
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                attr_accessor *ATTRIBUTES.keys
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                # Initialize the global configuration settings.
         | 
| 72 | 
            +
                def initialize
         | 
| 73 | 
            +
                  ATTRIBUTES.each do |key, options|
         | 
| 74 | 
            +
                    var = "squid_#{key}".upcase
         | 
| 75 | 
            +
                    value = ENV.fetch var, options[:default]
         | 
| 76 | 
            +
                    public_send "#{key}=", options[:as].call(value)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
            end
         | 
    
        data/lib/squid/format.rb
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            require 'active_support'
         | 
| 2 | 
            +
            require 'active_support/number_helper' # for number_to_rounded
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Squid
         | 
| 5 | 
            +
              module Format
         | 
| 6 | 
            +
                include ActiveSupport::NumberHelper
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def format_for(value, format)
         | 
| 9 | 
            +
                  case format
         | 
| 10 | 
            +
                    when :percentage then number_to_percentage value, precision: 1
         | 
| 11 | 
            +
                    when :currency then number_to_currency value
         | 
| 12 | 
            +
                    when :seconds then number_to_minutes_and_seconds value
         | 
| 13 | 
            +
                    when :float then number_to_delimited value
         | 
| 14 | 
            +
                    else number_to_delimited value.to_i
         | 
| 15 | 
            +
                  end.to_s
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def number_to_minutes_and_seconds(value)
         | 
| 19 | 
            +
                  "#{value.round / 60}:#{(value.round % 60).to_s.rjust 2, '0'}"
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
    
        data/lib/squid/graph.rb
    ADDED
    
    | @@ -0,0 +1,113 @@ | |
| 1 | 
            +
            require 'active_support'
         | 
| 2 | 
            +
            require 'active_support/core_ext/string/inflections' # for titleize
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'squid/axis'
         | 
| 5 | 
            +
            require 'squid/axis_label'
         | 
| 6 | 
            +
            require 'squid/gridline'
         | 
| 7 | 
            +
            require 'squid/plotter'
         | 
| 8 | 
            +
            require 'squid/point'
         | 
| 9 | 
            +
            require 'squid/settings'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            module Squid
         | 
| 12 | 
            +
              # @private
         | 
| 13 | 
            +
              class Graph
         | 
| 14 | 
            +
                extend Settings
         | 
| 15 | 
            +
                has_settings :baseline, :border, :chart, :colors, :every, :format, :height
         | 
| 16 | 
            +
                has_settings :legend, :line_widths, :steps, :ticks, :type, :labels
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def initialize(document, data = {}, settings = {})
         | 
| 19 | 
            +
                  @data, @settings = data, settings
         | 
| 20 | 
            +
                  @plot = Plotter.new document, bottom: bottom
         | 
| 21 | 
            +
                  @plot.paddings = {left: left.width, right: right.width} if @data.any?
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def draw
         | 
| 25 | 
            +
                  @plot.box(h: height, border: border) { draw_graph if @data.any? }
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              private
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def draw_graph
         | 
| 31 | 
            +
                  draw_legend if legend
         | 
| 32 | 
            +
                  draw_gridlines
         | 
| 33 | 
            +
                  draw_axis_labels
         | 
| 34 | 
            +
                  draw_charts if chart
         | 
| 35 | 
            +
                  draw_categories if baseline
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def draw_legend
         | 
| 39 | 
            +
                  labels = @data.keys.reverse.map{|key| key.to_s.titleize}
         | 
| 40 | 
            +
                  offset = legend.is_a?(Hash) ? legend.fetch(:offset, 0) : 0
         | 
| 41 | 
            +
                  @plot.legend labels, offset: offset, colors: colors, height: legend_height
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def draw_gridlines
         | 
| 45 | 
            +
                  options = {height: grid_height, count: steps, skip_baseline: baseline}
         | 
| 46 | 
            +
                  Gridline.for(options).each do |line|
         | 
| 47 | 
            +
                    @plot.horizontal_line line.y, line_width: 0.5, transparency: 0.25
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def draw_axis_labels
         | 
| 52 | 
            +
                  @plot.axis_labels AxisLabel.for(left, align: :right, height: grid_height)
         | 
| 53 | 
            +
                  @plot.axis_labels AxisLabel.for(right, align: :left, height: grid_height)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def draw_categories
         | 
| 57 | 
            +
                  labels = @data.values.first.keys.map{|key| key.to_s}
         | 
| 58 | 
            +
                  @plot.categories labels, every: every, ticks: ticks
         | 
| 59 | 
            +
                  @plot.horizontal_line 0.0
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def draw_charts
         | 
| 63 | 
            +
                  draw_chart right, type: :column, colors: colors[1..-1]
         | 
| 64 | 
            +
                  draw_chart left, colors: colors
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def draw_chart(axis, options = {})
         | 
| 68 | 
            +
                  args = {minmax: axis.minmax, height: grid_height, stack: stack?, labels: labels, format: format}
         | 
| 69 | 
            +
                  points = Point.for axis.data, args
         | 
| 70 | 
            +
                  case options.delete(:type) {type}
         | 
| 71 | 
            +
                    when :point then @plot.points points, options
         | 
| 72 | 
            +
                    when :line, :two_axis then @plot.lines points, options.merge(line_widths: line_widths)
         | 
| 73 | 
            +
                    when :column then @plot.columns points, options
         | 
| 74 | 
            +
                    when :stack then @plot.stacks points, options
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def left
         | 
| 79 | 
            +
                  @left ||= axis first: 0, last: (two_axis? ? 1 : @data.size)
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def right
         | 
| 83 | 
            +
                  @right ||= axis first: 1, last: (two_axis? ? 1 : 0)
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def axis(first:, last:)
         | 
| 87 | 
            +
                  series = @data.values[first, last].map(&:values)
         | 
| 88 | 
            +
                  Axis.new series, steps: steps, stack: stack?, format: format do |label|
         | 
| 89 | 
            +
                    @plot.width_of label
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def bottom
         | 
| 94 | 
            +
                  baseline ? 20 : 0
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def legend_height
         | 
| 98 | 
            +
                  15
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def grid_height
         | 
| 102 | 
            +
                  height - bottom - legend_height * (legend ? 2 : 1)
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def stack?
         | 
| 106 | 
            +
                  type == :stack
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def two_axis?
         | 
| 110 | 
            +
                  type == :two_axis
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            module Squid
         | 
| 2 | 
            +
              class Gridline
         | 
| 3 | 
            +
                def self.for(count:, skip_baseline:, height:)
         | 
| 4 | 
            +
                  return [] if count.zero?
         | 
| 5 | 
            +
                  height.step(0, -height/count.to_f).map do |y|
         | 
| 6 | 
            +
                    new y: y unless skip_baseline && y.zero?
         | 
| 7 | 
            +
                  end.compact
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                attr_reader :y
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(y:)
         | 
| 13 | 
            +
                  @y = y
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,187 @@ | |
| 1 | 
            +
            require 'active_support'
         | 
| 2 | 
            +
            require 'active_support/core_ext/array/wrap' # for Array#wrap
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Squid
         | 
| 5 | 
            +
              # A Plotter wraps a Prawn::Document object in order to provide new methods
         | 
| 6 | 
            +
              # like `gridline` or `ticks` used by Squid::Graph to plot graph elements.
         | 
| 7 | 
            +
              class Plotter
         | 
| 8 | 
            +
                attr_accessor :paddings
         | 
| 9 | 
            +
                # @param [Prawn::Document] a PDF document to wrap in a Plotter instance.
         | 
| 10 | 
            +
                def initialize(pdf, bottom:)
         | 
| 11 | 
            +
                  @pdf = pdf
         | 
| 12 | 
            +
                  @bottom = bottom
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # Draws a bounding box of the given height, rendering the block inside it.
         | 
| 16 | 
            +
                def box(x: 0, y: @pdf.cursor, w: @pdf.bounds.width, h:, border: false)
         | 
| 17 | 
            +
                  @pdf.bounding_box [x, y], width: w, height: h do
         | 
| 18 | 
            +
                    @pdf.stroke_bounds if border
         | 
| 19 | 
            +
                    yield
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # Draws the graph legend with the given labels.
         | 
| 24 | 
            +
                # @param [Array<LegendItem>] The labels to write as part of the legend.
         | 
| 25 | 
            +
                def legend(labels, height:, offset: 0, colors: [])
         | 
| 26 | 
            +
                  left = @pdf.bounds.width/2
         | 
| 27 | 
            +
                  box(x: left, y: @pdf.bounds.top, w: left, h: height) do
         | 
| 28 | 
            +
                    x = @pdf.bounds.right - offset
         | 
| 29 | 
            +
                    options = {size: 7, height: @pdf.bounds.height, valign: :center}
         | 
| 30 | 
            +
                    labels.each.with_index do |label, i|
         | 
| 31 | 
            +
                      color = Array.wrap(colors[labels.size - 1 - i]).first
         | 
| 32 | 
            +
                      x = legend_item label, x, color, options
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Draws a horizontal line.
         | 
| 38 | 
            +
                def horizontal_line(y, options = {})
         | 
| 39 | 
            +
                  with options do
         | 
| 40 | 
            +
                    at = y + @bottom
         | 
| 41 | 
            +
                    @pdf.stroke_horizontal_line left, @pdf.bounds.right - right, at: at
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def width_of(label)
         | 
| 46 | 
            +
                  @pdf.width_of(label, size: 8).ceil
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def axis_labels(labels)
         | 
| 50 | 
            +
                  labels.each do |label|
         | 
| 51 | 
            +
                    x = (label.align == :right) ? 0 : @pdf.bounds.right - label.width
         | 
| 52 | 
            +
                    y = label.y + @bottom + text_options[:height] / 2
         | 
| 53 | 
            +
                    options = text_options.merge width: label.width, at: [x, y]
         | 
| 54 | 
            +
                    @pdf.text_box label.label, options.merge(align: label.align)
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def categories(labels, every:, ticks:)
         | 
| 59 | 
            +
                  labels.each.with_index do |label, index|
         | 
| 60 | 
            +
                    w = width / labels.count.to_f
         | 
| 61 | 
            +
                    x = left + w * (index)
         | 
| 62 | 
            +
                    padding = 2
         | 
| 63 | 
            +
                    options = category_options.merge(width: every*w-2*padding, at: [x+padding-w*(every/2.0-0.5), @bottom])
         | 
| 64 | 
            +
                    @pdf.text_box label, options if (index % every).zero?
         | 
| 65 | 
            +
                    @pdf.stroke_vertical_line @bottom, @bottom - 2, at: x + w/2 if ticks
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def points(series, colors: [])
         | 
| 70 | 
            +
                  items(series, colors: colors) do |point, w, i, padding|
         | 
| 71 | 
            +
                    x, y = (point.index + 0.5)*w + left, point.y + @bottom
         | 
| 72 | 
            +
                    @pdf.fill_circle [x, y], 5
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def lines(series, colors: [], line_widths: [])
         | 
| 77 | 
            +
                  x, y = nil, nil
         | 
| 78 | 
            +
                  items(series, colors: colors) do |point, w, i, padding|
         | 
| 79 | 
            +
                    prev_x, prev_y = x, y
         | 
| 80 | 
            +
                    x, y = (point.index + 0.5)*w + left, point.y + @bottom
         | 
| 81 | 
            +
                    line_width = Array.wrap(line_widths).fetch(i, 1)
         | 
| 82 | 
            +
                    with line_width: line_width, cap_style: :round do
         | 
| 83 | 
            +
                      @pdf.line [prev_x, prev_y], [x,y] unless point.index.zero? || prev_y.nil? || prev_x > x
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def stacks(series, colors: [])
         | 
| 89 | 
            +
                  items(series, colors: colors, fill: true) do |point, w, i, padding|
         | 
| 90 | 
            +
                    x, y = point.index*w + padding + left, point.y + @bottom
         | 
| 91 | 
            +
                    @pdf.fill_rectangle [x, y], w - 2*padding, point.height
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def columns(series, colors: [])
         | 
| 96 | 
            +
                  items(series, colors: colors, fill: true, count: series.size) do |point, w, i, padding|
         | 
| 97 | 
            +
                    item_w = (w - 2 * padding)/ series.size
         | 
| 98 | 
            +
                    x, y = point.index*w + padding + left + i*item_w, point.y + @bottom
         | 
| 99 | 
            +
                    @pdf.fill_rectangle [x, y], item_w, point.height
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              private
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def left
         | 
| 106 | 
            +
                  @paddings[:left].zero? ? 0 : @paddings[:left] + 5
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def right
         | 
| 110 | 
            +
                  @paddings[:right].zero? ? 0 : @paddings[:right] + 5
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                def width
         | 
| 114 | 
            +
                  @pdf.bounds.width - left - right
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def category_options
         | 
| 118 | 
            +
                  text_options.merge align: :center, leading: -3, disable_wrap_by_char: true
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def text_options
         | 
| 122 | 
            +
                  options = {}
         | 
| 123 | 
            +
                  options[:height] = 20
         | 
| 124 | 
            +
                  options[:size] = 8
         | 
| 125 | 
            +
                  options[:valign] = :center
         | 
| 126 | 
            +
                  options[:overflow] = :shrink_to_fit
         | 
| 127 | 
            +
                  options
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def items(series, colors: [], fill: false, count: 1, &block)
         | 
| 131 | 
            +
                  series.reverse_each.with_index do |points, reverse_index|
         | 
| 132 | 
            +
                    index = series.size - reverse_index - 1
         | 
| 133 | 
            +
                    w = width / points.size.to_f
         | 
| 134 | 
            +
                    series_colors = Array.wrap(colors[index]).cycle
         | 
| 135 | 
            +
                    points.select(&:y).each do |point|
         | 
| 136 | 
            +
                      item point, series_colors.next, w, fill, index, count, &block
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def item(point, color, w, fill, index, count)
         | 
| 142 | 
            +
                  padding = w / 8
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  with transparency: 0.95, fill_color: color, stroke_color: color do
         | 
| 145 | 
            +
                    yield point, w, index, padding
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  with fill_color: (point.negative && fill ? 'ffffff' : color) do
         | 
| 149 | 
            +
                    options = [{size: 10, styles: [:bold], text: point.label}]
         | 
| 150 | 
            +
                    position = {align: :center, valign: :bottom, height: 20}
         | 
| 151 | 
            +
                    position[:width] = (w - 2*padding) / count
         | 
| 152 | 
            +
                    x = left + point.index*w + padding
         | 
| 153 | 
            +
                    x += index * position[:width] if count > 1
         | 
| 154 | 
            +
                    position[:at] = [x, point.y + @bottom + 24]
         | 
| 155 | 
            +
                    @pdf.formatted_text_box options, position
         | 
| 156 | 
            +
                  end if point.label
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                # Draws a single item of the legend, which includes the label and the
         | 
| 160 | 
            +
                # symbol with the matching color. Labels are written from right to left.
         | 
| 161 | 
            +
                # @param
         | 
| 162 | 
            +
                def legend_item(label, x, color, options)
         | 
| 163 | 
            +
                  size, symbol_padding, entry_padding = 5, 3, 12
         | 
| 164 | 
            +
                  x -= @pdf.width_of(label, size: 7).ceil
         | 
| 165 | 
            +
                  @pdf.text_box label, options.merge(at: [x, @pdf.bounds.height])
         | 
| 166 | 
            +
                  x -= (symbol_padding + size)
         | 
| 167 | 
            +
                  with fill_color: color do
         | 
| 168 | 
            +
                    @pdf.fill_rectangle [x, @pdf.bounds.height - size], size, size
         | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
                  x - entry_padding
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
             | 
| 174 | 
            +
             | 
| 175 | 
            +
                # Convenience method to wrap a block by setting and unsetting a Prawn
         | 
| 176 | 
            +
                # property such as line_width.
         | 
| 177 | 
            +
                def with(new_values = {})
         | 
| 178 | 
            +
                  transparency = new_values.delete(:transparency) { 1.0 }
         | 
| 179 | 
            +
                  old_values = Hash[new_values.map{|k,_| [k,@pdf.public_send(k)]}]
         | 
| 180 | 
            +
                  new_values.each{|k, new_value| @pdf.public_send "#{k}=", new_value }
         | 
| 181 | 
            +
                  @pdf.transparent(transparency) do
         | 
| 182 | 
            +
                    @pdf.stroke { yield }
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
                  old_values.each{|k, old_value| @pdf.public_send "#{k}=", old_value }
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
              end
         | 
| 187 | 
            +
            end
         |