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 +4 -4
- data/LICENSE.txt +1 -0
- data/README.md +36 -29
- data/lib/youplot/aggregation.rb +140 -0
- data/lib/youplot/backends/unicode_plot.rb +52 -24
- data/lib/youplot/command.rb +185 -67
- data/lib/youplot/dsv.rb +16 -8
- data/lib/youplot/options.rb +20 -0
- data/lib/youplot/parser.rb +60 -32
- data/lib/youplot/version.rb +1 -1
- data/logo.svg +319 -0
- data/sig/youplot/aggregation.rbs +5 -0
- metadata +6 -7
- data/lib/youplot/backends/processing.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c344612a2c9055f37b06ea9e6cceb5199e18a261b1ce4bb4c2d3ec74f77657ad
|
|
4
|
+
data.tar.gz: 736041b6139ebc559b78176f3ed0908469e6fc8c21e0119ff4f62641a357ffc8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b318d624ece5511feacd5f1a9c50ea929b6c1c1f05d951a50f61869e52a66b8adf7d613c3042345b0455b2c36eca56088144ee0967f188c3cd7c12910ecf7579
|
|
7
|
+
data.tar.gz: 2570196b9263edea40227244909e5e9fe287854e7daa2d5d2f44bd1fa617faa5716b38979eed40a84c1687aac71be8b6005ed902d83aa1f6dce54fea60f4dd66
|
data/LICENSE.txt
CHANGED
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
|
-
|
|
93
|
-
python3 -c '
|
|
94
|
+
python3 - <<'PY' | uplot line
|
|
94
95
|
from math import sin, pi
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
303
|
-
bundle exec rake test
|
|
304
|
-
bundle exec rake install
|
|
305
|
-
bundle exec exe/uplot
|
|
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://
|
|
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 '
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|