mirror of
https://github.com/littlefs-project/littlefs.git
synced 2025-12-01 12:20:02 +00:00
Added plot.py for in-terminal plotting
This commit is contained in:
770
scripts/plot.py
Executable file
770
scripts/plot.py
Executable file
@ -0,0 +1,770 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Plot CSV files in terminal.
|
||||
#
|
||||
# Example:
|
||||
# ./scripts/plot.py bench.csv -xSIZE -ybench_read -W80 -H17
|
||||
#
|
||||
# Copyright (c) 2022, The littlefs authors.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
|
||||
import collections as co
|
||||
import csv
|
||||
import glob
|
||||
import io
|
||||
import itertools as it
|
||||
import math as m
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
CSV_PATHS = ['*.csv']
|
||||
COLORS = [
|
||||
'1;34', # bold blue
|
||||
'1;31', # bold red
|
||||
'1;32', # bold green
|
||||
'1;35', # bold purple
|
||||
'1;33', # bold yellow
|
||||
'1;36', # bold cyan
|
||||
'34', # blue
|
||||
'31', # red
|
||||
'32', # green
|
||||
'35', # purple
|
||||
'33', # yellow
|
||||
'36', # cyan
|
||||
]
|
||||
|
||||
CHARS_DOTS = " .':"
|
||||
CHARS_BRAILLE = (
|
||||
'⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
|
||||
'⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
|
||||
'⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
|
||||
'⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
|
||||
'⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
|
||||
'⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
|
||||
'⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
|
||||
'⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
|
||||
|
||||
SI_PREFIXES = {
|
||||
18: 'E',
|
||||
15: 'P',
|
||||
12: 'T',
|
||||
9: 'G',
|
||||
6: 'M',
|
||||
3: 'K',
|
||||
0: '',
|
||||
-3: 'm',
|
||||
-6: 'u',
|
||||
-9: 'n',
|
||||
-12: 'p',
|
||||
-15: 'f',
|
||||
-18: 'a',
|
||||
}
|
||||
|
||||
|
||||
# format a number to a strict character width using SI prefixes
|
||||
def si(x, w=4):
|
||||
if x == 0:
|
||||
return '0'
|
||||
# figure out prefix and scale
|
||||
p = 3*int(m.log(abs(x)*10, 10**3))
|
||||
p = min(18, max(-18, p))
|
||||
# format with enough digits
|
||||
s = '%.*f' % (w, abs(x) / (10.0**p))
|
||||
s = s.lstrip('0')
|
||||
# truncate but only digits that follow the dot
|
||||
if '.' in s:
|
||||
s = s[:max(s.find('.'), w-(2 if x < 0 else 1))]
|
||||
s = s.rstrip('0')
|
||||
s = s.rstrip('.')
|
||||
return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p])
|
||||
|
||||
def openio(path, mode='r'):
|
||||
if path == '-':
|
||||
if mode == 'r':
|
||||
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
|
||||
else:
|
||||
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
|
||||
else:
|
||||
return open(path, mode)
|
||||
|
||||
|
||||
# parse different data representations
|
||||
def dat(x):
|
||||
# allow the first part of an a/b fraction
|
||||
if '/' in x:
|
||||
x, _ = x.split('/', 1)
|
||||
|
||||
# first try as int
|
||||
try:
|
||||
return int(x, 0)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# then try as float
|
||||
try:
|
||||
x = float(x)
|
||||
# just don't allow infinity or nan
|
||||
if m.isinf(x) or m.isnan(x):
|
||||
raise ValueError("invalid dat %r" % x)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# else give up
|
||||
raise ValueError("invalid dat %r" % x)
|
||||
|
||||
# a hack log10 that preserves sign, and passes zero as zero
|
||||
def slog10(x):
|
||||
if x == 0:
|
||||
return x
|
||||
elif x > 0:
|
||||
return m.log10(x)
|
||||
else:
|
||||
return -m.log10(-x)
|
||||
|
||||
|
||||
class Plot:
|
||||
def __init__(self, width, height, *,
|
||||
xlim=None,
|
||||
ylim=None,
|
||||
xlog=False,
|
||||
ylog=False,
|
||||
**_):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.xlim = xlim or (0, width)
|
||||
self.ylim = ylim or (0, height)
|
||||
self.xlog = xlog
|
||||
self.ylog = ylog
|
||||
self.grid = [('',False)]*(self.width*self.height)
|
||||
|
||||
def scale(self, x, y):
|
||||
# scale and clamp
|
||||
try:
|
||||
if self.xlog:
|
||||
x = int(self.width * (
|
||||
(slog10(x)-slog10(self.xlim[0]))
|
||||
/ (slog10(self.xlim[1])-slog10(self.xlim[0]))))
|
||||
else:
|
||||
x = int(self.width * (
|
||||
(x-self.xlim[0])
|
||||
/ (self.xlim[1]-self.xlim[0])))
|
||||
if self.ylog:
|
||||
y = int(self.height * (
|
||||
(slog10(y)-slog10(self.ylim[0]))
|
||||
/ (slog10(self.ylim[1])-slog10(self.ylim[0]))))
|
||||
else:
|
||||
y = int(self.height * (
|
||||
(y-self.ylim[0])
|
||||
/ (self.ylim[1]-self.ylim[0])))
|
||||
except ZeroDivisionError:
|
||||
x = 0
|
||||
y = 0
|
||||
return x, y
|
||||
|
||||
def point(self, x, y, *,
|
||||
color=COLORS[0],
|
||||
char=True):
|
||||
# scale
|
||||
x, y = self.scale(x, y)
|
||||
|
||||
# ignore out of bounds points
|
||||
if x >= 0 and x < self.width and y >= 0 and y < self.height:
|
||||
self.grid[x + y*self.width] = (color, char)
|
||||
|
||||
def line(self, x1, y1, x2, y2, *,
|
||||
color=COLORS[0],
|
||||
char=True):
|
||||
# scale
|
||||
x1, y1 = self.scale(x1, y1)
|
||||
x2, y2 = self.scale(x2, y2)
|
||||
|
||||
# incremental error line algorithm
|
||||
ex = abs(x2 - x1)
|
||||
ey = -abs(y2 - y1)
|
||||
dx = +1 if x1 < x2 else -1
|
||||
dy = +1 if y1 < y2 else -1
|
||||
e = ex + ey
|
||||
|
||||
while True:
|
||||
if x1 >= 0 and x1 < self.width and y1 >= 0 and y1 < self.height:
|
||||
self.grid[x1 + y1*self.width] = (color, char)
|
||||
e2 = 2*e
|
||||
|
||||
if x1 == x2 and y1 == y2:
|
||||
break
|
||||
|
||||
if e2 > ey:
|
||||
e += ey
|
||||
x1 += dx
|
||||
|
||||
if x1 == x2 and y1 == y2:
|
||||
break
|
||||
|
||||
if e2 < ex:
|
||||
e += ex
|
||||
y1 += dy
|
||||
|
||||
if x2 >= 0 and x2 < self.width and y2 >= 0 and y2 < self.height:
|
||||
self.grid[x2 + y2*self.width] = (color, char)
|
||||
|
||||
def plot(self, coords, *,
|
||||
color=COLORS[0],
|
||||
char=True,
|
||||
line_char=True):
|
||||
# draw lines
|
||||
if line_char:
|
||||
for (x1, y1), (x2, y2) in zip(coords, coords[1:]):
|
||||
if y1 is not None and y2 is not None:
|
||||
self.line(x1, y1, x2, y2,
|
||||
color=color,
|
||||
char=line_char)
|
||||
|
||||
# draw points
|
||||
if char and (not line_char or char is not True):
|
||||
for x, y in coords:
|
||||
if y is not None:
|
||||
self.point(x, y,
|
||||
color=color,
|
||||
char=char)
|
||||
|
||||
def draw(self, row, *,
|
||||
dots=False,
|
||||
braille=False,
|
||||
color=False,
|
||||
**_):
|
||||
# scale if needed
|
||||
if braille:
|
||||
xscale, yscale = 2, 4
|
||||
elif dots:
|
||||
xscale, yscale = 1, 2
|
||||
else:
|
||||
xscale, yscale = 1, 1
|
||||
|
||||
y = self.height//yscale-1 - row
|
||||
row_ = []
|
||||
for x in range(self.width//xscale):
|
||||
best_f = ''
|
||||
best_c = False
|
||||
|
||||
# encode into a byte
|
||||
b = 0
|
||||
for i in range(xscale*yscale):
|
||||
f, c = self.grid[x*xscale+(xscale-1-(i%xscale))
|
||||
+ (y*yscale+(i//xscale))*self.width]
|
||||
if c:
|
||||
b |= 1 << i
|
||||
|
||||
if f:
|
||||
best_f = f
|
||||
if c and c is not True:
|
||||
best_c = c
|
||||
|
||||
# use byte to lookup character
|
||||
if b:
|
||||
if best_c:
|
||||
c = best_c
|
||||
elif braille:
|
||||
c = CHARS_BRAILLE[b]
|
||||
else:
|
||||
c = CHARS_DOTS[b]
|
||||
else:
|
||||
c = ' '
|
||||
|
||||
# color?
|
||||
if b and color and best_f:
|
||||
c = '\x1b[%sm%s\x1b[m' % (best_f, c)
|
||||
|
||||
# draw axis in blank spaces
|
||||
if not b:
|
||||
zx, zy = self.scale(0, 0)
|
||||
if x == zx // xscale and y == zy // yscale:
|
||||
c = '+'
|
||||
elif x == zx // xscale and y == 0:
|
||||
c = 'v'
|
||||
elif x == zx // xscale and y == self.height//yscale-1:
|
||||
c = '^'
|
||||
elif y == zy // yscale and x == 0:
|
||||
c = '<'
|
||||
elif y == zy // yscale and x == self.width//xscale-1:
|
||||
c = '>'
|
||||
elif x == zx // xscale:
|
||||
c = '|'
|
||||
elif y == zy // yscale:
|
||||
c = '-'
|
||||
|
||||
row_.append(c)
|
||||
|
||||
return ''.join(row_)
|
||||
|
||||
|
||||
def collect(csv_paths, renames=[]):
|
||||
# collect results from CSV files
|
||||
paths = []
|
||||
for path in csv_paths:
|
||||
if os.path.isdir(path):
|
||||
path = path + '/*.csv'
|
||||
|
||||
for path in glob.glob(path):
|
||||
paths.append(path)
|
||||
|
||||
results = []
|
||||
for path in paths:
|
||||
try:
|
||||
with openio(path) as f:
|
||||
reader = csv.DictReader(f, restval='')
|
||||
for r in reader:
|
||||
results.append(r)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if renames:
|
||||
for r in results:
|
||||
# make a copy so renames can overlap
|
||||
r_ = {}
|
||||
for new_k, old_k in renames:
|
||||
if old_k in r:
|
||||
r_[new_k] = r[old_k]
|
||||
r.update(r_)
|
||||
|
||||
return results
|
||||
|
||||
def dataset(results, x=None, y=None, defines={}):
|
||||
# organize by 'by', x, and y
|
||||
dataset = {}
|
||||
for i, r in enumerate(results):
|
||||
# filter results by matching defines
|
||||
if not all(k in r and r[k] in vs for k, vs in defines.items()):
|
||||
continue
|
||||
|
||||
# find xs
|
||||
if x is not None:
|
||||
if x not in r:
|
||||
continue
|
||||
try:
|
||||
x_ = dat(r[x])
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
x_ = i
|
||||
|
||||
# find ys
|
||||
if y is not None:
|
||||
if y not in r:
|
||||
y_ = None
|
||||
else:
|
||||
try:
|
||||
y_ = dat(r[y])
|
||||
except ValueError:
|
||||
y_ = None
|
||||
else:
|
||||
y_ = None
|
||||
|
||||
if y_ is not None:
|
||||
dataset[x_] = y_ + dataset.get(x_, 0)
|
||||
else:
|
||||
dataset[x_] = y_ or dataset.get(x_, None)
|
||||
|
||||
return dataset
|
||||
|
||||
def datasets(results, by=None, x=None, y=None, defines={}):
|
||||
# filter results by matching defines
|
||||
results_ = []
|
||||
for r in results:
|
||||
if all(k in r and r[k] in vs for k, vs in defines.items()):
|
||||
results_.append(r)
|
||||
results = results_
|
||||
|
||||
if by is not None:
|
||||
# find all 'by' values
|
||||
ks = set()
|
||||
for r in results:
|
||||
ks.add(tuple(r.get(k, '') for k in by))
|
||||
ks = sorted(ks)
|
||||
|
||||
# collect all datasets
|
||||
datasets = co.OrderedDict()
|
||||
for ks_ in (ks if by is not None else [()]):
|
||||
for x_ in (x if x is not None else [None]):
|
||||
for y_ in (y if y is not None else [None]):
|
||||
datasets[ks_ + (x_, y_)] = dataset(
|
||||
results,
|
||||
x_,
|
||||
y_,
|
||||
{by_: {k_} for by_, k_ in zip(by, ks_)}
|
||||
if by is not None else {})
|
||||
|
||||
return datasets
|
||||
|
||||
|
||||
def main(csv_paths, *,
|
||||
by=None,
|
||||
x=None,
|
||||
y=None,
|
||||
define=[],
|
||||
xlim=None,
|
||||
ylim=None,
|
||||
width=None,
|
||||
height=None,
|
||||
color=False,
|
||||
braille=False,
|
||||
colors=None,
|
||||
chars=None,
|
||||
line_chars=None,
|
||||
no_lines=False,
|
||||
legend=None,
|
||||
keep_open=False,
|
||||
sleep=None,
|
||||
**args):
|
||||
# figure out what color should be
|
||||
if color == 'auto':
|
||||
color = sys.stdout.isatty()
|
||||
elif color == 'always':
|
||||
color = True
|
||||
else:
|
||||
color = False
|
||||
|
||||
# allow shortened ranges
|
||||
if xlim is not None and len(xlim) == 1:
|
||||
xlim = (0, xlim[0])
|
||||
if ylim is not None and len(ylim) == 1:
|
||||
ylim = (0, ylim[0])
|
||||
|
||||
# seperate out renames
|
||||
renames = [k.split('=', 1)
|
||||
for k in it.chain(by or [], x or [], y or [])
|
||||
if '=' in k]
|
||||
if by is not None:
|
||||
by = [k.split('=', 1)[0] for k in by]
|
||||
if x is not None:
|
||||
x = [k.split('=', 1)[0] for k in x]
|
||||
if y is not None:
|
||||
y = [k.split('=', 1)[0] for k in y]
|
||||
|
||||
def draw(f):
|
||||
def writeln(s=''):
|
||||
f.write(s)
|
||||
f.write('\n')
|
||||
f.writeln = writeln
|
||||
|
||||
# first collect results from CSV files
|
||||
results = collect(csv_paths, renames)
|
||||
|
||||
# then extract the requested datasets
|
||||
datasets_ = datasets(results, by, x, y, dict(define))
|
||||
|
||||
# what colors to use?
|
||||
if colors is not None:
|
||||
colors_ = colors
|
||||
else:
|
||||
colors_ = COLORS
|
||||
|
||||
if chars is not None:
|
||||
chars_ = chars
|
||||
else:
|
||||
chars_ = [True]
|
||||
|
||||
if line_chars is not None:
|
||||
line_chars_ = line_chars
|
||||
elif not no_lines:
|
||||
line_chars_ = [True]
|
||||
else:
|
||||
line_chars_ = [False]
|
||||
|
||||
# build legend?
|
||||
legend_width = 0
|
||||
if legend:
|
||||
legend_ = []
|
||||
for i, k in enumerate(datasets_.keys()):
|
||||
label = '%s%s' % (
|
||||
'%s ' % chars_[i % len(chars_)]
|
||||
if chars is not None
|
||||
else '%s ' % line_chars_[i % len(line_chars_)]
|
||||
if line_chars is not None
|
||||
else '',
|
||||
','.join(k_ for i, k_ in enumerate(k)
|
||||
if k_
|
||||
if not (i == len(k)-2 and len(x) == 1)
|
||||
if not (i == len(k)-1 and len(y) == 1)))
|
||||
|
||||
if label:
|
||||
legend_.append(label)
|
||||
legend_width = max(legend_width, len(label)+1)
|
||||
|
||||
# find xlim/ylim
|
||||
if xlim is not None:
|
||||
xlim_ = xlim
|
||||
else:
|
||||
xlim_ = (
|
||||
min(it.chain([0], (k
|
||||
for r in datasets_.values()
|
||||
for k, v in r.items()
|
||||
if v is not None))),
|
||||
max(it.chain([0], (k
|
||||
for r in datasets_.values()
|
||||
for k, v in r.items()
|
||||
if v is not None))))
|
||||
|
||||
if ylim is not None:
|
||||
ylim_ = ylim
|
||||
else:
|
||||
ylim_ = (
|
||||
min(it.chain([0], (v
|
||||
for r in datasets_.values()
|
||||
for _, v in r.items()
|
||||
if v is not None))),
|
||||
max(it.chain([0], (v
|
||||
for r in datasets_.values()
|
||||
for _, v in r.items()
|
||||
if v is not None))))
|
||||
|
||||
# figure out our plot size
|
||||
if width is not None:
|
||||
width_ = width
|
||||
else:
|
||||
width_ = shutil.get_terminal_size((80, 8))[0]
|
||||
# make space for units
|
||||
width_ -= 5
|
||||
# make space for legend
|
||||
if legend in {'left', 'right'} and legend_:
|
||||
width_ -= legend_width
|
||||
# limit a bit
|
||||
width_ = max(2*4, width_)
|
||||
|
||||
if height is not None:
|
||||
height_ = height
|
||||
else:
|
||||
height_ = shutil.get_terminal_size((80, 8))[1]
|
||||
# make space for shell prompt
|
||||
if not keep_open:
|
||||
height_ -= 1
|
||||
# make space for units
|
||||
height_ -= 1
|
||||
# make space for legend
|
||||
if legend in {'above', 'below'} and legend_:
|
||||
legend_cols = min(len(legend_), max(1, width_//legend_width))
|
||||
height_ -= (len(legend_)+legend_cols-1) // legend_cols
|
||||
# limit a bit
|
||||
height_ = max(2, height_)
|
||||
|
||||
# create a plot and draw our coordinates
|
||||
plot = Plot(
|
||||
# scale if we're printing with dots or braille
|
||||
2*width_ if line_chars is None and braille else width_,
|
||||
4*height_ if line_chars is None and braille
|
||||
else 2*height_ if line_chars is None
|
||||
else height_,
|
||||
xlim=xlim_,
|
||||
ylim=ylim_,
|
||||
**args)
|
||||
|
||||
for i, (k, dataset) in enumerate(datasets_.items()):
|
||||
plot.plot(
|
||||
sorted((x,y) for x,y in dataset.items()),
|
||||
color=colors_[i % len(colors_)],
|
||||
char=chars_[i % len(chars_)],
|
||||
line_char=line_chars_[i % len(line_chars_)])
|
||||
|
||||
# draw legend=above?
|
||||
if legend == 'above' and legend_:
|
||||
for i in range(0, len(legend_), legend_cols):
|
||||
f.writeln('%4s %*s%s' % (
|
||||
'',
|
||||
max(width_ - sum(len(label)+1
|
||||
for label in legend_[i:i+legend_cols]),
|
||||
0) // 2,
|
||||
'',
|
||||
' '.join('%s%s%s' % (
|
||||
'\x1b[%sm' % colors_[j % len(colors_)] if color else '',
|
||||
legend_[j],
|
||||
'\x1b[m' if color else '')
|
||||
for j in range(i, min(i+legend_cols, len(legend_))))))
|
||||
for row in range(height_):
|
||||
f.writeln('%s%4s %s%s' % (
|
||||
# draw legend=left?
|
||||
('%s%-*s %s' % (
|
||||
'\x1b[%sm' % colors_[row % len(colors_)] if color else '',
|
||||
legend_width-1,
|
||||
legend_[row] if row < len(legend_) else '',
|
||||
'\x1b[m' if color else ''))
|
||||
if legend == 'left' and legend_ else '',
|
||||
# draw plot
|
||||
si(ylim_[0], 4) if row == height_-1
|
||||
else si(ylim_[1], 4) if row == 0
|
||||
else '',
|
||||
plot.draw(row,
|
||||
braille=line_chars is None and braille,
|
||||
dots=line_chars is None and not braille,
|
||||
color=color,
|
||||
**args),
|
||||
# draw legend=right?
|
||||
(' %s%s%s' % (
|
||||
'\x1b[%sm' % colors_[row % len(colors_)] if color else '',
|
||||
legend_[row] if row < len(legend_) else '',
|
||||
'\x1b[m' if color else ''))
|
||||
if legend == 'right' and legend_ else ''))
|
||||
f.writeln('%*s %-4s%*s%4s' % (
|
||||
4 + (legend_width if legend == 'left' and legend_ else 0),
|
||||
'',
|
||||
si(xlim_[0], 4),
|
||||
width_ - 2*4,
|
||||
'',
|
||||
si(xlim_[1], 4)))
|
||||
# draw legend=below?
|
||||
if legend == 'below' and legend_:
|
||||
for i in range(0, len(legend_), legend_cols):
|
||||
f.writeln('%4s %*s%s' % (
|
||||
'',
|
||||
max(width_ - sum(len(label)+1
|
||||
for label in legend_[i:i+legend_cols]),
|
||||
0) // 2,
|
||||
'',
|
||||
' '.join('%s%s%s' % (
|
||||
'\x1b[%sm' % colors_[j % len(colors_)] if color else '',
|
||||
legend_[j],
|
||||
'\x1b[m' if color else '')
|
||||
for j in range(i, min(i+legend_cols, len(legend_))))))
|
||||
|
||||
|
||||
last_lines = 1
|
||||
def redraw():
|
||||
nonlocal last_lines
|
||||
|
||||
canvas = io.StringIO()
|
||||
draw(canvas)
|
||||
canvas = canvas.getvalue().splitlines()
|
||||
|
||||
# give ourself a canvas
|
||||
while last_lines < len(canvas):
|
||||
sys.stdout.write('\n')
|
||||
last_lines += 1
|
||||
|
||||
for i, line in enumerate(canvas):
|
||||
jump = len(canvas)-1-i
|
||||
# move cursor, clear line, disable/reenable line wrapping
|
||||
sys.stdout.write('\r')
|
||||
if jump > 0:
|
||||
sys.stdout.write('\x1b[%dA' % jump)
|
||||
sys.stdout.write('\x1b[K')
|
||||
sys.stdout.write('\x1b[?7l')
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.write('\x1b[?7h')
|
||||
if jump > 0:
|
||||
sys.stdout.write('\x1b[%dB' % jump)
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
if keep_open:
|
||||
try:
|
||||
while True:
|
||||
redraw()
|
||||
# don't just flood open calls
|
||||
time.sleep(sleep or 0.1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
redraw()
|
||||
sys.stdout.write('\n')
|
||||
else:
|
||||
draw(sys.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot CSV files in terminal.")
|
||||
parser.add_argument(
|
||||
'csv_paths',
|
||||
nargs='*',
|
||||
default=CSV_PATHS,
|
||||
help="Description of where to find *.csv files. May be a directory "
|
||||
"or list of paths. Defaults to %r." % CSV_PATHS)
|
||||
parser.add_argument(
|
||||
'-b', '--by',
|
||||
type=lambda x: [x.strip() for x in x.split(',')],
|
||||
help="Fields to render as separate plots. All other fields will be "
|
||||
"summed. Can rename fields with new_name=old_name.")
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
type=lambda x: [x.strip() for x in x.split(',')],
|
||||
help="Fields to use for the x-axis. Can rename fields with "
|
||||
"new_name=old_name.")
|
||||
parser.add_argument(
|
||||
'-y',
|
||||
type=lambda x: [x.strip() for x in x.split(',')],
|
||||
required=True,
|
||||
help="Fields to use for the y-axis. Can rename fields with "
|
||||
"new_name=old_name.")
|
||||
parser.add_argument(
|
||||
'-D', '--define',
|
||||
type=lambda x: (lambda k, v: (k, set(v.split(','))))(*x.split('=', 1)),
|
||||
action='append',
|
||||
help="Only include rows where this field is this value (field=value). "
|
||||
"May include comma-separated options.")
|
||||
parser.add_argument(
|
||||
'--color',
|
||||
choices=['never', 'always', 'auto'],
|
||||
default='auto',
|
||||
help="When to use terminal colors. Defaults to 'auto'.")
|
||||
parser.add_argument(
|
||||
'--braille',
|
||||
action='store_true',
|
||||
help="Use unicode braille characters. Note that braille characters "
|
||||
"sometimes suffer from inconsistent widths.")
|
||||
parser.add_argument(
|
||||
'--colors',
|
||||
type=lambda x: x.split(','),
|
||||
help="Colors to use.")
|
||||
parser.add_argument(
|
||||
'--chars',
|
||||
help="Characters to use for points.")
|
||||
parser.add_argument(
|
||||
'--line-chars',
|
||||
help="Characters to use for lines.")
|
||||
parser.add_argument(
|
||||
'-L', '--no-lines',
|
||||
action='store_true',
|
||||
help="Only draw the data points.")
|
||||
parser.add_argument(
|
||||
'-W', '--width',
|
||||
type=lambda x: int(x, 0),
|
||||
help="Width in columns. A width of 0 indicates no limit. Defaults "
|
||||
"to terminal width or 80.")
|
||||
parser.add_argument(
|
||||
'-H', '--height',
|
||||
type=lambda x: int(x, 0),
|
||||
help="Height in rows. Defaults to terminal height or 8.")
|
||||
parser.add_argument(
|
||||
'-X', '--xlim',
|
||||
type=lambda x: tuple(dat(x) if x else None for x in x.split(',')),
|
||||
help="Range for the x-axis.")
|
||||
parser.add_argument(
|
||||
'-Y', '--ylim',
|
||||
type=lambda x: tuple(dat(x) if x else None for x in x.split(',')),
|
||||
help="Range for the y-axis.")
|
||||
parser.add_argument(
|
||||
'--xlog',
|
||||
action='store_true',
|
||||
help="Use a logarithmic x-axis.")
|
||||
parser.add_argument(
|
||||
'--ylog',
|
||||
action='store_true',
|
||||
help="Use a logarithmic y-axis.")
|
||||
parser.add_argument(
|
||||
'-l', '--legend',
|
||||
choices=['above', 'below', 'left', 'right'],
|
||||
help="Place a legend here.")
|
||||
parser.add_argument(
|
||||
'-k', '--keep-open',
|
||||
action='store_true',
|
||||
help="Continue to open and redraw the CSV files in a loop.")
|
||||
parser.add_argument(
|
||||
'-s', '--sleep',
|
||||
type=float,
|
||||
help="Time in seconds to sleep between redraws when running with -k. "
|
||||
"Defaults to 0.01.")
|
||||
sys.exit(main(**{k: v
|
||||
for k, v in vars(parser.parse_intermixed_args()).items()
|
||||
if v is not None}))
|
||||
Reference in New Issue
Block a user