plotext-plus 1.0.1__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.
@@ -0,0 +1,853 @@
1
+ import sys, shutil, os, re, math, inspect
2
+ from plotext_plus._dict import *
3
+
4
+ ###############################################
5
+ ######### Number Manipulation ##########
6
+ ###############################################
7
+
8
+ def round(n, d = 0): # the standard round(0.5) = 0 instead of 1; this version rounds 0.5 to 1
9
+ n *= 10 ** d
10
+ f = math.floor(n)
11
+ r = f if n - f < 0.5 else math.ceil(n)
12
+ return r * 10 ** (-d)
13
+
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
+
17
+ def replace(data, data2, element = None): # replace element in data with correspondent in data2 when element is found
18
+ res = []
19
+ for i in range(len(data)):
20
+ el = data[i] if data[i] != element else data2[i]
21
+ res.append(el)
22
+ return res
23
+
24
+ def try_float(data): # it turn a string into float if it can
25
+ try:
26
+ return float(data)
27
+ except:
28
+ return data
29
+
30
+ def quantile(data, q): # calculate the quantile of a given array
31
+ data = sorted(data)
32
+ index = q * (len(data) - 1)
33
+ if index.is_integer():
34
+ return data[int(index)]
35
+ else:
36
+ return (data[int(index)] + data[int(index) + 1]) / 2
37
+
38
+ ###############################################
39
+ ########### List Creation ##############
40
+ ###############################################
41
+
42
+ def linspace(lower, upper, length = 10): # it returns a lists of numbers from lower to upper with given length
43
+ slope = (upper - lower) / (length - 1) if length > 1 else 0
44
+ return [lower + x * slope for x in range(length)]
45
+
46
+ def sin(periods = 2, length = 200, amplitude = 1, phase = 0, decay = 0): # sinusoidal data with given parameters
47
+ f = 2 * math.pi * periods / (length - 1)
48
+ phase = math.pi * phase
49
+ d = decay / length
50
+ return [amplitude * math.sin(f * el + phase) * math.exp(- d * el) for el in range(length)]
51
+
52
+ def square(periods = 2, length = 200, amplitude = 1):
53
+ T = length / periods
54
+ step = lambda t: amplitude if t % T <= T / 2 else - amplitude
55
+ return [step(i) for i in range(length)]
56
+
57
+ def to_list(data, length): # eg: to_list(1, 3) = [1, 1 ,1]; to_list([1,2,3], 6) = [1, 2, 3, 1, 2, 3]
58
+ data = data if isinstance(data, list) else [data] * length
59
+ data = data * math.ceil(length / len(data)) if len(data) > 0 else []
60
+ return data[ : length]
61
+
62
+ def difference(data1, data2) : # elements in data1 not in date2
63
+ return [el for el in data1 if el not in data2]
64
+
65
+ ###############################################
66
+ ######### List Transformation ##########
67
+ ###############################################
68
+
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
+
72
+ def power10(data): # it apply log function to the data
73
+ return [10 ** el for el in data]
74
+
75
+ def floor(data): # it floors a list of data
76
+ return list(map(math.floor, data))
77
+
78
+ def repeat(data, length): # repeat the same data till length is reached
79
+ l = len(data) if type(data) == list else 1
80
+ data = join([data] * math.ceil(length / l))
81
+ return data[ : length]
82
+
83
+ ###############################################
84
+ ########## List Manipulation ###########
85
+ ###############################################
86
+
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
+
91
+ def join(data): # flatten lists at first level
92
+ #return [el for row in data for el in row]
93
+ return [el for row in data for el in (join(row) if type (row) == list else [row])]
94
+
95
+ def cumsum(data): # it returns the cumulative sums of a list; eg: cumsum([0,1,2,3,4]) = [0,1,3,6,10]
96
+ s = [0]
97
+ for i in range(len(data)):
98
+ s.append(s[-1] + data[i])
99
+ return s[1:]
100
+
101
+ ###############################################
102
+ ######### Matrix Manipulation ##########
103
+ ###############################################
104
+
105
+ def matrix_size(matrix): # cols, height
106
+ return [len(matrix[0]), len(matrix)] if matrix != [] else [0, 0]
107
+
108
+ def transpose(data, length = 1): # it needs no explanation
109
+ return [[]] * length if data == [] else list(map(list, zip(*data)))
110
+
111
+ def vstack(matrix, extra): # vertical stack of two matrices
112
+ return extra + matrix # + extra
113
+
114
+ def hstack(matrix, extra): # horizontal stack of two matrices
115
+ lm, le = len(matrix), len(extra)
116
+ l = max(lm, le)
117
+ return [matrix[i] + extra[i] for i in range(l)]
118
+
119
+ def turn_gray(matrix): # it takes a standard matrix and turns it into an grayscale one
120
+ M, m = max(join(matrix), default = 0), min(join(matrix), default = 0)
121
+ to_gray = lambda el: tuple([int(255 * (el - m) / (M - m))] * 3) if M != m else (127, 127, 127)
122
+ return [[to_gray(el) for el in l] for l in matrix]
123
+
124
+ def brush(*lists): # remove duplicates from lists x, y, z ...
125
+ l = min(map(len, lists))
126
+ lists = [el[:l] for el in lists]
127
+ z = list(zip(*lists))
128
+ z = no_duplicates(z)
129
+ #z = sorted(z)#, key = lambda x: x[0])
130
+ lists = transpose(z, len(lists))
131
+ return lists
132
+
133
+ ###############################################
134
+ ######### String Manipulation ###########
135
+ ###############################################
136
+
137
+ nl = "\n"
138
+
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
+
142
+ def format_time(time): # it properly formats the computational time
143
+ t = time if time is not None else 0
144
+ unit = 's' if t >= 1 else 'ms' if t >= 10 ** -3 else 'µs'
145
+ p = 0 if unit == 's' else 3 if unit == 'ms' else 6
146
+ t = round(10 ** p * t, 1)
147
+ l = len(str(int(t)))
148
+ t = str(t)
149
+ #t = ' ' * (3 - l) + t
150
+ return t[ : l + 2] + ' ' + unit
151
+
152
+ positive_color = 'green+'
153
+ negative_color = 'red'
154
+ title_color = 'cyan+'
155
+
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
+
159
+ def correct_coord(string, label, coord): # 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
160
+ l = len(label)
161
+ b, e = max(coord - l + 1, 0), min(coord + l, len(string) - 1)
162
+ data = [i for i in range(b, e) if string[i] is space]
163
+ b, e = min(data, default = coord - l + 1), max(data, default = coord + l)
164
+ b, e = e - l + 1, b + l
165
+ return (b + e - l) // 2
166
+
167
+ def no_char_duplicates(string, char): # it remove char duplicates from string
168
+ pattern = char + '{2,}'
169
+ string = re.sub(pattern, char, string)
170
+ return string
171
+
172
+ def read_lines(text, delimiter = None, columns = None): # from a long text to well formatted data
173
+ delimiter = " " if delimiter is None else delimiter
174
+ data = []
175
+ columns = len(no_char_duplicates(text[0], delimiter).split(delimiter)) if columns is None else columns
176
+ for i in range(len(text)):
177
+ row = text[i]
178
+ row = no_char_duplicates(row, delimiter)
179
+ row = row.split(delimiter)
180
+ row = [el.replace('\n', '') for el in row]
181
+ cols = len(row)
182
+ row = [row[col].replace('\n', '') if col in range(cols) else '' for col in range(columns)]
183
+ row = [try_float(el) for el in row]
184
+ data.append(row)
185
+ return data
186
+
187
+ def pad_string(num, length): # pad a number with spaces before to reach length
188
+ num = str(num)
189
+ l = len(num)
190
+ return num + ' ' * (length - l)
191
+
192
+ def max_length(strings):
193
+ strings = map(str, strings)
194
+ return max(map(len, strings), default = 0)
195
+
196
+ ###############################################
197
+ ########## File Manipulation ############
198
+ ###############################################
199
+
200
+ def correct_path(path):
201
+ folder, base = os.path.dirname(path), os.path.basename(path)
202
+ folder = os.path.expanduser("~") if folder in ['', '~'] else folder
203
+ path = os.path.join(folder, base)
204
+ return path
205
+
206
+ def is_file(path, log = True): # returns True if path exists
207
+ res = os.path.isfile(path)
208
+ print(format_strings("not a file:", path, negative_color)) if not res and log else None
209
+ return res
210
+
211
+ def script_folder(): # the folder of the script executed
212
+ return parent_folder(inspect.getfile(sys._getframe(1)))
213
+
214
+ def parent_folder(path, level = 1): # it return the parent folder of the path or file given; if level is higher then 1 the process is iterated
215
+ if level <= 0:
216
+ return path
217
+ elif level == 1:
218
+ return os.path.abspath(os.path.join(path, os.pardir))
219
+ else:
220
+ return parent_folder(parent_folder(path, level - 1))
221
+
222
+ def join_paths(*args): # 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
+ args = list(args)
224
+ args[0] = _correct_path(args[0]) if args[0] == "~" else args[0]
225
+ return os.path.abspath(os.path.join(*args))
226
+
227
+ def delete_file(path, log = True): # remove the file if it exists
228
+ path = correct_path(path)
229
+ if is_file(path):
230
+ os.remove(path)
231
+ print(format_strings("file removed:", path, negative_color)) if log else None
232
+
233
+ def read_data(path, delimiter = None, columns = None, first_row = None, log = True): # it turns a text file into data lists
234
+ path = correct_path(path)
235
+ first_row = 0 if first_row is None else int(first_row)
236
+ file = open(path, "r")
237
+ text = file.readlines()[first_row:]
238
+ file.close()
239
+ print(format_strings("data read from", path)) if log else None
240
+ return read_lines(text, delimiter, columns)
241
+
242
+ def write_data(data, path, delimiter = None, columns = None, log = True): # it turns a matrix into a text file
243
+ delimiter = " " if delimiter is None else delimiter
244
+ cols = len(data[0])
245
+ cols = range(1, cols + 1) if columns is None else columns
246
+ text = ""
247
+ for row in data:
248
+ row = [row[i - 1] for i in cols]
249
+ row = list(map(str, row))
250
+ text += delimiter.join(row) + '\n'
251
+ save_text(text, path, log = log)
252
+
253
+ def save_text(text, path, append = False, log = True): # it saves some text to the path selected
254
+ path = correct_path(path)
255
+ mode = "a" if append else "w+"
256
+ with open(path , mode, encoding='utf-8') as file:
257
+ file.write(text)
258
+ print(format_strings("text saved in", path)) if log else None
259
+
260
+ def download(url, path, log = True): # it download the url (image, video, gif etc) to path
261
+ from urllib.request import urlretrieve
262
+ path = correct_path(path)
263
+ urlretrieve(url, path)
264
+ print(format_strings('url saved in', path)) if log else None
265
+
266
+ ###############################################
267
+ ######### Platform Utilities ############
268
+ ###############################################
269
+
270
+ def is_ipython(): # true if running in ipython shenn
271
+ try:
272
+ __IPYTHON__
273
+ return True
274
+ except NameError:
275
+ return False
276
+
277
+ def platform(): # the platform (unix or windows) you are using plotext in
278
+ platform = sys.platform
279
+ if platform in {'win32', 'cygwin'}:
280
+ return 'windows'
281
+ else:
282
+ return 'unix'
283
+
284
+ platform = platform()
285
+
286
+ # to enable ascii escape color sequences
287
+ if platform == "windows":
288
+ import subprocess
289
+ subprocess.call('', shell = True)
290
+
291
+ def terminal_size(): # it returns the terminal size as [width, height]
292
+ try:
293
+ size = shutil.get_terminal_size()
294
+ return list(size)
295
+ except OSError:
296
+ return [None, None]
297
+
298
+ def terminal_width(): # returns terminal width, adjusted for banner borders when banners are enabled
299
+ width = terminal_size()[0]
300
+ if width is None:
301
+ return None
302
+
303
+ # Check if banners are enabled by checking the global output instance
304
+ try:
305
+ from plotext_plus._output import get_output_instance
306
+ output_instance = get_output_instance()
307
+ if hasattr(output_instance, 'use_banners') and output_instance.use_banners:
308
+ # Rich Panel with borders and padding=(0, 1) uses:
309
+ # - 2 characters for left/right borders
310
+ # - 2 characters for padding (1 on each side)
311
+ # Total: 4 characters of horizontal space
312
+ return max(width - 4, 1) # Ensure minimum width of 1
313
+ except:
314
+ pass # If anything fails, fall back to original width
315
+
316
+ return width
317
+
318
+ tw = terminal_width
319
+
320
+ terminal_height = lambda: terminal_size()[1]
321
+ th = terminal_height
322
+
323
+ def clear_terminal(lines = None): # it cleat the entire terminal, or the specified number of lines
324
+ if lines is None:
325
+ write('\033c')
326
+ else:
327
+ for r in range(lines):
328
+ write("\033[A") # moves the curson up
329
+ write("\033[2K") # clear the entire line
330
+
331
+ def write(string): # the print function used by plotext - now uses chuk-term backend
332
+ from plotext_plus._output import write as output_write
333
+ output_write(string)
334
+
335
+ class memorize: # it memorise the arguments of a function, when used as its decorator, to reduce computational time
336
+ def __init__(self, f):
337
+ self.f = f
338
+ self.memo = {}
339
+ def __call__(self, *args):
340
+ if not args in self.memo:
341
+ self.memo[args] = self.f(*args)
342
+ return self.memo[args]
343
+
344
+ ##############################################
345
+ ######### Marker Utilities ###########
346
+ ##############################################
347
+
348
+ space = ' ' # the default null character that appears as background to all plots
349
+ plot_marker = "hd" if platform == 'unix' else 'dot'
350
+
351
+ hd_markers = {hd_codes[el] : el for el in hd_codes}
352
+ fhd_markers = {fhd_codes[el] : el for el in fhd_codes}
353
+ braille_markers = {braille_codes[el] : el for el in braille_codes}
354
+ simple_bar_marker = '▇'
355
+
356
+ @memorize
357
+ def get_hd_marker(code):
358
+ return hd_codes[code] if len(code) == 4 else fhd_codes[code] if len(code) == 6 else braille_codes[code]
359
+
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
+
363
+ ##############################################
364
+ ########### Color Utilities ############
365
+ ##############################################
366
+
367
+ # A user could specify three types of colors
368
+ # an integer for 256 color codes
369
+ # a tuple for RGB color codes
370
+ # a string for 16 color codes or styles
371
+
372
+ # Along side the user needs to specify whatever it is for background / fullground / style
373
+ # which plotext calls 'character' = 0 / 1 / 2
374
+
375
+
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]
377
+
378
+ def get_color_code(color): # the color number code from color string
379
+ color = color.strip()
380
+ return color_codes[color]
381
+
382
+ def get_color_name(code): # the color string from color number code
383
+ codes = list(color_codes.values())
384
+ return colors[codes.index(code)] if code in codes else no_color
385
+
386
+ def is_string_color(color):
387
+ return isinstance(color, str) and color.strip() in colors
388
+
389
+ def is_integer_color(color):
390
+ return isinstance(color, int) and 0 <= color <= 255
391
+
392
+ def is_rgb_color(color):
393
+ is_rgb = isinstance(color, list) or isinstance(color, tuple)
394
+ is_rgb = is_rgb and len(color) == 3
395
+ is_rgb = is_rgb and all([is_integer_color(el) for el in color])
396
+ return is_rgb
397
+
398
+ def is_color(color):
399
+ return is_string_color(color) or is_integer_color(color) or is_rgb_color(color)
400
+
401
+ def colorize(string, color = None, style = None, background = None, show = False): # it paints a text with given fullground and background color
402
+ string = apply_ansi(string, background, 0)
403
+ string = apply_ansi(string, color, 1)
404
+ string = apply_ansi(string, style, 2)
405
+ if show:
406
+ print(string)
407
+ return string
408
+
409
+ def uncolorize(string): # remove color codes from colored string
410
+ colored = lambda: ansi_begin in string
411
+ while colored():
412
+ b = string.index(ansi_begin)
413
+ e = string[b : ].index('m') + b + 1
414
+ string = string.replace(string[b : e], '')
415
+ return string
416
+
417
+ def apply_ansi(string, color, character):
418
+ begin, end = ansi(color, character)
419
+ return begin + string + end
420
+
421
+ #ansi_begin = '\033['
422
+ ansi_begin = '\x1b['
423
+ ansi_end = ansi_begin + '0m'
424
+
425
+ @memorize
426
+ def colors_to_ansi(fullground, style, background):
427
+ color = [background, fullground, style]
428
+ return ''.join([ansi(color[i], i)[0] for i in range(3)])
429
+
430
+ @memorize
431
+ def ansi(color, character):
432
+ if color == no_color:
433
+ return ['', '']
434
+ col, fg, tp = '', '', ''
435
+ if character == 2 and is_style(color):
436
+ col = get_style_codes(color)
437
+ col = ';'.join([str(el) for el in col])
438
+ elif character != 2:
439
+ fg = '38;' if character == 1 else '48;'
440
+ tp = '5;'
441
+ if is_string_color(color):
442
+ col = str(get_color_code(color))
443
+ elif is_integer_color(color):
444
+ col = str(color)
445
+ elif is_rgb_color(color):
446
+ col = ';'.join([str(el) for el in color])
447
+ tp = '2;'
448
+ is_color = col != ''
449
+ begin = ansi_begin + fg + tp + col + 'm' if is_color else ''
450
+ end = ansi_end if is_color else ''
451
+ return [begin, end]
452
+
453
+ ## 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
+
455
+ def to_rgb(color):
456
+ if is_string_color(color): # from 0 to 1
457
+ color = get_color_code(color)
458
+ #color = type0_to_type1_codes[code]
459
+ if is_integer_color(color): # from 0 or 1 to 2
460
+ return type1_to_type2_codes[color]
461
+ return color
462
+
463
+ ##############################################
464
+ ############ Style Codes ##############
465
+ ##############################################
466
+
467
+ no_style = 'default'
468
+
469
+ styles = list(style_codes.keys()) + [no_style]
470
+
471
+ info_style = 'dim'
472
+
473
+ def get_style_code(style): # from single style to style number code
474
+ style = style.strip()
475
+ return style_codes[style]
476
+
477
+ def get_style_codes(style): # from many styles (separated by space) to as many number codes
478
+ style = style.strip().split()
479
+ codes = [get_style_code(el) for el in style if el in styles]
480
+ codes = no_duplicates(codes)
481
+ return codes
482
+
483
+ def get_style_name(code): # from style number code to style name
484
+ codes = list(style_codes.values())
485
+ return styles[codes.index(code)] if code in codes else no_style
486
+
487
+ def clean_styles(style): # it returns a well written sequence of styles (separated by spaces) from a possible confused one
488
+ codes = get_style_codes(style)
489
+ return ' '.join([get_style_name(el) for el in codes])
490
+
491
+ def is_style(style):
492
+ style = style.strip().split() if isinstance(style, str) else ['']
493
+ return any([el in styles for el in style])
494
+
495
+ ##############################################
496
+ ########### Plot Utilities ############
497
+ ##############################################
498
+
499
+ def set_data(x = None, y = None): # it return properly formatted x and y data lists
500
+ if x is None and y is None :
501
+ x, y = [], []
502
+ elif x is not None and y is None:
503
+ y = x
504
+ x = list(range(1, len(y) + 1))
505
+ lx, ly = len(x), len(y)
506
+ if lx != ly:
507
+ l = min(lx, ly)
508
+ x = x[ : l]
509
+ y = y[ : l]
510
+ return [list(x), list(y)]
511
+
512
+ ##############################################
513
+ ####### Figure Class Utilities ########
514
+ ##############################################
515
+
516
+ def set_sizes(sizes, size_max): # given certain widths (or heights) - some of them are None - it sets them so to respect max value
517
+ bins = len(sizes)
518
+ for s in range(bins):
519
+ size_set = sum([el for el in sizes[0 : s] + sizes[s + 1 : ] if el is not None])
520
+ available = max(size_max - size_set, 0)
521
+ to_set = len([el for el in sizes[s : ] if el is None])
522
+ sizes[s] = available // to_set if sizes[s] is None else sizes[s]
523
+ return sizes
524
+
525
+ def fit_sizes(sizes, size_max): # 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
+ bins = len(sizes)
527
+ s = bins - 1
528
+ #while (sum(sizes) != size_max if not_less else sum(sizes) > size_max) and s >= 0:
529
+ while sum(sizes) > size_max and s >= 0:
530
+ other_sizes = sum([sizes[i] for i in range(bins) if i != s])
531
+ sizes[s] = max(size_max - other_sizes, 0)
532
+ s -= 1
533
+ return sizes
534
+
535
+ ##############################################
536
+ ####### Build Class Utilities #########
537
+ ##############################################
538
+
539
+ def get_first(data, test = True): # if test take the first element, otherwise the second
540
+ return data[0] if test else data[1]
541
+
542
+ def apply_scale(data, test = False): # apply log scale if test
543
+ return log(data) if test else data
544
+
545
+ def reverse_scale(data, test = False): # apply log scale if test
546
+ return power10(data) if test else data
547
+
548
+ def replace_none(data, num_data): # replace None elements in data with correspondent in num_data
549
+ return [data[i] if data[i] is not None else num_data[i] for i in range(len(data))]
550
+
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
+
554
+ def get_lim(data): # it returns the data minimum and maximum limits
555
+ data = [el for el in data if numerical(el)]
556
+ m = min(data, default = 0)
557
+ M = max(data, default = 0)
558
+ m, M = (m, M) if m != M else (0.5 * m, 1.5 * m) if m == M != 0 else (-1, 1)
559
+ return [m, M]
560
+
561
+ def get_matrix_data(data, lim, bins): # from data to relative canvas coordinates
562
+ change = lambda el: 0.5 + (bins - 1) * (el - lim[0]) / (lim[1] - lim[0])
563
+ # round is so that for example 9.9999 = 10, otherwise the floor function will give different results
564
+ return [math.floor(round(change(el), 8)) if numerical(el) else el for el in data]
565
+
566
+ def get_lines(x, y, *other): # 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
+ # if len(x) * len(y) == 0:
568
+ # return [], [], *[[]] * len(other)
569
+ o = transpose(other, len(other))
570
+ xl, yl, ol = [[] for i in range(3)]
571
+ for n in range(len(x) - 1):
572
+ xn, yn = x[n : n + 2], y[n : n + 2]
573
+ xn, yn = get_line(xn, yn)
574
+ xl += xn[:-1]
575
+ yl += yn[:-1]
576
+ ol += [o[n]] * len(xn[:-1])
577
+ xl = xl + [x[-1]] if x != [] else xl
578
+ yl = yl + [y[-1]] if x != [] else yl
579
+ ol = ol + [o[-1]] if x != [] else ol
580
+ return [xl, yl] + transpose(ol, len(other))
581
+
582
+ def get_line(x, y): # 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
+ if not all_numerical(join([x, y])):
584
+ return x, y
585
+ x0, x1 = x
586
+ y0, y1 = y
587
+ dx, dy = int(x1) - int(x0), int(y1) - int(y0)
588
+ ax, ay = abs(dx), abs(dy)
589
+ a = int(max(ax, ay) + 1)
590
+ x = [int(el) for el in linspace(x0, x1, a)]
591
+ y = [int(el) for el in linspace(y0, y1, a)]
592
+ return [x, y]
593
+
594
+ def get_fill_level(fill, lim, bins):
595
+ if fill is False:
596
+ return False
597
+ elif isinstance(fill, str):
598
+ return fill
599
+ else:
600
+ fill = min(max(fill, lim[0]), lim[1])
601
+ fill = get_matrix_data([fill], lim, bins)[0]
602
+ return fill
603
+
604
+ def find_filling_values(x, y, y0):
605
+ xn, yn, yf = [[]] * 3
606
+ l = len(x);
607
+ while len(x) > 0:
608
+ i = len(xn)
609
+ xn.append(x[i])
610
+ yn.append(y[i])
611
+ J = [j for j in range(l) if x[j] == x[i]]
612
+ if J != []:
613
+ Y = [y[j] for j in J]
614
+ j = Y.index(min(Y))
615
+ J.pop(j)
616
+ [x.pop(j) for j in J]
617
+ [y.pop(j) for j in J]
618
+ yf.append(y[j])
619
+ return xn, yn, yf
620
+
621
+ def get_fill_boundaries(x, y):
622
+ xm = []
623
+ l = len(x)
624
+ for i in range(l):
625
+ xi, yi = x[i], y[i]
626
+ I = [j for j in range(l) if x[j] == xi and y[j] < yi]
627
+ Y = [y[j] for j in I]
628
+ m = min(Y, default = yi)
629
+ xm.append([x[i], m])
630
+ x, m = transpose(xm)
631
+ return m
632
+
633
+ def fill_data(x, y, y0, *other): # it fills x, y with y data points reaching y0; and c are the list of markers and colors that needs to be elongated
634
+ #y0 = get_fill_boundaries(x, y)
635
+ y0 = get_fill_boundaries(x, y) if isinstance(y0, str) else [y0] * len(x)
636
+ o = transpose(other, len(other))
637
+ xf, yf, of = [[] for i in range(3)]
638
+ xy = []
639
+ for i in range(len(x)):
640
+ xi, yi, y0i = x[i], y[i], y0[i]
641
+ if [xi, yi] not in xy:
642
+ xy.append([xi, yi])
643
+ yn = range(y0i, yi + 1) if y0i < yi else range(yi, y0i) if y0i > yi else [y0i]
644
+ yn = list(yn)
645
+ xn = [xi] * len(yn)
646
+ xf += xn
647
+ yf += yn
648
+ of += [o[i]] * len(xn)
649
+ return [xf, yf] + transpose(of, len(other))
650
+
651
+ def remove_outsiders(x, y, width, height, *other):
652
+ I = [i for i in range(len(x)) if x[i] in range(width) and y[i] in range(height)]
653
+ o = transpose(other, len(other))
654
+ return transpose([(x[i], y[i], *o[i]) for i in I], 2 + len(other))
655
+
656
+ def get_labels(ticks): # it returns the approximated string version of the data ticks
657
+ d = distinguishing_digit(ticks)
658
+ formatting_string = "{:." + str(d + 1) + "f}"
659
+ labels = [formatting_string.format(el) for el in ticks]
660
+ pos = [el.index('.') + d + 2 for el in labels]
661
+ labels = [labels[i][: pos[i]] for i in range(len(labels))]
662
+ all_integers = all(map(lambda el: el == int(el), ticks))
663
+ labels = [add_extra_zeros(el, d) if len(labels) > 1 else el for el in labels] if not all_integers else [str(int(el)) for el in ticks]
664
+ #sign = any([el < 0 for el in ticks])
665
+ #labels = ['+' + labels[i] if ticks[i] > 0 and sign else labels[i] for i in range(len(labels))]
666
+ return labels
667
+
668
+ def distinguishing_digit(data): # it return the minimum amount of decimal digits necessary to distinguish all elements of a list
669
+ #data = [el for el in data if 'e' not in str(el)]
670
+ d = [_distinguishing_digit(data[i], data[i + 1]) for i in range(len(data) - 1)]
671
+ return max(d, default = 1)
672
+
673
+ def _distinguishing_digit(a, b): # it return the minimum amount of decimal digits necessary to distinguish a from b (when both are rounded to those digits).
674
+ d = abs(a - b)
675
+ d = 0 if d == 0 else - math.log10(2 * d)
676
+ #d = round(d, 10)
677
+ d = 0 if d < 0 else math.ceil(d)
678
+ d = d + 1 if round(a, d) == round(b, d) else d
679
+ return d
680
+
681
+ def add_extra_zeros(label, d): # it adds 0s at the end of a label if necessary
682
+ zeros = len(label) - 1 - label.index('.' if 'e' not in label else 'e')
683
+ if zeros < d:
684
+ label += '0' * (d - zeros)
685
+ return label
686
+
687
+ def add_extra_spaces(labels, side): # it adds empty spaces before or after the labels if necessary
688
+ length = 0 if labels == [] else max_length(labels)
689
+ if side == "left":
690
+ labels = [space * (length - len(el)) + el for el in labels]
691
+ if side == "right":
692
+ labels = [el + space * (length - len(el)) for el in labels]
693
+ return labels
694
+
695
+ def hd_group(x, y, xf, yf): # it returns the real coordinates of the HD markers and the matrix that defines the marker
696
+ l, xfm, yfm = len(x), max(xf), max(yf)
697
+ xm = [el // xfm if numerical(el) else el for el in x]
698
+ ym = [el // yfm if numerical(el) else el for el in y]
699
+ m = {}
700
+ for i in range(l):
701
+ xyi = xm[i], ym[i]
702
+ xfi, yfi = xf[i], yf[i]
703
+ mi = [[0 for x in range(xfi)] for y in range(yfi)]
704
+ m[xyi] = mi
705
+ for i in range(l):
706
+ xyi = xm[i], ym[i]
707
+ if all_numerical(xyi):
708
+ xk, yk = x[i] % xfi, y[i] % yfi
709
+ xk, yk = math.floor(xk), math.floor(yk)
710
+ m[xyi][yk][xk] = 1
711
+ x, y = transpose(m.keys(), 2)
712
+ m = [tuple(join(el[::-1])) for el in m.values()]
713
+ return x, y, m
714
+
715
+ ###############################################
716
+ ############# Bar Functions ##############
717
+ ###############################################
718
+
719
+ def bars(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
720
+ # if x == []:
721
+ # return [], []
722
+ bins = len(x)
723
+ #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
724
+ bin_size_half = width / 2
725
+ # adjust the bar width according to the number of bins
726
+ if bins > 1:
727
+ bin_size_half *= (max(x) - min(x)) / (bins - 1)
728
+ xbar, ybar = [], []
729
+ for i in range(bins):
730
+ xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
731
+ ybar.append([minimum, y[i]])
732
+ return xbar, ybar
733
+
734
+ def set_multiple_bar_data(*args):
735
+ l = len(args)
736
+ Y = [] if l == 0 else args[0] if l == 1 else args[1]
737
+ Y = [Y] if not isinstance(Y, list) or len(Y) == 0 else Y
738
+ m = len(Y[0])
739
+ x = [] if l == 0 else list(range(1, m + 1)) if l == 1 else args[0]
740
+ return x, Y
741
+
742
+ def hist_data(data, bins = 10, norm = False): # it returns data in histogram form if norm is False. Otherwise, it returns data in density form where all bins sum to 1.
743
+ #data = [round(el, 15) for el in data]
744
+ # if data == []:
745
+ # return [], []
746
+ bins = 0 if len(data) == 0 else bins
747
+ m, M = min(data, default = 0), max(data, default = 0)
748
+ data = [(el - m) / (M - m) * bins if el != M else bins - 1 for el in data]
749
+ data = [int(el) for el in data]
750
+ histx = linspace(m, M, bins)
751
+ histy = [0] * bins
752
+ for el in data:
753
+ histy[el] += 1
754
+ if norm:
755
+ histy = [el / len(data) for el in histy]
756
+ return histx, histy
757
+
758
+ def single_bar(x, y, ylabel, marker, colors):
759
+ l = len(y)
760
+ lc = len(colors)
761
+ xs = colorize(str(x), 'gray+', 'bold')
762
+ bar = [marker * el for el in y]
763
+ bar = [apply_ansi(bar[i], colors[i % lc], 1) for i in range(l)]
764
+ ylabel = colorize(f'{ylabel:.2f}', 'gray+', 'bold')
765
+ bar = xs + space + ''.join(bar) + space + ylabel
766
+ return bar
767
+
768
+ def bar_data(*args, width = None, mode = 'stacked'):
769
+ x, Y = set_multiple_bar_data(*args)
770
+ x = list(map(str, x))
771
+ x = add_extra_spaces(x, 'right')
772
+ lx = len(x[0])
773
+ y = [sum(el) for el in transpose(Y)] if mode == 'stacked' else Y
774
+ ly = max_length([round(el, 2) for el in join(y)])
775
+
776
+ width_term = terminal_width()
777
+ width = width_term if width is None else min(width, width_term)
778
+ width = max(width, lx + ly + 2 + 1)
779
+
780
+ my = max(join(y))
781
+ my = 1 if my == 0 else my
782
+ dx = my / (width - lx - ly - 2)
783
+ Yi = [[round(el / dx, 0) for el in y] for y in Y]
784
+ Yi = transpose(Yi)
785
+
786
+ return x, y, Yi, width
787
+
788
+ def correct_marker(marker = None):
789
+ return simple_bar_marker if marker is None else marker[0]
790
+
791
+ def get_title(title, width):
792
+ out = ''
793
+ if title is not None:
794
+ l = len(uncolorize(title))
795
+ w1 = (width - 2 - l) // 2; w2 = width - l - 2 - w1
796
+ l1 = '─' * w1 + space
797
+ l2 = space + '─' * w2
798
+ out = colorize(l1 + title + l2, 'gray+', 'bold') + '\n'
799
+ return out
800
+
801
+ def get_simple_labels(marker, labels, colors, width):
802
+ out = '\n'
803
+ if labels != None:
804
+ l = len(labels)
805
+ lc = len(colors)
806
+ out = space.join([colorize(marker * 3, colors[i % lc]) + space + colorize(labels[i], 'gray+', 'bold') for i in range(l)])
807
+ out = '\n' + get_title(out, width)
808
+ return out
809
+
810
+ ###############################################
811
+ ############# Box Functions ##############
812
+ ###############################################
813
+
814
+ def box(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
815
+ # if x == []:
816
+ # return [], []
817
+ bins = len(x)
818
+ #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
819
+ bin_size_half = width / 2
820
+ # adjust the bar width according to the number of bins
821
+ if bins > 1:
822
+ bin_size_half *= (max(x) - min(x)) / (bins - 1)
823
+ c, q1, q2, q3, h, l = [], [], [], [], [], []
824
+ xbar, ybar, mybar = [], [], []
825
+
826
+ for i in range(bins):
827
+ c.append(x[i])
828
+ xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
829
+ q1.append(quantile(y[i], 0.25))
830
+ q2.append(quantile(y[i], 0.50))
831
+ q3.append(quantile(y[i], 0.75))
832
+ h.append(max(y[i]))
833
+ l.append(min(y[i]))
834
+
835
+ return q1, q2, q3, h, l, c, xbar
836
+
837
+ ##############################################
838
+ ########## Image Utilities #############
839
+ ##############################################
840
+
841
+ def update_size(size_old, size_new): # 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
+ size_old = [size_old[0], size_old[1] / 2]
843
+ ratio_old = size_old[1] / size_old[0]
844
+ size_new = replace(size_new, size_old)
845
+ ratio_new = size_new[1] / size_new[0]
846
+ #ratio_new = size_new[1] / size_new[0]
847
+ size_new = [1 if el == 0 else el for el in size_new]
848
+ return [int(size_new[0]), int(size_new[1])]
849
+
850
+ def image_to_matrix(image): # from image to a matrix of pixels
851
+ pixels = list(image.getdata())
852
+ width, height = image.size
853
+ return [pixels[i * width:(i + 1) * width] for i in range(height)]