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 +7 -0
- data/chart_fun.rb +5 -0
- data/src/axis.rb +55 -0
- data/src/chart.svg.erb +60 -0
- data/src/data_value.rb +38 -0
- data/src/plot.rb +114 -0
- metadata +62 -0
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
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: []
|