tchart 1.0.2
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/bin/tchart +5 -0
- data/lib/tchart.rb +27 -0
- data/lib/tchart/lang/tchart_error.rb +9 -0
- data/lib/tchart/model/bar.rb +24 -0
- data/lib/tchart/model/chart.rb +25 -0
- data/lib/tchart/model/command_line_args.rb +18 -0
- data/lib/tchart/model/coordinate.rb +29 -0
- data/lib/tchart/model/grid_line.rb +25 -0
- data/lib/tchart/model/label.rb +43 -0
- data/lib/tchart/model/layout.rb +85 -0
- data/lib/tchart/model/separator.rb +28 -0
- data/lib/tchart/model/settings.rb +68 -0
- data/lib/tchart/model/y_item.rb +44 -0
- data/lib/tchart/process/chart_builder.rb +63 -0
- data/lib/tchart/process/command_line_parser.rb +73 -0
- data/lib/tchart/process/data_parser.rb +76 -0
- data/lib/tchart/process/data_reader.rb +14 -0
- data/lib/tchart/process/items_parser.rb +156 -0
- data/lib/tchart/process/layout_builder.rb +106 -0
- data/lib/tchart/process/settings_parser.rb +63 -0
- data/lib/tchart/process/tex_builder.rb +85 -0
- data/lib/tchart/process/tex_writer.rb +13 -0
- data/lib/tchart/version.rb +3 -0
- data/test/integration_test.rb +82 -0
- data/test/tchart/model/bar_test.rb +17 -0
- data/test/tchart/model/coordinate_test.rb +10 -0
- data/test/tchart/model/grid_line_test.rb +17 -0
- data/test/tchart/model/label_test.rb +31 -0
- data/test/tchart/model/layout_test.rb +17 -0
- data/test/tchart/model/separator_test.rb +19 -0
- data/test/tchart/model/settings_test.rb +48 -0
- data/test/tchart/model/y_item_test.rb +24 -0
- data/test/tchart/process/chart_builder_test.rb +33 -0
- data/test/tchart/process/command_line_parser_test.rb +89 -0
- data/test/tchart/process/data_parser_test.rb +60 -0
- data/test/tchart/process/data_reader_test.rb +23 -0
- data/test/tchart/process/items_parser_test.rb +154 -0
- data/test/tchart/process/layout_builder_test.rb +189 -0
- data/test/tchart/process/settings_parser_test.rb +75 -0
- data/test/tchart/process/tex_builder_test.rb +120 -0
- data/test/tchart/process/tex_writer_test.rb +38 -0
- data/test/tchart_test.rb +47 -0
- metadata +154 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module TChart
|
2
|
+
|
3
|
+
#
|
4
|
+
# Responsible for constructing a Chart from a Layout and a collection
|
5
|
+
# of items (Separators and YItems).
|
6
|
+
#
|
7
|
+
class ChartBuilder
|
8
|
+
|
9
|
+
def self.build(layout, items) # => Chart
|
10
|
+
ChartBuilder.new(layout, items).build
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(layout, items)
|
14
|
+
@layout = layout
|
15
|
+
@items = items
|
16
|
+
@elements = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def build # => Chart
|
20
|
+
build_frame
|
21
|
+
build_x_items
|
22
|
+
build_y_items
|
23
|
+
Chart.new(@elements)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build_frame
|
29
|
+
@elements << new_frame_top
|
30
|
+
@elements << new_frame_bottom
|
31
|
+
end
|
32
|
+
|
33
|
+
def new_frame_top # => GridLine
|
34
|
+
GridLine.new(xy(0, @layout.y_axis_length), xy(@layout.x_axis_length, @layout.y_axis_length))
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_frame_bottom # => GridLine
|
38
|
+
GridLine.new(xy(0, 0), xy(@layout.x_axis_length, 0))
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_x_items
|
42
|
+
@layout.x_axis_tick_dates.zip(@layout.x_axis_tick_x_coordinates).each do |date, x|
|
43
|
+
@elements << new_x_label(date.year, x)
|
44
|
+
@elements << new_x_gridline(x)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_x_label(year, x) # => Label
|
49
|
+
Label.build_xlabel(xy(x, @layout.x_axis_label_y_coordinate), @layout.x_axis_label_width, year.to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
def new_x_gridline(x) # => GridLine
|
53
|
+
GridLine.new(xy(x, 0), xy(x, @layout.y_axis_length))
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_y_items
|
57
|
+
@items.zip(@layout.y_axis_tick_y_coordinates).each do |item, y|
|
58
|
+
@elements.concat item.build(@layout, y)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module TChart
|
2
|
+
|
3
|
+
#
|
4
|
+
# Responsible for parsing command line options and arguments.
|
5
|
+
#
|
6
|
+
class CommandLineParser
|
7
|
+
|
8
|
+
def self.parse(argv) # => [ CommandLineArgs, [] ] or [ nil, [ String, ... ] ]
|
9
|
+
CommandLineParser.new.parse(argv)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(argv) # => [ CommandLineArgs, [] ] or [ nil, [ String, ... ] ]
|
13
|
+
parse_options(argv)
|
14
|
+
[ parse_args(argv), [] ]
|
15
|
+
rescue TChartError => e
|
16
|
+
[ nil, [ e.message ] ]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse_options(argv)
|
22
|
+
argv.map { |arg| arg.strip.downcase }.each do |arg|
|
23
|
+
case
|
24
|
+
when arg == "-h" || arg == "--help"
|
25
|
+
raise_usage
|
26
|
+
when arg == "-v" || arg == "--version"
|
27
|
+
raise_version
|
28
|
+
when arg.start_with?("-")
|
29
|
+
raise_usage
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_args(argv) # => CommandLineArgs
|
35
|
+
raise_usage unless argv.length == 2
|
36
|
+
data_filename, tex_filename = argv
|
37
|
+
raise_data_filename_not_found(data_filename) unless File.exists?(data_filename)
|
38
|
+
raise_data_filename_not_a_file(data_filename) unless File.file?(data_filename)
|
39
|
+
raise_tex_filename_not_a_file(tex_filename) if File.exists?(tex_filename) && ! File.file?(tex_filename)
|
40
|
+
raise_same_filename(data_filename, tex_filename) if same_file?(data_filename, tex_filename)
|
41
|
+
CommandLineArgs.new(data_filename, tex_filename)
|
42
|
+
end
|
43
|
+
|
44
|
+
def same_file?(filename1, filename2)
|
45
|
+
File.expand_path(filename1) == File.expand_path(filename2)
|
46
|
+
end
|
47
|
+
|
48
|
+
def raise_version
|
49
|
+
raise TChartError, TChart::Version
|
50
|
+
end
|
51
|
+
|
52
|
+
def raise_usage
|
53
|
+
raise TChartError, "Usage: tchart [ --version | --help | input-data-filename output-tikz-filename ]"
|
54
|
+
end
|
55
|
+
|
56
|
+
def raise_data_filename_not_found(data_filename)
|
57
|
+
raise TChartError, "Error: input data file \"#{data_filename}\" not found."
|
58
|
+
end
|
59
|
+
|
60
|
+
def raise_data_filename_not_a_file(data_filename)
|
61
|
+
raise TChartError, "Error: input data file \"#{data_filename}\" is not a file."
|
62
|
+
end
|
63
|
+
|
64
|
+
def raise_tex_filename_not_a_file(tex_filename)
|
65
|
+
raise TChartError, "Error: existing output data file \"#{tex_filename}\" is not a file."
|
66
|
+
end
|
67
|
+
|
68
|
+
def raise_same_filename(data_filename, tex_filename)
|
69
|
+
raise TChartError, "Error: input \"#{data_filename}\" and output \"#{tex_filename}\" refer to the same file."
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TChart
|
2
|
+
|
3
|
+
#
|
4
|
+
# Responsible for parsing source data. Not responsible for
|
5
|
+
# aquiring the source data, e.g. not responsible for reading
|
6
|
+
# the source data from an input file.
|
7
|
+
#
|
8
|
+
class DataParser
|
9
|
+
|
10
|
+
#
|
11
|
+
# source_name is used in error messages; for example, if the
|
12
|
+
# source data was read from a file, then source_name would be
|
13
|
+
# the name of that file.
|
14
|
+
#
|
15
|
+
def self.parse(source_name, source_data) # => [ settings, items, errors ]
|
16
|
+
DataParser.new(source_name, source_data).parse
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(source_name, source_data)
|
20
|
+
@source_name = source_name
|
21
|
+
@source_data = source_data
|
22
|
+
@line_number = nil
|
23
|
+
@errors = []
|
24
|
+
@settings_parser = SettingsParser.new
|
25
|
+
@items_parser = ItemsParser.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse # => [ settings, items, errors ]
|
29
|
+
non_blank_source_lines.each { |line| parse_line(line) }
|
30
|
+
check_for_items if @errors.empty?
|
31
|
+
[ @settings_parser.settings, @items_parser.items, @errors ]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
#
|
37
|
+
# Return source data lines that are not empty after
|
38
|
+
# comments have been removed.
|
39
|
+
#
|
40
|
+
def non_blank_source_lines # => Enumerator
|
41
|
+
Enumerator.new do |yielder|
|
42
|
+
@source_data.each_with_index do |line, index|
|
43
|
+
@line_number = index + 1
|
44
|
+
line = remove_comments(line).strip
|
45
|
+
yielder.yield line if line.length > 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# "item # A comment." => "item "
|
52
|
+
# "# A comment." => ""
|
53
|
+
# "C\# # A coment." => "C\# "
|
54
|
+
#
|
55
|
+
def remove_comments(line) # => line
|
56
|
+
line.sub(/(?<!\\)#.*$/, '')
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_line(line)
|
60
|
+
unless @settings_parser.parse?(line)
|
61
|
+
@items_parser.parse(line)
|
62
|
+
end
|
63
|
+
rescue TChartError => e
|
64
|
+
save_error "#{@source_name}, #{@line_number}: #{e.message}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_for_items
|
68
|
+
save_error "#{@source_name}: no items found" if @items_parser.items.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def save_error(message)
|
72
|
+
@errors << message
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TChart
|
2
|
+
|
3
|
+
#
|
4
|
+
# Responsible for reading source data from an input file
|
5
|
+
# and parsing it.
|
6
|
+
#
|
7
|
+
module DataReader
|
8
|
+
|
9
|
+
def self.read(filename) # => [ settings, items, errors ]
|
10
|
+
File.open(filename) { |f| DataParser.parse(filename, f) }
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module TChart
|
4
|
+
|
5
|
+
#
|
6
|
+
# Responsible for parsing a line of source data that contains either
|
7
|
+
# a separator or a data item. Also responsible for accumulating
|
8
|
+
# parsed items.
|
9
|
+
#
|
10
|
+
class ItemsParser
|
11
|
+
|
12
|
+
#
|
13
|
+
# The collection of parsed items, where an item is either an instance
|
14
|
+
# of YItem or of Separator. Starts empty and fills up with each
|
15
|
+
# successive call to #parse.
|
16
|
+
#
|
17
|
+
attr_reader :items
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@items = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse(line)
|
24
|
+
description, style, *date_range_strings = extract_fields(line)
|
25
|
+
raise_description_missing if description.nil?
|
26
|
+
raise_style_missing if style.nil? && date_range_strings.length > 0
|
27
|
+
if description.start_with?("---")
|
28
|
+
parse_separator
|
29
|
+
else
|
30
|
+
parse_y_item(description, style, date_range_strings)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse_y_item(description, style, date_range_strings)
|
37
|
+
date_ranges = parse_date_ranges(date_range_strings)
|
38
|
+
check_for_overlaps(date_ranges)
|
39
|
+
save_item YItem.new(description, style, date_ranges)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_separator
|
43
|
+
save_item Separator.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def save_item(item)
|
47
|
+
@items << item
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# 'a|b\|c|d | | e | \n' => [ 'a', 'b|c', 'd', nil, 'e' ]
|
52
|
+
#
|
53
|
+
def extract_fields(line) # => [ String, ... ]
|
54
|
+
line # => 'a|b\|c|d | | e | ' + "\n"
|
55
|
+
.sub( /(\s|\|)+$/, '' ) # => 'a|b\|c|d | | e'
|
56
|
+
.split( /(?<!\\)\|/ ) # => [ 'a', 'b\|c', 'd ', ' ', ' e' ]
|
57
|
+
.map { |field| remove_escapes(field) } # => [ 'a', 'b|c', 'd', ' ', 'e' ]
|
58
|
+
.map { |field| field.strip } # => [ 'a', 'b|c', 'd', '', 'e' ]
|
59
|
+
.map { |field| field.empty? ? nil : field } # => [ 'a', 'b|c', 'd', nil, 'e' ]
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# '\a\\b' => 'a\b'
|
64
|
+
#
|
65
|
+
def remove_escapes(line) # => String
|
66
|
+
line.gsub(/\\(.)/, '\1')
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_date_ranges(date_range_strings) # => [ Date..Date, ... ]
|
70
|
+
date_range_strings.map { |date_range_string| parse_date_range(date_range_string) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_date_range(date_range_string) # => Date..Date
|
74
|
+
y1, m1, d1, y2, m2, d2 = fill_in_missing_date_elements( * match_date_range(date_range_string) )
|
75
|
+
date_begin = build_date(y1.to_i, m1.to_i, d1.to_i)
|
76
|
+
date_end = build_date(y2.to_i, m2.to_i, d2.to_i)
|
77
|
+
raise_date_range_reversed(date_begin, date_end) if date_end < date_begin
|
78
|
+
date_begin..date_end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_date(year, month, day) # => Date
|
82
|
+
Date.new(year, month, day)
|
83
|
+
rescue ArgumentError => e
|
84
|
+
raise_invalid_date(year, month, day, e.message)
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Matches d, d-d, or d - d, where d can be: y, y.m, or y.m.d
|
89
|
+
# Examples: 2000, 2000-2001, 2000-2001.8, 2000.4.17 - 2001
|
90
|
+
#
|
91
|
+
def match_date_range(date_range_string) # => y1, m1, d1, y2, m2, d2
|
92
|
+
m = /^(\d+)(\.(\d+)(\.(\d+))?)?(\s*-\s*(\d+)(\.(\d+)(\.(\d+))?)?)?$/.match(date_range_string)
|
93
|
+
raise_invalid_date_range(date_range_string) if not m
|
94
|
+
[ m[1], m[3], m[5], m[7], m[9], m[11] ]
|
95
|
+
end
|
96
|
+
|
97
|
+
def fill_in_missing_date_elements(y1, m1, d1, y2, m2, d2) # => y1, m1, d1, y2, m2, d2
|
98
|
+
[ y1, m1 || 1, d1 || 1,
|
99
|
+
y2 || y1, m2 || 12, d2 || -1 ]
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_for_overlaps(date_ranges)
|
103
|
+
first, *rest = date_ranges
|
104
|
+
return if rest.empty?
|
105
|
+
rest.each { |range| raise_date_ranges_overlap(first, range) if overlap?(first, range) }
|
106
|
+
check_for_overlaps(rest)
|
107
|
+
end
|
108
|
+
|
109
|
+
def overlap?(range1, range2)
|
110
|
+
range1.include?(range2.first) or range2.include?(range1.first)
|
111
|
+
end
|
112
|
+
|
113
|
+
def raise_description_missing
|
114
|
+
raise TChartError, "description is missing"
|
115
|
+
end
|
116
|
+
|
117
|
+
def raise_style_missing
|
118
|
+
raise TChartError, "style is missing"
|
119
|
+
end
|
120
|
+
|
121
|
+
def raise_invalid_date(year, month, day, message)
|
122
|
+
raise TChartError, "#{year}.#{month}.#{day}: #{message}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def raise_invalid_date_range(date_range_as_string)
|
126
|
+
raise TChartError, "bad date range \"#{date_range_as_string}\"; expecting 2000.4.17-2001.7.21 | 2002.4-2003, etc."
|
127
|
+
end
|
128
|
+
|
129
|
+
def raise_date_range_reversed(date_begin, date_end)
|
130
|
+
raise TChartError, "date range end #{d2s(date_end)} before start #{d2s(date_begin)}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def raise_date_ranges_overlap(range1, range2)
|
134
|
+
raise TChartError, "date range #{dr2s(range1)} overlaps #{dr2s(range2)}"
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# 'd2s' means 'date to string'
|
139
|
+
#
|
140
|
+
# 2001.3.17 => "2001.3.17"
|
141
|
+
#
|
142
|
+
def d2s(date) # => String
|
143
|
+
date.strftime('%Y.%-m.%-d')
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# 'dr2s' means 'date range to string'
|
148
|
+
#
|
149
|
+
# 2003.3.17..2007.11.2 => "2003.3.17-2007.11.2"
|
150
|
+
#
|
151
|
+
def dr2s(date_range) # => String
|
152
|
+
"#{d2s(date_range.begin)}-#{d2s(date_range.end)}"
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module TChart
|
2
|
+
|
3
|
+
#
|
4
|
+
# Responsible for calculating chart metrics and storing them
|
5
|
+
# in an instance of Layout. Metrics depend on chart settings
|
6
|
+
# and the number and dates ranges of the items being plotted.
|
7
|
+
#
|
8
|
+
class LayoutBuilder
|
9
|
+
|
10
|
+
def self.build(settings, items) # => [ layout, errors ]
|
11
|
+
layout = build_layout(settings, items)
|
12
|
+
errors = check_layout(layout)
|
13
|
+
[ layout, errors ]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def self.build_layout(settings, items) # => Layout
|
19
|
+
layout = Layout.new
|
20
|
+
layout.x_axis_tick_dates = calc_x_axis_tick_dates( *calc_items_date_range(items) )
|
21
|
+
layout.x_axis_length = calc_x_axis_length(settings)
|
22
|
+
layout.x_axis_tick_x_coordinates = calc_x_axis_tick_x_coordinates(layout.x_axis_tick_dates, layout.x_axis_length)
|
23
|
+
layout.y_axis_length = calc_y_axis_length(settings, items)
|
24
|
+
layout.y_axis_label_x_coordinate = calc_y_axis_label_x_coordinate(settings)
|
25
|
+
layout.y_axis_tick_y_coordinates = calc_y_axis_tick_y_coordinates(settings, items)
|
26
|
+
layout.x_axis_label_y_coordinate = settings.x_axis_label_y_coordinate
|
27
|
+
layout.x_axis_label_width = settings.x_axis_label_width
|
28
|
+
layout.y_axis_label_width = settings.y_axis_label_width
|
29
|
+
layout
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.check_layout(layout) # => [ String, String, ... ]
|
33
|
+
errors = []
|
34
|
+
errors << plot_area_too_narrow_error(layout.x_axis_length) if layout.x_axis_length < 1
|
35
|
+
errors
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.plot_area_too_narrow_error(x_axis_length)
|
39
|
+
"plot area is too narrow (#{x_axis_length}, min is 1); is chart_width too small, or x_axis_label_width or y_axis_label_width too large?"
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.calc_items_date_range(items) # [ Date, Date ]
|
43
|
+
earliest, latest = nil, nil
|
44
|
+
items.each do |item|
|
45
|
+
item.date_ranges.each do |date_range|
|
46
|
+
earliest = date_range.begin if earliest.nil? or date_range.begin < earliest
|
47
|
+
latest = date_range.end if latest.nil? or latest < date_range.end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
current_year = Date.today.year
|
51
|
+
earliest ||= Date.new(current_year, 1, 1)
|
52
|
+
latest ||= Date.new(current_year, 12, 31)
|
53
|
+
[earliest, latest]
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.calc_x_axis_tick_dates(earliest, latest) # => [ Date, Date, ... ]
|
57
|
+
# if ten or fewer years to cover, create a tick every year
|
58
|
+
from_year = earliest.year # round down to Jan 1st of year
|
59
|
+
to_year = latest.year + 1 # +1 to round up to Jan 1st of the following year
|
60
|
+
return make_tick_dates(from_year, to_year, 1) if to_year - from_year <= 10
|
61
|
+
|
62
|
+
# if fifty or fewer years to cover, create a tick every five years
|
63
|
+
from_year = (from_year / 5.0).floor * 5 # round down to nearest 1/2 decade
|
64
|
+
to_year = (to_year / 5.0).ceil * 5 # round up to nearest 1/2 decade
|
65
|
+
return make_tick_dates(from_year, to_year, 5) if to_year - from_year <= 50
|
66
|
+
|
67
|
+
# create a tick every ten years
|
68
|
+
from_year = (from_year / 10.0).floor * 10 # round down to nearest decade
|
69
|
+
to_year = (to_year / 10.0).ceil * 10 # round up to nearest decade
|
70
|
+
return make_tick_dates(from_year, to_year, 10)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.make_tick_dates(from_year, to_year, interval) # => [ Date, Date, ... ]
|
74
|
+
(from_year..to_year).step(interval).map { |year| Date.new(year,1,1) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.calc_x_axis_tick_x_coordinates(x_axis_tick_dates, x_axis_length) # => [ Numeric, Numeric, ... ]
|
78
|
+
num_coords = x_axis_tick_dates.size
|
79
|
+
x_interval = x_axis_length / (num_coords - 1.0)
|
80
|
+
(0..x_axis_length).step(x_interval).to_a
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# 1/2 x_axis_label_width for the left margin, and 1/2 x_axis_label_width for the right margin
|
85
|
+
#
|
86
|
+
def self.calc_x_axis_length(settings) # => Numeric
|
87
|
+
settings.chart_width - settings.y_axis_label_width - settings.x_axis_label_width
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# +1 for top and bottom margins, each of which is half the line height
|
92
|
+
#
|
93
|
+
def self.calc_y_axis_length(settings, items) # => Numeric
|
94
|
+
(items.length + 1) * settings.line_height
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.calc_y_axis_label_x_coordinate(settings) # => Numeric
|
98
|
+
0 - ((settings.y_axis_label_width / 2.0) + (settings.x_axis_label_width / 2.0))
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.calc_y_axis_tick_y_coordinates(settings, items) # => [ Numeric, Numeric, ... ]
|
102
|
+
(settings.line_height * items.length).step(settings.line_height, -settings.line_height).to_a
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|