squid 0.0.0 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|