25 Commits

Author SHA1 Message Date
kojix2
286e90ec23 v0.2.0 2020-08-16 13:43:50 +09:00
kojix2
f3cd03196d Rubocop auto correct 2020-08-16 13:43:37 +09:00
kojix2
c4d31108bb Improved error messages2 2020-08-16 13:34:41 +09:00
kojix2
9d58b1aaf9 Improved error messages. 2020-08-16 13:20:52 +09:00
kojix2
a09a703c33 Separation of Plot and Command Modules 2020-08-16 12:46:45 +09:00
kojix2
4a14e4716a Correct the loading order. 2020-08-16 00:04:00 +09:00
kojix2
3c9f2fc9fc Update README 2020-08-15 23:47:23 +09:00
kojix2
c0f5cbc7d5 Move preprocessing.rb 2020-08-15 23:37:02 +09:00
kojix2
57efdfcc2c Fix a typo 2020-08-15 23:19:32 +09:00
kojix2
d17143f7fa Rename Preprocess -> Preprocessing 2020-08-15 23:18:33 +09:00
kojix2
c426378bda Separate the Preprocess module into new files 2020-08-15 23:15:30 +09:00
kojix2
02857a46a3 Prevent option parser from starting automatically 2020-08-15 23:06:55 +09:00
kojix2
ff7b0680aa Use Data struct 2020-08-15 22:17:44 +09:00
kojix2
e887dc3f5a Add name to Params 2020-08-15 22:13:06 +09:00
kojix2
1d1ae9e1b0 Add Preprocess module 2020-08-15 22:12:42 +09:00
kojix2
715236b5b4 Improve comments 2020-08-15 19:42:18 +09:00
kojix2
22460f3689 Add raw_input variable 2020-08-15 19:41:53 +09:00
kojix2
1548675967 Fixied stuck blocks. 2020-08-15 17:48:25 +09:00
kojix2
45fc89605c Shorten the line. It's not cool, but it's easy to maintain. 2020-08-15 17:28:51 +09:00
kojix2
ee3608a106 Use symbols as plot_type 2020-08-15 17:10:41 +09:00
kojix2
ff1176797f Use Stract as params 2020-08-15 16:46:03 +09:00
kojix2
6db2372c29 Minor style changes 2020-08-15 16:02:31 +09:00
kojix2
59f0fdddca Fix a typo 2020-08-15 16:01:43 +09:00
kojix2
3c51e0c902 refactoring... 2020-08-15 15:45:46 +09:00
kojix2
ee74b32815 Add version number 2020-08-14 13:47:04 +09:00
8 changed files with 445 additions and 220 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ko_fi: kojix2
patreon: kojix2

View File

@@ -1,4 +1,4 @@
# Uplot # uplot
[![Build Status](https://travis-ci.com/kojix2/uplot.svg?branch=master)](https://travis-ci.com/kojix2/uplot) [![Build Status](https://travis-ci.com/kojix2/uplot.svg?branch=master)](https://travis-ci.com/kojix2/uplot)
[![Gem Version](https://badge.fury.io/rb/u-plot.svg)](https://badge.fury.io/rb/u-plot) [![Gem Version](https://badge.fury.io/rb/u-plot.svg)](https://badge.fury.io/rb/u-plot)
@@ -50,6 +50,8 @@ ruby -r numo/narray -e "puts Numo::DFloat.new(1000).rand_norm.to_a" \
## Contributing ## Contributing
Bug reports and pull requests are welcome on GitHub at [https://github.com/kojix2/uplot](https://github.com/kojix2/uplot).
## License ## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

View File

@@ -2,4 +2,4 @@
require 'uplot' require 'uplot'
Uplot::Command.new(ARGV).run Uplot::Command.new.run

View File

@@ -1,6 +1,8 @@
require 'unicode_plot' require 'unicode_plot'
require 'uplot/version' require 'uplot/version'
require 'uplot/command.rb' require 'uplot/preprocessing'
require 'uplot/plot'
require 'uplot/command'
module Uplot module Uplot
end end

View File

@@ -1,76 +1,204 @@
require 'optparse' require 'optparse'
require 'csv' require_relative 'preprocessing'
require 'unicode_plot'
module Uplot module Uplot
Data = Struct.new(:headers, :series)
class Command class Command
def initialize(argv) Params = Struct.new(
@params = {} # Sort me!
@ptype = nil :title,
:width,
:height,
:border,
:margin,
:padding,
:color,
:xlabel,
:ylabel,
:labels,
:symbol,
:xscale,
:nbins,
:closed,
:canvas,
:xlim,
:ylim,
:grid,
:name
) do
def to_hc
to_h.compact
end
end
attr_accessor :params, :plot_type
attr_reader :raw_inputs, :data, :fmt
def initialize
@params = Params.new
@plot_type = nil
@headers = nil @headers = nil
@delimiter = "\t" @delimiter = "\t"
@transpose = false @transpose = false
@output = false @output = false
@count = false @count = false
@fmt = 'xyy' @fmt = 'xyy'
@raw_inputs = []
@debug = false @debug = false
parse_options(argv)
end end
def create_parser def create_parser
OptionParser.new OptionParser.new do |opt|
.on('-o', '--output', TrueClass) { |v| @output = v } opt.program_name = 'uplot'
.on('-d', '--delimiter VAL', String) { |v| @delimiter = v } opt.version = Uplot::VERSION
.on('-H', '--headers', TrueClass) { |v| @headers = v } opt.on('-o', '--output', TrueClass) do |v|
.on('-T', '--transpose', TrueClass) { |v| @transpose = v } @output = v
.on('-t', '--title VAL', String) { |v| @params[:title] = v } end
.on('-w', '--width VAL', Numeric) { |v| @params[:width] = v } .on('-d', '--delimiter VAL', String) do |v|
.on('-h', '--height VAL', Numeric) { |v| @params[:height] = v } @delimiter = v
.on('-b', '--border VAL', Numeric) { |v| @params[:border] = v } end
.on('-m', '--margin VAL', Numeric) { |v| @params[:margin] = v } .on('-H', '--headers', TrueClass) do |v|
.on('-p', '--padding VAL', Numeric) { |v| @params[:padding] = v } @headers = v
.on('-c', '--color VAL', String) { |v| @params[:color] = v.to_sym } end
.on('-x', '--xlabel VAL', String) { |v| @params[:xlabel] = v } .on('-T', '--transpose', TrueClass) do |v|
.on('-y', '--ylabel VAL', String) { |v| @params[:ylabel] = v } @transpose = v
.on('-l', '--labels', TrueClass) { |v| @params[:labels] = v } end
.on('--fmt VAL', String) { |v| @fmt = v } .on('-t', '--title VAL', String) do |v|
.on('--debug', TrueClass) { |v| @debug = v } params.title = v
end
.on('-w', '--width VAL', Numeric) do |v|
params.width = v
end
.on('-h', '--height VAL', Numeric) do |v|
params.height = v
end
.on('-b', '--border VAL', Numeric) do |v|
params.border = v
end
.on('-m', '--margin VAL', Numeric) do |v|
params.margin = v
end
.on('-p', '--padding VAL', Numeric) do |v|
params.padding = v
end
.on('-c', '--color VAL', String) do |v|
params.color = v.to_sym
end
.on('-x', '--xlabel VAL', String) do |v|
params.xlabel = v
end
.on('-y', '--ylabel VAL', String) do |v|
params.ylabel = v
end
.on('-l', '--labels', TrueClass) do |v|
params.labels = v
end
.on('--fmt VAL', String) do |v|
@fmt = v
end
.on('--debug', TrueClass) do |v|
@debug = v
end
end
end end
def parse_options(argv) def parse_options(argv = ARGV)
main_parser = create_parser main_parser = create_parser
parsers = Hash.new { |h, k| h[k] = create_parser } parsers = Hash.new { |h, k| h[k] = create_parser }
parsers['barplot'] = parsers['bar']
.on('--symbol VAL', String) { |v| @params[:symbol] = v } parsers[:barplot] = \
.on('--xscale VAL', String) { |v| @params[:xscale] = v } parsers[:bar]
.on('--count', TrueClass) { |v| @count = v } .on('--symbol VAL', String) do |v|
parsers['count'] = parsers['c'] # barplot -c params.symbol = v
.on('--symbol VAL', String) { |v| @params[:symbol] = v } end
parsers['histogram'] = parsers['hist'] .on('--xscale VAL', String) do |v|
.on('-n', '--nbins VAL', Numeric) { |v| @params[:nbins] = v } params.xscale = v
.on('--closed VAL', String) { |v| @params[:closed] = v } end
.on('--symbol VAL', String) { |v| @params[:symbol] = v } .on('--count', TrueClass) do |v|
parsers['lineplot'] = parsers['line'] @count = v
.on('--canvas VAL', String) { |v| @params[:canvas] = v } end
.on('--xlim VAL', String) { |v| @params[:xlim] = get_lim(v) }
.on('--ylim VAL', String) { |v| @params[:ylim] = get_lim(v) } parsers[:count] = \
parsers['lineplots'] = parsers['lines'] parsers[:c] # barplot -c
.on('--canvas VAL', String) { |v| @params[:canvas] = v } .on('--symbol VAL', String) do |v|
.on('--xlim VAL', String) { |v| @params[:xlim] = get_lim(v) } params.symbol = v
.on('--ylim VAL', String) { |v| @params[:ylim] = get_lim(v) } end
parsers['scatter'] = parsers['s']
.on('--canvas VAL', String) { |v| @params[:canvas] = v } parsers[:histogram] = \
.on('--xlim VAL', String) { |v| @params[:xlim] = get_lim(v) } parsers[:hist]
.on('--ylim VAL', String) { |v| @params[:ylim] = get_lim(v) } .on('-n', '--nbins VAL', Numeric) do |v|
parsers['density'] = parsers['d'] params.nbins = v
.on('--grid', TrueClass) { |v| @params[:grid] = v } end
.on('--xlim VAL', String) { |v| @params[:xlim] = get_lim(v) } .on('--closed VAL', String) do |v|
.on('--ylim VAL', String) { |v| @params[:ylim] = get_lim(v) } params.closed = v
parsers['boxplot'] = parsers['box'] end
.on('--xlim VAL', String) { |v| @params[:xlim] = get_lim(v) } .on('--symbol VAL', String) do |v|
params.symbol = v
end
parsers[:lineplot] = \
parsers[:line]
.on('--canvas VAL', String) do |v|
params.canvas = v
end
.on('--xlim VAL', String) do |v|
params.xlim = get_lim(v)
end
.on('--ylim VAL', String) do |v|
params.ylim = get_lim(v)
end
parsers[:lineplots] = \
parsers[:lines]
.on('--canvas VAL', String) do |v|
params.canvas = v
end
.on('--xlim VAL', String) do |v|
params.xlim = get_lim(v)
end
.on('--ylim VAL', String) do |v|
params.ylim = get_lim(v)
end
parsers[:scatter] = \
parsers[:s]
.on('--canvas VAL', String) do |v|
params.canvas = v
end
.on('--xlim VAL', String) do |v|
params.xlim = get_lim(v)
end
.on('--ylim VAL', String) do |v|
params.ylim = get_lim(v)
end
parsers[:density] = \
parsers[:d]
.on('--grid', TrueClass) do |v|
params.grid = v
end
.on('--xlim VAL', String) do |v|
params.xlim = get_lim(v)
end
.on('--ylim VAL', String) do |v|
params.ylim = get_lim(v)
end
parsers[:boxplot] = \
parsers[:box]
.on('--xlim VAL', String) do |v|
params.xlim = get_lim(v)
end
# Preventing the generation of new sub-commands
parsers.default = nil parsers.default = nil
main_parser.banner = <<~MSG # Usage and help messages
main_parser.banner = \
<<~MSG
Program: uplot (Tools for plotting on the terminal) Program: uplot (Tools for plotting on the terminal)
Version: #{Uplot::VERSION} (using unicode_plot #{UnicodePlot::VERSION}) Version: #{Uplot::VERSION} (using unicode_plot #{UnicodePlot::VERSION})
@@ -88,17 +216,17 @@ module Uplot
exit 1 exit 1
end end
@ptype = argv.shift @plot_type = argv.shift&.to_sym
unless parsers.has_key?(@ptype) unless parsers.has_key?(plot_type)
if @ptype.nil? if plot_type.nil?
warn main_parser.help warn main_parser.help
else else
warn "uplot: unrecognized command '#{@ptype}'" warn "uplot: unrecognized command '#{plot_type}'"
end end
exit 1 exit 1
end end
parser = parsers[@ptype] parser = parsers[plot_type]
begin begin
parser.parse!(argv) unless argv.empty? parser.parse!(argv) unless argv.empty?
@@ -108,162 +236,41 @@ module Uplot
end end
end end
def get_lim(str)
str.split(/-|:|\.\./)[0..1].map(&:to_f)
end
def run def run
parse_options
# Sometimes the input file does not end with a newline code. # Sometimes the input file does not end with a newline code.
while input = Kernel.gets(nil) while input = Kernel.gets(nil)
input.freeze input.freeze
data, headers = preprocess(input) @raw_inputs << input
pp input: input, data: data, headers: headers if @debug @data = Preprocessing.input(input, @delimiter, @headers, @transpose)
case @ptype pp @data if @debug
when 'bar', 'barplot' case plot_type
barplot(data, headers) when :bar, :barplot
when 'count', 'c' Plot.barplot(data, params, @count)
@count = true when :count, :c
barplot(data, headers) Plot.barplot(data, params, count = true)
when 'hist', 'histogram' when :hist, :histogram
histogram(data, headers) Plot.histogram(data, params)
when 'line', 'lineplot' when :line, :lineplot
line(data, headers) Plot.line(data, params)
when 'lines', 'lineplots' when :lines, :lineplots
lines(data, headers) Plot.lines(data, params, fmt)
when 'scatter', 'scatterplot' when :scatter, :scatterplot
scatter(data, headers) Plot.scatter(data, params, fmt)
when 'density' when :density
density(data, headers) Plot.density(data, params, fmt)
when 'box', 'boxplot' when :box, :boxplot
boxplot(data, headers) Plot.boxplot(data, params)
else
raise "unrecognized plot_type: #{plot_type}"
end.render($stderr) end.render($stderr)
print input if @output print input if @output
end end
end end
# https://stackoverflow.com/q/26016632
def transpose2(arr) # Should be renamed
Array.new(arr.map(&:length).max) { |i| arr.map { |e| e[i] } }
end
def preprocess(input)
data = CSV.parse(input, col_sep: @delimiter)
data.delete([]) # Remove blank lines.
data.delete_if { |i| i.all? nil } # Room for improvement.
p parsed_csv: data if @debug
headers = nil
if @transpose
if @headers
headers = []
# each but destructive like map
data.each { |series| headers << series.shift }
end
else
headers = data.shift if @headers
data = transpose2(data)
end
[data, headers]
end
def preprocess_count(data)
if Enumerable.method_defined? :tally
data[0].tally
else # https://github.com/marcandre/backports tally
data[0].each_with_object(Hash.new(0)) { |item, res| res[item] += 1 }
.tap { |h| h.default = nil }
end.sort { |a, b| a[1] <=> b[1] }.reverse.transpose
end
def barplot(data, headers)
data = preprocess_count(data) if @count
@params[:title] ||= headers[1] if headers
UnicodePlot.barplot(data[0], data[1].map(&:to_f), **@params)
end
def histogram(data, headers)
@params[:title] ||= headers[0] if headers # labels?
series = data[0].map(&:to_f)
UnicodePlot.histogram(series, **@params.compact)
end
def get_lim(str)
str.split(/-|:|\.\./)[0..1].map(&:to_f)
end
def line(data, headers)
if data.size == 1
@params[:ylabel] ||= headers[0] if headers
y = data[0].map(&:to_f)
UnicodePlot.lineplot(y, **@params.compact)
else
@params[:xlabel] ||= headers[0] if headers
@params[:ylabel] ||= headers[1] if headers
x = data[0].map(&:to_f)
y = data[1].map(&:to_f)
UnicodePlot.lineplot(x, y, **@params.compact)
end
end
def get_method2(method1)
(method1.to_s + '!').to_sym
end
def xyy_plot(data, headers, method1) # improve method name
method2 = get_method2(method1)
data.map! { |series| series.map(&:to_f) }
@params[:name] ||= headers[1] if headers
@params[:xlabel] ||= headers[0] if headers
@params[:ylim] ||= data[1..-1].flatten.minmax # need?
plot = UnicodePlot.public_send(method1, data[0], data[1], **@params.compact)
2.upto(data.size - 1) do |i|
UnicodePlot.public_send(method2, plot, data[0], data[i], name: headers[i])
end
plot
end
def xyxy_plot(data, headers, method1) # improve method name
method2 = get_method2(method1)
data.map! { |series| series.map(&:to_f) }
data = data.each_slice(2).to_a
@params[:name] ||= headers[0] if headers
@params[:xlim] = data.map(&:first).flatten.minmax
@params[:ylim] = data.map(&:last).flatten.minmax
x1, y1 = data.shift
plot = UnicodePlot.public_send(method1, x1, y1, **@params.compact)
data.each_with_index do |(xi, yi), i|
UnicodePlot.public_send(method2, plot, xi, yi, name: headers[(i + 1) * 2])
end
plot
end
def lines(data, headers)
case @fmt
when 'xyy'
xyy_plot(data, headers, :lineplot)
when 'xyxy'
xyxy_plot(data, headers, :lineplot)
end
end
def scatter(data, headers)
case @fmt
when 'xyy'
xyy_plot(data, headers, :scatterplot)
when 'xyxy'
xyxy_plot(data, headers, :scatterplot)
end
end
def density(data, headers)
case @fmt
when 'xyy'
xyy_plot(data, headers, :densityplot)
when 'xyxy'
xyxy_plot(data, headers, :densityplot)
end
end
def boxplot(data, headers)
headers ||= (1..data.size).map(&:to_s)
data.map! { |series| series.map(&:to_f) }
UnicodePlot.boxplot(headers, data, **@params.compact)
end
end end
end end

145
lib/uplot/plot.rb Normal file
View File

@@ -0,0 +1,145 @@
require 'unicode_plot'
module Uplot
# plotting functions.
module Plot
module_function
def barplot(data, params, count = false)
headers = data.headers
series = data.series
if count
series = Preprocessing.count(series[0])
params.title = headers[0] if headers
end
params.title ||= headers[1] if headers
labels = series[0]
values = series[1].map(&:to_f)
UnicodePlot.barplot(labels, values, **params.to_hc)
end
def histogram(data, params)
headers = data.headers
series = data.series
params.title ||= data.headers[0] if headers
values = series[0].map(&:to_f)
UnicodePlot.histogram(values, **params.to_hc)
end
def line(data, params)
headers = data.headers
series = data.series
if series.size == 1
# If there is only one series, it is assumed to be sequential data.
params.ylabel ||= headers[0] if headers
y = series[0].map(&:to_f)
UnicodePlot.lineplot(y, **params.to_hc)
else
# If there are 2 or more series,
# assume that the first 2 series are the x and y series respectively.
if headers
params.xlabel ||= headers[0]
params.ylabel ||= headers[1]
end
x = series[0].map(&:to_f)
y = series[1].map(&:to_f)
UnicodePlot.lineplot(x, y, **params.to_hc)
end
end
def get_method2(method1)
(method1.to_s + '!').to_sym
end
def xyy_plot(data, method1, params)
headers = data.headers
series = data.series
method2 = get_method2(method1)
series.map! { |s| s.map(&:to_f) }
if headers
params.name ||= headers[1]
params.xlabel ||= headers[0]
end
params.ylim ||= series[1..-1].flatten.minmax # why need?
plot = UnicodePlot.public_send(method1, series[0], series[1], **params.to_hc)
2.upto(series.size - 1) do |i|
UnicodePlot.public_send(method2, plot, series[0], series[i], name: headers&.[](i))
end
plot
end
def xyxy_plot(data, method1, params)
headers = data.headers
series = data.series
method2 = get_method2(method1)
series.map! { |s| s.map(&:to_f) }
series = series.each_slice(2).to_a
params.name ||= headers[0] if headers
params.xlim = series.map(&:first).flatten.minmax # why need?
params.ylim = series.map(&:last).flatten.minmax # why need?
x1, y1 = series.shift
plot = UnicodePlot.public_send(method1, x1, y1, **params.to_hc)
series.each_with_index do |(xi, yi), i|
UnicodePlot.public_send(method2, plot, xi, yi, name: headers&.[]((i + 1) * 2))
end
plot
end
def lines(data, params, fmt = 'xyy')
check_series_size(data, fmt)
case fmt
when 'xyy'
xyy_plot(data, :lineplot, params)
when 'xyxy'
xyxy_plot(data, :lineplot, params)
end
end
def scatter(data, params, fmt = 'xyy')
check_series_size(data, fmt)
case fmt
when 'xyy'
xyy_plot(data, :scatterplot, params)
when 'xyxy'
xyxy_plot(data, :scatterplot, params)
end
end
def density(data, params, fmt = 'xyy')
check_series_size(data, fmt)
case fmt
when 'xyy'
xyy_plot(data, :densityplot, params)
when 'xyxy'
xyxy_plot(data, :densityplot, params)
end
end
def boxplot(data, params)
headers = data.headers
series = data.series
headers ||= (1..series.size).map(&:to_s)
series.map! { |s| s.map(&:to_f) }
UnicodePlot.boxplot(headers, series, **params.to_hc)
end
def check_series_size(data, fmt)
series = data.series
if series.size == 1
warn 'uplot: There is only one series of input data. Please check the delimiter.'
warn ''
warn " Headers: \e[35m#{data.headers.inspect}\e[0m"
warn " The first item is: \e[35m\"#{series[0][0]}\"\e[0m"
warn " The last item is : \e[35m\"#{series[0][-1]}\"\e[0m"
exit 1
end
if fmt == 'xyxy' && series.size.odd?
warn 'uplot: In the xyxy format, the number of series must be even.'
warn ''
warn " Number of series: \e[35m#{series.size}\e[0m"
warn " Headers: \e[35m#{data.headers.inspect}\e[0m"
exit 1
end
end
end
end

View File

@@ -0,0 +1,67 @@
require 'csv'
module Uplot
module Preprocessing
module_function
def input(input, delimiter, headers, transpose)
arr = read_csv(input, delimiter)
headers = get_headers(arr, headers, transpose)
series = get_series(arr, headers, transpose)
Data.new(headers, series)
end
def read_csv(input, delimiter)
CSV.parse(input, col_sep: delimiter)
.delete_if do |i|
i == [] or i.all? nil
end
end
# Transpose different sized ruby arrays
# https://stackoverflow.com/q/26016632
def transpose2(arr)
Array.new(arr.map(&:length).max) { |i| arr.map { |e| e[i] } }
end
def get_headers(arr, headers, transpose)
if headers
if transpose
arr.map(&:first)
else
arr[0]
end
end
end
def get_series(arr, headers, transpose)
if transpose
if headers
arr.map { |row| row[1..-1] }
else
arr
end
else
if headers
transpose2(arr[1..-1])
else
transpose2(arr)
end
end
end
def count(arr)
# tally was added in Ruby 2.7
if Enumerable.method_defined? :tally
arr.tally
else
# https://github.com/marcandre/backports
arr.each_with_object(Hash.new(0)) { |item, res| res[item] += 1 }
.tap { |h| h.default = nil }
end
.sort { |a, b| a[1] <=> b[1] }
.reverse
.transpose
end
end
end

View File

@@ -1,3 +1,3 @@
module Uplot module Uplot
VERSION = '0.1.4'.freeze VERSION = '0.2.0'.freeze
end end