diff --git a/lib/youplot.rb b/lib/youplot.rb index ce379d6..78f0bad 100644 --- a/lib/youplot.rb +++ b/lib/youplot.rb @@ -3,7 +3,6 @@ require 'unicode_plot' require 'youplot/version' require 'youplot/preprocessing' -require 'youplot/plot' require 'youplot/command' module YouPlot diff --git a/lib/youplot/backends/unicode_plot_backend.rb b/lib/youplot/backends/unicode_plot_backend.rb new file mode 100644 index 0000000..9ead9b2 --- /dev/null +++ b/lib/youplot/backends/unicode_plot_backend.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require 'unicode_plot' + +module YouPlot + # plotting functions. + module Backends + module UnicodePlotBackend + module_function + + def barplot(data, params, count: false) + headers = data.headers + series = data.series + # `uplot count` + if count + series = Preprocessing.count_values(series[0]) + params.title = headers[0] if headers + end + if series.size == 1 + # If there is only one series, use the line number for label. + params.title ||= headers[0] if headers + labels = Array.new(series[0].size) { |i| (i + 1).to_s } + values = series[0].map(&:to_f) + else + params.title ||= headers[1] if headers + labels = series[0] + values = series[1].map(&:to_f) + end + 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_sym + end + + def plot_xyy(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 plot_xyxy(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 plot_fmt(data, fmt, method1, params) + case fmt + when 'xyy' + plot_xyy(data, method1, params) + when 'xyxy' + plot_xyxy(data, method1, params) + else + raise "Unknown format: #{fmt}" + end + end + + def lines(data, params, fmt = 'xyy') + check_series_size(data, fmt) + plot_fmt(data, fmt, :lineplot, params) + end + + def scatter(data, params, fmt = 'xyy') + check_series_size(data, fmt) + plot_fmt(data, fmt, :scatterplot, params) + end + + def density(data, params, fmt = 'xyy') + check_series_size(data, fmt) + plot_fmt(data, fmt, :densityplot, params) + 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 colors(color_names = false) + UnicodePlot::StyledPrinter::TEXT_COLORS.each do |k, v| + print v + print k + unless color_names + print "\t" + print ' ●' + end + print "\033[0m" + print "\t" + end + puts + end + + def check_series_size(data, fmt) + series = data.series + if series.size == 1 + warn 'youplot: 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 'YouPlot: 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 +end diff --git a/lib/youplot/command.rb b/lib/youplot/command.rb index 3a6c78f..8d392ac 100644 --- a/lib/youplot/command.rb +++ b/lib/youplot/command.rb @@ -3,6 +3,9 @@ require_relative 'preprocessing' require_relative 'command/parser' +# FIXME +require_relative 'backends/unicode_plot_backend' + module YouPlot Data = Struct.new(:headers, :series) @@ -11,8 +14,9 @@ module YouPlot attr_reader :data, :fmt, :parser def initialize - @params = Params.new - @parser = Parser.new + @params = Params.new + @parser = Parser.new + @backend = YouPlot::Backends::UnicodePlotBackend end def run @@ -28,7 +32,7 @@ module YouPlot @debug = parser.debug if command == :colors - Plot.colors(parser.color_names) + @backend.colors(parser.color_names) exit end @@ -39,21 +43,21 @@ module YouPlot pp @data if @debug plot = case command when :bar, :barplot - Plot.barplot(data, params) + @backend.barplot(data, params) when :count, :c - Plot.barplot(data, params, count: true) + @backend.barplot(data, params, count: true) when :hist, :histogram - Plot.histogram(data, params) + @backend.histogram(data, params) when :line, :lineplot - Plot.line(data, params) + @backend.line(data, params) when :lines, :lineplots - Plot.lines(data, params, fmt) + @backend.lines(data, params, fmt) when :scatter, :s - Plot.scatter(data, params, fmt) + @backend.scatter(data, params, fmt) when :density, :d - Plot.density(data, params, fmt) + @backend.density(data, params, fmt) when :box, :boxplot - Plot.boxplot(data, params) + @backend.boxplot(data, params) else raise "unrecognized plot_type: #{command}" end diff --git a/lib/youplot/plot.rb b/lib/youplot/plot.rb deleted file mode 100644 index 2bb4347..0000000 --- a/lib/youplot/plot.rb +++ /dev/null @@ -1,165 +0,0 @@ -# frozen_string_literal: true - -require 'unicode_plot' - -module YouPlot - # plotting functions. - module Plot - module_function - - def barplot(data, params, count: false) - headers = data.headers - series = data.series - # `uplot count` - if count - series = Preprocessing.count_values(series[0]) - params.title = headers[0] if headers - end - if series.size == 1 - # If there is only one series, use the line number for label. - params.title ||= headers[0] if headers - labels = Array.new(series[0].size) { |i| (i + 1).to_s } - values = series[0].map(&:to_f) - else - params.title ||= headers[1] if headers - labels = series[0] - values = series[1].map(&:to_f) - end - 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_sym - end - - def plot_xyy(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 plot_xyxy(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 plot_fmt(data, fmt, method1, params) - case fmt - when 'xyy' - plot_xyy(data, method1, params) - when 'xyxy' - plot_xyxy(data, method1, params) - else - raise "Unknown format: #{fmt}" - end - end - - def lines(data, params, fmt = 'xyy') - check_series_size(data, fmt) - plot_fmt(data, fmt, :lineplot, params) - end - - def scatter(data, params, fmt = 'xyy') - check_series_size(data, fmt) - plot_fmt(data, fmt, :scatterplot, params) - end - - def density(data, params, fmt = 'xyy') - check_series_size(data, fmt) - plot_fmt(data, fmt, :densityplot, params) - 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 colors(color_names = false) - UnicodePlot::StyledPrinter::TEXT_COLORS.each do |k, v| - print v - print k - unless color_names - print "\t" - print ' ●' - end - print "\033[0m" - print "\t" - end - puts - end - - def check_series_size(data, fmt) - series = data.series - if series.size == 1 - warn 'youplot: 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 'YouPlot: 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