ruby-charts 0.1.0 → 0.1.1a

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3986814fddda6f6d9676fe18ae7902e5f4e0adf11a85d5f02449a423ae7b87a
4
- data.tar.gz: 7fab1d76371520a89cae1986662d79925fcbcfddc19937ccf01c255890feb2f7
3
+ metadata.gz: 2116fe2589c00f881430f403989c0d16e37c471b4b707dd88f3c7253c006e376
4
+ data.tar.gz: 54aa88a4bb74b449e3bfd5d7c5975733f010120b16e63ffe4266e9742b15d19d
5
5
  SHA512:
6
- metadata.gz: 92b831ad1da8ab3f5a4510bf5193240eebdd1adbb8c2f85ea0ce34b7b50e4a63c322858223a8179694055d7d87cc0518ff498866f36beca77fb1b30e33a47b46
7
- data.tar.gz: 9645e4e4c0c69d1a238fa200c574bebcb8b9fd9b9980fdf049482fbbeab00ff039869644f91b64aad9d993a86ea5e224941c62276fa7d9433c00573656ad8cec
6
+ metadata.gz: 2b6c6bf3e0748b3f43a60a3c7e7f57ea24deb5cf0ac9504172672b5ef488c88f03d11844980735350e21cc21e80cc9171b75c20d64b25eb9ba4972b9b5f1db90
7
+ data.tar.gz: f5a2bf50d115eac0e51f0fd44cc538e11ec4fc7da247623e44224946baba5efa0dd5b5df6721001e2af6c312c2beb7432bb222ab4297b70502db02047814a43b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Giménez Silva Germán Alberto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
File without changes
@@ -0,0 +1,66 @@
1
+ # lib/ruby_charts/builder.rb
2
+
3
+ module RubyCharts
4
+ class Builder
5
+ def initialize(data)
6
+ @data = data
7
+ @chart_type = :bar
8
+ @title = 'Chart'
9
+ @subtitle = nil
10
+ @colors = nil
11
+ @legend = nil
12
+ end
13
+
14
+ def type(chart_type)
15
+ @chart_type = chart_type
16
+ self
17
+ end
18
+
19
+ def title(text)
20
+ @title = text
21
+ self
22
+ end
23
+
24
+ def subtitle(text)
25
+ @subtitle = text
26
+ self
27
+ end
28
+
29
+ def colors(*color_array)
30
+ @colors = color_array.flatten
31
+ self
32
+ end
33
+
34
+ def legend(position: :right)
35
+ @legend = position
36
+ self
37
+ end
38
+
39
+ def save_png(filename)
40
+ chart = build_chart
41
+ chart.title(@title)
42
+ chart.subtitle(@subtitle)
43
+ chart.colors(*@colors) if @colors
44
+ chart.legend(position: @legend) if @legend
45
+
46
+ chart.save_png(filename)
47
+ end
48
+
49
+ private
50
+
51
+ def build_chart
52
+ case @chart_type
53
+ when :pie
54
+ Charts::PieChart.new(@data)
55
+ when :bar, :vertical_bar
56
+ Charts::BarChart.new(@data, :vertical)
57
+ when :horizontal_bar
58
+ Charts::BarChart.new(@data, :horizontal)
59
+ when :line
60
+ Charts::LineChart.new(@data)
61
+ else
62
+ raise "Unknown chart type: #{@chart_type}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,4 +1,4 @@
1
- # app/lib/ruby_charts/charts/bar_chart.rb
1
+ # lib/ruby_charts/charts/bar_chart.rb
2
2
 
3
3
  module RubyCharts
4
4
  module Charts
@@ -18,7 +18,7 @@ module RubyCharts
18
18
  self
19
19
  end
20
20
 
21
- private
21
+ protected
22
22
 
23
23
  def draw_chart_content
24
24
  if @orientation == :horizontal
@@ -28,96 +28,74 @@ module RubyCharts
28
28
  end
29
29
  end
30
30
 
31
+ private
32
+
31
33
  def draw_vertical_bars
32
34
  chart_width = @options[:width] - (2 * @options[:padding])
33
35
  chart_height = @options[:height] - (2 * @options[:padding]) - 100
34
36
 
35
- max_value = @data[:values].max
36
- bar_width = chart_width / @data[:labels].length * 0.7
37
+ max_value = @data[:values].max.to_f
38
+ bar_width = (chart_width / @data[:labels].length) * 0.6
37
39
  spacing = chart_width / @data[:labels].length
38
40
 
39
41
  @data[:labels].each_with_index do |label, index|
40
42
  value = @data[:values][index]
41
- bar_height = (value / max_value.to_f) * chart_height
43
+ bar_height = (value / max_value) * chart_height
42
44
 
43
45
  x1 = @options[:padding] + (index * spacing) + ((spacing - bar_width) / 2)
44
46
  y1 = @options[:height] - @options[:padding] - bar_height.to_i
45
47
  x2 = x1 + bar_width.to_i
46
48
  y2 = @options[:height] - @options[:padding]
47
49
 
48
- color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
50
+ color = get_color(index)
49
51
 
50
52
  # Draw bar
51
53
  @img.filled_rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, color)
54
+ @img.rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, [100, 100, 100])
52
55
 
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
- })
56
+ # Draw value label on top of bar
57
+ draw_text(value.to_i.to_s, x1.to_i + 5, y1 - 10, 10, [50, 50, 50])
60
58
 
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
- })
59
+ # Draw label below bar
60
+ draw_text(label, x1.to_i, y2 + 20, 10, [50, 50, 50])
74
61
  end
75
62
 
76
63
  # Draw axes
77
- draw_axes
64
+ draw_axes(max_value)
78
65
  end
79
66
 
80
67
  def draw_horizontal_bars
81
- chart_width = @options[:width] - (2 * @options[:padding])
68
+ chart_width = @options[:width] - (2 * @options[:padding]) - 150
82
69
  chart_height = @options[:height] - (2 * @options[:padding]) - 100
83
70
 
84
- max_value = @data[:values].max
85
- bar_height = chart_height / @data[:labels].length * 0.7
71
+ max_value = @data[:values].max.to_f
72
+ bar_height = (chart_height / @data[:labels].length) * 0.6
86
73
  spacing = chart_height / @data[:labels].length
87
74
 
88
75
  @data[:labels].each_with_index do |label, index|
89
76
  value = @data[:values][index]
90
- bar_width = (value / max_value.to_f) * chart_width
77
+ bar_width = (value / max_value) * chart_width
91
78
 
92
- y1 = @options[:padding] + (index * spacing) + ((spacing - bar_height) / 2)
93
- x1 = @options[:padding]
79
+ y1 = @options[:padding] + 50 + (index * spacing) + ((spacing - bar_height) / 2)
80
+ x1 = @options[:padding] + 150
94
81
  y2 = y1 + bar_height.to_i
95
82
  x2 = x1 + bar_width.to_i
96
83
 
97
- color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
84
+ color = get_color(index)
98
85
 
99
86
  # Draw bar
100
87
  @img.filled_rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, color)
88
+ @img.rectangle(x1.to_i, y1.to_i, x2.to_i, y2.to_i, [100, 100, 100])
101
89
 
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
- })
90
+ # Draw label on left
91
+ draw_text(label, 20, y1.to_i + 15, 10, [50, 50, 50])
109
92
 
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
- })
93
+ # Draw value on right
94
+ draw_text(value.to_i.to_s, x2 + 10, y1.to_i + 15, 10, [50, 50, 50])
117
95
  end
118
96
  end
119
97
 
120
- def draw_axes
98
+ def draw_axes(max_val)
121
99
  # X axis
122
100
  @img.line(
123
101
  @options[:padding],
@@ -130,16 +108,29 @@ module RubyCharts
130
108
  # Y axis
131
109
  @img.line(
132
110
  @options[:padding],
133
- @options[:padding],
111
+ @options[:padding] + 70,
134
112
  @options[:padding],
135
113
  @options[:height] - @options[:padding],
136
114
  [0, 0, 0]
137
115
  )
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) }
116
+
117
+ # Grid lines and Y labels
118
+ 5.times do |i|
119
+ value = (max_val * i / 4).to_i
120
+ y = @options[:height] - @options[:padding] - (i * (@options[:height] - 2 * @options[:padding] - 100) / 4)
121
+
122
+ # Grid line
123
+ @img.line(
124
+ @options[:padding],
125
+ y.to_i,
126
+ @options[:width] - @options[:padding],
127
+ y.to_i,
128
+ [230, 230, 230]
129
+ )
130
+
131
+ # Y label
132
+ draw_text(value.to_s, 20, y.to_i + 5, 9, [100, 100, 100])
133
+ end
143
134
  end
144
135
  end
145
136
  end
@@ -1,8 +1,23 @@
1
- # app/lib/ruby_charts/charts/base_chart.rb
1
+ # lib/ruby_charts/charts/base_chart.rb
2
+
3
+ require_relative '../config'
2
4
 
3
5
  module RubyCharts
4
6
  module Charts
5
7
  class BaseChart
8
+ COLORS = [
9
+ [255, 107, 107], # Red
10
+ [78, 205, 196], # Teal
11
+ [69, 183, 209], # Blue
12
+ [255, 160, 122], # Orange
13
+ [154, 216, 200], # Mint
14
+ [247, 220, 111], # Yellow
15
+ [187, 143, 206], # Purple
16
+ [133, 193, 226], # Light Blue
17
+ [248, 184, 139], # Peach
18
+ [170, 190, 198] # Gray
19
+ ].freeze
20
+
6
21
  attr_accessor :data, :options
7
22
 
8
23
  def initialize(data)
@@ -12,10 +27,10 @@ module RubyCharts
12
27
  height: 700,
13
28
  title: 'Chart',
14
29
  subtitle: nil,
15
- labels: true,
16
- colors: default_colors,
30
+ colors: COLORS,
17
31
  padding: 80,
18
- font_size: 12
32
+ font: Config.font(:regular),
33
+ font_bold: Config.font(:bold)
19
34
  }
20
35
  end
21
36
 
@@ -29,23 +44,13 @@ module RubyCharts
29
44
  self
30
45
  end
31
46
 
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
47
+ def colors(*color_array)
48
+ @options[:colors] = color_array.flatten
44
49
  self
45
50
  end
46
51
 
47
- def labels(format: :text)
48
- @options[:label_format] = format
52
+ def font(path)
53
+ @options[:font] = path
49
54
  self
50
55
  end
51
56
 
@@ -61,59 +66,102 @@ module RubyCharts
61
66
  draw_background
62
67
  draw_title
63
68
  draw_chart_content
64
- draw_legend if @options[:legend]
65
69
 
66
70
  @img
67
71
  end
68
72
 
69
73
  def save_png(filename)
70
- render.save_png(filename)
74
+ render.save(filename)
71
75
  puts "✓ Chart saved: #{filename}"
76
+ filename
72
77
  end
73
78
 
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
79
+ protected
87
80
 
88
81
  def draw_background
89
- @img.filled_rectangle(0, 0, @options[:width] - 1, @options[:height] - 1, [255, 255, 255])
82
+ @img.filled_rectangle(
83
+ 0, 0,
84
+ @options[:width] - 1,
85
+ @options[:height] - 1,
86
+ [255, 255, 255]
87
+ )
90
88
 
91
- # Subtle border
92
- @img.rectangle(0, 0, @options[:width] - 1, @options[:height] - 1, [200, 200, 200])
89
+ @img.rectangle(
90
+ 0, 0,
91
+ @options[:width] - 1,
92
+ @options[:height] - 1,
93
+ [200, 200, 200]
94
+ )
93
95
  end
94
96
 
95
97
  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
- })
98
+ # Title background
99
+ @img.filled_rectangle(
100
+ 0, 0,
101
+ @options[:width] - 1,
102
+ 70,
103
+ [250, 250, 250]
104
+ )
103
105
 
104
- # Subtitle
105
- if @options[:subtitle]
106
- @img.text(@options[:subtitle], {
106
+ # Title border
107
+ @img.line(0, 70, @options[:width] - 1, 70, [200, 200, 200])
108
+
109
+ # Title text
110
+ if File.exist?(@options[:font_bold])
111
+ @img.text_ft(
112
+ @options[:title],
107
113
  x: @options[:padding],
108
114
  y: 50,
109
- size: 14,
115
+ font: @options[:font_bold],
116
+ size: 28,
117
+ color: [30, 30, 30]
118
+ )
119
+ end
120
+
121
+ # Subtitle text
122
+ if @options[:subtitle] && File.exist?(@options[:font])
123
+ @img.text_ft(
124
+ @options[:subtitle],
125
+ x: @options[:padding],
126
+ y: 65,
127
+ font: @options[:font],
128
+ size: 12,
110
129
  color: [100, 100, 100]
111
- })
130
+ )
112
131
  end
113
132
  end
114
133
 
115
134
  def draw_chart_content
116
- raise NotImplementedError, "Subclasses must implement draw_chart_content"
135
+ raise NotImplementedError, 'Subclasses must implement draw_chart_content'
136
+ end
137
+
138
+ def get_color(index)
139
+ @options[:colors][index % @options[:colors].length]
140
+ end
141
+
142
+ def draw_text(text, x, y, size, color)
143
+ if File.exist?(@options[:font])
144
+ @img.text_ft(
145
+ text,
146
+ x: x,
147
+ y: y,
148
+ font: @options[:font],
149
+ size: size,
150
+ color: color
151
+ )
152
+ end
153
+ end
154
+
155
+ def measure_text(text, size)
156
+ if File.exist?(@options[:font])
157
+ @img.text_bbox(
158
+ text,
159
+ font: @options[:font],
160
+ size: size
161
+ )
162
+ else
163
+ [text.length * size / 2, size]
164
+ end
117
165
  end
118
166
  end
119
167
  end
@@ -1,84 +1,92 @@
1
- # app/lib/ruby_charts/charts/line_chart.rb
1
+ # lib/ruby_charts/charts/line_chart.rb
2
2
 
3
3
  module RubyCharts
4
4
  module Charts
5
5
  class LineChart < BaseChart
6
- private
6
+ protected
7
7
 
8
8
  def draw_chart_content
9
9
  chart_width = @options[:width] - (2 * @options[:padding])
10
10
  chart_height = @options[:height] - (2 * @options[:padding]) - 100
11
11
 
12
- max_value = @data[:values].max
13
- min_value = @data[:values].min
12
+ max_value = @data[:values].max.to_f
13
+ min_value = @data[:values].min.to_f
14
14
  range = max_value - min_value
15
+ range = 1 if range == 0
15
16
 
16
17
  # Draw grid
17
- draw_grid(chart_width, chart_height, max_value, min_value)
18
+ draw_grid(chart_width, chart_height)
18
19
 
19
- # Draw line
20
+ # Calculate points
20
21
  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)
22
+ x = @options[:padding] + (index * chart_width / ([@data[:values].length - 1, 1].max))
23
+ y = @options[:height] - @options[:padding] - ((value - min_value) / range * chart_height)
23
24
  [x.to_i, y.to_i]
24
25
  end
25
26
 
26
- color = hex_to_rgb(@options[:colors].first)
27
+ color = get_color(0)
27
28
 
28
29
  # Draw line segments
29
30
  points.each_cons(2) do |p1, p2|
30
31
  @img.line(p1[0], p1[1], p2[0], p2[1], color)
31
32
  end
32
33
 
33
- # Draw points
34
+ # Draw points as circles
34
35
  points.each_with_index do |point, index|
35
- @img.filled_circle(point[0], point[1], 4, color)
36
+ @img.filled_circle(point[0], point[1], 5, color)
37
+ @img.circle(point[0], point[1], 5, [100, 100, 100])
36
38
 
37
39
  # 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
- })
40
+ draw_text(@data[:labels][index], point[0] - 20, point[1] - 20, 9, [50, 50, 50])
44
41
  end
45
42
 
46
43
  # Draw axes
47
44
  draw_axes(max_value, min_value)
48
45
  end
49
46
 
50
- def draw_grid(width, height, max_val, min_val)
47
+ private
48
+
49
+ def draw_grid(width, height)
51
50
  # Horizontal grid lines
52
51
  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])
52
+ y = @options[:padding] + 70 + (i * height / 4)
53
+ @img.line(
54
+ @options[:padding],
55
+ y,
56
+ @options[:width] - @options[:padding],
57
+ y,
58
+ [230, 230, 230]
59
+ )
61
60
  end
62
61
  end
63
62
 
64
63
  def draw_axes(max_val, min_val)
64
+ # X axis
65
+ @img.line(
66
+ @options[:padding],
67
+ @options[:height] - @options[:padding],
68
+ @options[:width] - @options[:padding],
69
+ @options[:height] - @options[:padding],
70
+ [0, 0, 0]
71
+ )
72
+
73
+ # Y axis
74
+ @img.line(
75
+ @options[:padding],
76
+ @options[:padding] + 70,
77
+ @options[:padding],
78
+ @options[:height] - @options[:padding],
79
+ [0, 0, 0]
80
+ )
81
+
65
82
  # Y axis labels
66
83
  5.times do |i|
67
- value = min_val + ((max_val - min_val) * i / 4)
84
+ value = (min_val + ((max_val - min_val) * i / 4)).to_i
68
85
  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
- })
86
+
87
+ draw_text(value.to_s, 20, y.to_i + 5, 9, [100, 100, 100])
75
88
  end
76
89
  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
90
  end
83
91
  end
84
92
  end
@@ -1,62 +1,65 @@
1
- # app/lib/ruby_charts/charts/pie_chart.rb
1
+ # lib/ruby_charts/charts/pie_chart.rb
2
2
 
3
3
  module RubyCharts
4
4
  module Charts
5
5
  class PieChart < BaseChart
6
- private
6
+ protected
7
7
 
8
8
  def draw_chart_content
9
9
  chart_area_width = @options[:width] - (2 * @options[:padding])
10
- chart_area_height = @options[:height] - (2 * @options[:padding]) - 100
10
+ chart_area_height = @options[:height] - (2 * @options[:padding]) - 120
11
11
 
12
12
  center_x = @options[:padding] + (chart_area_width / 2)
13
- center_y = @options[:padding] + 50 + (chart_area_height / 2)
13
+ center_y = @options[:padding] + 70 + (chart_area_height / 2)
14
14
  radius = [chart_area_width, chart_area_height].min / 3
15
15
 
16
- total = @data[:values].sum
17
- current_angle = 0
16
+ total = @data[:values].sum.to_f
17
+ current_angle = -90 # Start from top
18
18
 
19
19
  @data[:labels].each_with_index do |label, index|
20
20
  value = @data[:values][index]
21
- percentage = (value / total.to_f) * 100
21
+ percentage = (value / total) * 100
22
22
  angle = (percentage / 100.0) * 360
23
23
 
24
- color = hex_to_rgb(@options[:colors][index % @options[:colors].length])
24
+ color = get_color(index)
25
25
 
26
- # Draw pie slice
26
+ # Draw pie slice using polygon
27
27
  draw_pie_slice(center_x, center_y, radius, current_angle, angle, color)
28
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
29
+ # Draw percentage label
30
+ draw_slice_label(center_x, center_y, radius, current_angle, angle, percentage)
35
31
 
36
- draw_pie_label(center_x, center_y, radius, current_angle, angle, label_text)
32
+ # Draw legend
33
+ draw_legend_item(index, label, percentage, color)
37
34
 
38
35
  current_angle += angle
39
36
  end
40
37
  end
41
38
 
39
+ private
40
+
42
41
  def draw_pie_slice(cx, cy, radius, start_angle, angle, color)
43
- # Draw filled circle slice
44
- # Using ruby-libgd polygon
42
+ # Build polygon points for pie slice
45
43
  points = [[cx, cy]]
46
44
 
47
- steps = (angle / 5).ceil # 5-degree increments for smooth curve
45
+ steps = (angle / 5).ceil
48
46
  (0..steps).each do |i|
49
47
  current_angle = start_angle + (angle * i / steps)
50
48
  rad = (current_angle * Math::PI / 180)
49
+
51
50
  x = (cx + radius * Math.cos(rad)).to_i
52
51
  y = (cy + radius * Math.sin(rad)).to_i
53
52
  points << [x, y]
54
53
  end
55
54
 
55
+ # Draw filled polygon (pie slice)
56
56
  @img.filled_polygon(points, color)
57
+
58
+ # Draw border
59
+ @img.polygon(points, [100, 100, 100])
57
60
  end
58
61
 
59
- def draw_pie_label(cx, cy, radius, start_angle, angle, text)
62
+ def draw_slice_label(cx, cy, radius, start_angle, angle, percentage)
60
63
  label_angle = start_angle + (angle / 2)
61
64
  label_radius = radius * 0.65
62
65
 
@@ -64,17 +67,27 @@ module RubyCharts
64
67
  label_x = (cx + label_radius * Math.cos(rad)).to_i
65
68
  label_y = (cy + label_radius * Math.sin(rad)).to_i
66
69
 
67
- @img.text(text, {
68
- x: label_x - 30,
69
- y: label_y - 7,
70
- size: 11,
71
- color: [255, 255, 255]
72
- })
70
+ text = "#{percentage.round(1)}%"
71
+
72
+ draw_text(text, label_x - 25, label_y, 11, [255, 255, 255])
73
73
  end
74
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) }
75
+ def draw_legend_item(index, label, percentage, color)
76
+ # Legend at bottom
77
+ items_per_row = 3
78
+ row = index / items_per_row
79
+ col = index % items_per_row
80
+
81
+ legend_x = @options[:padding] + (col * (@options[:width] - 2 * @options[:padding]) / 3)
82
+ legend_y = @options[:height] - 50 + (row * 25)
83
+
84
+ # Legend color box
85
+ @img.filled_rectangle(legend_x, legend_y, legend_x + 15, legend_y + 15, color)
86
+ @img.rectangle(legend_x, legend_y, legend_x + 15, legend_y + 15, [100, 100, 100])
87
+
88
+ # Legend text
89
+ text = "#{label}: #{percentage.round(1)}%"
90
+ draw_text(text, legend_x + 25, legend_y + 13, 10, [50, 50, 50])
78
91
  end
79
92
  end
80
93
  end
@@ -0,0 +1,38 @@
1
+ # lib/ruby_charts/config.rb
2
+
3
+ module RubyCharts
4
+ class Config
5
+ # Default system fonts (adjust for your OS)
6
+ DEFAULT_FONTS = {
7
+ regular: '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
8
+ bold: '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
9
+ }.freeze
10
+
11
+ # macOS alternatives
12
+ MACOS_FONTS = {
13
+ regular: '/Library/Fonts/Arial.ttf',
14
+ bold: '/Library/Fonts/Arial Bold.ttf'
15
+ }.freeze
16
+
17
+ # Windows alternatives
18
+ WINDOWS_FONTS = {
19
+ regular: 'C:\\Windows\\Fonts\\arial.ttf',
20
+ bold: 'C:\\Windows\\Fonts\\arialbd.ttf'
21
+ }.freeze
22
+
23
+ def self.font(style = :regular)
24
+ case RUBY_PLATFORM
25
+ when /darwin/
26
+ MACOS_FONTS[style]
27
+ when /mswin|mingw/
28
+ WINDOWS_FONTS[style]
29
+ else
30
+ DEFAULT_FONTS[style]
31
+ end
32
+ end
33
+
34
+ def self.font_exists?(path)
35
+ File.exist?(path)
36
+ end
37
+ end
38
+ end
@@ -1,49 +0,0 @@
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,24 @@
1
+ # lib/ruby_charts/loaders/csv_loader.rb
2
+
3
+ module RubyCharts
4
+ module Loaders
5
+ class CSVLoader
6
+ def self.load(filepath, options = {})
7
+ raise "File not found: #{filepath}" unless File.exist?(filepath)
8
+
9
+ rows = CSV.read(filepath)
10
+ headers = rows[0]
11
+ data_rows = rows[1..-1]
12
+
13
+ labels = data_rows.map { |row| row[0] }
14
+ values = data_rows.map { |row| row[1].to_f }
15
+
16
+ {
17
+ labels: labels,
18
+ values: values,
19
+ type: options[:type] || :bar
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # lib/ruby_charts/loaders/xlsx_loader.rb
2
+
3
+ module RubyCharts
4
+ module Loaders
5
+ class XLSXLoader
6
+ def self.load(filepath, options = {})
7
+ raise "File not found: #{filepath}" unless File.exist?(filepath)
8
+
9
+ workbook = Roo::Excelx.new(filepath)
10
+ sheet = options[:sheet] || workbook.default_sheet
11
+ workbook.sheet(sheet)
12
+
13
+ labels = []
14
+ values = []
15
+
16
+ (2..workbook.last_row).each do |row|
17
+ label = workbook.cell(row, 1)
18
+ value = workbook.cell(row, 2)
19
+
20
+ next if label.nil? || value.nil?
21
+
22
+ labels << label.to_s
23
+ values << value.to_f
24
+ end
25
+
26
+ {
27
+ labels: labels,
28
+ values: values,
29
+ type: options[:type] || :bar
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # lib/ruby_charts/loaders/yaml_loader.rb
2
+
3
+ module RubyCharts
4
+ module Loaders
5
+ class YAMLLoader
6
+ def self.load(filepath, options = {})
7
+ raise "File not found: #{filepath}" unless File.exist?(filepath)
8
+
9
+ data = YAML.safe_load_file(filepath)
10
+
11
+ if data.is_a?(Hash) && data['data']
12
+ data = data['data']
13
+ end
14
+
15
+ labels = []
16
+ values = []
17
+
18
+ if data.is_a?(Array)
19
+ data.each do |item|
20
+ if item.is_a?(Hash)
21
+ labels << item['name'] || item['label']
22
+ values << (item['value'] || item['amount']).to_f
23
+ end
24
+ end
25
+ elsif data.is_a?(Hash)
26
+ data.each do |key, value|
27
+ labels << key
28
+ values << value.to_f
29
+ end
30
+ end
31
+
32
+ {
33
+ labels: labels,
34
+ values: values,
35
+ type: options[:type] || :bar
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # lib/ruby_charts/version.rb
2
+
3
+ module RubyCharts
4
+ VERSION = '0.1.0'
5
+ end
data/lib/ruby_charts.rb CHANGED
@@ -1,90 +1,39 @@
1
- # app/lib/ruby_charts.rb
1
+ # lib/ruby_charts.rb
2
+
3
+ require 'gd'
4
+ require 'csv'
5
+ require 'yaml'
6
+ require 'roo'
7
+
8
+ require_relative 'ruby_charts/version'
9
+ require_relative 'ruby_charts/loaders/csv_loader'
10
+ require_relative 'ruby_charts/loaders/xlsx_loader'
11
+ require_relative 'ruby_charts/loaders/yaml_loader'
12
+ require_relative 'ruby_charts/charts/base_chart'
13
+ require_relative 'ruby_charts/charts/pie_chart'
14
+ require_relative 'ruby_charts/charts/bar_chart'
15
+ require_relative 'ruby_charts/charts/line_chart'
16
+ require_relative 'ruby_charts/builder'
2
17
 
3
18
  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
19
+ class Error < StandardError; end
20
+
21
+ def self.from_csv(filepath, options = {})
22
+ data = Loaders::CSVLoader.load(filepath, options)
23
+ Builder.new(data)
24
+ end
25
+
26
+ def self.from_xlsx(filepath, options = {})
27
+ data = Loaders::XLSXLoader.load(filepath, options)
28
+ Builder.new(data)
29
+ end
30
+
31
+ def self.from_yaml(filepath, options = {})
32
+ data = Loaders::YAMLLoader.load(filepath, options)
33
+ Builder.new(data)
34
+ end
35
+
36
+ def self.from_hash(data_hash)
37
+ Builder.new(data_hash)
89
38
  end
90
39
  end
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-charts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1a
5
5
  platform: ruby
6
6
  authors:
7
7
  - Germán Silva
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.3.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: roo
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -37,19 +51,42 @@ dependencies:
37
51
  - - ">="
38
52
  - !ruby/object:Gem::Version
39
53
  version: 2.10.0
40
- description: Generate publication-ready pie, bar, and line charts from data files
54
+ - !ruby/object:Gem::Dependency
55
+ name: yaml
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Generate pie, bar, and line charts from CSV/XLSX/YAML
41
69
  email:
42
70
  - ggerman@gmail.com
43
71
  executables: []
44
72
  extensions: []
45
73
  extra_rdoc_files: []
46
74
  files:
75
+ - LICENSE
76
+ - README.md
47
77
  - lib/ruby_charts.rb
78
+ - lib/ruby_charts/builder.rb
48
79
  - lib/ruby_charts/charts/bar_chart.rb
49
80
  - lib/ruby_charts/charts/base_chart.rb
50
81
  - lib/ruby_charts/charts/line_chart.rb
51
82
  - lib/ruby_charts/charts/pie_chart.rb
83
+ - lib/ruby_charts/config.rb
52
84
  - lib/ruby_charts/data_loader.rb
85
+ - lib/ruby_charts/loaders/csv_loader.rb
86
+ - lib/ruby_charts/loaders/xlsx_loader.rb
87
+ - lib/ruby_charts/loaders/yaml_loader.rb
88
+ - lib/ruby_charts/version.rb
89
+ - spec/ruby_charts_spec.rb
53
90
  homepage: https://github.com/ggerman/ruby-charts
54
91
  licenses:
55
92
  - MIT
@@ -68,7 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
105
  - !ruby/object:Gem::Version
69
106
  version: '0'
70
107
  requirements: []
71
- rubygems_version: 4.0.3
108
+ rubygems_version: 4.0.11
72
109
  specification_version: 4
73
- summary: Professional charts from CSV/XLSX/YAML using ruby-libgd
74
- test_files: []
110
+ summary: Professional charts from data files using ruby-libgd
111
+ test_files:
112
+ - spec/ruby_charts_spec.rb