1import sys, shutil, os, re, math, inspect
2from plotext._dict import *
3
4###############################################
5######### Number Manipulation ##########
6###############################################
7
8def 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
14def 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
17def 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
24def try_float(data): # it turn a string into float if it can
25 try:
26 return float(data)
27 except:
28 return data
29
30def 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
42def 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
46def 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
52def 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
57def 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
62def 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
69def 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
72def power10(data): # it apply log function to the data
73 return [10 ** el for el in data]
74
75def floor(data): # it floors a list of data
76 return list(map(math.floor, data))
77
78def 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
87def 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
91def 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
95def 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
105def matrix_size(matrix): # cols, height
106 return [len(matrix[0]), len(matrix)] if matrix != [] else [0, 0]
107
108def transpose(data, length = 1): # it needs no explanation
109 return [[]] * length if data == [] else list(map(list, zip(*data)))
110
111def vstack(matrix, extra): # vertical stack of two matrices
112 return extra + matrix # + extra
113
114def 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
119def 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
124def 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
137nl = "\n"
138
139def 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
142def 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
152positive_color = 'green+'
153negative_color = 'red'
154title_color = 'cyan+'
155
156def 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
159def 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
167def 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
172def 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
187def 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
192def max_length(strings):
193 strings = map(str, strings)
194 return max(map(len, strings), default = 0)
195
196###############################################
197########## File Manipulation ############
198###############################################
199
200def 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
206def 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
211def script_folder(): # the folder of the script executed
212 return parent_folder(inspect.getfile(sys._getframe(1)))
213
214def 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
222def 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
227def 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
233def 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
242def 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
253def 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
260def 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
270def is_ipython(): # true if running in ipython shenn
271 try:
272 __IPYTHON__
273 return True
274 except NameError:
275 return False
276
277def 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
284platform = platform()
285
286# to enable ascii escape color sequences
287if platform == "windows":
288 import subprocess
289 subprocess.call('', shell = True)
290
291def 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
298terminal_width = lambda: terminal_size()[0]
299tw = terminal_width
300
301terminal_height = lambda: terminal_size()[1]
302th = terminal_height
303
304def clear_terminal(lines = None): # it cleat the entire terminal, or the specified number of lines
305 if lines is None:
306 write('\033c')
307 else:
308 for r in range(lines):
309 write("\033[A") # moves the curson up
310 write("\033[2K") # clear the entire line
311
312def write(string): # the print function used by plotext
313 sys.stdout.write(string)
314
315class memorize: # it memorise the arguments of a function, when used as its decorator, to reduce computational time
316 def __init__(self, f):
317 self.f = f
318 self.memo = {}
319 def __call__(self, *args):
320 if not args in self.memo:
321 self.memo[args] = self.f(*args)
322 return self.memo[args]
323
324##############################################
325######### Marker Utilities ###########
326##############################################
327
328space = ' ' # the default null character that appears as background to all plots
329plot_marker = "hd" if platform == 'unix' else 'dot'
330
331hd_markers = {hd_codes[el] : el for el in hd_codes}
332fhd_markers = {fhd_codes[el] : el for el in fhd_codes}
333braille_markers = {braille_codes[el] : el for el in braille_codes}
334simple_bar_marker = '▇'
335
336@memorize
337def get_hd_marker(code):
338 return hd_codes[code] if len(code) == 4 else fhd_codes[code] if len(code) == 6 else braille_codes[code]
339
340def marker_factor(marker, hd, fhd, braille): # useful to improve the resolution of the canvas for higher resolution markers
341 return hd if marker == 'hd' else fhd if marker == 'fhd' else braille if marker == 'braille' else 1
342
343##############################################
344########### Color Utilities ############
345##############################################
346
347# A user could specify three types of colors
348 # an integer for 256 color codes
349 # a tuple for RGB color codes
350 # a string for 16 color codes or styles
351
352# Along side the user needs to specify whatever it is for background / fullground / style
353# which plotext calls 'character' = 0 / 1 / 2
354
355
356#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]
357
358def get_color_code(color): # the color number code from color string
359 color = color.strip()
360 return color_codes[color]
361
362def get_color_name(code): # the color string from color number code
363 codes = list(color_codes.values())
364 return colors[codes.index(code)] if code in codes else no_color
365
366def is_string_color(color):
367 return isinstance(color, str) and color.strip() in colors
368
369def is_integer_color(color):
370 return isinstance(color, int) and 0 <= color <= 255
371
372def is_rgb_color(color):
373 is_rgb = isinstance(color, list) or isinstance(color, tuple)
374 is_rgb = is_rgb and len(color) == 3
375 is_rgb = is_rgb and all([is_integer_color(el) for el in color])
376 return is_rgb
377
378def is_color(color):
379 return is_string_color(color) or is_integer_color(color) or is_rgb_color(color)
380
381def colorize(string, color = None, style = None, background = None, show = False): # it paints a text with given fullground and background color
382 string = apply_ansi(string, background, 0)
383 string = apply_ansi(string, color, 1)
384 string = apply_ansi(string, style, 2)
385 if show:
386 print(string)
387 return string
388
389def uncolorize(string): # remove color codes from colored string
390 colored = lambda: ansi_begin in string
391 while colored():
392 b = string.index(ansi_begin)
393 e = string[b : ].index('m') + b + 1
394 string = string.replace(string[b : e], '')
395 return string
396
397def apply_ansi(string, color, character):
398 begin, end = ansi(color, character)
399 return begin + string + end
400
401#ansi_begin = '\033['
402ansi_begin = '\x1b['
403ansi_end = ansi_begin + '0m'
404
405@memorize
406def colors_to_ansi(fullground, style, background):
407 color = [background, fullground, style]
408 return ''.join([ansi(color[i], i)[0] for i in range(3)])
409
410@memorize
411def ansi(color, character):
412 if color == no_color:
413 return ['', '']
414 col, fg, tp = '', '', ''
415 if character == 2 and is_style(color):
416 col = get_style_codes(color)
417 col = ';'.join([str(el) for el in col])
418 elif character != 2:
419 fg = '38;' if character == 1 else '48;'
420 tp = '5;'
421 if is_string_color(color):
422 col = str(get_color_code(color))
423 elif is_integer_color(color):
424 col = str(color)
425 elif is_rgb_color(color):
426 col = ';'.join([str(el) for el in color])
427 tp = '2;'
428 is_color = col != ''
429 begin = ansi_begin + fg + tp + col + 'm' if is_color else ''
430 end = ansi_end if is_color else ''
431 return [begin, end]
432
433## 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
434
435def to_rgb(color):
436 if is_string_color(color): # from 0 to 1
437 color = get_color_code(color)
438 #color = type0_to_type1_codes[code]
439 if is_integer_color(color): # from 0 or 1 to 2
440 return type1_to_type2_codes[color]
441 return color
442
443##############################################
444############ Style Codes ##############
445##############################################
446
447no_style = 'default'
448
449styles = list(style_codes.keys()) + [no_style]
450
451info_style = 'dim'
452
453def get_style_code(style): # from single style to style number code
454 style = style.strip()
455 return style_codes[style]
456
457def get_style_codes(style): # from many styles (separated by space) to as many number codes
458 style = style.strip().split()
459 codes = [get_style_code(el) for el in style if el in styles]
460 codes = no_duplicates(codes)
461 return codes
462
463def get_style_name(code): # from style number code to style name
464 codes = list(style_codes.values())
465 return styles[codes.index(code)] if code in codes else no_style
466
467def clean_styles(style): # it returns a well written sequence of styles (separated by spaces) from a possible confused one
468 codes = get_style_codes(style)
469 return ' '.join([get_style_name(el) for el in codes])
470
471def is_style(style):
472 style = style.strip().split() if isinstance(style, str) else ['']
473 return any([el in styles for el in style])
474
475##############################################
476########### Plot Utilities ############
477##############################################
478
479def set_data(x = None, y = None): # it return properly formatted x and y data lists
480 if x is None and y is None :
481 x, y = [], []
482 elif x is not None and y is None:
483 y = x
484 x = list(range(1, len(y) + 1))
485 lx, ly = len(x), len(y)
486 if lx != ly:
487 l = min(lx, ly)
488 x = x[ : l]
489 y = y[ : l]
490 return [list(x), list(y)]
491
492##############################################
493####### Figure Class Utilities ########
494##############################################
495
496def set_sizes(sizes, size_max): # given certain widths (or heights) - some of them are None - it sets them so to respect max value
497 bins = len(sizes)
498 for s in range(bins):
499 size_set = sum([el for el in sizes[0 : s] + sizes[s + 1 : ] if el is not None])
500 available = max(size_max - size_set, 0)
501 to_set = len([el for el in sizes[s : ] if el is None])
502 sizes[s] = available // to_set if sizes[s] is None else sizes[s]
503 return sizes
504
505def 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
506 bins = len(sizes)
507 s = bins - 1
508 #while (sum(sizes) != size_max if not_less else sum(sizes) > size_max) and s >= 0:
509 while sum(sizes) > size_max and s >= 0:
510 other_sizes = sum([sizes[i] for i in range(bins) if i != s])
511 sizes[s] = max(size_max - other_sizes, 0)
512 s -= 1
513 return sizes
514
515##############################################
516####### Build Class Utilities #########
517##############################################
518
519def get_first(data, test = True): # if test take the first element, otherwise the second
520 return data[0] if test else data[1]
521
522def apply_scale(data, test = False): # apply log scale if test
523 return log(data) if test else data
524
525def reverse_scale(data, test = False): # apply log scale if test
526 return power10(data) if test else data
527
528def replace_none(data, num_data): # replace None elements in data with correspondent in num_data
529 return [data[i] if data[i] is not None else num_data[i] for i in range(len(data))]
530
531numerical = lambda el: not (el is None or math.isnan(el)) or isinstance(el, str) # in the case of string datetimes
532all_numerical = lambda data: all([numerical(el) for el in data])
533
534def get_lim(data): # it returns the data minimum and maximum limits
535 data = [el for el in data if numerical(el)]
536 m = min(data, default = 0)
537 M = max(data, default = 0)
538 m, M = (m, M) if m != M else (0.5 * m, 1.5 * m) if m == M != 0 else (-1, 1)
539 return [m, M]
540
541def get_matrix_data(data, lim, bins): # from data to relative canvas coordinates
542 change = lambda el: 0.5 + (bins - 1) * (el - lim[0]) / (lim[1] - lim[0])
543 # round is so that for example 9.9999 = 10, otherwise the floor function will give different results
544 return [math.floor(round(change(el), 8)) if numerical(el) else el for el in data]
545
546def 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
547 # if len(x) * len(y) == 0:
548 # return [], [], *[[]] * len(other)
549 o = transpose(other, len(other))
550 xl, yl, ol = [[] for i in range(3)]
551 for n in range(len(x) - 1):
552 xn, yn = x[n : n + 2], y[n : n + 2]
553 xn, yn = get_line(xn, yn)
554 xl += xn[:-1]
555 yl += yn[:-1]
556 ol += [o[n]] * len(xn[:-1])
557 xl = xl + [x[-1]] if x != [] else xl
558 yl = yl + [y[-1]] if x != [] else yl
559 ol = ol + [o[-1]] if x != [] else ol
560 return [xl, yl] + transpose(ol, len(other))
561
562def 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.
563 if not all_numerical(join([x, y])):
564 return x, y
565 x0, x1 = x
566 y0, y1 = y
567 dx, dy = int(x1) - int(x0), int(y1) - int(y0)
568 ax, ay = abs(dx), abs(dy)
569 a = int(max(ax, ay) + 1)
570 x = [int(el) for el in linspace(x0, x1, a)]
571 y = [int(el) for el in linspace(y0, y1, a)]
572 return [x, y]
573
574def get_fill_level(fill, lim, bins):
575 if fill is False:
576 return False
577 elif isinstance(fill, str):
578 return fill
579 else:
580 fill = min(max(fill, lim[0]), lim[1])
581 fill = get_matrix_data([fill], lim, bins)[0]
582 return fill
583
584def find_filling_values(x, y, y0):
585 xn, yn, yf = [[]] * 3
586 l = len(x);
587 while len(x) > 0:
588 i = len(xn)
589 xn.append(x[i])
590 yn.append(y[i])
591 J = [j for j in range(l) if x[j] == x[i]]
592 if J != []:
593 Y = [y[j] for j in J]
594 j = Y.index(min(Y))
595 J.pop(j)
596 [x.pop(j) for j in J]
597 [y.pop(j) for j in J]
598 yf.append(y[j])
599 return xn, yn, yf
600
601def get_fill_boundaries(x, y):
602 xm = []
603 l = len(x)
604 for i in range(l):
605 xi, yi = x[i], y[i]
606 I = [j for j in range(l) if x[j] == xi and y[j] < yi]
607 Y = [y[j] for j in I]
608 m = min(Y, default = yi)
609 xm.append([x[i], m])
610 x, m = transpose(xm)
611 return m
612
613def 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
614 #y0 = get_fill_boundaries(x, y)
615 y0 = get_fill_boundaries(x, y) if isinstance(y0, str) else [y0] * len(x)
616 o = transpose(other, len(other))
617 xf, yf, of = [[] for i in range(3)]
618 xy = []
619 for i in range(len(x)):
620 xi, yi, y0i = x[i], y[i], y0[i]
621 if [xi, yi] not in xy:
622 xy.append([xi, yi])
623 yn = range(y0i, yi + 1) if y0i < yi else range(yi, y0i) if y0i > yi else [y0i]
624 yn = list(yn)
625 xn = [xi] * len(yn)
626 xf += xn
627 yf += yn
628 of += [o[i]] * len(xn)
629 return [xf, yf] + transpose(of, len(other))
630
631def remove_outsiders(x, y, width, height, *other):
632 I = [i for i in range(len(x)) if x[i] in range(width) and y[i] in range(height)]
633 o = transpose(other, len(other))
634 return transpose([(x[i], y[i], *o[i]) for i in I], 2 + len(other))
635
636def get_labels(ticks): # it returns the approximated string version of the data ticks
637 d = distinguishing_digit(ticks)
638 formatting_string = "{:." + str(d + 1) + "f}"
639 labels = [formatting_string.format(el) for el in ticks]
640 pos = [el.index('.') + d + 2 for el in labels]
641 labels = [labels[i][: pos[i]] for i in range(len(labels))]
642 all_integers = all(map(lambda el: el == int(el), ticks))
643 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]
644 #sign = any([el < 0 for el in ticks])
645 #labels = ['+' + labels[i] if ticks[i] > 0 and sign else labels[i] for i in range(len(labels))]
646 return labels
647
648def distinguishing_digit(data): # it return the minimum amount of decimal digits necessary to distinguish all elements of a list
649 #data = [el for el in data if 'e' not in str(el)]
650 d = [_distinguishing_digit(data[i], data[i + 1]) for i in range(len(data) - 1)]
651 return max(d, default = 1)
652
653def _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).
654 d = abs(a - b)
655 d = 0 if d == 0 else - math.log10(2 * d)
656 #d = round(d, 10)
657 d = 0 if d < 0 else math.ceil(d)
658 d = d + 1 if round(a, d) == round(b, d) else d
659 return d
660
661def add_extra_zeros(label, d): # it adds 0s at the end of a label if necessary
662 zeros = len(label) - 1 - label.index('.' if 'e' not in label else 'e')
663 if zeros < d:
664 label += '0' * (d - zeros)
665 return label
666
667def add_extra_spaces(labels, side): # it adds empty spaces before or after the labels if necessary
668 length = 0 if labels == [] else max_length(labels)
669 if side == "left":
670 labels = [space * (length - len(el)) + el for el in labels]
671 if side == "right":
672 labels = [el + space * (length - len(el)) for el in labels]
673 return labels
674
675def hd_group(x, y, xf, yf): # it returns the real coordinates of the HD markers and the matrix that defines the marker
676 l, xfm, yfm = len(x), max(xf), max(yf)
677 xm = [el // xfm if numerical(el) else el for el in x]
678 ym = [el // yfm if numerical(el) else el for el in y]
679 m = {}
680 for i in range(l):
681 xyi = xm[i], ym[i]
682 xfi, yfi = xf[i], yf[i]
683 mi = [[0 for x in range(xfi)] for y in range(yfi)]
684 m[xyi] = mi
685 for i in range(l):
686 xyi = xm[i], ym[i]
687 if all_numerical(xyi):
688 xk, yk = x[i] % xfi, y[i] % yfi
689 xk, yk = math.floor(xk), math.floor(yk)
690 m[xyi][yk][xk] = 1
691 x, y = transpose(m.keys(), 2)
692 m = [tuple(join(el[::-1])) for el in m.values()]
693 return x, y, m
694
695###############################################
696############# Bar Functions ##############
697###############################################
698
699def bars(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
700 # if x == []:
701 # return [], []
702 bins = len(x)
703 #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
704 bin_size_half = width / 2
705 # adjust the bar width according to the number of bins
706 if bins > 1:
707 bin_size_half *= (max(x) - min(x)) / (bins - 1)
708 xbar, ybar = [], []
709 for i in range(bins):
710 xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
711 ybar.append([minimum, y[i]])
712 return xbar, ybar
713
714def set_multiple_bar_data(*args):
715 l = len(args)
716 Y = [] if l == 0 else args[0] if l == 1 else args[1]
717 Y = [Y] if not isinstance(Y, list) or len(Y) == 0 else Y
718 m = len(Y[0])
719 x = [] if l == 0 else list(range(1, m + 1)) if l == 1 else args[0]
720 return x, Y
721
722def 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.
723 #data = [round(el, 15) for el in data]
724 # if data == []:
725 # return [], []
726 bins = 0 if len(data) == 0 else bins
727 m, M = min(data, default = 0), max(data, default = 0)
728 data = [(el - m) / (M - m) * bins if el != M else bins - 1 for el in data]
729 data = [int(el) for el in data]
730 histx = linspace(m, M, bins)
731 histy = [0] * bins
732 for el in data:
733 histy[el] += 1
734 if norm:
735 histy = [el / len(data) for el in histy]
736 return histx, histy
737
738def single_bar(x, y, ylabel, marker, colors):
739 l = len(y)
740 lc = len(colors)
741 xs = colorize(str(x), 'gray+', 'bold')
742 bar = [marker * el for el in y]
743 bar = [apply_ansi(bar[i], colors[i % lc], 1) for i in range(l)]
744 ylabel = colorize(f'{ylabel:.2f}', 'gray+', 'bold')
745 bar = xs + space + ''.join(bar) + space + ylabel
746 return bar
747
748def bar_data(*args, width = None, mode = 'stacked'):
749 x, Y = set_multiple_bar_data(*args)
750 x = list(map(str, x))
751 x = add_extra_spaces(x, 'right')
752 lx = len(x[0])
753 y = [sum(el) for el in transpose(Y)] if mode == 'stacked' else Y
754 ly = max_length([round(el, 2) for el in join(y)])
755
756 width_term = terminal_width()
757 width = width_term if width is None else min(width, width_term)
758 width = max(width, lx + ly + 2 + 1)
759
760 my = max(join(y))
761 my = 1 if my == 0 else my
762 dx = my / (width - lx - ly - 2)
763 Yi = [[round(el / dx, 0) for el in y] for y in Y]
764 Yi = transpose(Yi)
765
766 return x, y, Yi, width
767
768def correct_marker(marker = None):
769 return simple_bar_marker if marker is None else marker[0]
770
771def get_title(title, width):
772 out = ''
773 if title is not None:
774 l = len(uncolorize(title))
775 w1 = (width - 2 - l) // 2; w2 = width - l - 2 - w1
776 l1 = '─' * w1 + space
777 l2 = space + '─' * w2
778 out = colorize(l1 + title + l2, 'gray+', 'bold') + '\n'
779 return out
780
781def get_simple_labels(marker, labels, colors, width):
782 out = '\n'
783 if labels != None:
784 l = len(labels)
785 lc = len(colors)
786 out = space.join([colorize(marker * 3, colors[i % lc]) + space + colorize(labels[i], 'gray+', 'bold') for i in range(l)])
787 out = '\n' + get_title(out, width)
788 return out
789
790###############################################
791############# Box Functions ##############
792###############################################
793
794def box(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
795 # if x == []:
796 # return [], []
797 bins = len(x)
798 #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
799 bin_size_half = width / 2
800 # adjust the bar width according to the number of bins
801 if bins > 1:
802 bin_size_half *= (max(x) - min(x)) / (bins - 1)
803 c, q1, q2, q3, h, l = [], [], [], [], [], []
804 xbar, ybar, mybar = [], [], []
805
806 for i in range(bins):
807 c.append(x[i])
808 xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
809 q1.append(quantile(y[i], 0.25))
810 q2.append(quantile(y[i], 0.50))
811 q3.append(quantile(y[i], 0.75))
812 h.append(max(y[i]))
813 l.append(min(y[i]))
814
815 return q1, q2, q3, h, l, c, xbar
816
817##############################################
818########## Image Utilities #############
819##############################################
820
821def 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
822 size_old = [size_old[0], size_old[1] / 2]
823 ratio_old = size_old[1] / size_old[0]
824 size_new = replace(size_new, size_old)
825 ratio_new = size_new[1] / size_new[0]
826 #ratio_new = size_new[1] / size_new[0]
827 size_new = [1 if el == 0 else el for el in size_new]
828 return [int(size_new[0]), int(size_new[1])]
829
830def image_to_matrix(image): # from image to a matrix of pixels
831 pixels = list(image.getdata())
832 width, height = image.size
833 return [pixels[i * width:(i + 1) * width] for i in range(height)]