youplot 0.4.6 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d73be033d215d1df04dfef6be988a55e669699a4a8e6c408ac38bb4ff0c680a6
4
- data.tar.gz: 5f84877f99e1d5f52290caf33ed16e2ae67ff2465ac9acf50ae33630131f225e
3
+ metadata.gz: c344612a2c9055f37b06ea9e6cceb5199e18a261b1ce4bb4c2d3ec74f77657ad
4
+ data.tar.gz: 736041b6139ebc559b78176f3ed0908469e6fc8c21e0119ff4f62641a357ffc8
5
5
  SHA512:
6
- metadata.gz: 2a593cd01599a76949ea17ca00f5b24b6a6f92c971b68281775c8a0ddb7d25d9c6269c64ee2a79ea482abadeb9c44f2b5d91cfe0298b81a6da3139405d8bf1c8
7
- data.tar.gz: 4912f82e6e82c9533b13f8d16b0120a7e40adb8bc55d4026d7dc08bbc2efb4c186e9b7653b58baa7990661ffd299331e6a937032cee0c3623f6f0b2eaf420fc2
6
+ metadata.gz: b318d624ece5511feacd5f1a9c50ea929b6c1c1f05d951a50f61869e52a66b8adf7d613c3042345b0455b2c36eca56088144ee0967f188c3cd7c12910ecf7579
7
+ data.tar.gz: 2570196b9263edea40227244909e5e9fe287854e7daa2d5d2f44bd1fa617faa5716b38979eed40a84c1687aac71be8b6005ed902d83aa1f6dce54fea60f4dd66
data/LICENSE.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
3
  Copyright (c) 2020 kojix2
4
+ Copyright (c) 2025 Red Data Tools
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  <hr>
4
4
  <a href="https://github.com/red-data-tools/YouPlot/actions/workflows/ci.yml"><img alt="Build Status" src="https://github.com/red-data-tools/YouPlot/workflows/test/badge.svg"></a>
5
5
  <a href="https://rubygems.org/gems/youplot/"><img alt="Gem Version" src="https://badge.fury.io/rb/youplot.svg"></a>
6
- <a href="https://zenodo.org/badge/latestdoi/283230219"><img alt="DOI" src="https://zenodo.org/badge/283230219.svg"></a>
7
6
  <a href="https://rubydoc.info/gems/youplot/"><img alt="Docs Stable" src="https://img.shields.io/badge/docs-stable-blue.svg"></a>
8
7
  <a href="LICENSE.txt"><img alt="The MIT License" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
9
8
 
@@ -36,6 +35,8 @@ conda install -c conda-forge compilers
36
35
  gem install youplot
37
36
  ```
38
37
 
38
+ :crystal_ball: [YouPlot2](https://github.com/red-data-tools/YouPlot2) - Experimental project with pre-built binaries
39
+
39
40
  ## Quick Start
40
41
 
41
42
  <img alt="barplot" src="https://user-images.githubusercontent.com/5798442/101999903-d36a2d00-3d24-11eb-9361-b89116f44122.png" width=160> <img alt="histogram" src="https://user-images.githubusercontent.com/5798442/101999820-21cafc00-3d24-11eb-86db-e410d19b07df.png" width=160> <img alt="scatter" src="https://user-images.githubusercontent.com/5798442/101999827-27284680-3d24-11eb-9903-551857eaa69c.png" width=160> <img alt="density" src="https://user-images.githubusercontent.com/5798442/101999828-2abbcd80-3d24-11eb-902c-2f44266fa6ae.png" width=160> <img alt="boxplot" src="https://user-images.githubusercontent.com/5798442/101999830-2e4f5480-3d24-11eb-8891-728c18bf5b35.png" width=160>
@@ -55,10 +56,9 @@ curl -sL https://git.io/ISLANDScsv \
55
56
  <img alt="barplot" src="https://user-images.githubusercontent.com/5798442/101999903-d36a2d00-3d24-11eb-9361-b89116f44122.png">
56
57
  </p>
57
58
 
58
-
59
+ For offline users: sorts files in a directory by size and shows a bar graph.
59
60
 
60
61
  ```sh
61
- # For offline user: Sorts files in a directory by size and shows a bar graph.
62
62
  ls -l | awk '{print $9, $5}' | sort -nk 2 | uplot bar -d ' '
63
63
  ```
64
64
 
@@ -88,12 +88,15 @@ curl -sL https://git.io/AirPassengers \
88
88
  <img alt="lineplot" src="https://user-images.githubusercontent.com/5798442/101999825-24c5ec80-3d24-11eb-99f4-c642e8d221bc.png">
89
89
  </p>
90
90
 
91
+ For offline users: calculates sin values from 0 to 2*pi and plots a sine wave.
92
+
91
93
  ```sh
92
- # For offline users: Calculates sin values (0-2*pi) and plots a sine wave.
93
- python3 -c '
94
+ python3 - <<'PY' | uplot line
94
95
  from math import sin, pi
95
- data = "\n".join(f"{i*pi/50}\t{sin(i*pi/50)}" for i in range(101))
96
- print(data)' | uplot line
96
+
97
+ for i in range(101):
98
+ print(f"{i*pi/50}\t{sin(i*pi/50)}")
99
+ PY
97
100
  ```
98
101
 
99
102
  ### scatter
@@ -109,8 +112,9 @@ curl -sL https://git.io/IRIStsv \
109
112
  </p>
110
113
 
111
114
 
115
+ For offline users:
116
+
112
117
  ```sh
113
- # For offline users
114
118
  cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot scatter -H -d, -t IRIS
115
119
  ```
116
120
 
@@ -126,8 +130,9 @@ curl -sL https://git.io/IRIStsv \
126
130
  <img alt="density" src="https://user-images.githubusercontent.com/5798442/101999828-2abbcd80-3d24-11eb-902c-2f44266fa6ae.png">
127
131
  </p>
128
132
 
133
+ For offline users:
134
+
129
135
  ```sh
130
- # For offline users
131
136
  cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot density -H -d, -t IRIS
132
137
  ```
133
138
 
@@ -143,8 +148,9 @@ curl -sL https://git.io/IRIStsv \
143
148
  <img alt="boxplot" src="https://user-images.githubusercontent.com/5798442/101999830-2e4f5480-3d24-11eb-8891-728c18bf5b35.png">
144
149
  </p>
145
150
 
151
+ For offline users:
152
+
146
153
  ```sh
147
- # For offline users
148
154
  cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot boxplot -H -d, -t IRIS
149
155
  ```
150
156
 
@@ -186,7 +192,7 @@ cat gencode.v35.annotation.gff3 | grep -v '#' | grep 'gene' | cut -f1 \
186
192
  `uplot` is the shortened form of `youplot`. You can use either.
187
193
 
188
194
  | Command | Description |
189
- |------------------------------------------------|-----------------------------------|
195
+ | ---------------------------------------------- | --------------------------------- |
190
196
  | `cat data.tsv \| uplot <command> [options]` | Take input from stdin |
191
197
  | `uplot <command> [options] data.tsv ...` | Take input from files |
192
198
  | `pipeline1 \| uplot <command> -O \| pipeline2` | Outputs data from stdin to stdout |
@@ -195,19 +201,19 @@ cat gencode.v35.annotation.gff3 | grep -v '#' | grep 'gene' | cut -f1 \
195
201
 
196
202
  The following sub-commands are available.
197
203
 
198
- | command | short | how it works |
199
- |-----------|-------|----------------------------------------|
200
- | barplot | bar | draw a horizontal barplot |
201
- | histogram | hist | draw a horizontal histogram |
202
- | lineplot | line | draw a line chart |
203
- | lineplots | lines | draw a line chart with multiple series |
204
- | scatter | s | draw a scatter plot |
205
- | density | d | draw a density plot |
206
- | boxplot | box | draw a horizontal boxplot |
207
- | | | |
204
+ | command | short | how it works |
205
+ | --------- | ----- | -------------------------------------------------------- |
206
+ | barplot | bar | draw a horizontal barplot |
207
+ | histogram | hist | draw a horizontal histogram |
208
+ | lineplot | line | draw a line chart |
209
+ | lineplots | lines | draw a line chart with multiple series |
210
+ | scatter | s | draw a scatter plot |
211
+ | density | d | draw a density plot |
212
+ | boxplot | box | draw a horizontal boxplot |
213
+ | | | |
208
214
  | count | c | draw a barplot based on the number of occurrences (slow) |
209
- | | | |
210
- | colors | color | show the list of available colors |
215
+ | | | |
216
+ | colors | color | show the list of available colors |
211
217
 
212
218
  ### Output the plot
213
219
 
@@ -296,13 +302,14 @@ Please feel free to send us your pull requests.
296
302
 
297
303
  ### Development
298
304
 
305
+ Fork the main repository by clicking the Fork button.
306
+
299
307
  ```sh
300
- # fork the main repository by clicking the Fork button.
301
308
  git clone https://github.com/your_name/YouPlot
302
- bundle install # Install the gem dependencies
303
- bundle exec rake test # Run the test
304
- bundle exec rake install # Installation from source code
305
- bundle exec exe/uplot # Run youplot (Try out the edited code)
309
+ bundle install
310
+ bundle exec rake test
311
+ bundle exec rake install
312
+ bundle exec exe/uplot
306
313
  ```
307
314
 
308
315
  Do you need commit rights to my repository?
@@ -311,7 +318,7 @@ bundle exec exe/uplot # Run youplot (Try out the edited code)
311
318
 
312
319
  ### Acknowledgements
313
320
 
314
- * [sampo grafiikka](https://jypg.net/sampo_grafiikka) - Project logo creation
321
+ * [sampo grafiikka](https://lepo.sampo-grafiikka.com/) - Project logo creation
315
322
  * [yutaas](https://github.com/yutaas) - English proofreading
316
323
 
317
324
  ## License
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouPlot
4
+ module Aggregation
5
+ module_function
6
+
7
+ def count_values(arr, tally: true, reverse: false)
8
+ # tally was added in Ruby 2.7
9
+ result = \
10
+ if tally && Enumerable.method_defined?(:tally)
11
+ arr.tally
12
+ else
13
+ # value_counts Enumerable::Statistics
14
+ arr.value_counts(dropna: false)
15
+ end
16
+
17
+ sort_cache = {}
18
+
19
+ # sorting
20
+ result = result.sort do |a, b|
21
+ # compare values
22
+ r = b[1] <=> a[1]
23
+ # If the values are the same, compare by name
24
+ r = natural_compare(a[0], b[0], sort_cache) if r.zero?
25
+ r
26
+ end
27
+
28
+ # --reverse option
29
+ result.reverse! if reverse
30
+
31
+ # prepare for barplot
32
+ result.transpose
33
+ end
34
+
35
+ # Natural order comparison for tie-breaking when counts are equal.
36
+ # Fast paths handle text-only and pure numeric labels.
37
+ # Mixed labels still use chunked comparison (e.g. "chr1" vs "chr10").
38
+ def natural_compare(a, b, cache = nil)
39
+ aa = natural_sort_key(a, cache)
40
+ bb = natural_sort_key(b, cache)
41
+
42
+ # Fast path: both labels are text-only, so plain string comparison is enough.
43
+ return aa[:string] <=> bb[:string] if aa[:type] == :text && bb[:type] == :text
44
+
45
+ # Fast path: both labels are pure numbers, so compare numerically first.
46
+ if aa[:type] == :numeric && bb[:type] == :numeric
47
+ r = aa[:numeric] <=> bb[:numeric]
48
+ return r unless r.zero?
49
+
50
+ # Tiebreaker for equivalent numeric values (e.g. "1" and "01")
51
+ return aa[:string] <=> bb[:string]
52
+ end
53
+
54
+ # Fallback path: at least one label mixes text and digits.
55
+ ta = ensure_natural_tokens(aa)
56
+ tb = ensure_natural_tokens(bb)
57
+ max = [ta.size, tb.size].max
58
+
59
+ 0.upto(max - 1) do |i|
60
+ xa = ta[i]
61
+ xb = tb[i]
62
+
63
+ return -1 if xa.nil?
64
+ return 1 if xb.nil?
65
+
66
+ r = if xa[0] == :num && xb[0] == :num
67
+ compare_integer_strings(xa[1], xb[1])
68
+ else
69
+ xa[1] <=> xb[1]
70
+ end
71
+
72
+ return r unless r.zero?
73
+ end
74
+
75
+ aa[:string] <=> bb[:string]
76
+ end
77
+
78
+ # Classifies a value for natural sorting and caches the result per label.
79
+ def natural_sort_key(value, cache = nil)
80
+ str = value.to_s
81
+ return cache[str] if cache && cache.key?(str)
82
+
83
+ key = if str.match?(/\d/)
84
+ numeric = parse_numeric(str)
85
+ if numeric
86
+ # Pure numeric labels get a dedicated fast path.
87
+ { type: :numeric, string: str, numeric: numeric }
88
+ else
89
+ # Mixed labels fall back to chunked natural comparison.
90
+ { type: :mixed, string: str, tokens: nil }
91
+ end
92
+ else
93
+ # Text-only labels get a dedicated fast path.
94
+ { type: :text, string: str, tokens: nil }
95
+ end
96
+
97
+ cache ? cache[str] = key : key
98
+ end
99
+
100
+ # Memoizes token pairs for fallback chunked comparison.
101
+ def ensure_natural_tokens(key)
102
+ key[:tokens] ||= natural_tokens(key[:string])
103
+ end
104
+
105
+ # Parses a string as a numeric value if it matches pure number format.
106
+ # Returns Float or nil.
107
+ def parse_numeric(str)
108
+ return nil unless str.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/)
109
+
110
+ str.to_f
111
+ end
112
+
113
+ # Splits a string into [type, token] pairs for natural comparison.
114
+ # Type is :num for digit-only chunks, :text for anything else.
115
+ # E.g. "chr10" => [[:text, "chr"], [:num, "10"]]
116
+ def natural_tokens(str)
117
+ str.scan(/\d+|\D+/).map do |tok|
118
+ kind = tok.match?(/\A\d+\z/) ? :num : :text
119
+ [kind, tok]
120
+ end
121
+ end
122
+
123
+ # Compares two numeric strings, handling leading zeros.
124
+ # Order: by length (sans leading zeros), then numeric value, then original.
125
+ def compare_integer_strings(a, b)
126
+ aa = a.sub(/\A0+/, '')
127
+ bb = b.sub(/\A0+/, '')
128
+ aa = '0' if aa.empty?
129
+ bb = '0' if bb.empty?
130
+
131
+ r = aa.length <=> bb.length
132
+ return r unless r.zero?
133
+
134
+ r = aa <=> bb
135
+ return r unless r.zero?
136
+
137
+ a <=> b
138
+ end
139
+ end
140
+ end
@@ -3,7 +3,7 @@
3
3
  # UnicodePlot - Plot your data by Unicode characters
4
4
  # https://github.com/red-data-tools/unicode_plot.rb
5
5
 
6
- require_relative 'processing'
6
+ require_relative '../aggregation'
7
7
  require 'unicode_plot'
8
8
 
9
9
  # If the line color is specified as a number, the program will display an error
@@ -40,13 +40,14 @@ module YouPlot
40
40
  series = data.series
41
41
  # `uplot count`
42
42
  if count
43
- series = Processing.count_values(series[0], reverse: reverse)
43
+ series = YouPlot::Aggregation.count_values(series[0], reverse: reverse)
44
44
  params.title = headers[0] if headers
45
45
  end
46
46
  if series.size == 1
47
47
  # If there is only one series.use the line number for label.
48
48
  params.title ||= headers[0] if headers
49
49
  labels = Array.new(series[0].size) { |i| (i + 1).to_s }
50
+ raw_values = series[0]
50
51
  values = series[0].map(&:to_f)
51
52
  else
52
53
  # If there are 2 or more series...
@@ -61,11 +62,28 @@ module YouPlot
61
62
  end
62
63
  params.title ||= headers[y_col] if headers
63
64
  labels = series[x_col]
64
- values = series[y_col].map(&:to_f)
65
+ raw_values = series[y_col]
66
+ values = if count
67
+ series[y_col].map(&:to_i)
68
+ else
69
+ series[y_col].map(&:to_f)
70
+ end
65
71
  end
72
+ values = values.map(&:to_i) if count || integer_display_values?(values, raw_values)
66
73
  ::UnicodePlot.barplot(labels, values, **params.to_hc)
67
74
  end
68
75
 
76
+ # True only for integer literals: "3" => true, "3.0" => false.
77
+ def integer_display_values?(values, raw_values)
78
+ values.all? { |v| v.finite? && v == v.to_i } &&
79
+ raw_values.all? { |v| integer_literal?(v) }
80
+ end
81
+
82
+ def integer_literal?(value)
83
+ # Matches "3" and "-12", but not "3.0".
84
+ value.to_s.match?(/\A[+-]?\d+\z/)
85
+ end
86
+
69
87
  def histogram(data, params)
70
88
  headers = data.headers
71
89
  series = data.series
@@ -200,27 +218,37 @@ module YouPlot
200
218
 
201
219
  def check_series_size(data, fmt)
202
220
  series = data.series
203
- if series.size == 1
204
- warn <<~EOS
205
- YouPlot: There is only one series of input data. Please check the delimiter.
206
-
207
- Headers: \e[35m#{data.headers.inspect}\e[0m
208
- The first item is: \e[35m\"#{series[0][0]}\"\e[0m
209
- The last item is : \e[35m\"#{series[0][-1]}\"\e[0m
210
- EOS
211
- # NOTE: Error messages cannot be colored.
212
- YouPlot.run_as_executable ? exit(1) : raise(Error)
213
- end
214
- if fmt == 'xyxy' && series.size.odd?
215
- warn <<~EOS
216
- YouPlot: In the xyxy format, the number of series must be even.
217
-
218
- Number of series: \e[35m#{series.size}\e[0m
219
- Headers: \e[35m#{data.headers.inspect}\e[0m
220
- EOS
221
- # NOTE: Error messages cannot be colored.
222
- YouPlot.run_as_executable ? exit(1) : raise(Error)
223
- end
221
+ raise_if_single_series(data, series) if series.size == 1
222
+ raise_if_odd_series_for_xyxy(data, fmt, series)
223
+ end
224
+
225
+ def raise_if_single_series(data, series)
226
+ warn <<~EOS
227
+ YouPlot: There is only one series of input data. Please check the delimiter.
228
+
229
+ Headers: \e[35m#{data.headers.inspect}\e[0m
230
+ The first item is: \e[35m\"#{series[0][0]}\"\e[0m
231
+ The last item is : \e[35m\"#{series[0][-1]}\"\e[0m
232
+ EOS
233
+ raise_plot_error
234
+ end
235
+
236
+ def raise_if_odd_series_for_xyxy(data, fmt, series)
237
+ return unless fmt == 'xyxy'
238
+ return if series.size.even?
239
+
240
+ warn <<~EOS
241
+ YouPlot: In the xyxy format, the number of series must be even.
242
+
243
+ Number of series: \e[35m#{series.size}\e[0m
244
+ Headers: \e[35m#{data.headers.inspect}\e[0m
245
+ EOS
246
+ raise_plot_error
247
+ end
248
+
249
+ def raise_plot_error
250
+ # NOTE: Error messages cannot be colored.
251
+ YouPlot.run_as_executable ? exit(1) : raise(Error)
224
252
  end
225
253
  end
226
254
  end