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
|