plotext-plus 1.0.8__py3-none-any.whl → 1.0.10__py3-none-any.whl
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.
- plotext_plus/__init__.py +20 -15
- plotext_plus/__main__.py +1 -0
- plotext_plus/_api.py +632 -371
- plotext_plus/_build.py +765 -149
- plotext_plus/_core.py +609 -164
- plotext_plus/_date.py +50 -32
- plotext_plus/_default.py +35 -28
- plotext_plus/_dict.py +807 -103
- plotext_plus/_doc.py +867 -296
- plotext_plus/_doc_utils.py +279 -245
- plotext_plus/_figure.py +1295 -303
- plotext_plus/_global.py +238 -140
- plotext_plus/_matrix.py +217 -63
- plotext_plus/_monitor.py +1036 -489
- plotext_plus/_output.py +29 -23
- plotext_plus/_shtab.py +2 -0
- plotext_plus/_themes.py +363 -247
- plotext_plus/_utility.py +581 -313
- plotext_plus/api.py +418 -332
- plotext_plus/charts.py +47 -24
- plotext_plus/core.py +570 -177
- plotext_plus/mcp_cli.py +15 -13
- plotext_plus/mcp_server.py +842 -166
- plotext_plus/plotext_cli.py +414 -275
- plotext_plus/plotting.py +86 -70
- plotext_plus/themes.py +10 -13
- plotext_plus/utilities.py +33 -33
- plotext_plus/utils.py +240 -140
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/METADATA +7 -2
- plotext_plus-1.0.10.dist-info/RECORD +33 -0
- plotext_plus-1.0.8.dist-info/RECORD +0 -33
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/WHEEL +0 -0
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/entry_points.txt +0 -0
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/licenses/LICENSE +0 -0
plotext_plus/_utility.py
CHANGED
|
@@ -1,33 +1,50 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import inspect
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from plotext_plus._dict import * # noqa: F403
|
|
3
9
|
|
|
4
10
|
###############################################
|
|
5
11
|
######### Number Manipulation ##########
|
|
6
12
|
###############################################
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
|
|
15
|
+
def round(
|
|
16
|
+
n, d=0
|
|
17
|
+
): # the standard round(0.5) = 0 instead of 1; this version rounds 0.5 to 1
|
|
18
|
+
n *= 10**d
|
|
10
19
|
f = math.floor(n)
|
|
11
20
|
r = f if n - f < 0.5 else math.ceil(n)
|
|
12
21
|
return r * 10 ** (-d)
|
|
13
22
|
|
|
14
|
-
def mean(x, y, p = 1): # mean of x and y with optional power p; if p tends to 0 the minimum is returned; if p tends to infinity the max is returned; p = 1 is the standard mean
|
|
15
|
-
return ((x ** p + y ** p) / 2) ** (1 / p)
|
|
16
23
|
|
|
17
|
-
def
|
|
24
|
+
def mean(
|
|
25
|
+
x, y, p=1
|
|
26
|
+
): # mean of x and y with optional power p; if p tends to 0 the minimum is returned; if p tends to infinity the max is returned; p = 1 is the standard mean
|
|
27
|
+
return ((x**p + y**p) / 2) ** (1 / p)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def replace(
|
|
31
|
+
data, data2, element=None
|
|
32
|
+
): # replace element in data with correspondent in data2 when element is found
|
|
18
33
|
res = []
|
|
19
34
|
for i in range(len(data)):
|
|
20
35
|
el = data[i] if data[i] != element else data2[i]
|
|
21
36
|
res.append(el)
|
|
22
37
|
return res
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
|
|
40
|
+
def try_float(data):
|
|
25
41
|
try:
|
|
26
42
|
return float(data)
|
|
27
|
-
except:
|
|
43
|
+
except (ValueError, TypeError):
|
|
28
44
|
return data
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
|
|
47
|
+
def quantile(data, q): # calculate the quantile of a given array
|
|
31
48
|
data = sorted(data)
|
|
32
49
|
index = q * (len(data) - 1)
|
|
33
50
|
if index.is_integer():
|
|
@@ -35,183 +52,259 @@ def quantile(data, q): # calculate the quantile of a given array
|
|
|
35
52
|
else:
|
|
36
53
|
return (data[int(index)] + data[int(index) + 1]) / 2
|
|
37
54
|
|
|
55
|
+
|
|
38
56
|
###############################################
|
|
39
57
|
########### List Creation ##############
|
|
40
58
|
###############################################
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
|
|
61
|
+
def linspace(
|
|
62
|
+
lower, upper, length=10
|
|
63
|
+
): # it returns a lists of numbers from lower to upper with given length
|
|
43
64
|
slope = (upper - lower) / (length - 1) if length > 1 else 0
|
|
44
65
|
return [lower + x * slope for x in range(length)]
|
|
45
66
|
|
|
46
|
-
|
|
67
|
+
|
|
68
|
+
def sin(
|
|
69
|
+
periods=2, length=200, amplitude=1, phase=0, decay=0
|
|
70
|
+
): # sinusoidal data with given parameters
|
|
47
71
|
f = 2 * math.pi * periods / (length - 1)
|
|
48
|
-
phase =
|
|
72
|
+
phase = math.pi * phase
|
|
49
73
|
d = decay / length
|
|
50
|
-
return [
|
|
74
|
+
return [
|
|
75
|
+
amplitude * math.sin(f * el + phase) * math.exp(-d * el) for el in range(length)
|
|
76
|
+
]
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
|
|
79
|
+
def square(periods=2, length=200, amplitude=1):
|
|
80
|
+
period_length = length / periods
|
|
81
|
+
def step(t):
|
|
82
|
+
return amplitude if t % period_length <= period_length / 2 else -amplitude
|
|
55
83
|
return [step(i) for i in range(length)]
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
|
|
86
|
+
def to_list(
|
|
87
|
+
data, length
|
|
88
|
+
): # eg: to_list(1, 3) = [1, 1 ,1]; to_list([1,2,3], 6) = [1, 2, 3, 1, 2, 3]
|
|
58
89
|
data = data if isinstance(data, list) else [data] * length
|
|
59
90
|
data = data * math.ceil(length / len(data)) if len(data) > 0 else []
|
|
60
|
-
return data[
|
|
91
|
+
return data[:length]
|
|
92
|
+
|
|
61
93
|
|
|
62
|
-
def difference(data1, data2)
|
|
94
|
+
def difference(data1, data2): # elements in data1 not in date2
|
|
63
95
|
return [el for el in data1 if el not in data2]
|
|
64
96
|
|
|
97
|
+
|
|
65
98
|
###############################################
|
|
66
99
|
######### List Transformation ##########
|
|
67
100
|
###############################################
|
|
68
101
|
|
|
69
|
-
def log(data): # it apply log function to the data
|
|
70
|
-
return [math.log10(el) for el in data] if isinstance(data, list) else math.log10(data)
|
|
71
102
|
|
|
72
|
-
def
|
|
73
|
-
return
|
|
103
|
+
def log(data): # it apply log function to the data
|
|
104
|
+
return (
|
|
105
|
+
[math.log10(el) for el in data] if isinstance(data, list) else math.log10(data)
|
|
106
|
+
)
|
|
107
|
+
|
|
74
108
|
|
|
75
|
-
def
|
|
109
|
+
def power10(data): # it apply log function to the data
|
|
110
|
+
return [10**el for el in data]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def floor(data): # it floors a list of data
|
|
76
114
|
return list(map(math.floor, data))
|
|
77
115
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
|
|
117
|
+
def repeat(data, length): # repeat the same data till length is reached
|
|
118
|
+
data_len = len(data) if isinstance(data, list) else 1
|
|
119
|
+
data = join([data] * math.ceil(length / data_len))
|
|
120
|
+
return data[:length]
|
|
121
|
+
|
|
82
122
|
|
|
83
123
|
###############################################
|
|
84
124
|
########## List Manipulation ###########
|
|
85
125
|
###############################################
|
|
86
126
|
|
|
87
|
-
def no_duplicates(data): # removes duplicates from a list
|
|
88
|
-
return list(set(list(data)))
|
|
89
|
-
#return list(dict.fromkeys(data)) # it takes double time
|
|
90
127
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
return
|
|
128
|
+
def no_duplicates(data): # removes duplicates from a list
|
|
129
|
+
return list(set(data))
|
|
130
|
+
# return list(dict.fromkeys(data)) # it takes double time
|
|
94
131
|
|
|
95
|
-
|
|
132
|
+
|
|
133
|
+
def join(data): # flatten lists at first level
|
|
134
|
+
# return [el for row in data for el in row]
|
|
135
|
+
return [el for row in data for el in (join(row) if isinstance(row, list) else [row])]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cumsum(
|
|
139
|
+
data,
|
|
140
|
+
): # it returns the cumulative sums of a list; eg: cumsum([0,1,2,3,4]) = [0,1,3,6,10]
|
|
96
141
|
s = [0]
|
|
97
142
|
for i in range(len(data)):
|
|
98
143
|
s.append(s[-1] + data[i])
|
|
99
144
|
return s[1:]
|
|
100
145
|
|
|
146
|
+
|
|
101
147
|
###############################################
|
|
102
148
|
######### Matrix Manipulation ##########
|
|
103
149
|
###############################################
|
|
104
150
|
|
|
151
|
+
|
|
105
152
|
def matrix_size(matrix): # cols, height
|
|
106
153
|
return [len(matrix[0]), len(matrix)] if matrix != [] else [0, 0]
|
|
107
154
|
|
|
108
|
-
def transpose(data, length = 1): # it needs no explanation
|
|
109
|
-
return [[]] * length if data == [] else list(map(list, zip(*data)))
|
|
110
155
|
|
|
111
|
-
def
|
|
112
|
-
return
|
|
156
|
+
def transpose(data, length=1): # it needs no explanation
|
|
157
|
+
return [[]] * length if data == [] else list(map(list, zip(*data, strict=False)))
|
|
113
158
|
|
|
114
|
-
|
|
159
|
+
|
|
160
|
+
def vstack(matrix, extra): # vertical stack of two matrices
|
|
161
|
+
return extra + matrix # + extra
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def hstack(matrix, extra): # horizontal stack of two matrices
|
|
115
165
|
lm, le = len(matrix), len(extra)
|
|
116
|
-
|
|
117
|
-
return [matrix[i] + extra[i] for i in range(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
max_length = max(lm, le)
|
|
167
|
+
return [matrix[i] + extra[i] for i in range(max_length)]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def turn_gray(matrix): # it takes a standard matrix and turns it into an grayscale one
|
|
171
|
+
max_val, m = max(join(matrix), default=0), min(join(matrix), default=0)
|
|
172
|
+
def to_gray(el):
|
|
173
|
+
return (
|
|
174
|
+
tuple([int(255 * (el - m) / (max_val - m))] * 3) if m != max_val else (127, 127, 127)
|
|
175
|
+
)
|
|
176
|
+
return [[to_gray(el) for el in row] for row in matrix]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def brush(*lists): # remove duplicates from lists x, y, z ...
|
|
180
|
+
min_length = min(map(len, lists))
|
|
181
|
+
lists = [el[:min_length] for el in lists]
|
|
182
|
+
z = list(zip(*lists, strict=False))
|
|
128
183
|
z = no_duplicates(z)
|
|
129
|
-
#z = sorted(z)#, key = lambda x: x[0])
|
|
184
|
+
# z = sorted(z)#, key = lambda x: x[0])
|
|
130
185
|
lists = transpose(z, len(lists))
|
|
131
186
|
return lists
|
|
132
187
|
|
|
188
|
+
|
|
133
189
|
###############################################
|
|
134
190
|
######### String Manipulation ###########
|
|
135
191
|
###############################################
|
|
136
192
|
|
|
137
193
|
nl = "\n"
|
|
138
194
|
|
|
139
|
-
def only_spaces(string): # it returns True if string is made of only empty spaces or is None or ''
|
|
140
|
-
return (type(string) == str) and (string == len(string) * space) #and len(string) != 0
|
|
141
195
|
|
|
142
|
-
def
|
|
196
|
+
def only_spaces(
|
|
197
|
+
string,
|
|
198
|
+
): # it returns True if string is made of only empty spaces or is None or ''
|
|
199
|
+
return isinstance(string, str) and (
|
|
200
|
+
string == len(string) * space
|
|
201
|
+
) # and len(string) != 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def format_time(time): # it properly formats the computational time
|
|
143
205
|
t = time if time is not None else 0
|
|
144
|
-
unit =
|
|
145
|
-
p = 0 if unit ==
|
|
146
|
-
t = round(10
|
|
147
|
-
|
|
206
|
+
unit = "s" if t >= 1 else "ms" if t >= 10**-3 else "µs"
|
|
207
|
+
p = 0 if unit == "s" else 3 if unit == "ms" else 6
|
|
208
|
+
t = round(10**p * t, 1)
|
|
209
|
+
str_length = len(str(int(t)))
|
|
148
210
|
t = str(t)
|
|
149
|
-
#t = ' ' * (3 -
|
|
150
|
-
return t[
|
|
211
|
+
# t = ' ' * (3 - str_length) + t
|
|
212
|
+
return t[: str_length + 2] + " " + unit
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
positive_color = "green+"
|
|
216
|
+
negative_color = "red"
|
|
217
|
+
title_color = "cyan+"
|
|
218
|
+
|
|
151
219
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
def format_strings(
|
|
221
|
+
string1, string2, color=positive_color
|
|
222
|
+
): # returns string1 in bold and with color + string2 with a pre-formatted style
|
|
223
|
+
return colorize(string1, color, "bold") + " " + colorize(string2, style=info_style)
|
|
155
224
|
|
|
156
|
-
def format_strings(string1, string2, color = positive_color): # returns string1 in bold and with color + string2 with a pre-formatted style
|
|
157
|
-
return colorize(string1, color, "bold") + " " + colorize(string2, style = info_style)
|
|
158
225
|
|
|
159
|
-
def correct_coord(
|
|
160
|
-
|
|
161
|
-
|
|
226
|
+
def correct_coord(
|
|
227
|
+
string, label, coord
|
|
228
|
+
): # In the attempt to insert a label in string at given coordinate, the coordinate is adjusted so not to hit the borders of the string
|
|
229
|
+
label_length = len(label)
|
|
230
|
+
b, e = max(coord - label_length + 1, 0), min(coord + label_length, len(string) - 1)
|
|
162
231
|
data = [i for i in range(b, e) if string[i] is space]
|
|
163
|
-
b, e = min(data, default
|
|
164
|
-
b, e = e -
|
|
165
|
-
return (b + e -
|
|
232
|
+
b, e = min(data, default=coord - label_length + 1), max(data, default=coord + label_length)
|
|
233
|
+
b, e = e - label_length + 1, b + label_length
|
|
234
|
+
return (b + e - label_length) // 2
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
|
|
236
|
+
|
|
237
|
+
def no_char_duplicates(string, char): # it remove char duplicates from string
|
|
238
|
+
pattern = char + "{2,}"
|
|
169
239
|
string = re.sub(pattern, char, string)
|
|
170
240
|
return string
|
|
171
241
|
|
|
172
|
-
|
|
242
|
+
|
|
243
|
+
def read_lines(
|
|
244
|
+
text, delimiter=None, columns=None
|
|
245
|
+
): # from a long text to well formatted data
|
|
173
246
|
delimiter = " " if delimiter is None else delimiter
|
|
174
247
|
data = []
|
|
175
|
-
columns =
|
|
248
|
+
columns = (
|
|
249
|
+
len(no_char_duplicates(text[0], delimiter).split(delimiter))
|
|
250
|
+
if columns is None
|
|
251
|
+
else columns
|
|
252
|
+
)
|
|
176
253
|
for i in range(len(text)):
|
|
177
254
|
row = text[i]
|
|
178
255
|
row = no_char_duplicates(row, delimiter)
|
|
179
256
|
row = row.split(delimiter)
|
|
180
|
-
row = [el.replace(
|
|
257
|
+
row = [el.replace("\n", "") for el in row]
|
|
181
258
|
cols = len(row)
|
|
182
|
-
row = [
|
|
259
|
+
row = [
|
|
260
|
+
row[col].replace("\n", "") if col in range(cols) else ""
|
|
261
|
+
for col in range(columns)
|
|
262
|
+
]
|
|
183
263
|
row = [try_float(el) for el in row]
|
|
184
264
|
data.append(row)
|
|
185
265
|
return data
|
|
186
266
|
|
|
187
|
-
|
|
267
|
+
|
|
268
|
+
def pad_string(num, length): # pad a number with spaces before to reach length
|
|
188
269
|
num = str(num)
|
|
189
|
-
|
|
190
|
-
return num +
|
|
270
|
+
num_length = len(num)
|
|
271
|
+
return num + " " * (length - num_length)
|
|
272
|
+
|
|
191
273
|
|
|
192
274
|
def max_length(strings):
|
|
193
275
|
strings = map(str, strings)
|
|
194
|
-
return max(map(len, strings), default
|
|
276
|
+
return max(map(len, strings), default=0)
|
|
277
|
+
|
|
195
278
|
|
|
196
279
|
###############################################
|
|
197
280
|
########## File Manipulation ############
|
|
198
281
|
###############################################
|
|
199
282
|
|
|
283
|
+
|
|
200
284
|
def correct_path(path):
|
|
201
285
|
folder, base = os.path.dirname(path), os.path.basename(path)
|
|
202
|
-
folder = os.path.expanduser("~") if folder in [
|
|
286
|
+
folder = os.path.expanduser("~") if folder in ["", "~"] else folder
|
|
203
287
|
path = os.path.join(folder, base)
|
|
204
288
|
return path
|
|
205
289
|
|
|
206
|
-
|
|
290
|
+
|
|
291
|
+
def is_file(path, log=True): # returns True if path exists
|
|
207
292
|
res = os.path.isfile(path)
|
|
208
|
-
|
|
293
|
+
(
|
|
294
|
+
print(format_strings("not a file:", path, negative_color))
|
|
295
|
+
if not res and log
|
|
296
|
+
else None
|
|
297
|
+
)
|
|
209
298
|
return res
|
|
210
299
|
|
|
211
|
-
|
|
300
|
+
|
|
301
|
+
def script_folder(): # the folder of the script executed
|
|
212
302
|
return parent_folder(inspect.getfile(sys._getframe(1)))
|
|
213
303
|
|
|
214
|
-
|
|
304
|
+
|
|
305
|
+
def parent_folder(
|
|
306
|
+
path, level=1
|
|
307
|
+
): # it return the parent folder of the path or file given; if level is higher then 1 the process is iterated
|
|
215
308
|
if level <= 0:
|
|
216
309
|
return path
|
|
217
310
|
elif level == 1:
|
|
@@ -219,27 +312,36 @@ def parent_folder(path, level = 1): # it return the parent folder of the path or
|
|
|
219
312
|
else:
|
|
220
313
|
return parent_folder(parent_folder(path, level - 1))
|
|
221
314
|
|
|
222
|
-
|
|
315
|
+
|
|
316
|
+
def join_paths(
|
|
317
|
+
*args,
|
|
318
|
+
): # it join a list of string in a proper file path; if the first argument is ~ it is turnded into the used home folder path
|
|
223
319
|
args = list(args)
|
|
224
|
-
args[0] =
|
|
320
|
+
args[0] = correct_path(args[0]) if args[0] == "~" else args[0]
|
|
225
321
|
return os.path.abspath(os.path.join(*args))
|
|
226
322
|
|
|
227
|
-
|
|
323
|
+
|
|
324
|
+
def delete_file(path, log=True): # remove the file if it exists
|
|
228
325
|
path = correct_path(path)
|
|
229
326
|
if is_file(path):
|
|
230
327
|
os.remove(path)
|
|
231
328
|
print(format_strings("file removed:", path, negative_color)) if log else None
|
|
232
329
|
|
|
233
|
-
|
|
330
|
+
|
|
331
|
+
def read_data(
|
|
332
|
+
path, delimiter=None, columns=None, first_row=None, log=True
|
|
333
|
+
): # it turns a text file into data lists
|
|
234
334
|
path = correct_path(path)
|
|
235
335
|
first_row = 0 if first_row is None else int(first_row)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
file.close()
|
|
336
|
+
with open(path) as file:
|
|
337
|
+
text = file.readlines()[first_row:]
|
|
239
338
|
print(format_strings("data read from", path)) if log else None
|
|
240
339
|
return read_lines(text, delimiter, columns)
|
|
241
340
|
|
|
242
|
-
|
|
341
|
+
|
|
342
|
+
def write_data(
|
|
343
|
+
data, path, delimiter=None, columns=None, log=True
|
|
344
|
+
): # it turns a matrix into a text file
|
|
243
345
|
delimiter = " " if delimiter is None else delimiter
|
|
244
346
|
cols = len(data[0])
|
|
245
347
|
cols = range(1, cols + 1) if columns is None else columns
|
|
@@ -247,158 +349,217 @@ def write_data(data, path, delimiter = None, columns = None, log = True): # it t
|
|
|
247
349
|
for row in data:
|
|
248
350
|
row = [row[i - 1] for i in cols]
|
|
249
351
|
row = list(map(str, row))
|
|
250
|
-
text += delimiter.join(row) +
|
|
251
|
-
save_text(text, path, log
|
|
352
|
+
text += delimiter.join(row) + "\n"
|
|
353
|
+
save_text(text, path, log=log)
|
|
354
|
+
|
|
252
355
|
|
|
253
|
-
def save_text(
|
|
356
|
+
def save_text(
|
|
357
|
+
text, path, append=False, log=True
|
|
358
|
+
): # it saves some text to the path selected
|
|
254
359
|
path = correct_path(path)
|
|
255
360
|
mode = "a" if append else "w+"
|
|
256
|
-
with open(path
|
|
361
|
+
with open(path, mode, encoding="utf-8") as file:
|
|
257
362
|
file.write(text)
|
|
258
363
|
print(format_strings("text saved in", path)) if log else None
|
|
259
364
|
|
|
260
|
-
|
|
365
|
+
|
|
366
|
+
def download(
|
|
367
|
+
url, path, log=True
|
|
368
|
+
): # it download the url (image, video, gif etc) to path
|
|
369
|
+
from urllib.parse import urlparse
|
|
261
370
|
from urllib.request import urlretrieve
|
|
371
|
+
|
|
372
|
+
# Validate URL scheme for security (B310)
|
|
373
|
+
parsed_url = urlparse(url)
|
|
374
|
+
allowed_schemes = {'http', 'https'}
|
|
375
|
+
if parsed_url.scheme.lower() not in allowed_schemes:
|
|
376
|
+
raise ValueError(f"URL scheme '{parsed_url.scheme}' not allowed. Only {allowed_schemes} are permitted.")
|
|
377
|
+
|
|
378
|
+
# Validate URL has a network location
|
|
379
|
+
if not parsed_url.netloc:
|
|
380
|
+
raise ValueError("Invalid URL: missing network location")
|
|
381
|
+
|
|
262
382
|
path = correct_path(path)
|
|
263
|
-
urlretrieve(url, path)
|
|
264
|
-
print(format_strings(
|
|
383
|
+
urlretrieve(url, path) # noqa: S310 # URL scheme already validated above
|
|
384
|
+
print(format_strings("url saved in", path)) if log else None
|
|
385
|
+
|
|
265
386
|
|
|
266
387
|
###############################################
|
|
267
388
|
######### Platform Utilities ############
|
|
268
389
|
###############################################
|
|
269
390
|
|
|
270
|
-
|
|
391
|
+
|
|
392
|
+
def is_ipython() -> bool: # true if running in ipython shenn
|
|
271
393
|
try:
|
|
272
|
-
__IPYTHON__
|
|
394
|
+
__IPYTHON__ # noqa: B018 # Intentional check for IPython existence
|
|
273
395
|
return True
|
|
274
396
|
except NameError:
|
|
275
397
|
return False
|
|
276
398
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
399
|
+
|
|
400
|
+
def platform() -> str: # the platform (unix or windows) you are using plotext in
|
|
401
|
+
platform = sys.platform
|
|
402
|
+
if platform in {"win32", "cygwin"}:
|
|
403
|
+
return "windows"
|
|
404
|
+
else:
|
|
405
|
+
return "unix"
|
|
406
|
+
|
|
283
407
|
|
|
284
408
|
platform = platform()
|
|
285
409
|
|
|
286
|
-
# to enable ascii escape color sequences
|
|
410
|
+
# to enable ascii escape color sequences on Windows
|
|
287
411
|
if platform == "windows":
|
|
288
|
-
import
|
|
289
|
-
|
|
412
|
+
import os
|
|
413
|
+
# Enable ANSI escape sequences on Windows
|
|
414
|
+
# This is safer than using subprocess with shell=True
|
|
415
|
+
os.system("") # nosec B605 # noqa: S605,S607 - minimal safe command for Windows ANSI enabling
|
|
290
416
|
|
|
291
|
-
|
|
417
|
+
|
|
418
|
+
def terminal_size(): # it returns the terminal size as [width, height]
|
|
292
419
|
try:
|
|
293
420
|
size = shutil.get_terminal_size()
|
|
294
421
|
return list(size)
|
|
295
422
|
except OSError:
|
|
296
423
|
return [None, None]
|
|
297
424
|
|
|
298
|
-
|
|
425
|
+
|
|
426
|
+
def terminal_width(): # returns terminal width, adjusted for banner borders when banners are enabled
|
|
299
427
|
width = terminal_size()[0]
|
|
300
428
|
if width is None:
|
|
301
429
|
return None
|
|
302
|
-
|
|
430
|
+
|
|
303
431
|
# Check if banners are enabled by checking the global output instance
|
|
304
432
|
try:
|
|
305
433
|
from plotext_plus._output import get_output_instance
|
|
434
|
+
|
|
306
435
|
output_instance = get_output_instance()
|
|
307
|
-
if hasattr(output_instance,
|
|
436
|
+
if hasattr(output_instance, "use_banners") and output_instance.use_banners:
|
|
308
437
|
# Rich Panel with borders and padding=(0, 1) uses:
|
|
309
438
|
# - 2 characters for left/right borders
|
|
310
439
|
# - 2 characters for padding (1 on each side)
|
|
311
440
|
# Total: 4 characters of horizontal space
|
|
312
441
|
return max(width - 4, 1) # Ensure minimum width of 1
|
|
313
|
-
except:
|
|
442
|
+
except (ImportError, AttributeError):
|
|
314
443
|
pass # If anything fails, fall back to original width
|
|
315
|
-
|
|
444
|
+
|
|
316
445
|
return width
|
|
317
446
|
|
|
447
|
+
|
|
318
448
|
tw = terminal_width
|
|
319
449
|
|
|
320
|
-
terminal_height
|
|
450
|
+
def terminal_height():
|
|
451
|
+
return terminal_size()[1]
|
|
321
452
|
th = terminal_height
|
|
322
453
|
|
|
323
|
-
|
|
454
|
+
|
|
455
|
+
def clear_terminal(
|
|
456
|
+
lines=None,
|
|
457
|
+
): # it cleat the entire terminal, or the specified number of lines
|
|
324
458
|
if lines is None:
|
|
325
|
-
write(
|
|
459
|
+
write("\033c")
|
|
326
460
|
else:
|
|
327
|
-
for
|
|
328
|
-
write("\033[A")
|
|
329
|
-
write("\033[2K")
|
|
461
|
+
for _r in range(lines):
|
|
462
|
+
write("\033[A") # moves the curson up
|
|
463
|
+
write("\033[2K") # clear the entire line
|
|
464
|
+
|
|
330
465
|
|
|
331
|
-
def write(string):
|
|
466
|
+
def write(string): # the print function used by plotext - now uses chuk-term backend
|
|
332
467
|
from plotext_plus._output import write as output_write
|
|
468
|
+
|
|
333
469
|
output_write(string)
|
|
334
470
|
|
|
335
|
-
|
|
471
|
+
|
|
472
|
+
class Memorize: # it memorise the arguments of a function, when used as its decorator, to reduce computational time
|
|
336
473
|
def __init__(self, f):
|
|
337
474
|
self.f = f
|
|
338
475
|
self.memo = {}
|
|
476
|
+
|
|
339
477
|
def __call__(self, *args):
|
|
340
|
-
if not
|
|
478
|
+
if args not in self.memo:
|
|
341
479
|
self.memo[args] = self.f(*args)
|
|
342
480
|
return self.memo[args]
|
|
343
481
|
|
|
482
|
+
|
|
344
483
|
##############################################
|
|
345
484
|
######### Marker Utilities ###########
|
|
346
485
|
##############################################
|
|
347
486
|
|
|
348
|
-
space =
|
|
349
|
-
plot_marker = "hd" if platform ==
|
|
487
|
+
space = " " # the default null character that appears as background to all plots
|
|
488
|
+
plot_marker = "hd" if platform == "unix" else "dot"
|
|
350
489
|
|
|
351
|
-
hd_markers = {hd_codes[el]
|
|
352
|
-
fhd_markers = {fhd_codes[el]
|
|
353
|
-
braille_markers = {braille_codes[el]
|
|
354
|
-
simple_bar_marker =
|
|
490
|
+
hd_markers = {hd_codes[el]: el for el in hd_codes}
|
|
491
|
+
fhd_markers = {fhd_codes[el]: el for el in fhd_codes}
|
|
492
|
+
braille_markers = {braille_codes[el]: el for el in braille_codes}
|
|
493
|
+
simple_bar_marker = "▇"
|
|
355
494
|
|
|
356
|
-
|
|
495
|
+
|
|
496
|
+
@Memorize
|
|
357
497
|
def get_hd_marker(code):
|
|
358
|
-
|
|
498
|
+
return (
|
|
499
|
+
hd_codes[code]
|
|
500
|
+
if len(code) == 4
|
|
501
|
+
else fhd_codes[code] if len(code) == 6 else braille_codes[code]
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def marker_factor(
|
|
506
|
+
marker, hd, fhd, braille
|
|
507
|
+
): # useful to improve the resolution of the canvas for higher resolution markers
|
|
508
|
+
return (
|
|
509
|
+
hd
|
|
510
|
+
if marker == "hd"
|
|
511
|
+
else fhd if marker == "fhd" else braille if marker == "braille" else 1
|
|
512
|
+
)
|
|
359
513
|
|
|
360
|
-
def marker_factor(marker, hd, fhd, braille): # useful to improve the resolution of the canvas for higher resolution markers
|
|
361
|
-
return hd if marker == 'hd' else fhd if marker == 'fhd' else braille if marker == 'braille' else 1
|
|
362
514
|
|
|
363
515
|
##############################################
|
|
364
516
|
########### Color Utilities ############
|
|
365
517
|
##############################################
|
|
366
518
|
|
|
367
519
|
# A user could specify three types of colors
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
520
|
+
# an integer for 256 color codes
|
|
521
|
+
# a tuple for RGB color codes
|
|
522
|
+
# a string for 16 color codes or styles
|
|
371
523
|
|
|
372
524
|
# Along side the user needs to specify whatever it is for background / fullground / style
|
|
373
525
|
# which plotext calls 'character' = 0 / 1 / 2
|
|
374
526
|
|
|
375
527
|
|
|
376
|
-
#colors_no_plus = [el for el in colors if '+' not in el and el + '+' not in colors and el is not no_color] # basically just [black, white]
|
|
528
|
+
# colors_no_plus = [el for el in colors if '+' not in el and el + '+' not in colors and el is not no_color] # basically just [black, white]
|
|
377
529
|
|
|
378
|
-
|
|
530
|
+
|
|
531
|
+
def get_color_code(color): # the color number code from color string
|
|
379
532
|
color = color.strip()
|
|
380
533
|
return color_codes[color]
|
|
381
534
|
|
|
382
|
-
|
|
535
|
+
|
|
536
|
+
def get_color_name(code): # the color string from color number code
|
|
383
537
|
codes = list(color_codes.values())
|
|
384
538
|
return colors[codes.index(code)] if code in codes else no_color
|
|
385
539
|
|
|
540
|
+
|
|
386
541
|
def is_string_color(color):
|
|
387
542
|
return isinstance(color, str) and color.strip() in colors
|
|
388
543
|
|
|
544
|
+
|
|
389
545
|
def is_integer_color(color):
|
|
390
546
|
return isinstance(color, int) and 0 <= color <= 255
|
|
391
547
|
|
|
548
|
+
|
|
392
549
|
def is_rgb_color(color):
|
|
393
|
-
is_rgb = isinstance(color, list
|
|
550
|
+
is_rgb = isinstance(color, (list, tuple))
|
|
394
551
|
is_rgb = is_rgb and len(color) == 3
|
|
395
|
-
is_rgb = is_rgb and all(
|
|
552
|
+
is_rgb = is_rgb and all(is_integer_color(el) for el in color)
|
|
396
553
|
return is_rgb
|
|
397
554
|
|
|
555
|
+
|
|
398
556
|
def is_color(color):
|
|
399
557
|
return is_string_color(color) or is_integer_color(color) or is_rgb_color(color)
|
|
400
558
|
|
|
401
|
-
|
|
559
|
+
|
|
560
|
+
def colorize(
|
|
561
|
+
string, color=None, style=None, background=None, show=False
|
|
562
|
+
): # it paints a text with given fullground and background color
|
|
402
563
|
string = apply_ansi(string, background, 0)
|
|
403
564
|
string = apply_ansi(string, color, 1)
|
|
404
565
|
string = apply_ansi(string, style, 2)
|
|
@@ -406,164 +567,209 @@ def colorize(string, color = None, style = None, background = None, show = False
|
|
|
406
567
|
print(string)
|
|
407
568
|
return string
|
|
408
569
|
|
|
409
|
-
|
|
410
|
-
|
|
570
|
+
|
|
571
|
+
def uncolorize(string): # remove color codes from colored string
|
|
572
|
+
def colored():
|
|
573
|
+
return ansi_begin in string
|
|
411
574
|
while colored():
|
|
412
575
|
b = string.index(ansi_begin)
|
|
413
|
-
e = string[b
|
|
414
|
-
string = string.replace(string[b
|
|
576
|
+
e = string[b:].index("m") + b + 1
|
|
577
|
+
string = string.replace(string[b:e], "")
|
|
415
578
|
return string
|
|
416
579
|
|
|
580
|
+
|
|
417
581
|
def apply_ansi(string, color, character):
|
|
418
582
|
begin, end = ansi(color, character)
|
|
419
583
|
return begin + string + end
|
|
420
584
|
|
|
421
|
-
#ansi_begin = '\033['
|
|
422
|
-
ansi_begin = '\x1b['
|
|
423
|
-
ansi_end = ansi_begin + '0m'
|
|
424
585
|
|
|
425
|
-
|
|
586
|
+
# ansi_begin = '\033['
|
|
587
|
+
ansi_begin = "\x1b["
|
|
588
|
+
ansi_end = ansi_begin + "0m"
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@Memorize
|
|
426
592
|
def colors_to_ansi(fullground, style, background):
|
|
427
593
|
color = [background, fullground, style]
|
|
428
|
-
return
|
|
594
|
+
return "".join([ansi(color[i], i)[0] for i in range(3)])
|
|
429
595
|
|
|
430
|
-
|
|
596
|
+
|
|
597
|
+
@Memorize
|
|
431
598
|
def ansi(color, character):
|
|
432
599
|
if color == no_color:
|
|
433
|
-
return [
|
|
434
|
-
col, fg, tp =
|
|
600
|
+
return ["", ""]
|
|
601
|
+
col, fg, tp = "", "", ""
|
|
435
602
|
if character == 2 and is_style(color):
|
|
436
603
|
col = get_style_codes(color)
|
|
437
|
-
col =
|
|
604
|
+
col = ";".join([str(el) for el in col])
|
|
438
605
|
elif character != 2:
|
|
439
|
-
fg =
|
|
440
|
-
tp =
|
|
606
|
+
fg = "38;" if character == 1 else "48;"
|
|
607
|
+
tp = "5;"
|
|
441
608
|
if is_string_color(color):
|
|
442
609
|
col = str(get_color_code(color))
|
|
443
610
|
elif is_integer_color(color):
|
|
444
611
|
col = str(color)
|
|
445
612
|
elif is_rgb_color(color):
|
|
446
|
-
col =
|
|
447
|
-
tp =
|
|
448
|
-
is_color = col !=
|
|
449
|
-
begin = ansi_begin + fg + tp + col +
|
|
450
|
-
end = ansi_end if is_color else
|
|
613
|
+
col = ";".join([str(el) for el in color])
|
|
614
|
+
tp = "2;"
|
|
615
|
+
is_color = col != ""
|
|
616
|
+
begin = ansi_begin + fg + tp + col + "m" if is_color else ""
|
|
617
|
+
end = ansi_end if is_color else ""
|
|
451
618
|
return [begin, end]
|
|
452
619
|
|
|
620
|
+
|
|
453
621
|
## This section is useful to produce html colored version of the plot and to translate all color types (types 0 and 1) in rgb (type 2 in plotext) and avoid confusion. the match is almost exact and it depends on the terminal i suppose
|
|
454
622
|
|
|
623
|
+
|
|
455
624
|
def to_rgb(color):
|
|
456
|
-
if is_string_color(color):
|
|
625
|
+
if is_string_color(color): # from 0 to 1
|
|
457
626
|
color = get_color_code(color)
|
|
458
|
-
#color = type0_to_type1_codes[code]
|
|
459
|
-
if is_integer_color(color):
|
|
627
|
+
# color = type0_to_type1_codes[code]
|
|
628
|
+
if is_integer_color(color): # from 0 or 1 to 2
|
|
460
629
|
return type1_to_type2_codes[color]
|
|
461
630
|
return color
|
|
462
631
|
|
|
632
|
+
|
|
463
633
|
##############################################
|
|
464
634
|
############ Style Codes ##############
|
|
465
635
|
##############################################
|
|
466
636
|
|
|
467
|
-
no_style =
|
|
637
|
+
no_style = "default"
|
|
468
638
|
|
|
469
639
|
styles = list(style_codes.keys()) + [no_style]
|
|
470
640
|
|
|
471
|
-
info_style =
|
|
641
|
+
info_style = "dim"
|
|
472
642
|
|
|
473
|
-
|
|
643
|
+
|
|
644
|
+
def get_style_code(style): # from single style to style number code
|
|
474
645
|
style = style.strip()
|
|
475
646
|
return style_codes[style]
|
|
476
647
|
|
|
477
|
-
|
|
648
|
+
|
|
649
|
+
def get_style_codes(
|
|
650
|
+
style,
|
|
651
|
+
): # from many styles (separated by space) to as many number codes
|
|
478
652
|
style = style.strip().split()
|
|
479
653
|
codes = [get_style_code(el) for el in style if el in styles]
|
|
480
654
|
codes = no_duplicates(codes)
|
|
481
655
|
return codes
|
|
482
656
|
|
|
483
|
-
|
|
657
|
+
|
|
658
|
+
def get_style_name(code): # from style number code to style name
|
|
484
659
|
codes = list(style_codes.values())
|
|
485
660
|
return styles[codes.index(code)] if code in codes else no_style
|
|
486
661
|
|
|
487
|
-
|
|
662
|
+
|
|
663
|
+
def clean_styles(
|
|
664
|
+
style,
|
|
665
|
+
): # it returns a well written sequence of styles (separated by spaces) from a possible confused one
|
|
488
666
|
codes = get_style_codes(style)
|
|
489
|
-
return
|
|
667
|
+
return " ".join([get_style_name(el) for el in codes])
|
|
668
|
+
|
|
490
669
|
|
|
491
670
|
def is_style(style):
|
|
492
|
-
style = style.strip().split() if isinstance(style, str) else [
|
|
493
|
-
return any(
|
|
671
|
+
style = style.strip().split() if isinstance(style, str) else [""]
|
|
672
|
+
return any(el in styles for el in style)
|
|
673
|
+
|
|
494
674
|
|
|
495
675
|
##############################################
|
|
496
676
|
########### Plot Utilities ############
|
|
497
677
|
##############################################
|
|
498
678
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
679
|
+
|
|
680
|
+
def set_data(x=None, y=None): # it return properly formatted x and y data lists
|
|
681
|
+
if x is None and y is None:
|
|
682
|
+
x, y = [], []
|
|
683
|
+
elif x is not None and y is None:
|
|
684
|
+
y = x
|
|
685
|
+
x = list(range(1, len(y) + 1))
|
|
686
|
+
lx, ly = len(x), len(y)
|
|
687
|
+
if lx != ly:
|
|
688
|
+
min_length = min(lx, ly)
|
|
689
|
+
x = x[:min_length]
|
|
690
|
+
y = y[:min_length]
|
|
691
|
+
return [list(x), list(y)]
|
|
692
|
+
|
|
511
693
|
|
|
512
694
|
##############################################
|
|
513
695
|
####### Figure Class Utilities ########
|
|
514
696
|
##############################################
|
|
515
697
|
|
|
516
|
-
|
|
698
|
+
|
|
699
|
+
def set_sizes(
|
|
700
|
+
sizes, size_max
|
|
701
|
+
): # given certain widths (or heights) - some of them are None - it sets them so to respect max value
|
|
517
702
|
bins = len(sizes)
|
|
518
703
|
for s in range(bins):
|
|
519
|
-
size_set = sum([el for el in sizes[0
|
|
704
|
+
size_set = sum([el for el in sizes[0:s] + sizes[s + 1 :] if el is not None])
|
|
520
705
|
available = max(size_max - size_set, 0)
|
|
521
|
-
to_set = len([el for el in sizes[s
|
|
706
|
+
to_set = len([el for el in sizes[s:] if el is None])
|
|
522
707
|
sizes[s] = available // to_set if sizes[s] is None else sizes[s]
|
|
523
708
|
return sizes
|
|
524
709
|
|
|
525
|
-
|
|
710
|
+
|
|
711
|
+
def fit_sizes(
|
|
712
|
+
sizes, size_max
|
|
713
|
+
): # honestly forgot the point of this function: yeeeeei :-) but it is useful - probably assumes all sizes not None (due to set_sizes) and reduces those that exceed size_max from last one to first
|
|
526
714
|
bins = len(sizes)
|
|
527
715
|
s = bins - 1
|
|
528
|
-
#while (sum(sizes) != size_max if not_less else sum(sizes) > size_max) and s >= 0:
|
|
716
|
+
# while (sum(sizes) != size_max if not_less else sum(sizes) > size_max) and s >= 0:
|
|
529
717
|
while sum(sizes) > size_max and s >= 0:
|
|
530
718
|
other_sizes = sum([sizes[i] for i in range(bins) if i != s])
|
|
531
719
|
sizes[s] = max(size_max - other_sizes, 0)
|
|
532
720
|
s -= 1
|
|
533
721
|
return sizes
|
|
534
722
|
|
|
723
|
+
|
|
535
724
|
##############################################
|
|
536
725
|
####### Build Class Utilities #########
|
|
537
726
|
##############################################
|
|
538
727
|
|
|
539
|
-
|
|
728
|
+
|
|
729
|
+
def get_first(data, test=True): # if test take the first element, otherwise the second
|
|
540
730
|
return data[0] if test else data[1]
|
|
541
731
|
|
|
542
|
-
|
|
732
|
+
|
|
733
|
+
def apply_scale(data, test=False): # apply log scale if test
|
|
543
734
|
return log(data) if test else data
|
|
544
735
|
|
|
545
|
-
|
|
736
|
+
|
|
737
|
+
def reverse_scale(data, test=False): # apply log scale if test
|
|
546
738
|
return power10(data) if test else data
|
|
547
739
|
|
|
548
|
-
|
|
740
|
+
|
|
741
|
+
def replace_none(
|
|
742
|
+
data, num_data
|
|
743
|
+
): # replace None elements in data with correspondent in num_data
|
|
549
744
|
return [data[i] if data[i] is not None else num_data[i] for i in range(len(data))]
|
|
550
745
|
|
|
551
|
-
numerical = lambda el: not (el is None or math.isnan(el)) or isinstance(el, str) # in the case of string datetimes
|
|
552
|
-
all_numerical = lambda data: all([numerical(el) for el in data])
|
|
553
746
|
|
|
554
|
-
def
|
|
747
|
+
def numerical(el):
|
|
748
|
+
return not (el is None or math.isnan(el)) or isinstance(
|
|
749
|
+
el, str
|
|
750
|
+
) # in the case of string datetimes
|
|
751
|
+
def all_numerical(data):
|
|
752
|
+
return all(numerical(el) for el in data)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def get_lim(data): # it returns the data minimum and maximum limits
|
|
555
756
|
data = [el for el in data if numerical(el)]
|
|
556
|
-
m = min(data, default
|
|
557
|
-
|
|
558
|
-
m,
|
|
559
|
-
return [m,
|
|
757
|
+
m = min(data, default=0)
|
|
758
|
+
max_val = max(data, default=0)
|
|
759
|
+
m, max_val = (m, max_val) if m != max_val else (0.5 * m, 1.5 * m) if m == max_val != 0 else (-1, 1)
|
|
760
|
+
return [m, max_val]
|
|
761
|
+
|
|
560
762
|
|
|
561
|
-
def get_matrix_data(data, lim, bins):
|
|
562
|
-
change
|
|
763
|
+
def get_matrix_data(data, lim, bins): # from data to relative canvas coordinates
|
|
764
|
+
def change(el):
|
|
765
|
+
return 0.5 + (bins - 1) * (el - lim[0]) / (lim[1] - lim[0])
|
|
563
766
|
# round is so that for example 9.9999 = 10, otherwise the floor function will give different results
|
|
564
767
|
return [math.floor(round(change(el), 8)) if numerical(el) else el for el in data]
|
|
565
768
|
|
|
566
|
-
|
|
769
|
+
|
|
770
|
+
def get_lines(
|
|
771
|
+
x, y, *other
|
|
772
|
+
): # it returns the lines between all couples of data points like x[i], y[i] to x[i + 1], y[i + 1]; other are the lisXt of markers and colors that needs to be elongated
|
|
567
773
|
# if len(x) * len(y) == 0:
|
|
568
774
|
# return [], [], *[[]] * len(other)
|
|
569
775
|
o = transpose(other, len(other))
|
|
@@ -579,7 +785,10 @@ def get_lines(x, y, *other): # it returns the lines between all couples of data
|
|
|
579
785
|
ol = ol + [o[-1]] if x != [] else ol
|
|
580
786
|
return [xl, yl] + transpose(ol, len(other))
|
|
581
787
|
|
|
582
|
-
|
|
788
|
+
|
|
789
|
+
def get_line(
|
|
790
|
+
x, y
|
|
791
|
+
): # it returns a line of points from x[0],y[0] to x[1],y[1] distanced between each other in x and y by at least 1.
|
|
583
792
|
if not all_numerical(join([x, y])):
|
|
584
793
|
return x, y
|
|
585
794
|
x0, x1 = x
|
|
@@ -591,6 +800,7 @@ def get_line(x, y): # it returns a line of points from x[0],y[0] to x[1],y[1] di
|
|
|
591
800
|
y = [int(el) for el in linspace(y0, y1, a)]
|
|
592
801
|
return [x, y]
|
|
593
802
|
|
|
803
|
+
|
|
594
804
|
def get_fill_level(fill, lim, bins):
|
|
595
805
|
if fill is False:
|
|
596
806
|
return False
|
|
@@ -601,37 +811,42 @@ def get_fill_level(fill, lim, bins):
|
|
|
601
811
|
fill = get_matrix_data([fill], lim, bins)[0]
|
|
602
812
|
return fill
|
|
603
813
|
|
|
814
|
+
|
|
604
815
|
def find_filling_values(x, y, y0):
|
|
605
816
|
xn, yn, yf = [[]] * 3
|
|
606
|
-
|
|
817
|
+
x_length = len(x)
|
|
607
818
|
while len(x) > 0:
|
|
608
819
|
i = len(xn)
|
|
609
820
|
xn.append(x[i])
|
|
610
821
|
yn.append(y[i])
|
|
611
|
-
|
|
612
|
-
if
|
|
613
|
-
|
|
614
|
-
j =
|
|
615
|
-
|
|
616
|
-
[x.pop(j) for j in
|
|
617
|
-
[y.pop(j) for j in
|
|
822
|
+
indices = [j for j in range(x_length) if x[j] == x[i]]
|
|
823
|
+
if indices != []:
|
|
824
|
+
y_subset = [y[j] for j in indices]
|
|
825
|
+
j = y_subset.index(min(y_subset))
|
|
826
|
+
indices.pop(j)
|
|
827
|
+
[x.pop(j) for j in indices]
|
|
828
|
+
[y.pop(j) for j in indices]
|
|
618
829
|
yf.append(y[j])
|
|
619
830
|
return xn, yn, yf
|
|
620
831
|
|
|
832
|
+
|
|
621
833
|
def get_fill_boundaries(x, y):
|
|
622
834
|
xm = []
|
|
623
|
-
|
|
624
|
-
for i in range(
|
|
835
|
+
x_length = len(x)
|
|
836
|
+
for i in range(x_length):
|
|
625
837
|
xi, yi = x[i], y[i]
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
m = min(
|
|
838
|
+
indices = [j for j in range(x_length) if x[j] == xi and y[j] < yi]
|
|
839
|
+
y_values = [y[j] for j in indices]
|
|
840
|
+
m = min(y_values, default=yi)
|
|
629
841
|
xm.append([x[i], m])
|
|
630
842
|
x, m = transpose(xm)
|
|
631
843
|
return m
|
|
632
844
|
|
|
633
|
-
|
|
634
|
-
|
|
845
|
+
|
|
846
|
+
def fill_data(
|
|
847
|
+
x, y, y0, *other
|
|
848
|
+
): # it fills x, y with y data points reaching y0; and c are the list of markers and colors that needs to be elongated
|
|
849
|
+
# y0 = get_fill_boundaries(x, y)
|
|
635
850
|
y0 = get_fill_boundaries(x, y) if isinstance(y0, str) else [y0] * len(x)
|
|
636
851
|
o = transpose(other, len(other))
|
|
637
852
|
xf, yf, of = [[] for i in range(3)]
|
|
@@ -640,7 +855,11 @@ def fill_data(x, y, y0, *other): # it fills x, y with y data points reaching y0;
|
|
|
640
855
|
xi, yi, y0i = x[i], y[i], y0[i]
|
|
641
856
|
if [xi, yi] not in xy:
|
|
642
857
|
xy.append([xi, yi])
|
|
643
|
-
yn =
|
|
858
|
+
yn = (
|
|
859
|
+
range(y0i, yi + 1)
|
|
860
|
+
if y0i < yi
|
|
861
|
+
else range(yi, y0i) if y0i > yi else [y0i]
|
|
862
|
+
)
|
|
644
863
|
yn = list(yn)
|
|
645
864
|
xn = [xi] * len(yn)
|
|
646
865
|
xf += xn
|
|
@@ -648,43 +867,59 @@ def fill_data(x, y, y0, *other): # it fills x, y with y data points reaching y0;
|
|
|
648
867
|
of += [o[i]] * len(xn)
|
|
649
868
|
return [xf, yf] + transpose(of, len(other))
|
|
650
869
|
|
|
870
|
+
|
|
651
871
|
def remove_outsiders(x, y, width, height, *other):
|
|
652
|
-
|
|
872
|
+
indices = [i for i in range(len(x)) if x[i] in range(width) and y[i] in range(height)]
|
|
653
873
|
o = transpose(other, len(other))
|
|
654
|
-
return transpose([(x[i], y[i], *o[i]) for i in
|
|
874
|
+
return transpose([(x[i], y[i], *o[i]) for i in indices], 2 + len(other))
|
|
655
875
|
|
|
656
|
-
|
|
876
|
+
|
|
877
|
+
def get_labels(ticks): # it returns the approximated string version of the data ticks
|
|
657
878
|
d = distinguishing_digit(ticks)
|
|
658
879
|
formatting_string = "{:." + str(d + 1) + "f}"
|
|
659
880
|
labels = [formatting_string.format(el) for el in ticks]
|
|
660
|
-
pos = [el.index(
|
|
881
|
+
pos = [el.index(".") + d + 2 for el in labels]
|
|
661
882
|
labels = [labels[i][: pos[i]] for i in range(len(labels))]
|
|
662
|
-
all_integers = all(
|
|
663
|
-
labels =
|
|
664
|
-
|
|
665
|
-
|
|
883
|
+
all_integers = all(el == int(el) for el in ticks)
|
|
884
|
+
labels = (
|
|
885
|
+
[add_extra_zeros(el, d) if len(labels) > 1 else el for el in labels]
|
|
886
|
+
if not all_integers
|
|
887
|
+
else [str(int(el)) for el in ticks]
|
|
888
|
+
)
|
|
889
|
+
# sign = any([el < 0 for el in ticks])
|
|
890
|
+
# labels = ['+' + labels[i] if ticks[i] > 0 and sign else labels[i] for i in range(len(labels))]
|
|
666
891
|
return labels
|
|
667
892
|
|
|
668
|
-
|
|
669
|
-
|
|
893
|
+
|
|
894
|
+
def distinguishing_digit(
|
|
895
|
+
data,
|
|
896
|
+
): # it return the minimum amount of decimal digits necessary to distinguish all elements of a list
|
|
897
|
+
# data = [el for el in data if 'e' not in str(el)]
|
|
670
898
|
d = [_distinguishing_digit(data[i], data[i + 1]) for i in range(len(data) - 1)]
|
|
671
|
-
return max(d, default
|
|
899
|
+
return max(d, default=1)
|
|
672
900
|
|
|
673
|
-
|
|
901
|
+
|
|
902
|
+
def _distinguishing_digit(
|
|
903
|
+
a, b
|
|
904
|
+
): # it return the minimum amount of decimal digits necessary to distinguish a from b (when both are rounded to those digits).
|
|
674
905
|
d = abs(a - b)
|
|
675
|
-
d = 0 if d == 0 else -
|
|
676
|
-
#d = round(d, 10)
|
|
906
|
+
d = 0 if d == 0 else -math.log10(2 * d)
|
|
907
|
+
# d = round(d, 10)
|
|
677
908
|
d = 0 if d < 0 else math.ceil(d)
|
|
678
909
|
d = d + 1 if round(a, d) == round(b, d) else d
|
|
679
910
|
return d
|
|
680
911
|
|
|
681
|
-
|
|
682
|
-
|
|
912
|
+
|
|
913
|
+
def add_extra_zeros(label, d): # it adds 0s at the end of a label if necessary
|
|
914
|
+
zeros = len(label) - 1 - label.index("." if "e" not in label else "e")
|
|
683
915
|
if zeros < d:
|
|
684
|
-
label +=
|
|
916
|
+
label += "0" * (d - zeros)
|
|
685
917
|
return label
|
|
686
918
|
|
|
687
|
-
|
|
919
|
+
|
|
920
|
+
def add_extra_spaces(
|
|
921
|
+
labels, side
|
|
922
|
+
): # it adds empty spaces before or after the labels if necessary
|
|
688
923
|
length = 0 if labels == [] else max_length(labels)
|
|
689
924
|
if side == "left":
|
|
690
925
|
labels = [space * (length - len(el)) + el for el in labels]
|
|
@@ -692,17 +927,20 @@ def add_extra_spaces(labels, side): # it adds empty spaces before or after the l
|
|
|
692
927
|
labels = [el + space * (length - len(el)) for el in labels]
|
|
693
928
|
return labels
|
|
694
929
|
|
|
695
|
-
|
|
696
|
-
|
|
930
|
+
|
|
931
|
+
def hd_group(
|
|
932
|
+
x, y, xf, yf
|
|
933
|
+
): # it returns the real coordinates of the HD markers and the matrix that defines the marker
|
|
934
|
+
x_length, xfm, yfm = len(x), max(xf), max(yf)
|
|
697
935
|
xm = [el // xfm if numerical(el) else el for el in x]
|
|
698
936
|
ym = [el // yfm if numerical(el) else el for el in y]
|
|
699
937
|
m = {}
|
|
700
|
-
for i in range(
|
|
938
|
+
for i in range(x_length):
|
|
701
939
|
xyi = xm[i], ym[i]
|
|
702
940
|
xfi, yfi = xf[i], yf[i]
|
|
703
941
|
mi = [[0 for x in range(xfi)] for y in range(yfi)]
|
|
704
942
|
m[xyi] = mi
|
|
705
|
-
for i in range(
|
|
943
|
+
for i in range(x_length):
|
|
706
944
|
xyi = xm[i], ym[i]
|
|
707
945
|
if all_numerical(xyi):
|
|
708
946
|
xk, yk = x[i] % xfi, y[i] % yfi
|
|
@@ -712,42 +950,50 @@ def hd_group(x, y, xf, yf): # it returns the real coordinates of the HD markers
|
|
|
712
950
|
m = [tuple(join(el[::-1])) for el in m.values()]
|
|
713
951
|
return x, y, m
|
|
714
952
|
|
|
953
|
+
|
|
715
954
|
###############################################
|
|
716
955
|
############# Bar Functions ##############
|
|
717
956
|
###############################################
|
|
718
957
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
958
|
+
|
|
959
|
+
def bars(
|
|
960
|
+
x, y, width, minimum
|
|
961
|
+
): # given the bars center coordinates and height, it returns the full bar coordinates
|
|
962
|
+
# if x == []:
|
|
963
|
+
# return [], []
|
|
964
|
+
bins = len(x)
|
|
965
|
+
# bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
|
|
966
|
+
bin_size_half = width / 2
|
|
967
|
+
# adjust the bar width according to the number of bins
|
|
968
|
+
if bins > 1:
|
|
969
|
+
bin_size_half *= (max(x) - min(x)) / (bins - 1)
|
|
970
|
+
xbar, ybar = [], []
|
|
971
|
+
for i in range(bins):
|
|
972
|
+
xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
|
|
973
|
+
ybar.append([minimum, y[i]])
|
|
974
|
+
return xbar, ybar
|
|
975
|
+
|
|
733
976
|
|
|
734
977
|
def set_multiple_bar_data(*args):
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
m = len(
|
|
739
|
-
x = [] if
|
|
740
|
-
return x,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
978
|
+
arg_count = len(args)
|
|
979
|
+
y_values = [] if arg_count == 0 else args[0] if arg_count == 1 else args[1]
|
|
980
|
+
y_values = [y_values] if not isinstance(y_values, list) or len(y_values) == 0 else y_values
|
|
981
|
+
m = len(y_values[0])
|
|
982
|
+
x = [] if arg_count == 0 else list(range(1, m + 1)) if arg_count == 1 else args[0]
|
|
983
|
+
return x, y_values
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def hist_data(
|
|
987
|
+
data, bins=10, norm=False
|
|
988
|
+
): # it returns data in histogram form if norm is False. Otherwise, it returns data in density form where all bins sum to 1.
|
|
989
|
+
# data = [round(el, 15) for el in data]
|
|
744
990
|
# if data == []:
|
|
745
991
|
# return [], []
|
|
746
992
|
bins = 0 if len(data) == 0 else bins
|
|
747
|
-
m,
|
|
748
|
-
data = [(el - m) / (
|
|
993
|
+
m, max_val = min(data, default=0), max(data, default=0)
|
|
994
|
+
data = [(el - m) / (max_val - m) * bins if el != max_val else bins - 1 for el in data]
|
|
749
995
|
data = [int(el) for el in data]
|
|
750
|
-
histx = linspace(m,
|
|
996
|
+
histx = linspace(m, max_val, bins)
|
|
751
997
|
histy = [0] * bins
|
|
752
998
|
for el in data:
|
|
753
999
|
histy[el] += 1
|
|
@@ -755,22 +1001,24 @@ def hist_data(data, bins = 10, norm = False): # it returns data in histogram for
|
|
|
755
1001
|
histy = [el / len(data) for el in histy]
|
|
756
1002
|
return histx, histy
|
|
757
1003
|
|
|
1004
|
+
|
|
758
1005
|
def single_bar(x, y, ylabel, marker, colors):
|
|
759
|
-
|
|
1006
|
+
y_length = len(y)
|
|
760
1007
|
lc = len(colors)
|
|
761
|
-
xs = colorize(str(x),
|
|
1008
|
+
xs = colorize(str(x), "gray+", "bold")
|
|
762
1009
|
bar = [marker * el for el in y]
|
|
763
|
-
bar = [apply_ansi(bar[i], colors[i % lc], 1) for i in range(
|
|
764
|
-
ylabel = colorize(f
|
|
765
|
-
bar = xs + space +
|
|
1010
|
+
bar = [apply_ansi(bar[i], colors[i % lc], 1) for i in range(y_length)]
|
|
1011
|
+
ylabel = colorize(f"{ylabel:.2f}", "gray+", "bold")
|
|
1012
|
+
bar = xs + space + "".join(bar) + space + ylabel
|
|
766
1013
|
return bar
|
|
767
1014
|
|
|
768
|
-
|
|
769
|
-
|
|
1015
|
+
|
|
1016
|
+
def bar_data(*args, width=None, mode="stacked"):
|
|
1017
|
+
x, y_values = set_multiple_bar_data(*args)
|
|
770
1018
|
x = list(map(str, x))
|
|
771
|
-
x = add_extra_spaces(x,
|
|
1019
|
+
x = add_extra_spaces(x, "right")
|
|
772
1020
|
lx = len(x[0])
|
|
773
|
-
y = [sum(el) for el in transpose(
|
|
1021
|
+
y = [sum(el) for el in transpose(y_values)] if mode == "stacked" else y_values
|
|
774
1022
|
ly = max_length([round(el, 2) for el in join(y)])
|
|
775
1023
|
|
|
776
1024
|
width_term = terminal_width()
|
|
@@ -780,48 +1028,63 @@ def bar_data(*args, width = None, mode = 'stacked'):
|
|
|
780
1028
|
my = max(join(y))
|
|
781
1029
|
my = 1 if my == 0 else my
|
|
782
1030
|
dx = my / (width - lx - ly - 2)
|
|
783
|
-
|
|
784
|
-
|
|
1031
|
+
y_scaled = [[round(el / dx, 0) for el in y] for y in y_values]
|
|
1032
|
+
y_scaled = transpose(y_scaled)
|
|
785
1033
|
|
|
786
|
-
return x, y,
|
|
1034
|
+
return x, y, y_scaled, width
|
|
787
1035
|
|
|
788
|
-
|
|
1036
|
+
|
|
1037
|
+
def correct_marker(marker=None):
|
|
789
1038
|
return simple_bar_marker if marker is None else marker[0]
|
|
790
1039
|
|
|
1040
|
+
|
|
791
1041
|
def get_title(title, width):
|
|
792
|
-
out =
|
|
1042
|
+
out = ""
|
|
793
1043
|
if title is not None:
|
|
794
|
-
|
|
795
|
-
w1 = (width - 2 -
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1044
|
+
title_length = len(uncolorize(title))
|
|
1045
|
+
w1 = (width - 2 - title_length) // 2
|
|
1046
|
+
w2 = width - title_length - 2 - w1
|
|
1047
|
+
l1 = "─" * w1 + space
|
|
1048
|
+
l2 = space + "─" * w2
|
|
1049
|
+
out = colorize(l1 + title + l2, "gray+", "bold") + "\n"
|
|
799
1050
|
return out
|
|
800
1051
|
|
|
1052
|
+
|
|
801
1053
|
def get_simple_labels(marker, labels, colors, width):
|
|
802
|
-
out =
|
|
803
|
-
if labels
|
|
804
|
-
|
|
1054
|
+
out = "\n"
|
|
1055
|
+
if labels is not None:
|
|
1056
|
+
label_count = len(labels)
|
|
805
1057
|
lc = len(colors)
|
|
806
|
-
out =
|
|
807
|
-
|
|
1058
|
+
out = space.join(
|
|
1059
|
+
[
|
|
1060
|
+
colorize(marker * 3, colors[i % lc])
|
|
1061
|
+
+ space
|
|
1062
|
+
+ colorize(labels[i], "gray+", "bold")
|
|
1063
|
+
for i in range(label_count)
|
|
1064
|
+
]
|
|
1065
|
+
)
|
|
1066
|
+
out = "\n" + get_title(out, width)
|
|
808
1067
|
return out
|
|
809
1068
|
|
|
1069
|
+
|
|
810
1070
|
###############################################
|
|
811
1071
|
############# Box Functions ##############
|
|
812
1072
|
###############################################
|
|
813
1073
|
|
|
814
|
-
|
|
1074
|
+
|
|
1075
|
+
def box(
|
|
1076
|
+
x, y, width, minimum
|
|
1077
|
+
): # given the bars center coordinates and height, it returns the full bar coordinates
|
|
815
1078
|
# if x == []:
|
|
816
1079
|
# return [], []
|
|
817
1080
|
bins = len(x)
|
|
818
|
-
#bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
|
|
1081
|
+
# bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
|
|
819
1082
|
bin_size_half = width / 2
|
|
820
1083
|
# adjust the bar width according to the number of bins
|
|
821
1084
|
if bins > 1:
|
|
822
1085
|
bin_size_half *= (max(x) - min(x)) / (bins - 1)
|
|
823
|
-
c, q1, q2, q3, h,
|
|
824
|
-
xbar,
|
|
1086
|
+
c, q1, q2, q3, h, low_vals = [], [], [], [], [], []
|
|
1087
|
+
xbar, _ybar, _mybar = [], [], []
|
|
825
1088
|
|
|
826
1089
|
for i in range(bins):
|
|
827
1090
|
c.append(x[i])
|
|
@@ -830,24 +1093,29 @@ def box(x, y, width, minimum): # given the bars center coordinates and height, i
|
|
|
830
1093
|
q2.append(quantile(y[i], 0.50))
|
|
831
1094
|
q3.append(quantile(y[i], 0.75))
|
|
832
1095
|
h.append(max(y[i]))
|
|
833
|
-
|
|
1096
|
+
low_vals.append(min(y[i]))
|
|
1097
|
+
|
|
1098
|
+
return q1, q2, q3, h, low_vals, c, xbar
|
|
834
1099
|
|
|
835
|
-
return q1, q2, q3, h, l, c, xbar
|
|
836
1100
|
|
|
837
1101
|
##############################################
|
|
838
1102
|
########## Image Utilities #############
|
|
839
1103
|
##############################################
|
|
840
1104
|
|
|
841
|
-
|
|
1105
|
+
|
|
1106
|
+
def update_size(
|
|
1107
|
+
size_old, size_new
|
|
1108
|
+
): # it resize an image to the desired size, maintaining or not its size ratio and adding or not a pixel averaging factor with resample = True
|
|
842
1109
|
size_old = [size_old[0], size_old[1] / 2]
|
|
843
|
-
|
|
1110
|
+
size_old[1] / size_old[0]
|
|
844
1111
|
size_new = replace(size_new, size_old)
|
|
845
|
-
|
|
846
|
-
#ratio_new = size_new[1] / size_new[0]
|
|
1112
|
+
size_new[1] / size_new[0]
|
|
1113
|
+
# ratio_new = size_new[1] / size_new[0]
|
|
847
1114
|
size_new = [1 if el == 0 else el for el in size_new]
|
|
848
1115
|
return [int(size_new[0]), int(size_new[1])]
|
|
849
1116
|
|
|
850
|
-
|
|
1117
|
+
|
|
1118
|
+
def image_to_matrix(image): # from image to a matrix of pixels
|
|
851
1119
|
pixels = list(image.getdata())
|
|
852
1120
|
width, height = image.size
|
|
853
|
-
return [pixels[i * width:(i + 1) * width] for i in range(height)]
|
|
1121
|
+
return [pixels[i * width : (i + 1) * width] for i in range(height)]
|