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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -17
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +11 -0
  6. data/MIT-LICENSE +21 -0
  7. data/README.md +146 -2
  8. data/Rakefile +26 -1
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/examples/manual.rb +7 -0
  12. data/examples/readme/01-basic.rb +5 -0
  13. data/examples/readme/02-type-point.rb +5 -0
  14. data/examples/readme/03-type-line.rb +5 -0
  15. data/examples/readme/04-color.rb +5 -0
  16. data/examples/readme/05-gridlines.rb +5 -0
  17. data/examples/readme/06-ticks.rb +5 -0
  18. data/examples/readme/07-baseline.rb +5 -0
  19. data/examples/readme/08-every.rb +5 -0
  20. data/examples/readme/09-legend.rb +5 -0
  21. data/examples/readme/10-legend-offset.rb +5 -0
  22. data/examples/readme/11-format.rb +5 -0
  23. data/examples/readme/12-labels.rb +5 -0
  24. data/examples/readme/13-border.rb +5 -0
  25. data/examples/readme/14-height.rb +5 -0
  26. data/examples/readme/15-multiple-columns.rb +5 -0
  27. data/examples/readme/16-multiple-stacks.rb +5 -0
  28. data/examples/readme/17-two-axis.rb +7 -0
  29. data/examples/readme/readme.rb +28 -0
  30. data/examples/readme.rb +7 -0
  31. data/examples/screenshots/readme_00.png +0 -0
  32. data/examples/screenshots/readme_01.png +0 -0
  33. data/examples/screenshots/readme_02.png +0 -0
  34. data/examples/screenshots/readme_03.png +0 -0
  35. data/examples/screenshots/readme_04.png +0 -0
  36. data/examples/screenshots/readme_05.png +0 -0
  37. data/examples/screenshots/readme_06.png +0 -0
  38. data/examples/screenshots/readme_07.png +0 -0
  39. data/examples/screenshots/readme_08.png +0 -0
  40. data/examples/screenshots/readme_09.png +0 -0
  41. data/examples/screenshots/readme_10.png +0 -0
  42. data/examples/screenshots/readme_11.png +0 -0
  43. data/examples/screenshots/readme_12.png +0 -0
  44. data/examples/screenshots/readme_13.png +0 -0
  45. data/examples/screenshots/readme_14.png +0 -0
  46. data/examples/screenshots/readme_15.png +0 -0
  47. data/examples/screenshots/readme_16.png +0 -0
  48. data/examples/screenshots/readme_17.png +0 -0
  49. data/examples/squid/baseline.rb +9 -0
  50. data/examples/squid/basic.rb +9 -0
  51. data/examples/squid/border.rb +9 -0
  52. data/examples/squid/columns.rb +9 -0
  53. data/examples/squid/every.rb +11 -0
  54. data/examples/squid/format.rb +9 -0
  55. data/examples/squid/gridlines.rb +9 -0
  56. data/examples/squid/height.rb +9 -0
  57. data/examples/squid/labels.rb +9 -0
  58. data/examples/squid/legend.rb +9 -0
  59. data/examples/squid/legend_offset.rb +9 -0
  60. data/examples/squid/line.rb +9 -0
  61. data/examples/squid/line_width.rb +9 -0
  62. data/examples/squid/lines.rb +11 -0
  63. data/examples/squid/point.rb +9 -0
  64. data/examples/squid/points.rb +9 -0
  65. data/examples/squid/squid.rb +48 -0
  66. data/examples/squid/stacks.rb +10 -0
  67. data/examples/squid/ticks.rb +9 -0
  68. data/examples/squid/two_axis.rb +10 -0
  69. data/lib/squid/axis.rb +61 -0
  70. data/lib/squid/axis_label.rb +15 -0
  71. data/lib/squid/config.rb +46 -0
  72. data/lib/squid/configuration.rb +80 -0
  73. data/lib/squid/format.rb +22 -0
  74. data/lib/squid/graph.rb +113 -0
  75. data/lib/squid/gridline.rb +16 -0
  76. data/lib/squid/plotter.rb +187 -0
  77. data/lib/squid/point.rb +40 -0
  78. data/lib/squid/settings.rb +15 -0
  79. data/lib/squid/version.rb +1 -1
  80. data/lib/squid.rb +35 -3
  81. data/squid.gemspec +22 -14
  82. metadata +176 -23
  83. data/HISTORY.md +0 -1
  84. data/LICENSE.txt +0 -22
  85. 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
@@ -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
@@ -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
@@ -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