From cba2f554c93ae247e94aebc3d900e72bd95e60bb Mon Sep 17 00:00:00 2001 From: claudiob Date: Wed, 5 Aug 2015 09:40:00 -0700 Subject: [PATCH] Add legend --- .travis.yml | 8 +++- gemfiles/Gemfile.activesupport-3.x | 4 ++ gemfiles/Gemfile.activesupport-4.x | 4 ++ lib/squid.rb | 3 ++ lib/squid/base.rb | 23 +++++++++ lib/squid/graph.rb | 24 ++++------ 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 +- 15 files changed, 193 insertions(+), 25 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..1baf903 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 \ No newline at end of file diff --git a/gemfiles/Gemfile.activesupport-3.x b/gemfiles/Gemfile.activesupport-3.x new file mode 100644 index 0000000..4da1ddf --- /dev/null +++ b/gemfiles/Gemfile.activesupport-3.x @@ -0,0 +1,4 @@ +source 'http://rubygems.org' + +gem 'activesupport', '~> 3.0' +gemspec path: '../' \ No newline at end of file diff --git a/gemfiles/Gemfile.activesupport-4.x b/gemfiles/Gemfile.activesupport-4.x new file mode 100644 index 0000000..1a3e0ae --- /dev/null +++ b/gemfiles/Gemfile.activesupport-4.x @@ -0,0 +1,4 @@ +source 'http://rubygems.org' + +gem 'activesupport', '~> 4.0' +gemspec path: '../' \ No newline at end of file diff --git a/lib/squid.rb b/lib/squid.rb index d3db183..8509bdd 100644 --- a/lib/squid.rb +++ b/lib/squid.rb @@ -1,4 +1,7 @@ require 'prawn' + +require 'active_support/core_ext/string/inflections' # for titleize + require 'squid/config' require 'squid/interface' diff --git a/lib/squid/base.rb b/lib/squid/base.rb new file mode 100644 index 0000000..1619c36 --- /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 \ No newline at end of file diff --git a/lib/squid/graph.rb b/lib/squid/graph.rb index e307155..01fcb94 100644 --- a/lib/squid/graph.rb +++ b/lib/squid/graph.rb @@ -1,32 +1,24 @@ -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_legend draw_baseline end 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 - private + def draw_legend + Legend.new(pdf, data.keys).draw + end + # 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 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'