charts 0.0.9

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4da2755eb0417c5b0c4745ba3c48731b7885b976
4
+ data.tar.gz: aa2f198109cf7b2778a0e96a373d26f28747c613
5
+ SHA512:
6
+ metadata.gz: aeb9f5516f4c5f4636158833639d369a1ab67c9b2b1a7a5beda73d13512a6fb78c981372c608ac13abf11171c51f22b0892a3caf6db58bece66418bce3111ae8
7
+ data.tar.gz: c7500f8d6819e229ef05890ace09bc27b66d597afd71bae33bd83adab7ff4b2a692e54dda9fdf18850ca4bbd5b9fa9b6f4a10117308d2c5e9c2c301962ee61db
@@ -0,0 +1,34 @@
1
+ class Charts::BarChart::Bar
2
+ attr_reader :graph, :data_value, :set_nr, :bar_nr_in_set
3
+
4
+ def initialize(graph, data_value, set_nr, bar_nr_in_set)
5
+ @graph = graph
6
+ @data_value = data_value
7
+ @set_nr = set_nr
8
+ @bar_nr_in_set = bar_nr_in_set
9
+ end
10
+
11
+ def draw
12
+ graph.renderer.rect x, y, width, height, fill: graph.colors[set_nr], class: 'bar'
13
+ end
14
+
15
+ def x
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def y
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def width
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def height
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def bar_number_in_graph
32
+ set_nr + bar_nr_in_set * graph.set_count
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ class Charts::BarChart::HorizontalBar < Charts::BarChart::Bar
2
+ def x
3
+ x_margin + x_offset
4
+ end
5
+
6
+ def x_margin
7
+ graph.outer_margin
8
+ end
9
+
10
+ def x_offset
11
+ graph.inner_width * [data_value, graph.base_line].min
12
+ end
13
+
14
+ def y
15
+ (y_margin + y_offset).floor.to_i
16
+ end
17
+
18
+ def y_margin
19
+ graph.outer_margin + graph.bar_margin + graph.group_margin * bar_nr_in_set
20
+ end
21
+
22
+ def y_offset
23
+ graph.bar_outer_width * bar_number_in_graph
24
+ end
25
+
26
+ def width
27
+ graph.inner_width * (data_value - graph.base_line).abs
28
+ end
29
+
30
+ def height
31
+ graph.bar_inner_width.floor.to_i
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ class Charts::BarChart::VerticalBar < Charts::BarChart::Bar
2
+ def x
3
+ (x_margin + x_offset).floor.to_i
4
+ end
5
+
6
+ def x_margin
7
+ graph.outer_margin + graph.bar_margin + graph.group_margin * bar_nr_in_set
8
+ end
9
+
10
+ def x_offset
11
+ graph.bar_outer_width * bar_number_in_graph
12
+ end
13
+
14
+ def y
15
+ y_margin + y_offset
16
+ end
17
+
18
+ def y_margin
19
+ graph.outer_margin
20
+ end
21
+
22
+ def y_offset
23
+ graph.inner_height * [(1 - data_value), (1 - graph.base_line)].min
24
+ end
25
+
26
+ def width
27
+ graph.bar_inner_width.floor.to_i
28
+ end
29
+
30
+ def height
31
+ graph.inner_height * (data_value - graph.base_line).abs
32
+ end
33
+ end
@@ -0,0 +1,130 @@
1
+ class Charts::BarChart < Charts::Chart
2
+ include Charts::Grid
3
+ include Charts::Legend
4
+
5
+ attr_reader :max_value,
6
+ :min_value,
7
+ :set_count,
8
+ :group_count,
9
+ :total_bar_count,
10
+ :base_line
11
+
12
+ def initialize_instance_variables
13
+ @set_count = data.count
14
+ @group_count = data.map(&:count).max
15
+ @total_bar_count = set_count * group_count
16
+ @max_value = calc_max
17
+ @min_value = calc_min
18
+ @base_line = calc_base_line
19
+ end
20
+
21
+ def default_options
22
+ super.merge(
23
+ width: 600,
24
+ height: 400,
25
+ include_zero: true,
26
+ group_margin: 20,
27
+ bar_margin: 3,
28
+ label_height: 20,
29
+ label_margin: 10,
30
+ direction: :vertical
31
+ )
32
+ end
33
+
34
+ def validate_arguments(data, options)
35
+ super(data, options)
36
+ raise ArgumentError unless data.is_a? Array
37
+ end
38
+
39
+ def prepare_data
40
+ data.map do |set|
41
+ set.map do |value|
42
+ value.nil? ? nil : normalize(value)
43
+ end
44
+ end
45
+ end
46
+
47
+ def pre_draw
48
+ super
49
+ draw_grid
50
+ draw_group_labels
51
+ draw_labels
52
+ end
53
+
54
+ def draw_group_labels
55
+ return if options[:group_labels].nil? || group_labels.empty?
56
+ raise ArgumentError if group_labels.count != group_count
57
+ group_label_style = {
58
+ text_anchor: 'middle',
59
+ writing_mode: (vertical? ? 'lr' : 'tb'),
60
+ class: 'group_label'
61
+ }
62
+ group_labels.each_with_index do |group_label, i|
63
+ if vertical?
64
+ x = outer_margin + (i + 0.5) * all_bars_width / group_count + i * group_margin
65
+ y = outer_margin + inner_height + renderer.font_size
66
+ else
67
+ x = outer_margin - renderer.font_size
68
+ y = outer_margin + (i + 0.5) * all_bars_width / group_count + i * group_margin
69
+ end
70
+ renderer.text group_label, x, y, group_label_style
71
+ end
72
+ end
73
+
74
+ def draw
75
+ prepared_data.each_with_index do |set, set_nr|
76
+ set.each_with_index do |data_value, bar_nr_in_set|
77
+ bar_class = vertical? ? VerticalBar : HorizontalBar
78
+ bar_class.new(self, data_value, set_nr, bar_nr_in_set).draw unless data_value.nil?
79
+ end
80
+ end
81
+ end
82
+
83
+ def inner_width
84
+ width - 2 * outer_margin
85
+ end
86
+
87
+ def bar_inner_width
88
+ bar_outer_width - 2 * bar_margin
89
+ end
90
+
91
+ def bar_outer_width
92
+ all_bars_width.to_f / total_bar_count
93
+ end
94
+
95
+ def all_bars_width
96
+ (vertical? ? inner_width : inner_height) - sum_of_group_margins
97
+ end
98
+
99
+ def sum_of_group_margins
100
+ (group_count - 1) * group_margin
101
+ end
102
+
103
+ def inner_height
104
+ height - 2 * outer_margin - label_total_height
105
+ end
106
+
107
+ def calc_max
108
+ max = data.map{ |d| d.reject(&:nil?).max }.max
109
+ max = 0 if max < 0 && include_zero
110
+ options[:max] || max
111
+ end
112
+
113
+ def calc_min
114
+ min = data.map{ |d| d.reject(&:nil?).min }.min
115
+ min = 0 if min > 0 && include_zero
116
+ options[:min] || min
117
+ end
118
+
119
+ def calc_base_line
120
+ [[normalize(0), 0].max, 1].min # zero value normalized and clamped between 0 and 1
121
+ end
122
+
123
+ def normalize(value)
124
+ (value.to_f - min_value) / (max_value - min_value)
125
+ end
126
+
127
+ def vertical?
128
+ direction == :vertical
129
+ end
130
+ end
@@ -0,0 +1,48 @@
1
+ module Charts::Grid
2
+ def draw_grid
3
+ lines.each { |l| l.draw }
4
+ end
5
+
6
+ def lines
7
+ grid_line_class = vertical? ? HorizontalGridLine : VerticalGridLine
8
+ grid_line_values.map { |v| grid_line_class.new(self, v) }
9
+ end
10
+
11
+ def grid_line_values
12
+ (0..number_of_grid_lines).map do |i|
13
+ value = i.to_f * rounded_spread / number_of_grid_lines + min_value
14
+ if spread_log10 < 1
15
+ value.round(-spread_order_of_magnitude + 1)
16
+ else
17
+ value.round
18
+ end
19
+ end
20
+ end
21
+
22
+ def number_of_grid_lines
23
+ (3..7).find { |line_count| spread_factor % line_count == 0 } || 4
24
+ end
25
+
26
+ def spread
27
+ max_value - min_value
28
+ end
29
+
30
+ def spread_order_of_magnitude
31
+ spread_log10.floor
32
+ end
33
+
34
+ def spread_log10
35
+ Math.log10(spread)
36
+ end
37
+
38
+ def rounded_spread
39
+ rs = spread_factor * 10 ** spread_order_of_magnitude
40
+ spread_log10 % 1 == 0 ? rs / 10 : rs
41
+ end
42
+
43
+ def spread_factor
44
+ f = (spread.to_f / 10 ** spread_order_of_magnitude).floor
45
+ spread_log10 % 1 == 0 ? 10 * f : f
46
+ end
47
+
48
+ end
@@ -0,0 +1,45 @@
1
+ class Charts::Grid::GridLine
2
+ attr_accessor :graph, :value
3
+
4
+ def initialize(graph, value)
5
+ @graph = graph
6
+ @value = value
7
+ end
8
+
9
+ def draw
10
+ graph.renderer.line x1, y1, x2, y2, graph.renderer.grid_line_style
11
+ graph.renderer.text label_text, label_x, label_y, label_style
12
+ end
13
+
14
+ def x1
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def x2
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def y1
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def y2
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def label_x
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def label_y
35
+ raise NotImplementedError
36
+ end
37
+
38
+ def label_text
39
+ if graph.spread_order_of_magnitude <= 0
40
+ value.to_f
41
+ else
42
+ value
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ class Charts::Grid::HorizontalGridLine < Charts::Grid::GridLine
2
+ def x1
3
+ graph.outer_margin
4
+ end
5
+
6
+ def x2
7
+ graph.width - graph.outer_margin
8
+ end
9
+
10
+ def y1
11
+ graph.outer_margin + graph.inner_height * (1 - graph.normalize(value))
12
+ end
13
+
14
+ def y2
15
+ y1
16
+ end
17
+
18
+ def label_x
19
+ x1 - 5
20
+ end
21
+
22
+ def label_y
23
+ y1 + graph.renderer.font_size / 3
24
+ end
25
+
26
+ def label_style
27
+ { text_anchor: 'end' }
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ class Charts::Grid::VerticalGridLine < Charts::Grid::GridLine
2
+ def y1
3
+ graph.outer_margin
4
+ end
5
+
6
+ def y2
7
+ graph.outer_margin + graph.inner_height
8
+ end
9
+
10
+ def x1
11
+ graph.outer_margin + graph.inner_width * graph.normalize(value)
12
+ end
13
+
14
+ def x2
15
+ x1
16
+ end
17
+
18
+ def label_y
19
+ y2 + graph.renderer.font_size + 5
20
+ end
21
+
22
+ def label_x
23
+ x1
24
+ end
25
+
26
+ def label_style
27
+ {
28
+ text_anchor: 'middle'
29
+ }
30
+ end
31
+ end
@@ -0,0 +1,66 @@
1
+ module Charts
2
+ class Dispatcher
3
+ attr_reader :options
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def render
10
+ if options[:filename]
11
+ graph.render
12
+ else
13
+ puts graph.render
14
+ end
15
+ end
16
+
17
+ def graph
18
+ if type == :txt
19
+ SymbolCountChart.new(data, graph_options)
20
+ elsif [:svg, :png, :jpg, :gif].include? type
21
+ if style == :circle
22
+ CircleCountChart.new(data, graph_options)
23
+ elsif style == :cross
24
+ CrossCountChart.new(data, graph_options)
25
+ elsif style == :manikin
26
+ ManikinCountChart.new(data, graph_options)
27
+ elsif style == :bar
28
+ BarChart.new(data, graph_options)
29
+ elsif style == :pie
30
+ PieChart.new(data, graph_options)
31
+ end
32
+ end
33
+ end
34
+
35
+ def data
36
+ options[:data]
37
+ end
38
+
39
+ def graph_options
40
+ options.select do |key, _value|
41
+ [
42
+ :background_color,
43
+ :colors,
44
+ :columns,
45
+ :filename,
46
+ :group_labels,
47
+ :height,
48
+ :item_height,
49
+ :item_width,
50
+ :labels,
51
+ :title,
52
+ :type,
53
+ :width
54
+ ].include? key
55
+ end
56
+ end
57
+
58
+ def type
59
+ options[:type].to_sym
60
+ end
61
+
62
+ def style
63
+ options[:style].to_sym
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,183 @@
1
+ require 'optparse'
2
+
3
+ class Charts::OptParser
4
+ attr_reader :args, :options, :parser
5
+
6
+ FORMATS = [:txt, :svg, :png, :jpg, :gif].freeze
7
+ STYLES = [:circle, :cross, :manikin, :bar, :pie].freeze
8
+ DATA_EXAMPLE_ARGS = '-d 8,7'.freeze
9
+ COLOR_EXAMPLE_ARGS = '--colors red,gold'.freeze
10
+ FLOAT_INTEGER_REGEX = /^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$/.freeze
11
+
12
+ def initialize(args)
13
+ @args = args.empty? ? ['--help'] : args
14
+ # The options specified on the command line will be collected in *options*.
15
+ # We set default values here.
16
+ @options = {
17
+ type: :svg,
18
+ style: :circle
19
+ }
20
+ @parser = opt_parser
21
+ end
22
+
23
+ # Return a hash describing the options.
24
+ def parse
25
+ parser.parse!(args) # this sets the options
26
+ post_process_options
27
+ infer_type_from_filename
28
+ validate_options
29
+
30
+ options
31
+ end
32
+
33
+ def post_process_options
34
+ # force 2-dimensional array for bar graph if only one data set is provided
35
+ if options[:style] == :bar and !options[:data].first.is_a? Array
36
+ options[:data] = [options[:data]]
37
+ end
38
+ end
39
+
40
+ def infer_type_from_filename
41
+ return if options[:help]
42
+ return unless options[:filename]
43
+ type = options[:filename].match(/.*\.(#{FORMATS.join("|")})/)
44
+ if type
45
+ options[:type] = type[1].to_sym
46
+ else
47
+ options[:type] = false
48
+ end
49
+ end
50
+
51
+ def validate_options
52
+ return if options[:help]
53
+ unless options[:type]
54
+ raise 'No type provided. Set a type with --type flag or by setting a valid --filename'
55
+ end
56
+ unless options[:data]
57
+ raise "No data provided. Please pass in data using the --data flag: 'bin/charts #{DATA_EXAMPLE_ARGS}'"
58
+ end
59
+ end
60
+
61
+ def opt_parser
62
+ OptionParser.new do |opts|
63
+ opts.banner = 'Usage: bin/charts [options]'
64
+ opts.on(
65
+ '-d DATA',
66
+ '--data DATA',
67
+ Array,
68
+ "Provide multiple data points, ie: 'bin/charts #{DATA_EXAMPLE_ARGS}'"
69
+ ) do |data|
70
+ data = data.map { |d| d.match(FLOAT_INTEGER_REGEX) ? Float(d) : nil }
71
+ # if multiple --data arguments are provided,
72
+ # the data option becomes a two dimensional array
73
+ if !options[:data]
74
+ options[:data] = data
75
+ else
76
+ options[:data] = [options[:data]] unless options[:data].first.is_a? Array
77
+ options[:data].push(data)
78
+ end
79
+ end
80
+ opts.on(
81
+ '-f FILENAME',
82
+ '--filename FILENAME',
83
+ "Set the filename the result is stored in. Supported formats are: : #{FORMATS.join(', ')}"
84
+ ) do |filename|
85
+ options[:filename] = filename
86
+ end
87
+ opts.on(
88
+ '-s STYLE',
89
+ '--style STYLE',
90
+ STYLES,
91
+ "Choose the graph style: #{STYLES.join(', ')} (circle, cross and manikin are count graphs)"
92
+ ) do |style|
93
+ options[:style] = style
94
+ end
95
+ opts.on(
96
+ '-t TITLE',
97
+ '--title TITLE',
98
+ "Set the title"
99
+ ) do |title|
100
+ options[:title] = title
101
+ end
102
+ opts.on(
103
+ '--labels LABELS',
104
+ Array,
105
+ "Set the labels to be used, ie: 'bin/charts #{DATA_EXAMPLE_ARGS} --labels Failures,Successes' "
106
+ ) do |labels|
107
+ options[:labels] = labels
108
+ end
109
+ opts.on(
110
+ '--group-labels GROUP_LABELS',
111
+ Array,
112
+ "Set the group-labels to be used, ie: 'bin/charts #{DATA_EXAMPLE_ARGS} --style bar --group-labels Summer,Winter' "
113
+ ) do |group_labels|
114
+ options[:group_labels] = group_labels
115
+ end
116
+ opts.on(
117
+ '--colors COLORS',
118
+ Array,
119
+ "Set the colors to be used, ie: 'bin/charts #{DATA_EXAMPLE_ARGS} #{COLOR_EXAMPLE_ARGS}' "
120
+ ) do |colors|
121
+ options[:colors] = colors
122
+ end
123
+ opts.on(
124
+ '--columns COLUMNS',
125
+ Integer,
126
+ 'Set number of columns'
127
+ ) do |columns|
128
+ options[:columns] = columns
129
+ end
130
+ opts.on(
131
+ '-w WIDTH',
132
+ '--width WIDTH (not for count graphs)',
133
+ Integer,
134
+ 'Sets the image width'
135
+ ) do |width|
136
+ options[:width] = width
137
+ end
138
+ opts.on(
139
+ '-h HEIGHT',
140
+ '--height HEIGHT (not for count graphs)',
141
+ Integer,
142
+ 'Sets the image height'
143
+ ) do |height|
144
+ options[:height] = height
145
+ end
146
+ opts.on(
147
+ '--item-width WIDTH',
148
+ Integer,
149
+ 'Sets the width of the individual item (count graphs only)'
150
+ ) do |item_width|
151
+ options[:item_width] = item_width
152
+ end
153
+ opts.on(
154
+ '--item-height HEIGHT',
155
+ Integer,
156
+ 'Sets the height of the individual item (count graphs only)'
157
+ ) do |item_height|
158
+ options[:item_height] = item_height
159
+ end
160
+ opts.on(
161
+ '--type TYPE',
162
+ FORMATS,
163
+ "If no filename is provided, output is sent to STDOUT, choose the format: #{FORMATS.join(', ')}"
164
+ ) do |type|
165
+ options[:type] = type
166
+ end
167
+ opts.on(
168
+ '--help',
169
+ 'Prints this help'
170
+ ) do
171
+ puts opts
172
+ options[:help] = true
173
+ end
174
+ opts.on(
175
+ '--background_color BACKGROUNDCOLOR',
176
+ Array,
177
+ "Set the backgroundcolor to be used, ie: 'bin/charts #{DATA_EXAMPLE_ARGS} --background_color Silver"
178
+ ) do |background_color|
179
+ options[:background_color] = background_color
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,105 @@
1
+ class Charts::Chart
2
+ attr_reader :data,
3
+ :options,
4
+ :prepared_data,
5
+ :renderer
6
+
7
+ def initialize(data, opts = {})
8
+ validate_arguments(data, opts)
9
+ @data = data
10
+ @options = default_options.merge opts
11
+ create_options_methods
12
+ initialize_instance_variables
13
+ @prepared_data = prepare_data
14
+ end
15
+
16
+ def validate_arguments(data, options)
17
+ raise ArgumentError.new('Data missing') if data.empty?
18
+ raise ArgumentError.new('Data not an array') unless data.is_a? Array
19
+ raise ArgumentError.new('Options missing') unless options.is_a? Hash
20
+ if options[:outer_margin] and !options[:outer_margin].is_a?(Numeric)
21
+ raise ArgumentError.new('outer_margin not a number')
22
+ end
23
+ if options[:colors]
24
+ unless options[:colors].is_a? Array
25
+ raise ArgumentError.new('colors not an array')
26
+ end
27
+ if options[:colors].any? and data.count > options[:colors].count
28
+ raise ArgumentError.new('not enough colors')
29
+ end
30
+ end
31
+ if options[:labels]
32
+ unless options[:labels].is_a? Array
33
+ raise ArgumentError.new('labels not an array')
34
+ end
35
+ if options[:labels].any? and data.count > options[:labels].count
36
+ raise ArgumentError.new('not enough labels')
37
+ end
38
+ end
39
+ end
40
+
41
+ def default_options
42
+ {
43
+ title: nil,
44
+ type: :svg,
45
+ outer_margin: 30,
46
+ background_color: 'white',
47
+ labels: [],
48
+ colors: [
49
+ '#e41a1d',
50
+ '#377eb9',
51
+ '#4daf4b',
52
+ '#984ea4',
53
+ '#ff7f01',
54
+ '#ffff34',
55
+ '#a65629',
56
+ '#f781c0',
57
+ '#888888'
58
+ ]
59
+ }
60
+ end
61
+
62
+ def prepare_data
63
+ data
64
+ end
65
+
66
+ def render
67
+ pre_draw
68
+ draw
69
+ post_draw
70
+ end
71
+
72
+ def pre_draw
73
+ @renderer = Charts::Renderer.new(self)
74
+ draw_background
75
+ draw_title
76
+ end
77
+
78
+ def draw
79
+ raise NotImplementedError
80
+ end
81
+
82
+ def post_draw
83
+ renderer.post_draw
84
+ end
85
+
86
+ def draw_background
87
+ renderer.rect 0, 0, width, height, fill: background_color, class: 'background_color'
88
+ end
89
+
90
+ def draw_title
91
+ return unless options[:title]
92
+ x = width / 2
93
+ y = outer_margin / 2 + 2 * renderer.font_size / 5
94
+ renderer.text options[:title], x, y, text_anchor: 'middle', class: 'title'
95
+ end
96
+
97
+ def initialize_instance_variables
98
+ end
99
+
100
+ def create_options_methods
101
+ options.each do |key, value|
102
+ define_singleton_method key, proc { value }
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,9 @@
1
+ class Charts::CircleCountChart < Charts::CountChart
2
+ def draw_item(x, y, color)
3
+ cx = x + item_width / 2
4
+ cy = y + item_height / 2
5
+ radius = item_width / 2
6
+
7
+ renderer.circle cx, cy, radius, fill: color
8
+ end
9
+ end
@@ -0,0 +1,83 @@
1
+ class Charts::CountChart < Charts::Chart
2
+ def default_options
3
+ super.merge(
4
+ columns: 10,
5
+ inner_margin: 2,
6
+ item_width: 20,
7
+ item_height: 20
8
+ )
9
+ end
10
+
11
+ def validate_arguments(data, options)
12
+ super(data, options)
13
+ raise ArgumentError if options[:inner_margin] and !options[:inner_margin].is_a?(Numeric)
14
+ raise ArgumentError unless data.all? { |x| Integer(x) }
15
+ end
16
+
17
+ def prepare_data
18
+ prepared_data = []
19
+ data.each_with_index do |value, index|
20
+ value.to_i.times { prepared_data << colors[index].to_s }
21
+ end
22
+ prepared_data.each_slice(columns).to_a
23
+ end
24
+
25
+ def draw
26
+ prepared_data.each_with_index do |row, row_count|
27
+ row.each_with_index do |color, column_count|
28
+ x = offset_x(column_count) + inner_margin + outer_margin
29
+ y = offset_y(row_count) + inner_margin + outer_margin
30
+ draw_item(x, y, color)
31
+ end
32
+ end
33
+ draw_labels
34
+ end
35
+
36
+ def draw_labels
37
+ return if labels.empty?
38
+ data.each_with_index do |data, index|
39
+ x = inner_margin + outer_margin
40
+ y = offset_y(prepared_data.count + (index + 1)) + inner_margin + outer_margin
41
+ draw_item(x, y, colors[index])
42
+ draw_label_text(x, y, labels[index]) # expand total image size according to labels
43
+ end
44
+ end
45
+
46
+ def draw_label_text(x, y, label)
47
+ x = x + item_width + inner_margin
48
+ y = y + item_height / 2 + 2 * renderer.font_size / 5
49
+ renderer.text(label, x, y, class: 'label_text')
50
+ end
51
+
52
+ def offset_x(column_count)
53
+ column_count * outer_item_width
54
+ end
55
+
56
+ def offset_y(row_count)
57
+ row_count * outer_item_height
58
+ end
59
+
60
+ def outer_item_width
61
+ item_width + 2 * inner_margin
62
+ end
63
+
64
+ def outer_item_height
65
+ item_height + 2 * inner_margin
66
+ end
67
+
68
+ def draw_item(_x, _y, _color)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ def width
73
+ prepared_data.first.count * outer_item_width + (2 * outer_margin) # + label_count?
74
+ end
75
+
76
+ def height
77
+ (prepared_data.count + label_count) * outer_item_height + (2 * outer_margin)
78
+ end
79
+
80
+ def label_count
81
+ labels.any? ? (labels.count + 1) : 0
82
+ end
83
+ end
@@ -0,0 +1,13 @@
1
+ class Charts::CrossCountChart < Charts::CountChart
2
+ def draw_item(x, y, color)
3
+ left = x + 4
4
+ right = x + item_width - 4
5
+ top = y + 4
6
+ bottom = y + item_height - 4
7
+
8
+ style = { stroke: color, stroke_width: 6, stroke_linecap: 'round' }
9
+
10
+ renderer.line left, top, right, bottom, style
11
+ renderer.line left, bottom, right, top, style
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ class Charts::ManikinCountChart < Charts::CountChart
2
+ def draw_item(x, y, color)
3
+ head x + width_percent(50), y, style(color)
4
+ body x + width_percent(50), y, style(color)
5
+ arms x + width_percent(50), y, style(color)
6
+ end
7
+
8
+ def head(x, y, style)
9
+ cy = y + height_percent(20)
10
+ radius = height_percent(10)
11
+
12
+ renderer.circle x, cy, radius, style.merge(class: 'head')
13
+ end
14
+
15
+ def body(x, y, style)
16
+ top = y + height_percent(40)
17
+ bottom = y + height_percent(95)
18
+
19
+ renderer.line x, top, x, bottom, style.merge(stroke_width: width_percent(30), class: 'body')
20
+ end
21
+
22
+ def arms(x, y, style)
23
+ top = y + height_percent(40)
24
+ bottom = y + height_percent(70)
25
+ left_x = x - width_percent(25)
26
+ right_x = x + width_percent(25)
27
+
28
+ renderer.line left_x, top, left_x, bottom, style.merge(class: 'left-arm')
29
+ renderer.line right_x, top, right_x, bottom, style.merge(class: 'right-arm')
30
+ end
31
+
32
+ def style(color)
33
+ {
34
+ fill: color,
35
+ stroke: color,
36
+ stroke_width: width_percent(10)
37
+ }
38
+ end
39
+
40
+ def width_percent(multiplicator)
41
+ multiplicator * item_width / 100
42
+ end
43
+
44
+ def height_percent(multiplicator)
45
+ multiplicator * item_height / 100
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ class Charts::SymbolCountChart < Charts::CountChart
2
+ def render
3
+ render = prepared_data.map { |row| row.map(&:chr).join }.join("\n")
4
+ if options[:filename]
5
+ File.open(options[:filename], 'w') { |file| file.write(render) }
6
+ else
7
+ render
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ module Charts::Legend
2
+ def draw_labels
3
+ return if options[:labels].nil? || labels.empty?
4
+ label_row_length_sum = 0
5
+ label_row = 1
6
+ labels.each_with_index do |label, index|
7
+ x = outer_margin + label_row_length_sum
8
+ y = height - outer_margin - label_total_height + label_row * (label_height + label_margin)
9
+ label_row_length_sum += label.length * 10 + label_height + 2 * label_margin
10
+ if label_row_length_sum > inner_width
11
+ label_row_length_sum = 0
12
+ label_row += 1
13
+ end
14
+ renderer.rect x, y, label_height, label_height, fill: colors[index], stroke: colors[index]
15
+ label_x = x + label_height + label_margin
16
+ label_y = y + label_height - renderer.font_size / 3
17
+ renderer.text label, label_x, label_y, text_anchor: 'start', class: 'label'
18
+ end
19
+ end
20
+
21
+ def label_total_height
22
+ return 0 if options[:labels].nil? || labels.empty?
23
+ label_rows * (label_height + label_margin)
24
+ end
25
+
26
+ def label_rows
27
+ return 0 if options[:labels].nil? || labels.empty?
28
+ avg_character_width = 10
29
+ ((labels.join.length * avg_character_width + 2 * labels.count * label_margin) / inner_width.to_f).ceil
30
+ end
31
+ end
@@ -0,0 +1,78 @@
1
+ class Charts::PieChart < Charts::Chart
2
+ include Charts::Legend
3
+
4
+ attr_reader :sum,
5
+ :sub_sums
6
+
7
+ def validate_arguments(data, options)
8
+ super(data, options)
9
+ raise ArgumentError unless data.is_a? Array
10
+ raise ArgumentError if options[:labels] && !options[:labels].empty? && options[:labels].count != data.count
11
+ raise ArgumentError if options[:explode] && !options[:explode].empty? && options[:explode].count != data.count
12
+ end
13
+
14
+ def default_options
15
+ super.merge(
16
+ width: 600,
17
+ height: 400,
18
+ label_height: 20,
19
+ label_margin: 10,
20
+ explode: nil,
21
+ border_width: 2
22
+ )
23
+ end
24
+
25
+ def prepare_data
26
+ @sum = data.reduce 0, :+
27
+ normalized_data = data.map { |value| value.to_f / sum }
28
+ @sub_sums = (normalized_data.length + 1).times.map { |i| normalized_data.first(i).reduce 0.0, :+ }
29
+ normalized_data
30
+ end
31
+
32
+ def pre_draw
33
+ super
34
+ draw_labels
35
+ end
36
+
37
+ def draw
38
+ prepared_data.length.times do |i|
39
+ start_deg = Math::PI * 2 * sub_sums[i]
40
+ end_deg = Math::PI * 2 * sub_sums[i + 1]
41
+ middle_deg = (start_deg + end_deg) / 2
42
+ expl = explode ? explode[i] : 0
43
+ middle_x = center_x + expl * Math.sin(middle_deg)
44
+ middle_y = center_y - expl * Math.cos(middle_deg)
45
+ start_x = middle_x + radius * Math.sin(start_deg)
46
+ start_y = middle_y - radius * Math.cos(start_deg)
47
+ end_x = middle_x + radius * Math.sin(end_deg)
48
+ end_y = middle_y - radius * Math.cos(end_deg)
49
+ more_than_half = prepared_data[i] > 0.5 ? 1 : 0
50
+ path = "M#{middle_x} #{middle_y}
51
+ L#{start_x} #{start_y}
52
+ A#{radius} #{radius} 0 #{more_than_half} 1 #{end_x} #{end_y}
53
+ L#{middle_x} #{middle_y}
54
+ "
55
+ renderer.path(path, fill: colors[i], stroke: background_color, stroke_width: border_width)
56
+ end
57
+ end
58
+
59
+ def radius
60
+ [inner_width, inner_height].min / 2
61
+ end
62
+
63
+ def center_x
64
+ outer_margin + inner_width / 2
65
+ end
66
+
67
+ def center_y
68
+ outer_margin + inner_height / 2
69
+ end
70
+
71
+ def inner_width
72
+ width - 2 * outer_margin
73
+ end
74
+
75
+ def inner_height
76
+ height - 2 * outer_margin - label_total_height
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ class Charts::Renderer
2
+ attr_reader :graph
3
+
4
+ def initialize(graph)
5
+ @graph = graph
6
+ if graph.type == :svg
7
+ extend Charts::Renderer::SvgRenderer
8
+ else
9
+ extend Charts::Renderer::RvgRenderer
10
+ end
11
+ pre_draw
12
+ end
13
+
14
+ def post_draw
15
+ filename = graph.options[:filename]
16
+ if filename
17
+ save filename
18
+ else
19
+ print
20
+ end
21
+ end
22
+
23
+ def grid_line_style
24
+ {
25
+ stroke: '#BBBBBB',
26
+ stroke_width: 1
27
+ }
28
+ end
29
+
30
+ def font_style
31
+ {
32
+ font_family: 'arial',
33
+ font_size: font_size
34
+ }
35
+ end
36
+
37
+ def font_size
38
+ 16
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ require 'rvg/rvg'
2
+
3
+ module Charts::Renderer::RvgRenderer
4
+ attr_reader :rvg
5
+
6
+ def pre_draw
7
+ @rvg = Magick::RVG.new(graph.width, graph.height) do |canvas|
8
+ canvas.background_fill = 'white'
9
+ end
10
+ end
11
+
12
+ def print
13
+ rvg.draw.to_blob { |attrs| attrs.format = 'PNG' }
14
+ end
15
+
16
+ def save(filename)
17
+ rvg.draw.write filename
18
+ end
19
+
20
+ def line(x1, y1, x2, y2, style)
21
+ canvas(style) { |c| c.line x1, y1, x2, y2 }
22
+ end
23
+
24
+ def circle(cx, cy, radius, style)
25
+ canvas(style) { |c| c.circle radius, cx, cy }
26
+ end
27
+
28
+ def rect(x, y, width, height, style)
29
+ canvas(style) { |c| c.rect width, height, x, y }
30
+ end
31
+
32
+ def path(d, style)
33
+ canvas(style) { |c| c.path d }
34
+ end
35
+
36
+ def text(text, x, y, style = {})
37
+ canvas(font_style.merge(style)) { |c| c.text x, y, text }
38
+ end
39
+
40
+ def canvas(style)
41
+ style.delete(:class)
42
+ rvg.rvg(graph.width, graph.height) do |canvas|
43
+ yield(canvas).styles(style)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ require 'victor'
2
+
3
+ module Charts::Renderer::SvgRenderer
4
+ attr_reader :svg
5
+
6
+ def pre_draw
7
+ @svg = SVG.new width: graph.width, height: graph.height
8
+ end
9
+
10
+ def print
11
+ svg.render
12
+ end
13
+
14
+ def save(filename)
15
+ svg.save filename
16
+ end
17
+
18
+ def line(x1, y1, x2, y2, style = {})
19
+ svg.line style.merge(x1: x1, y1: y1, x2: x2, y2: y2)
20
+ end
21
+
22
+ def circle(cx, cy, radius, style = {})
23
+ svg.circle style.merge(cx: cx, cy: cy, r: radius)
24
+ end
25
+
26
+ def rect(x, y, width, height, style = {})
27
+ svg.rect style.merge(x: x, y: y, width: width, height: height)
28
+ end
29
+
30
+ def path(d, style = {})
31
+ svg.path style.merge(d: d)
32
+ end
33
+
34
+ def text(text, x, y, style = {})
35
+ svg.text text, font_style.merge(style).merge(x: x, y: y)
36
+ end
37
+ end
38
+
data/lib/charts.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Charts
2
+ end
3
+
4
+ # Includes:
5
+ require_relative 'charts/renderer/renderer'
6
+ require_relative 'charts/renderer/rvg_renderer'
7
+ require_relative 'charts/renderer/svg_renderer'
8
+
9
+ # Classes:
10
+ require_relative 'charts/chart'
11
+ require_relative 'charts/legend'
12
+ require_relative 'charts/bar_chart/grid/grid'
13
+ require_relative 'charts/bar_chart/grid/grid_line'
14
+ require_relative 'charts/bar_chart/grid/vertical_grid_line'
15
+ require_relative 'charts/bar_chart/grid/horizontal_grid_line'
16
+ require_relative 'charts/bar_chart/bar_chart'
17
+ require_relative 'charts/bar_chart/bar/bar'
18
+ require_relative 'charts/bar_chart/bar/vertical_bar'
19
+ require_relative 'charts/bar_chart/bar/horizontal_bar'
20
+ require_relative 'charts/pie_chart/pie_chart'
21
+ require_relative 'charts/count_chart/count_chart'
22
+ require_relative 'charts/count_chart/circle_count_chart'
23
+ require_relative 'charts/count_chart/cross_count_chart'
24
+ require_relative 'charts/count_chart/manikin_count_chart'
25
+ require_relative 'charts/count_chart/symbol_count_chart'
26
+
27
+ # Helper for command line charts
28
+ require_relative 'charts/bin/opt_parser'
29
+ require_relative 'charts/bin/dispatcher'
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: charts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Eike Send
8
+ - Maximilian Maintz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-01-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: victor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.1.3
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.1.3
28
+ - !ruby/object:Gem::Dependency
29
+ name: rmagick
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ description: Create charts, graphs and info graphics with ruby as SVG, JPG or PNG
43
+ images
44
+ email: charts@eike.se
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - lib/charts.rb
50
+ - lib/charts/bar_chart/bar/bar.rb
51
+ - lib/charts/bar_chart/bar/horizontal_bar.rb
52
+ - lib/charts/bar_chart/bar/vertical_bar.rb
53
+ - lib/charts/bar_chart/bar_chart.rb
54
+ - lib/charts/bar_chart/grid/grid.rb
55
+ - lib/charts/bar_chart/grid/grid_line.rb
56
+ - lib/charts/bar_chart/grid/horizontal_grid_line.rb
57
+ - lib/charts/bar_chart/grid/vertical_grid_line.rb
58
+ - lib/charts/bin/dispatcher.rb
59
+ - lib/charts/bin/opt_parser.rb
60
+ - lib/charts/chart.rb
61
+ - lib/charts/count_chart/circle_count_chart.rb
62
+ - lib/charts/count_chart/count_chart.rb
63
+ - lib/charts/count_chart/cross_count_chart.rb
64
+ - lib/charts/count_chart/manikin_count_chart.rb
65
+ - lib/charts/count_chart/symbol_count_chart.rb
66
+ - lib/charts/legend.rb
67
+ - lib/charts/pie_chart/pie_chart.rb
68
+ - lib/charts/renderer/renderer.rb
69
+ - lib/charts/renderer/rvg_renderer.rb
70
+ - lib/charts/renderer/svg_renderer.rb
71
+ homepage: http://github.com/eikes/charts
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.5.1
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Renders beautiful charts
95
+ test_files: []