chart_fun 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: 51d64455dae1b9a4e9f07726a206a085c165bdfbabbccee76edd7d6b10753f6d
4
+ data.tar.gz: 16b5e4f0feb4d5a36e6595d2e77a04c71984a8349bac378d5f2c33dd7d6fab86
5
+ SHA512:
6
+ metadata.gz: 49095ffe16fcdb7d464fe1f4990dab6c3bed4f48d85d95a149e6e16e0cab2396ebb01767902827275df0ec8afdceeb5313ecd0bd4ee634e0d83965c5a44c77e5
7
+ data.tar.gz: c8734a4850a64172a20e3262fffcf5f6a8487343ac6b4638ed33ecc35d46eb3693637f1cb16342bd37d64e95782dabc596a58ad55fbc063a2004ac1ff18479cc
data/chart_fun.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "erubi"
2
+ require 'date'
3
+ require_relative 'src/plot'
4
+ require_relative 'src/data_value'
5
+ require_relative 'src/axis'
data/src/axis.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Axis
4
+ attr_reader :title, :start_point, :end_point, :grid_lines
5
+
6
+ def initialize(min, max, start_point, end_point, count, title, grid_lines)
7
+ @min = min
8
+ @max = max
9
+ @start_point = start_point
10
+ @end_point = end_point
11
+ @type = min.type
12
+ @count = count
13
+ @title = title
14
+ @grid_lines = grid_lines
15
+ end
16
+
17
+ def labels
18
+ (0...@count).map do |i|
19
+ value = interpolate(i, 0, @count - 1, @min.value, @max.value)
20
+ {
21
+ text: label_string(value),
22
+ x: perpendicular[0] * 25 + interpolate(i, 0, @count - 1, @start_point[:x], @end_point[:x]),
23
+ y: perpendicular[1] * 25 + interpolate(i, 0, @count - 1, @start_point[:y], @end_point[:y])
24
+ }
25
+ end
26
+ end
27
+
28
+ def label_string(x)
29
+ case @type
30
+ when :date
31
+ Time.at(x).strftime("%Y-%m-%d")
32
+ else
33
+ x.to_s
34
+ end
35
+ end
36
+
37
+ def title_pos
38
+ {
39
+ x: perpendicular[0] * 75 + (@start_point[:x] + (@end_point[:x] - @start_point[:x]) / 2),
40
+ y: perpendicular[1] * 75 + (@start_point[:y] + (@end_point[:y] - @start_point[:y]) / 2)
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def perpendicular
47
+ direction = [@start_point[:x] - @end_point[:x], @start_point[:y] - @end_point[:y]]
48
+ perpendicular = [direction[1], -direction[0]]
49
+ perpendicular.map { |i| i / Math.sqrt(perpendicular[0] ** 2 + perpendicular[1] ** 2) }
50
+ end
51
+
52
+ def interpolate(value, from_low, from_high, to_low, to_high)
53
+ (value - from_low) * (to_high - to_low) / (from_high - from_low) + to_low
54
+ end
55
+ end
data/src/chart.svg.erb ADDED
@@ -0,0 +1,60 @@
1
+ <svg height="<%= @height %>" width="<%= @width %>" xmlns="http://www.w3.org/2000/svg" font-family="Arial" style="background-color: #333333">
2
+ <text x="<%= @width / 2 %>" y="50" fill="white" dominant-baseline="middle" text-anchor="middle" font-size="40">
3
+ <%= @title %>
4
+ </text>
5
+
6
+ <% [@x_axis, @y_axis].compact.each do |axis| %>
7
+ <line x1="<%= axis.start_point[:x] %>" y1="<%= axis.start_point[:y] %>" x2="<%= axis.end_point[:x] %>" y2="<%= axis.end_point[:y] %>" style="stroke:white;stroke-width:2"/>
8
+
9
+ <% axis.labels.each do |label| %>
10
+ <text x="<%= label[:x] %>" y="<%= label[:y] %>" fill="white" dominant-baseline="middle" text-anchor="middle">
11
+ <%= label[:text] %>
12
+ </text>
13
+ <% end %>
14
+ <% end %>
15
+
16
+ <% if @x_axis&.title %>
17
+ <text x="<%= @x_axis.title_pos[:x] %>" y="<%= @x_axis.title_pos[:y] %>" fill="white" dominant-baseline="middle" text-anchor="middle" font-size="30">
18
+ <%= @x_axis.title %>
19
+ </text>
20
+ <% end %>
21
+
22
+ <% if @y_axis&.title %>
23
+ <text x="<%= @y_axis.title_pos[:x] %>" y="<%= @y_axis.title_pos[:y] %>" fill="white" dominant-baseline="middle" text-anchor="middle" font-size="30" transform="rotate(-90 <%= @y_axis.title_pos[:x] %> <%= @y_axis.title_pos[:y] %>)">
24
+ <%= @y_axis.title %>
25
+ </text>
26
+ <% end %>
27
+
28
+ <% if @x_axis&.grid_lines %>
29
+ <% @x_axis.labels.each do |label| %>
30
+ <line x1="<%= label[:x] %>" y1="<%= plot_area[:bottom] %>" x2="<%= label[:x] %>" y2="<%= plot_area[:bottom] + 10 %>" style="stroke:white;stroke-width:2"/>
31
+ <line x1="<%= label[:x] %>" y1="<%= plot_area[:top] %>" x2="<%= label[:x] %>" y2="<%= plot_area[:bottom] %>" style="stroke:white;stroke-width:1"/>
32
+ <% end %>
33
+ <% end %>
34
+
35
+ <% if @y_axis&.grid_lines %>
36
+ <% @y_axis.labels.each do |label| %>
37
+ <line x1="<%= plot_area[:left] %>" y1="<%= label[:y] %>" x2="<%= plot_area[:left] - 10 %>" y2="<%= label[:y] %>" style="stroke:white;stroke-width:2"/>
38
+ <line x1="<%= plot_area[:left] %>" y1="<%= label[:y] %>" x2="<%= plot_area[:right] %>" y2="<%= label[:y] %>" style="stroke:white;stroke-width:1"/>
39
+ <% end %>
40
+ <% end %>
41
+
42
+ <% if @scatter %>
43
+ <% point_positions.each do |x, y| %>
44
+ <circle cx="<%= x %>" cy="<%= y %>" r="3" fill="white"/>
45
+ <% end %>
46
+ <% end %>
47
+
48
+ <% if @line %>
49
+ <% point_positions.each_cons(2) do |(x1, y1), (x2, y2)| %>
50
+ <line x1="<%= x1 %>" y1="<%= y1 %>" x2="<%= x2 %>" y2="<%= y2 %>" style="stroke:white;stroke-width:2"/>
51
+ <% end %>
52
+ <% end %>
53
+
54
+ <% if @trend_line %>
55
+ <mask id="plot_area">
56
+ <rect x="<%= plot_area[:left] %>" y="<%= plot_area[:top] %>" width="<%= plot_area[:width] %>" height="<%= plot_area[:height] %>" fill="white"/>
57
+ </mask>
58
+ <line x1="<%= trend_line_points[0][0] %>" y1="<%= trend_line_points[0][1] %>" x2="<%= trend_line_points[1][0] %>" y2="<%= trend_line_points[1][1] %>" style="stroke:white;stroke-width:2" mask="url(#plot_area)"/>
59
+ <% end %>
60
+ </svg>
data/src/data_value.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DataValue
4
+ attr_reader :type
5
+
6
+ def initialize(value)
7
+ @value = value
8
+ @type = if date_time?(value)
9
+ :date
10
+ elsif Integer(value)
11
+ :integer
12
+ else
13
+ :string
14
+ end
15
+ end
16
+
17
+ def value
18
+ case @type
19
+ when :date
20
+ DateTime.parse(@value).to_time.to_i
21
+ else
22
+ @value.to_i
23
+ end
24
+ end
25
+
26
+ def to_s
27
+ @value.to_s
28
+ end
29
+
30
+ private
31
+
32
+ def date_time?(val)
33
+ DateTime.parse(val)
34
+ true
35
+ rescue Date::Error
36
+ false
37
+ end
38
+ end
data/src/plot.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Plot
4
+ def initialize(data, height: 1000, width: 1000)
5
+ @height = height
6
+ @width = width
7
+ @data = data.map { |line| [DataValue.new(line[0]), DataValue.new(line[1])] }
8
+ end
9
+
10
+ def scatter
11
+ @scatter = true
12
+ self
13
+ end
14
+
15
+ def line
16
+ @line = true
17
+ self
18
+ end
19
+
20
+ def title(title)
21
+ @title = title
22
+ self
23
+ end
24
+
25
+ def x_axis(title: nil, count: 10, grid_lines: true)
26
+ @x_axis_options = { title: title, count: count, grid_lines: grid_lines }
27
+ self
28
+ end
29
+
30
+ def y_axis(title: nil, count: 10, grid_lines: true)
31
+ @y_axis_options = { title: title, count: count, grid_lines: grid_lines }
32
+ self
33
+ end
34
+
35
+ def trend_line
36
+ @trend_line = true
37
+ self
38
+ end
39
+
40
+ def to_svg
41
+ @x_axis = Axis.new(
42
+ min_x,
43
+ max_x,
44
+ { x: plot_area[:left], y: plot_area[:bottom] },
45
+ { x: plot_area[:right], y: plot_area[:bottom] },
46
+ @x_axis_options[:count],
47
+ @x_axis_options[:title],
48
+ @x_axis_options[:grid_lines]
49
+ )
50
+
51
+ @y_axis = Axis.new(
52
+ max_y,
53
+ min_y,
54
+ { x: plot_area[:left], y: plot_area[:top] },
55
+ { x: plot_area[:left], y: plot_area[:bottom] },
56
+ @y_axis_options[:count],
57
+ @y_axis_options[:title],
58
+ @y_axis_options[:grid_lines]
59
+ )
60
+
61
+ template = File.read(File.join(__dir__, 'chart.svg.erb'))
62
+ eval(Erubi::Engine.new(template).src)
63
+ end
64
+
65
+ def trend_line_points
66
+ x_values = point_positions.map(&:first)
67
+ y_values = point_positions.map(&:last)
68
+ x_mean = x_values.sum / x_values.size.to_f
69
+ y_mean = y_values.sum / y_values.size.to_f
70
+ slope = x_values.zip(y_values).map { |x, y| (x - x_mean) * (y - y_mean) }.sum / x_values.map { |x| (x - x_mean) ** 2 }.sum
71
+ intercept = y_mean - slope * x_mean
72
+ [
73
+ [plot_area[:left], slope * plot_area[:left] + intercept],
74
+ [plot_area[:right], slope * plot_area[:right] + intercept]
75
+ ]
76
+ end
77
+
78
+ private
79
+
80
+ def plot_area
81
+ top = @title ? 100 : 50
82
+ left = @y_axis_options&.key?(:title) ? 100 : 50
83
+ right = @width - 50
84
+ bottom = @x_axis_options&.key?(:title) ? @height - 100 : @height - 50
85
+ width = right - left
86
+ height = bottom - top
87
+ { top:, left:, right:, bottom:, width:, height: }
88
+ end
89
+
90
+ def point_positions
91
+ @data.map do |x, y|
92
+ [
93
+ interpolate(x.value, min_x.value, max_x.value, plot_area[:left], plot_area[:right]),
94
+ interpolate(y.value, min_y.value, max_y.value, plot_area[:bottom], plot_area[:top])
95
+ ]
96
+ end.sort_by(&:first)
97
+ end
98
+
99
+ def interpolate(value, fromLow, fromHigh, toLow, toHigh)
100
+ (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow
101
+ end
102
+
103
+ def x_values = @data.map(&:first)
104
+
105
+ def min_x = x_values.min_by(&:value)
106
+
107
+ def max_x = x_values.max_by(&:value)
108
+
109
+ def y_values = @data.map(&:last)
110
+
111
+ def min_y = y_values.min_by(&:value)
112
+
113
+ def max_y = y_values.max_by(&:value)
114
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chart_fun
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Max Hatfull
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: erubi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: A simple plotting library for Ruby
28
+ email:
29
+ - max.hatfull@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - chart_fun.rb
35
+ - src/axis.rb
36
+ - src/chart.svg.erb
37
+ - src/data_value.rb
38
+ - src/plot.rb
39
+ homepage: https://github.com/MaxHatfull/chart_fun
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.16
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: A simple plotting library for Ruby
62
+ test_files: []