ruby-charts 0.1.0

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
+ SHA256:
3
+ metadata.gz: c3986814fddda6f6d9676fe18ae7902e5f4e0adf11a85d5f02449a423ae7b87a
4
+ data.tar.gz: 7fab1d76371520a89cae1986662d79925fcbcfddc19937ccf01c255890feb2f7
5
+ SHA512:
6
+ metadata.gz: 92b831ad1da8ab3f5a4510bf5193240eebdd1adbb8c2f85ea0ce34b7b50e4a63c322858223a8179694055d7d87cc0518ff498866f36beca77fb1b30e33a47b46
7
+ data.tar.gz: 9645e4e4c0c69d1a238fa200c574bebcb8b9fd9b9980fdf049482fbbeab00ff039869644f91b64aad9d993a86ea5e224941c62276fa7d9433c00573656ad8cec
@@ -0,0 +1,146 @@
1
+ # app/lib/ruby_charts/charts/bar_chart.rb
2
+
3
+ module RubyCharts
4
+ module Charts
5
+ class BarChart < BaseChart
6
+ def initialize(data, orientation = :vertical)
7
+ super(data)
8
+ @orientation = orientation
9
+ end
10
+
11
+ def horizontal!
12
+ @orientation = :horizontal
13
+ self
14
+ end
15
+
16
+ def vertical!
17
+ @orientation = :vertical
18
+ self
19
+ end
20
+
21
+ private
22
+
23
+ def draw_chart_content
24
+ if @orientation == :horizontal
25
+ draw_horizontal_bars
26
+ else
27
+ draw_vertical_bars
28
+ end
29
+ end
30
+
31
+ def draw_vertical_bars
32
+ chart_width = @options[:width] - (2 * @options[:padding])
33
+ chart_height = @options[:height] - (2 * @options[:padding]) - 100
34
+
35
+ max_value = @data[:values].max
36
+ bar_width = chart_width / @data[:labels].length * 0.7
37
+ spacing = chart_width / @data[:labels].length
38
+
39
+ @data[:labels].each_with_index do |label, index|
40
+ value = @data[:values][index]
41
+ bar_height = (value / max_value.to_f) * chart_height
42
+
43
+ x1 = @options[:padding] + (index * spacing) + ((spacing - bar_width) / 2)
44
+ y1 = @options[:height] - @options[:padding] - bar_height.to_i
45
+ x2 = x1 + bar_width.to_i
46
+ y2 = @options[:height] - @options[:padding]
47
+
48
+ color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
49
+
50
+ # Draw bar
51
+ @img.filled_rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, color)
52
+
53
+ # Draw label
54
+ @img.text(label, {
55
+ x: x1.to_i + 5,
56
+ y: y2 + 10,
57
+ size: 10,
58
+ color: [50, 50, 50]
59
+ })
60
+
61
+ # Draw value on top
62
+ if @options[:label_format] == :percentage
63
+ label_text = "#{(value / @data[:values].sum.to_f * 100).round(1)}%"
64
+ else
65
+ label_text = value.to_s
66
+ end
67
+
68
+ @img.text(label_text, {
69
+ x: x1.to_i + 10,
70
+ y: y1 - 15,
71
+ size: 10,
72
+ color: [50, 50, 50]
73
+ })
74
+ end
75
+
76
+ # Draw axes
77
+ draw_axes
78
+ end
79
+
80
+ def draw_horizontal_bars
81
+ chart_width = @options[:width] - (2 * @options[:padding])
82
+ chart_height = @options[:height] - (2 * @options[:padding]) - 100
83
+
84
+ max_value = @data[:values].max
85
+ bar_height = chart_height / @data[:labels].length * 0.7
86
+ spacing = chart_height / @data[:labels].length
87
+
88
+ @data[:labels].each_with_index do |label, index|
89
+ value = @data[:values][index]
90
+ bar_width = (value / max_value.to_f) * chart_width
91
+
92
+ y1 = @options[:padding] + (index * spacing) + ((spacing - bar_height) / 2)
93
+ x1 = @options[:padding]
94
+ y2 = y1 + bar_height.to_i
95
+ x2 = x1 + bar_width.to_i
96
+
97
+ color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
98
+
99
+ # Draw bar
100
+ @img.filled_rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, color)
101
+
102
+ # Draw label
103
+ @img.text(label, {
104
+ x: @options[:padding] - 70,
105
+ y: y1.to_i + 5,
106
+ size: 10,
107
+ color: [50, 50, 50]
108
+ })
109
+
110
+ # Draw value
111
+ @img.text(value.to_s, {
112
+ x: x2 + 5,
113
+ y: y1.to_i + 5,
114
+ size: 10,
115
+ color: [50, 50, 50]
116
+ })
117
+ end
118
+ end
119
+
120
+ def draw_axes
121
+ # X axis
122
+ @img.line(
123
+ @options[:padding],
124
+ @options[:height] - @options[:padding],
125
+ @options[:width] - @options[:padding],
126
+ @options[:height] - @options[:padding],
127
+ [0, 0, 0]
128
+ )
129
+
130
+ # Y axis
131
+ @img.line(
132
+ @options[:padding],
133
+ @options[:padding],
134
+ @options[:padding],
135
+ @options[:height] - @options[:padding],
136
+ [0, 0, 0]
137
+ )
138
+ end
139
+
140
+ def hex_to_rgb(hex)
141
+ hex = hex.delete('#')
142
+ [hex[0..1], hex[2..3], hex[4..5]].map { |x| x.to_i(16) }
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,120 @@
1
+ # app/lib/ruby_charts/charts/base_chart.rb
2
+
3
+ module RubyCharts
4
+ module Charts
5
+ class BaseChart
6
+ attr_accessor :data, :options
7
+
8
+ def initialize(data)
9
+ @data = data
10
+ @options = {
11
+ width: 1200,
12
+ height: 700,
13
+ title: 'Chart',
14
+ subtitle: nil,
15
+ labels: true,
16
+ colors: default_colors,
17
+ padding: 80,
18
+ font_size: 12
19
+ }
20
+ end
21
+
22
+ def title(text)
23
+ @options[:title] = text
24
+ self
25
+ end
26
+
27
+ def subtitle(text)
28
+ @options[:subtitle] = text
29
+ self
30
+ end
31
+
32
+ def width(w)
33
+ @options[:width] = w
34
+ self
35
+ end
36
+
37
+ def height(h)
38
+ @options[:height] = h
39
+ self
40
+ end
41
+
42
+ def colors(color_array)
43
+ @options[:colors] = color_array
44
+ self
45
+ end
46
+
47
+ def labels(format: :text)
48
+ @options[:label_format] = format
49
+ self
50
+ end
51
+
52
+ def legend(position: :right)
53
+ @options[:legend] = position
54
+ self
55
+ end
56
+
57
+ def render
58
+ @img = GD::Image.new(@options[:width], @options[:height])
59
+ @img.fill([255, 255, 255])
60
+
61
+ draw_background
62
+ draw_title
63
+ draw_chart_content
64
+ draw_legend if @options[:legend]
65
+
66
+ @img
67
+ end
68
+
69
+ def save_png(filename)
70
+ render.save_png(filename)
71
+ puts "✓ Chart saved: #{filename}"
72
+ end
73
+
74
+ def save_pdf(filename)
75
+ # Requires ruby-pdf integration
76
+ render_to_pdf(filename)
77
+ end
78
+
79
+ private
80
+
81
+ def default_colors
82
+ [
83
+ '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
84
+ '#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B88B', '#ABEBC6'
85
+ ]
86
+ end
87
+
88
+ def draw_background
89
+ @img.filled_rectangle(0, 0, @options[:width] - 1, @options[:height] - 1, [255, 255, 255])
90
+
91
+ # Subtle border
92
+ @img.rectangle(0, 0, @options[:width] - 1, @options[:height] - 1, [200, 200, 200])
93
+ end
94
+
95
+ def draw_title
96
+ # Main title
97
+ @img.text(@options[:title], {
98
+ x: @options[:padding],
99
+ y: 20,
100
+ size: 24,
101
+ color: [30, 30, 30]
102
+ })
103
+
104
+ # Subtitle
105
+ if @options[:subtitle]
106
+ @img.text(@options[:subtitle], {
107
+ x: @options[:padding],
108
+ y: 50,
109
+ size: 14,
110
+ color: [100, 100, 100]
111
+ })
112
+ end
113
+ end
114
+
115
+ def draw_chart_content
116
+ raise NotImplementedError, "Subclasses must implement draw_chart_content"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,84 @@
1
+ # app/lib/ruby_charts/charts/line_chart.rb
2
+
3
+ module RubyCharts
4
+ module Charts
5
+ class LineChart < BaseChart
6
+ private
7
+
8
+ def draw_chart_content
9
+ chart_width = @options[:width] - (2 * @options[:padding])
10
+ chart_height = @options[:height] - (2 * @options[:padding]) - 100
11
+
12
+ max_value = @data[:values].max
13
+ min_value = @data[:values].min
14
+ range = max_value - min_value
15
+
16
+ # Draw grid
17
+ draw_grid(chart_width, chart_height, max_value, min_value)
18
+
19
+ # Draw line
20
+ points = @data[:values].each_with_index.map do |value, index|
21
+ x = @options[:padding] + (index * chart_width / (@data[:values].length - 1))
22
+ y = @options[:height] - @options[:padding] - ((value - min_value) / range.to_f * chart_height)
23
+ [x.to_i, y.to_i]
24
+ end
25
+
26
+ color = hex_to_rgb(@options[:colors].first)
27
+
28
+ # Draw line segments
29
+ points.each_cons(2) do |p1, p2|
30
+ @img.line(p1[0], p1[1], p2[0], p2[1], color)
31
+ end
32
+
33
+ # Draw points
34
+ points.each_with_index do |point, index|
35
+ @img.filled_circle(point[0], point[1], 4, color)
36
+
37
+ # Draw label
38
+ @img.text(@data[:labels][index], {
39
+ x: point[0] - 20,
40
+ y: point[1] - 20,
41
+ size: 9,
42
+ color: [50, 50, 50]
43
+ })
44
+ end
45
+
46
+ # Draw axes
47
+ draw_axes(max_value, min_value)
48
+ end
49
+
50
+ def draw_grid(width, height, max_val, min_val)
51
+ # Horizontal grid lines
52
+ 5.times do |i|
53
+ y = @options[:padding] + 50 + (i * height / 4)
54
+ @img.line(@options[:padding], y, @options[:width] - @options[:padding], y, [230, 230, 230])
55
+ end
56
+
57
+ # Vertical grid lines
58
+ (@data[:values].length - 1).times do |i|
59
+ x = @options[:padding] + (i * width / (@data[:values].length - 1))
60
+ @img.line(x, @options[:padding] + 50, x, @options[:height] - @options[:padding], [230, 230, 230])
61
+ end
62
+ end
63
+
64
+ def draw_axes(max_val, min_val)
65
+ # Y axis labels
66
+ 5.times do |i|
67
+ value = min_val + ((max_val - min_val) * i / 4)
68
+ y = @options[:height] - @options[:padding] - (i * (@options[:height] - 2 * @options[:padding] - 100) / 4)
69
+ @img.text(value.to_i.to_s, {
70
+ x: 30,
71
+ y: y.to_i - 7,
72
+ size: 9,
73
+ color: [100, 100, 100]
74
+ })
75
+ end
76
+ end
77
+
78
+ def hex_to_rgb(hex)
79
+ hex = hex.delete('#')
80
+ [hex[0..1], hex[2..3], hex[4..5]].map { |x| x.to_i(16) }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,81 @@
1
+ # app/lib/ruby_charts/charts/pie_chart.rb
2
+
3
+ module RubyCharts
4
+ module Charts
5
+ class PieChart < BaseChart
6
+ private
7
+
8
+ def draw_chart_content
9
+ chart_area_width = @options[:width] - (2 * @options[:padding])
10
+ chart_area_height = @options[:height] - (2 * @options[:padding]) - 100
11
+
12
+ center_x = @options[:padding] + (chart_area_width / 2)
13
+ center_y = @options[:padding] + 50 + (chart_area_height / 2)
14
+ radius = [chart_area_width, chart_area_height].min / 3
15
+
16
+ total = @data[:values].sum
17
+ current_angle = 0
18
+
19
+ @data[:labels].each_with_index do |label, index|
20
+ value = @data[:values][index]
21
+ percentage = (value / total.to_f) * 100
22
+ angle = (percentage / 100.0) * 360
23
+
24
+ color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
25
+
26
+ # Draw pie slice
27
+ draw_pie_slice(center_x, center_y, radius, current_angle, angle, color)
28
+
29
+ # Draw label
30
+ if @options[:label_format] == :percentage
31
+ label_text = "#{percentage.round(1)}%"
32
+ else
33
+ label_text = "#{label}: #{value}"
34
+ end
35
+
36
+ draw_pie_label(center_x, center_y, radius, current_angle, angle, label_text)
37
+
38
+ current_angle += angle
39
+ end
40
+ end
41
+
42
+ def draw_pie_slice(cx, cy, radius, start_angle, angle, color)
43
+ # Draw filled circle slice
44
+ # Using ruby-libgd polygon
45
+ points = [[cx, cy]]
46
+
47
+ steps = (angle / 5).ceil # 5-degree increments for smooth curve
48
+ (0..steps).each do |i|
49
+ current_angle = start_angle + (angle * i / steps)
50
+ rad = (current_angle * Math::PI / 180)
51
+ x = (cx + radius * Math.cos(rad)).to_i
52
+ y = (cy + radius * Math.sin(rad)).to_i
53
+ points << [x, y]
54
+ end
55
+
56
+ @img.filled_polygon(points, color)
57
+ end
58
+
59
+ def draw_pie_label(cx, cy, radius, start_angle, angle, text)
60
+ label_angle = start_angle + (angle / 2)
61
+ label_radius = radius * 0.65
62
+
63
+ rad = (label_angle * Math::PI / 180)
64
+ label_x = (cx + label_radius * Math.cos(rad)).to_i
65
+ label_y = (cy + label_radius * Math.sin(rad)).to_i
66
+
67
+ @img.text(text, {
68
+ x: label_x - 30,
69
+ y: label_y - 7,
70
+ size: 11,
71
+ color: [255, 255, 255]
72
+ })
73
+ end
74
+
75
+ def hex_to_rgb(hex)
76
+ hex = hex.delete('#')
77
+ [hex[0..1], hex[2..3], hex[4..5]].map { |x| x.to_i(16) }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,49 @@
1
+ # app/lib/ruby_charts/data_loader.rb
2
+
3
+ module RubyCharts
4
+ class DataLoader
5
+ def self.from_csv(filepath, options = {})
6
+ data = CSV.read(filepath, headers: true)
7
+ parse_data(data, options)
8
+ end
9
+
10
+ def self.from_xlsx(filepath, options = {})
11
+ require 'roo'
12
+ workbook = Roo::Excelx.new(filepath)
13
+ sheet = options[:sheet] || workbook.default_sheet
14
+ workbook.sheet(sheet)
15
+
16
+ headers = workbook.row(1)
17
+ rows = (2..workbook.last_row).map { |i| workbook.row(i) }
18
+
19
+ parse_rows(headers, rows, options)
20
+ end
21
+
22
+ def self.from_yaml(filepath, options = {})
23
+ data = YAML.safe_load_file(filepath)
24
+ parse_data(data, options)
25
+ end
26
+
27
+ private
28
+
29
+ def self.parse_data(data, options)
30
+ {
31
+ labels: data.map { |row| row[0] },
32
+ values: data.map { |row| row[1] },
33
+ series: extract_series(data, options)
34
+ }
35
+ end
36
+
37
+ def self.extract_series(data, options)
38
+ # Para multi-series charts
39
+ if data.first.is_a?(Hash) && data.first.keys.length > 2
40
+ data.first.keys.map do |key|
41
+ {
42
+ name: key,
43
+ values: data.map { |row| row[key] }
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,90 @@
1
+ # app/lib/ruby_charts.rb
2
+
3
+ module RubyCharts
4
+ class ChartBuilder
5
+ def initialize(data)
6
+ @data = data
7
+ end
8
+
9
+ def self.from_csv(filepath, options = {})
10
+ data = DataLoader.from_csv(filepath, options)
11
+ new(data)
12
+ end
13
+
14
+ def self.from_xlsx(filepath, options = {})
15
+ data = DataLoader.from_xlsx(filepath, options)
16
+ new(data)
17
+ end
18
+
19
+ def self.from_yaml(filepath, options = {})
20
+ data = DataLoader.from_yaml(filepath, options)
21
+ new(data)
22
+ end
23
+
24
+ def self.from_hash(data_hash)
25
+ new(data_hash)
26
+ end
27
+
28
+ def type(chart_type)
29
+ @chart_type = chart_type
30
+ self
31
+ end
32
+
33
+ def title(text)
34
+ @title = text
35
+ self
36
+ end
37
+
38
+ def subtitle(text)
39
+ @subtitle = text
40
+ self
41
+ end
42
+
43
+ def colors(*color_array)
44
+ @colors = color_array.flatten
45
+ self
46
+ end
47
+
48
+ def labels(format: :text)
49
+ @label_format = format
50
+ self
51
+ end
52
+
53
+ def legend(position: :right)
54
+ @legend = position
55
+ self
56
+ end
57
+
58
+ def save_png(filename)
59
+ chart = build_chart
60
+ chart.title(@title) if @title
61
+ chart.subtitle(@subtitle) if @subtitle
62
+ chart.colors(@colors) if @colors
63
+ chart.labels(format: @label_format) if @label_format
64
+ chart.legend(position: @legend) if @legend
65
+ chart.save_png(filename)
66
+ end
67
+
68
+ def save_pdf(filename)
69
+ chart = build_chart
70
+ chart.save_pdf(filename)
71
+ end
72
+
73
+ private
74
+
75
+ def build_chart
76
+ case @chart_type
77
+ when :pie
78
+ Charts::PieChart.new(@data)
79
+ when :bar, :vertical_bar
80
+ Charts::BarChart.new(@data, :vertical)
81
+ when :horizontal_bar
82
+ Charts::BarChart.new(@data, :horizontal)
83
+ when :line
84
+ Charts::LineChart.new(@data)
85
+ else
86
+ raise "Unknown chart type: #{@chart_type}"
87
+ end
88
+ end
89
+ end
90
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-charts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Germán Silva
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruby-libgd
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.3.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.3.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: roo
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.10.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 2.10.0
40
+ description: Generate publication-ready pie, bar, and line charts from data files
41
+ email:
42
+ - ggerman@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/ruby_charts.rb
48
+ - lib/ruby_charts/charts/bar_chart.rb
49
+ - lib/ruby_charts/charts/base_chart.rb
50
+ - lib/ruby_charts/charts/line_chart.rb
51
+ - lib/ruby_charts/charts/pie_chart.rb
52
+ - lib/ruby_charts/data_loader.rb
53
+ homepage: https://github.com/ggerman/ruby-charts
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 2.7.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 4.0.3
72
+ specification_version: 4
73
+ summary: Professional charts from CSV/XLSX/YAML using ruby-libgd
74
+ test_files: []