youplot 0.3.3 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/youplot/dsv.rb CHANGED
@@ -3,41 +3,49 @@
3
3
  require 'csv'
4
4
 
5
5
  module YouPlot
6
- # Read and interpret Delimiter-separated values format file or stream.
6
+ # Module to handle DSV (Delimiter-separated values) format.
7
+ # Extract header and series.
7
8
  module DSV
8
9
  module_function
9
10
 
10
11
  def parse(input, delimiter, headers, transpose)
11
- arr = parse_as_csv(input, delimiter)
12
+ # Parse as CSV
13
+ arr = CSV.parse(input, col_sep: delimiter)
14
+
15
+ # Remove blank lines
16
+ arr.delete_if do |i|
17
+ i == [] or i.all?(&:nil?)
18
+ end
19
+
20
+ # get header
12
21
  headers = get_headers(arr, headers, transpose)
22
+
23
+ # get series
13
24
  series = get_series(arr, headers, transpose)
14
- if headers.nil?
15
- Data.new(headers, series)
16
- else
17
- if headers.include?(nil)
18
- warn "\e[35mHeaders contains nil in it.\e[0m"
19
- elsif headers.include? ''
20
- warn "\e[35mHeaders contains \"\" in it.\e[0m"
21
- end
22
- h_size = headers.size
23
- s_size = series.size
24
- if h_size == s_size
25
- Data.new(headers, series)
26
- elsif h_size > s_size
27
- warn "\e[35mThe number of headers is greater than the number of series.\e[0m"
28
- exit 1
29
- elsif h_size < s_size
30
- warn "\e[35mThe number of headers is less than the number of series.\e[0m"
31
- exit 1
32
- end
25
+
26
+ # Return if No header
27
+ return Data.new(headers, series) if headers.nil?
28
+
29
+ # Warn if header contains nil
30
+ warn "\e[35mHeaders contains nil in it.\e[0m" if headers.include?(nil)
31
+
32
+ # Warn if header contains ''
33
+ warn "\e[35mHeaders contains \"\" in it.\e[0m" if headers.include? ''
34
+
35
+ # Make sure the number of elements in the header matches the number of series.
36
+ h_size = headers.size
37
+ s_size = series.size
38
+
39
+ if h_size > s_size
40
+ warn "\e[35mThe number of headers is greater than the number of series.\e[0m"
41
+ exit 1 if YouPlot.run_as_executable?
42
+
43
+ elsif h_size < s_size
44
+ warn "\e[35mThe number of headers is less than the number of series.\e[0m"
45
+ exit 1 if YouPlot.run_as_executable?
33
46
  end
34
- end
35
47
 
36
- def parse_as_csv(input, delimiter)
37
- CSV.parse(input, col_sep: delimiter)
38
- .delete_if do |i|
39
- i == [] or i.all? nil
40
- end
48
+ Data.new(headers, series) if h_size == s_size
41
49
  end
42
50
 
43
51
  # Transpose different sized ruby arrays
@@ -47,33 +55,33 @@ module YouPlot
47
55
  end
48
56
 
49
57
  def get_headers(arr, headers, transpose)
50
- if headers
51
- if transpose
52
- arr.map(&:first)
53
- else
54
- arr[0]
55
- end
56
- end
58
+ # header(-)
59
+ return nil unless headers
60
+
61
+ # header(+) trenspose(+)
62
+ return arr.map(&:first) if transpose
63
+
64
+ # header(+) transpose(-)
65
+ arr[0]
57
66
  end
58
67
 
59
68
  def get_series(arr, headers, transpose)
60
- if headers
61
- if arr.size > 1
62
- if transpose
63
- arr.map { |row| row[1..-1] }
64
- else
65
- transpose2(arr[1..-1])
66
- end
67
- else
68
- Array.new(arr[0].size, [])
69
- end
70
- else
71
- if transpose
72
- arr
73
- else
74
- transpose2(arr)
75
- end
69
+ # header(-)
70
+ unless headers
71
+ return arr if transpose
72
+
73
+ return transpose2(arr)
76
74
  end
75
+
76
+ # header(+) but no element in the series.
77
+ # TODO: should raise error?
78
+ return Array.new(arr[0].size, []) if arr.size == 1
79
+
80
+ # header(+) transpose(+)
81
+ return arr.map { |row| row[1..-1] } if transpose
82
+
83
+ # header(+) transpose(-)
84
+ transpose2(arr[1..-1])
77
85
  end
78
86
  end
79
87
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouPlot
4
+ # Command line options that are not Plot parameters
5
+ Options = Struct.new(
6
+ :delimiter,
7
+ :transpose,
8
+ :headers,
9
+ :pass,
10
+ :output,
11
+ :fmt,
12
+ :progressive,
13
+ :encoding,
14
+ :reverse, # count
15
+ :color_names, # color
16
+ :debug
17
+ )
18
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouPlot
4
+ # UnicodePlot parameters.
5
+ # Why Struct, not Hash?
6
+ # * The keys are static in Struct.
7
+ # * Struct does not conflict with keyword arguments. Hash dose.
8
+ Parameters = Struct.new(
9
+ # Sort me!
10
+ :title,
11
+ :width,
12
+ :height,
13
+ :border,
14
+ :margin,
15
+ :padding,
16
+ :color,
17
+ :xlabel,
18
+ :ylabel,
19
+ :labels,
20
+ :symbol,
21
+ :xscale,
22
+ :nbins,
23
+ :closed,
24
+ :canvas,
25
+ :xlim,
26
+ :ylim,
27
+ :grid,
28
+ :name
29
+ ) do
30
+ def to_hc
31
+ to_h.compact
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require_relative 'options'
5
+
6
+ module YouPlot
7
+ # Class for parsing command line options
8
+ class Parser
9
+ class Error < StandardError; end
10
+
11
+ attr_reader :command, :options, :params,
12
+ :main_parser, :sub_parser
13
+
14
+ def initialize
15
+ @command = nil
16
+
17
+ @options = Options.new(
18
+ "\t", # elimiter:
19
+ false, # transpose:
20
+ nil, # headers:
21
+ false, # pass:
22
+ $stderr, # output:
23
+ 'xyy', # fmt:
24
+ false, # progressive:
25
+ nil, # encoding:
26
+ false, # color_names:
27
+ false # debug:
28
+ )
29
+
30
+ @params = Parameters.new
31
+ end
32
+
33
+ def create_base_parser
34
+ OptionParser.new do |parser|
35
+ parser.program_name = 'YouPlot'
36
+ parser.version = YouPlot::VERSION
37
+ parser.summary_width = 23
38
+ parser.on_tail('') # Add a blank line at the end
39
+ parser.separator('')
40
+ parser.on('Common options:')
41
+ parser.on('-O', '--pass [FILE]', 'file to output input data to [stdout]',
42
+ 'for inserting YouPlot in the middle of Unix pipes') do |v|
43
+ options[:pass] = v || $stdout
44
+ end
45
+ parser.on('-o', '--output [FILE]', 'file to output plots to [stdout]',
46
+ 'If no option is specified, plot will print to stderr') do |v|
47
+ options[:output] = v || $stdout
48
+ end
49
+ parser.on('-d', '--delimiter DELIM', String, 'use DELIM instead of [TAB] for field delimiter') do |v|
50
+ options[:delimiter] = v
51
+ end
52
+ parser.on('-H', '--headers', TrueClass, 'specify that the input has header row') do |v|
53
+ options[:headers] = v
54
+ end
55
+ parser.on('-T', '--transpose', TrueClass, 'transpose the axes of the input data') do |v|
56
+ options[:transpose] = v
57
+ end
58
+ parser.on('-t', '--title STR', String, 'print string on the top of plot') do |v|
59
+ params.title = v
60
+ end
61
+ parser.on('-x', '--xlabel STR', String, 'print string on the bottom of the plot') do |v|
62
+ params.xlabel = v
63
+ end
64
+ parser.on('-y', '--ylabel STR', String, 'print string on the far left of the plot') do |v|
65
+ params.ylabel = v
66
+ end
67
+ parser.on('-w', '--width INT', Numeric, 'number of characters per row') do |v|
68
+ params.width = v
69
+ end
70
+ parser.on('-h', '--height INT', Numeric, 'number of rows') do |v|
71
+ params.height = v
72
+ end
73
+ border_options = UnicodePlot::BORDER_MAP.keys.join(', ')
74
+ parser.on('-b', '--border STR', String, 'specify the style of the bounding box', "(#{border_options})") do |v|
75
+ params.border = v.to_sym
76
+ end
77
+ parser.on('-m', '--margin INT', Numeric, 'number of spaces to the left of the plot') do |v|
78
+ params.margin = v
79
+ end
80
+ parser.on('--padding INT', Numeric, 'space of the left and right of the plot') do |v|
81
+ params.padding = v
82
+ end
83
+ parser.on('-c', '--color VAL', String, 'color of the drawing') do |v|
84
+ params.color = v =~ /\A[0-9]+\z/ ? v.to_i : v.to_sym
85
+ end
86
+ parser.on('--[no-]labels', TrueClass, 'hide the labels') do |v|
87
+ params.labels = v
88
+ end
89
+ parser.on('-p', '--progress', TrueClass, 'progressive mode [experimental]') do |v|
90
+ options[:progressive] = v
91
+ end
92
+ parser.on('-C', '--color-output', TrueClass, 'colorize even if writing to a pipe') do |_v|
93
+ UnicodePlot::IOContext.define_method(:color?) { true } # FIXME
94
+ end
95
+ parser.on('-M', '--monochrome', TrueClass, 'no colouring even if writing to a tty') do |_v|
96
+ UnicodePlot::IOContext.define_method(:color?) { false } # FIXME
97
+ end
98
+ parser.on('--encoding STR', String, 'Specify the input encoding') do |v|
99
+ options[:encoding] = v
100
+ end
101
+ # Optparse adds the help option, but it doesn't show up in usage.
102
+ # This is why you need the code below.
103
+ parser.on('--help', 'print sub-command help menu') do
104
+ puts parser.help
105
+ exit if YouPlot.run_as_executable?
106
+ end
107
+ parser.on('--debug', TrueClass, 'print preprocessed data') do |v|
108
+ options[:debug] = v
109
+ end
110
+ # yield opt if block_given?
111
+ end
112
+ end
113
+
114
+ def create_main_parser
115
+ @main_parser = create_base_parser
116
+ main_parser.banner = \
117
+ <<~MSG
118
+
119
+ Program: YouPlot (Tools for plotting on the terminal)
120
+ Version: #{YouPlot::VERSION} (using UnicodePlot #{UnicodePlot::VERSION})
121
+ Source: https://github.com/red-data-tools/YouPlot
122
+
123
+ Usage: uplot <command> [options] <in.tsv>
124
+
125
+ Commands:
126
+ barplot bar draw a horizontal barplot
127
+ histogram hist draw a horizontal histogram
128
+ lineplot line draw a line chart
129
+ lineplots lines draw a line chart with multiple series
130
+ scatter s draw a scatter plot
131
+ density d draw a density plot
132
+ boxplot box draw a horizontal boxplot
133
+ colors color show the list of available colors
134
+
135
+ count c draw a baplot based on the number of
136
+ occurrences (slow)
137
+
138
+ General options:
139
+ --help print command specific help menu
140
+ --version print the version of YouPlot
141
+ MSG
142
+
143
+ # Help for the main parser is simple.
144
+ # Simply show the banner above.
145
+ main_parser.on('--help', 'print sub-command help menu') do
146
+ puts main_parser.banner
147
+ puts
148
+ exit if YouPlot.run_as_executable?
149
+ end
150
+ end
151
+
152
+ def sub_parser_add_symbol
153
+ sub_parser.on_head('--symbol STR', String, 'character to be used to plot the bars') do |v|
154
+ params.symbol = v
155
+ end
156
+ end
157
+
158
+ def sub_parser_add_xscale
159
+ xscale_options = UnicodePlot::ValueTransformer::PREDEFINED_TRANSFORM_FUNCTIONS.keys.join(', ')
160
+ sub_parser.on_head('--xscale STR', String, "axis scaling (#{xscale_options})") do |v|
161
+ params.xscale = v.to_sym
162
+ end
163
+ end
164
+
165
+ def sub_parser_add_canvas
166
+ canvas_types = UnicodePlot::Canvas::CANVAS_CLASS_MAP.keys.join(', ')
167
+ sub_parser.on_head('--canvas STR', String, 'type of canvas', "(#{canvas_types})") do |v|
168
+ params.canvas = v.to_sym
169
+ end
170
+ end
171
+
172
+ def sub_parser_add_xlim
173
+ sub_parser.on_head('--xlim FLOAT,FLOAT', Array, 'plotting range for the x coordinate') do |v|
174
+ params.xlim = v.map(&:to_f)
175
+ end
176
+ end
177
+
178
+ def sub_parser_add_ylim
179
+ sub_parser.on_head('--ylim FLOAT,FLOAT', Array, 'plotting range for the y coordinate') do |v|
180
+ params.ylim = v.map(&:to_f)
181
+ end
182
+ end
183
+
184
+ def sub_parser_add_grid
185
+ sub_parser.on_head('--[no-]grid', TrueClass, 'draws grid-lines at the origin') do |v|
186
+ params.grid = v
187
+ end
188
+ end
189
+
190
+ def sub_parser_add_fmt_xyxy
191
+ sub_parser.on_head('--fmt STR', String,
192
+ 'xyxy : header is like x1, y1, x2, y2, x3, y3...',
193
+ 'xyy : header is like x, y1, y2, y2, y3...') do |v|
194
+ options[:fmt] = v
195
+ end
196
+ end
197
+
198
+ def sub_parser_add_fmt_yx
199
+ sub_parser.on_head('--fmt STR', String,
200
+ 'xy : header is like x, y...',
201
+ 'yx : header is like y, x...') do |v|
202
+ options[:fmt] = v
203
+ end
204
+ end
205
+
206
+ def create_sub_parser
207
+ @sub_parser = create_base_parser
208
+ sub_parser.banner = \
209
+ <<~MSG
210
+
211
+ Usage: YouPlot #{command} [options] <in.tsv>
212
+
213
+ Options for #{command}:
214
+ MSG
215
+
216
+ case command
217
+
218
+ # If you type only `uplot` in the terminal.
219
+ when nil
220
+ warn main_parser.banner
221
+ warn "\n"
222
+ exit 1 if YouPlot.run_as_executable?
223
+
224
+ when :barplot, :bar
225
+ sub_parser_add_symbol
226
+ sub_parser_add_fmt_yx
227
+ sub_parser_add_xscale
228
+
229
+ when :count, :c
230
+ sub_parser.on_head('-r', '--reverse', TrueClass, 'reverse the result of comparisons') do |v|
231
+ options.reverse = v
232
+ end
233
+ sub_parser_add_symbol
234
+ sub_parser_add_xscale
235
+
236
+ when :histogram, :hist
237
+ sub_parser_add_symbol
238
+ sub_parser.on_head('--closed STR', String, 'side of the intervals to be closed [left]') do |v|
239
+ params.closed = v
240
+ end
241
+ sub_parser.on_head('-n', '--nbins INT', Numeric, 'approximate number of bins') do |v|
242
+ params.nbins = v
243
+ end
244
+
245
+ when :lineplot, :line
246
+ sub_parser_add_canvas
247
+ sub_parser_add_grid
248
+ sub_parser_add_fmt_yx
249
+ sub_parser_add_ylim
250
+ sub_parser_add_xlim
251
+
252
+ when :lineplots, :lines
253
+ sub_parser_add_canvas
254
+ sub_parser_add_grid
255
+ sub_parser_add_fmt_xyxy
256
+ sub_parser_add_ylim
257
+ sub_parser_add_xlim
258
+
259
+ when :scatter, :s
260
+ sub_parser_add_canvas
261
+ sub_parser_add_grid
262
+ sub_parser_add_fmt_xyxy
263
+ sub_parser_add_ylim
264
+ sub_parser_add_xlim
265
+
266
+ when :density, :d
267
+ sub_parser_add_canvas
268
+ sub_parser_add_grid
269
+ sub_parser_add_fmt_xyxy
270
+ sub_parser_add_ylim
271
+ sub_parser_add_xlim
272
+
273
+ when :boxplot, :box
274
+ sub_parser_add_xlim
275
+
276
+ when :colors, :color, :colours, :colour
277
+ sub_parser.on_head('-n', '--names', TrueClass, 'show color names only') do |v|
278
+ options[:color_names] = v
279
+ end
280
+
281
+ else
282
+ error_message = "uplot: unrecognized command '#{command}'"
283
+ if YouPlot.run_as_executable?
284
+ warn error_message
285
+ exit 1
286
+ else
287
+ raise Error, error_message
288
+ end
289
+ end
290
+ end
291
+
292
+ def parse_options(argv = ARGV)
293
+ begin
294
+ create_main_parser.order!(argv)
295
+ rescue OptionParser::ParseError => e
296
+ warn "uplot: #{e.message}"
297
+ exit 1 if YouPlot.run_as_executable?
298
+ end
299
+
300
+ @command = argv.shift&.to_sym
301
+
302
+ begin
303
+ create_sub_parser&.parse!(argv)
304
+ rescue OptionParser::ParseError => e
305
+ warn "uplot: #{e.message}"
306
+ exit 1 if YouPlot.run_as_executable?
307
+ end
308
+ end
309
+ end
310
+ end