From 82c98c8bba8f5509cad5d77e13a67dbc72703e1f Mon Sep 17 00:00:00 2001 From: claudiob Date: Wed, 5 Aug 2015 08:25:41 -0700 Subject: [PATCH 1/3] Add baseline, spanning the graph width --- lib/squid/graph.rb | 13 +++++++++++-- spec/graph/baseline_spec.rb | 16 ++++++++++++++++ spec/graph/height_spec.rb | 2 +- spec/graph/width_spec.rb | 15 --------------- 4 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 spec/graph/baseline_spec.rb delete mode 100644 spec/graph/width_spec.rb diff --git a/lib/squid/graph.rb b/lib/squid/graph.rb index ee18490..e307155 100644 --- a/lib/squid/graph.rb +++ b/lib/squid/graph.rb @@ -12,9 +12,10 @@ def initialize(document, settings = {}) @settings = settings end + # Draws the graph. def draw - bounding_box [0, pdf.cursor], width: bounds.width, height: height do - stroke_bounds + bounding_box [0, cursor], width: bounds.width, height: height do + draw_baseline end end @@ -23,5 +24,13 @@ def method_missing(method, *args, &block) return super unless pdf.respond_to?(method) pdf.send method, *args, &block end + + private + + # Draws the baseline of a graph. + # Must run after draw_graph in order to draw the line on top of the graph. + def draw_baseline + stroke_horizontal_line 0, bounds.width, at: cursor - height + end end end \ No newline at end of file diff --git a/spec/graph/baseline_spec.rb b/spec/graph/baseline_spec.rb new file mode 100644 index 0000000..43318b0 --- /dev/null +++ b/spec/graph/baseline_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'Graph baseline' do + let(:pdf) { Prawn::Document.new } + let(:baseline_width) { + pdf.chart + output = pdf.render + line = PDF::Inspector::Graphics::Line.analyze output + baseline_starts, baseline_ends = line.points.map(&:first) + baseline_ends - baseline_starts + } + + it 'spans the whole width of the page' do + expect(baseline_width).to eq pdf.bounds.width + end +end diff --git a/spec/graph/height_spec.rb b/spec/graph/height_spec.rb index 47a107b..ffa66d1 100644 --- a/spec/graph/height_spec.rb +++ b/spec/graph/height_spec.rb @@ -11,7 +11,7 @@ output = pdf.render line = PDF::Inspector::Graphics::Line.analyze output y = line.points.each_slice(2).map{|x| x.first.last} - height = y.first - y.last + y.first - y.last } it 'uses the Squid.configuration value by default' do diff --git a/spec/graph/width_spec.rb b/spec/graph/width_spec.rb deleted file mode 100644 index 6d9f392..0000000 --- a/spec/graph/width_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe Squid::Graph do - let(:pdf) { Prawn::Document.new } - let(:data) { {} } - let(:output) { pdf.render } - let(:rectangle) { PDF::Inspector::Graphics::Rectangle.analyze output } - - it 'fills the width of the page (or equivalent bounding box)' do - pdf.chart - - graph_width = rectangle.rectangles.first[:width] - expect(graph_width).to eq pdf.bounds.width - end -end From 625468c54a846eca9fe271a6329e72ed3729f438 Mon Sep 17 00:00:00 2001 From: claudiob Date: Wed, 5 Aug 2015 08:42:31 -0700 Subject: [PATCH 2/3] Extract common let in spec_helper --- spec/graph/baseline_spec.rb | 14 ++++---------- spec/graph/height_spec.rb | 27 +++++++++++++-------------- spec/spec_helper.rb | 7 +++++++ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/spec/graph/baseline_spec.rb b/spec/graph/baseline_spec.rb index 43318b0..fddbfe5 100644 --- a/spec/graph/baseline_spec.rb +++ b/spec/graph/baseline_spec.rb @@ -1,16 +1,10 @@ require 'spec_helper' -describe 'Graph baseline' do - let(:pdf) { Prawn::Document.new } - let(:baseline_width) { - pdf.chart - output = pdf.render - line = PDF::Inspector::Graphics::Line.analyze output - baseline_starts, baseline_ends = line.points.map(&:first) - baseline_ends - baseline_starts - } +describe 'Graph baseline', inspect: true do + let(:baseline) { inspected_line.points.map &:first } it 'spans the whole width of the page' do - expect(baseline_width).to eq pdf.bounds.width + pdf.chart + expect(baseline.last - baseline.first).to eq pdf.bounds.width end end diff --git a/spec/graph/height_spec.rb b/spec/graph/height_spec.rb index ffa66d1..466f7c1 100644 --- a/spec/graph/height_spec.rb +++ b/spec/graph/height_spec.rb @@ -1,21 +1,17 @@ require 'spec_helper' -describe 'Graph height' do - let(:default_height) { Squid.configuration.height } - let(:options) { {} } - let(:height) { - pdf = Prawn::Document.new +describe 'Graph height', inspect: true do + before do pdf.stroke_horizontal_rule pdf.chart options pdf.stroke_horizontal_rule - output = pdf.render - line = PDF::Inspector::Graphics::Line.analyze output - y = line.points.each_slice(2).map{|x| x.first.last} - y.first - y.last - } + end + + let(:y) { inspected_line.points.each_slice(2).map{|x| x.first.last} } + let(:height) { y.first - y.last } - it 'uses the Squid.configuration value by default' do - expect(height).to eq default_height + context 'uses the Squid.configuration value by default' do + it { expect(height).to eq Squid.configuration.height } end context 'can be set calling chart with the :height option' do @@ -24,8 +20,11 @@ end context 'can be set with Squid.config' do - before { Squid.configure {|config| config.height = 400} } - after { Squid.configure {|config| config.height = default_height } } + before(:all) do + @original_height = Squid.configuration.height + Squid.configure {|config| config.height = 400} + end + after(:all) { Squid.configure {|config| config.height = @original_height} } it { expect(height).to eq 400.0 } end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 623f02c..4a5d01d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,3 +16,10 @@ require 'pdf/inspector' require 'squid' + +RSpec.shared_context 'PDF Inspector', inspect: true do + let(:pdf) { Prawn::Document.new } + let(:output) { pdf.render } + let(:inspected_line) { PDF::Inspector::Graphics::Line.analyze output } + let(:options) { {} } +end From 1910c389ac281b9ce3aff0d072df77f93e044773 Mon Sep 17 00:00:00 2001 From: claudiob Date: Wed, 5 Aug 2015 09:40:00 -0700 Subject: [PATCH 3/3] Add legend --- .travis.yml | 8 +++- gemfiles/Gemfile.activesupport-3.x | 4 ++ gemfiles/Gemfile.activesupport-4.x | 4 ++ lib/squid.rb | 5 +- lib/squid/base.rb | 23 +++++++++ lib/squid/configuration.rb | 2 +- lib/squid/graph.rb | 31 ++++-------- lib/squid/graph/legend.rb | 77 ++++++++++++++++++++++++++++++ lib/squid/interface.rb | 4 +- manual/squid/height.rb | 8 ++-- manual/squid/legend.rb | 15 ++++++ manual/squid/squid.rb | 1 + spec/graph/height_spec.rb | 2 +- spec/graph/legend_spec.rb | 36 ++++++++++++++ spec/spec_helper.rb | 6 +++ squid.gemspec | 3 +- 16 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 gemfiles/Gemfile.activesupport-3.x create mode 100644 gemfiles/Gemfile.activesupport-4.x create mode 100644 lib/squid/base.rb create mode 100644 lib/squid/graph/legend.rb create mode 100644 manual/squid/legend.rb create mode 100644 spec/graph/legend_spec.rb diff --git a/.travis.yml b/.travis.yml index fe36128..5668027 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: ruby -rvm: - - 2.2.2 notifications: email: true +matrix: + include: + - rvm: 2.0.0 + gemfile: gemfiles/Gemfile.activesupport-3.x + - rvm: 2.2.2 + gemfile: gemfiles/Gemfile.activesupport-4.x diff --git a/gemfiles/Gemfile.activesupport-3.x b/gemfiles/Gemfile.activesupport-3.x new file mode 100644 index 0000000..1016162 --- /dev/null +++ b/gemfiles/Gemfile.activesupport-3.x @@ -0,0 +1,4 @@ +source 'http://rubygems.org' + +gem 'activesupport', '~> 3.0' +gemspec path: '../' diff --git a/gemfiles/Gemfile.activesupport-4.x b/gemfiles/Gemfile.activesupport-4.x new file mode 100644 index 0000000..a2193bb --- /dev/null +++ b/gemfiles/Gemfile.activesupport-4.x @@ -0,0 +1,4 @@ +source 'http://rubygems.org' + +gem 'activesupport', '~> 4.0' +gemspec path: '../' diff --git a/lib/squid.rb b/lib/squid.rb index d3db183..45bb293 100644 --- a/lib/squid.rb +++ b/lib/squid.rb @@ -1,6 +1,9 @@ require 'prawn' + +require 'active_support/core_ext/string/inflections' # for titleize + require 'squid/config' require 'squid/interface' module Squid -end \ No newline at end of file +end diff --git a/lib/squid/base.rb b/lib/squid/base.rb new file mode 100644 index 0000000..c82aea0 --- /dev/null +++ b/lib/squid/base.rb @@ -0,0 +1,23 @@ +require 'squid/settings' + +module Squid + # Abstract class that delegates unhandled calls to a +pdf+ object which + # is convenient when working with Prawn methods + class Base + extend Settings + + attr_reader :pdf, :data + + def initialize(document, data = {}, settings = {}) + @pdf = document + @data = data + @settings = settings + end + + # Delegates all unhandled calls to object returned by +pdf+ method. + def method_missing(method, *args, &block) + return super unless pdf.respond_to?(method) + pdf.send method, *args, &block + end + end +end diff --git a/lib/squid/configuration.rb b/lib/squid/configuration.rb index 2bb3b94..fc0ad87 100644 --- a/lib/squid/configuration.rb +++ b/lib/squid/configuration.rb @@ -33,4 +33,4 @@ def initialize @height = ENV.fetch('SQUID_HEIGHT', '200').to_f end end -end \ No newline at end of file +end diff --git a/lib/squid/graph.rb b/lib/squid/graph.rb index e307155..4b9f259 100644 --- a/lib/squid/graph.rb +++ b/lib/squid/graph.rb @@ -1,36 +1,21 @@ -require 'squid/settings' +require 'squid/base' +require 'squid/graph/legend' module Squid - class Graph - extend Settings + class Graph < Base has_settings :height - attr_reader :pdf - - def initialize(document, settings = {}) - @pdf = document - @settings = settings - end - # Draws the graph. def draw bounding_box [0, cursor], width: bounds.width, height: height do - draw_baseline - end - end + Legend.new(pdf, data.keys).draw - # Delegates all unhandled calls to object returned by +pdf+ method. - def method_missing(method, *args, &block) - return super unless pdf.respond_to?(method) - pdf.send method, *args, &block + # The baseline is last so it’s drawn on top of any graph element. + stroke_horizontal_line 0, bounds.width, at: cursor - height + end end private - # Draws the baseline of a graph. - # Must run after draw_graph in order to draw the line on top of the graph. - def draw_baseline - stroke_horizontal_line 0, bounds.width, at: cursor - height - end end -end \ No newline at end of file +end diff --git a/lib/squid/graph/legend.rb b/lib/squid/graph/legend.rb new file mode 100644 index 0000000..d4b84c6 --- /dev/null +++ b/lib/squid/graph/legend.rb @@ -0,0 +1,77 @@ +require 'squid/base' + +module Squid + # Adds a legend to the graph. The legend is drawn in a floating bounding box + # at the top of the graph, in the right side. The dimensions of the legend + # are fixed: height, width and font size cannot be customized. + # When multiple series are provided, the legend includes all of them, ordered + # from the right to the left. + class Legend < Base + + def draw + float do + bounding_box [width, cursor+height*2], width: width, height: height do + right_margin = bounds.right + data.each do |series| + right_margin = draw_label series, right_margin + right_margin = draw_square series, right_margin + right_margin -= label_padding + end + end + end + end + + private + + # Writes the name of the series, left-aligned, with a small font size. + # @param [Symbol, String] series The series to add to the legend + # @param [Integer] x The current right-margin of the legend + # @return [Integer] The updated right-margin (after adding the label) + def draw_label(series, x) + label = series.to_s.titleize + x -= width_of label, size: font_size + text_box label, at: [x, bounds.top], size: font_size, height: height, valign: :center + x + end + + # Draws a square with the same color of the series (next to the label). + # @param [Symbol, String] series The series to add to the legend + # @param [Integer] x The current right-margin of the legend + # @return [Integer] The updated right-margin (after adding the label) + def draw_square(series, x) + x -= square_size + square_padding + fill_rectangle [x, bounds.height - square_size], square_size, square_size + x + end + + # Restrict the legend to the right part of the graph + def width + bounds.width/2 + end + + # Restrict the legend to a specific vertical space + def height + 15 + end + + # Ensure the label fits in the height of the legend + def font_size + height/2 + end + + # Ensure the square fits in the height of the legend + def square_size + height/3 + end + + # The horizontal distance left between the labels of two series + def label_padding + height + end + + # The horizontal distance left between the squares of two series + def square_padding + height/5 + end + end +end diff --git a/lib/squid/interface.rb b/lib/squid/interface.rb index 92dd0d2..edeecb0 100644 --- a/lib/squid/interface.rb +++ b/lib/squid/interface.rb @@ -2,8 +2,8 @@ module Squid module Interface - def chart(settings = {}) - Graph.new(self, settings).draw + def chart(data = {}, settings = {}) + Graph.new(self, data, settings).draw end end end diff --git a/manual/squid/height.rb b/manual/squid/height.rb index 5ef4490..fc0c33d 100644 --- a/manual/squid/height.rb +++ b/manual/squid/height.rb @@ -3,10 +3,12 @@ # filename = File.basename(__FILE__).gsub('.rb', '.pdf') Prawn::ManualBuilder::Example.generate(filename) do + data = {views: {2013 => 182, 2014 => 46, 2015 => 102}} + text 'Default height:' - chart - move_down 20 + chart data + move_down 30 text 'Custom height:' - chart height: 100 + chart data, height: 100 end diff --git a/manual/squid/legend.rb b/manual/squid/legend.rb new file mode 100644 index 0000000..d4bc079 --- /dev/null +++ b/manual/squid/legend.rb @@ -0,0 +1,15 @@ +# By default, chart adds a legend in the top-right corner of the chart, +# listing the labels of all the series in the graph. +# +filename = File.basename(__FILE__).gsub('.rb', '.pdf') +Prawn::ManualBuilder::Example.generate(filename) do + views = {2013 => 182, 2014 => 46, 2015 => 102} + uniques = {2013 => 110, 2014 => 30, 2015 => 88} + + text 'One series:' + chart views: views + move_down 30 + + text 'Two series:' + chart views: views, uniques: uniques +end diff --git a/manual/squid/squid.rb b/manual/squid/squid.rb index b7366f5..60ea6d5 100644 --- a/manual/squid/squid.rb +++ b/manual/squid/squid.rb @@ -16,6 +16,7 @@ p.section 'Basics' do |s| s.example 'basic' + s.example 'legend' end p.section 'Styling' do |s| diff --git a/spec/graph/height_spec.rb b/spec/graph/height_spec.rb index 466f7c1..6aa7757 100644 --- a/spec/graph/height_spec.rb +++ b/spec/graph/height_spec.rb @@ -3,7 +3,7 @@ describe 'Graph height', inspect: true do before do pdf.stroke_horizontal_rule - pdf.chart options + pdf.chart data, options pdf.stroke_horizontal_rule end diff --git a/spec/graph/legend_spec.rb b/spec/graph/legend_spec.rb new file mode 100644 index 0000000..3455173 --- /dev/null +++ b/spec/graph/legend_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'Graph legend', inspect: true do + before { pdf.chart data, options } + + context 'given no series, does not have any text' do + it { expect(inspected_text.strings).to be_empty } + end + + context 'given one series' do + let(:data) { {views: views} } + + it 'includes the titleized name of the series' do + expect(inspected_text.strings).to eq ['Views'] + end + + it 'draws a small square representing the series' do + square = inspected_rectangle.rectangles.first + expect(square[:width]).to be 5.0 + expect(square[:height]).to be 5.0 + end + end + + context 'given two series' do + let(:data) { {views: views, uniques: uniques} } + + it 'includes the titleized names of both series' do + expect(inspected_text.strings).to eq ['Views', 'Uniques'] + end + + it 'prints both names on the same text line' do + lines = inspected_text.positions.map(&:last).uniq + expect(lines).to be_one + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4a5d01d..ed68144 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,5 +21,11 @@ let(:pdf) { Prawn::Document.new } let(:output) { pdf.render } let(:inspected_line) { PDF::Inspector::Graphics::Line.analyze output } + let(:inspected_text) { PDF::Inspector::Text.analyze output } + let(:inspected_rectangle) { PDF::Inspector::Graphics::Rectangle.analyze output } + let(:data) { {} } let(:options) { {} } + + let(:views) { {2013 => 182, 2014 => 46, 2015 => 102} } + let(:uniques) { {2013 => 110, 2014 => 30, 2015 => 88} } end diff --git a/squid.gemspec b/squid.gemspec index 8460df9..6366aa9 100644 --- a/squid.gemspec +++ b/squid.gemspec @@ -19,9 +19,10 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'prawn', '~> 2.0' + spec.add_dependency 'activesupport' # 3 or 4, see gemfiles/ + spec.add_development_dependency 'pdf-inspector', '~> 1.2' spec.add_development_dependency 'prawn-manual_builder', '~> 0.2.0' - spec.add_development_dependency 'bundler', '~> 1.9' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.3' spec.add_development_dependency 'coveralls', '~> 0.8.2'