plot.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. #!/usr/bin/env python3
  2. #
  3. # Plot CSV files in terminal.
  4. #
  5. # Example:
  6. # ./scripts/plot.py bench.csv -xSIZE -ybench_read -W80 -H17
  7. #
  8. # Copyright (c) 2022, The littlefs authors.
  9. # SPDX-License-Identifier: BSD-3-Clause
  10. #
  11. import collections as co
  12. import csv
  13. import glob
  14. import io
  15. import itertools as it
  16. import math as m
  17. import os
  18. import shutil
  19. import time
  20. CSV_PATHS = ['*.csv']
  21. COLORS = [
  22. '1;34', # bold blue
  23. '1;31', # bold red
  24. '1;32', # bold green
  25. '1;35', # bold purple
  26. '1;33', # bold yellow
  27. '1;36', # bold cyan
  28. '34', # blue
  29. '31', # red
  30. '32', # green
  31. '35', # purple
  32. '33', # yellow
  33. '36', # cyan
  34. ]
  35. CHARS_DOTS = " .':"
  36. CHARS_BRAILLE = (
  37. '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
  38. '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
  39. '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
  40. '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
  41. '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
  42. '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
  43. '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
  44. '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
  45. SI_PREFIXES = {
  46. 18: 'E',
  47. 15: 'P',
  48. 12: 'T',
  49. 9: 'G',
  50. 6: 'M',
  51. 3: 'K',
  52. 0: '',
  53. -3: 'm',
  54. -6: 'u',
  55. -9: 'n',
  56. -12: 'p',
  57. -15: 'f',
  58. -18: 'a',
  59. }
  60. # format a number to a strict character width using SI prefixes
  61. def si(x, w=4):
  62. if x == 0:
  63. return '0'
  64. # figure out prefix and scale
  65. p = 3*int(m.log(abs(x)*10, 10**3))
  66. p = min(18, max(-18, p))
  67. # format with enough digits
  68. s = '%.*f' % (w, abs(x) / (10.0**p))
  69. s = s.lstrip('0')
  70. # truncate but only digits that follow the dot
  71. if '.' in s:
  72. s = s[:max(s.find('.'), w-(2 if x < 0 else 1))]
  73. s = s.rstrip('0')
  74. s = s.rstrip('.')
  75. return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p])
  76. def openio(path, mode='r'):
  77. if path == '-':
  78. if mode == 'r':
  79. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  80. else:
  81. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  82. else:
  83. return open(path, mode)
  84. # parse different data representations
  85. def dat(x):
  86. # allow the first part of an a/b fraction
  87. if '/' in x:
  88. x, _ = x.split('/', 1)
  89. # first try as int
  90. try:
  91. return int(x, 0)
  92. except ValueError:
  93. pass
  94. # then try as float
  95. try:
  96. x = float(x)
  97. # just don't allow infinity or nan
  98. if m.isinf(x) or m.isnan(x):
  99. raise ValueError("invalid dat %r" % x)
  100. except ValueError:
  101. pass
  102. # else give up
  103. raise ValueError("invalid dat %r" % x)
  104. # a hack log10 that preserves sign, and passes zero as zero
  105. def slog10(x):
  106. if x == 0:
  107. return x
  108. elif x > 0:
  109. return m.log10(x)
  110. else:
  111. return -m.log10(-x)
  112. class Plot:
  113. def __init__(self, width, height, *,
  114. xlim=None,
  115. ylim=None,
  116. xlog=False,
  117. ylog=False,
  118. **_):
  119. self.width = width
  120. self.height = height
  121. self.xlim = xlim or (0, width)
  122. self.ylim = ylim or (0, height)
  123. self.xlog = xlog
  124. self.ylog = ylog
  125. self.grid = [('',False)]*(self.width*self.height)
  126. def scale(self, x, y):
  127. # scale and clamp
  128. try:
  129. if self.xlog:
  130. x = int(self.width * (
  131. (slog10(x)-slog10(self.xlim[0]))
  132. / (slog10(self.xlim[1])-slog10(self.xlim[0]))))
  133. else:
  134. x = int(self.width * (
  135. (x-self.xlim[0])
  136. / (self.xlim[1]-self.xlim[0])))
  137. if self.ylog:
  138. y = int(self.height * (
  139. (slog10(y)-slog10(self.ylim[0]))
  140. / (slog10(self.ylim[1])-slog10(self.ylim[0]))))
  141. else:
  142. y = int(self.height * (
  143. (y-self.ylim[0])
  144. / (self.ylim[1]-self.ylim[0])))
  145. except ZeroDivisionError:
  146. x = 0
  147. y = 0
  148. return x, y
  149. def point(self, x, y, *,
  150. color=COLORS[0],
  151. char=True):
  152. # scale
  153. x, y = self.scale(x, y)
  154. # ignore out of bounds points
  155. if x >= 0 and x < self.width and y >= 0 and y < self.height:
  156. self.grid[x + y*self.width] = (color, char)
  157. def line(self, x1, y1, x2, y2, *,
  158. color=COLORS[0],
  159. char=True):
  160. # scale
  161. x1, y1 = self.scale(x1, y1)
  162. x2, y2 = self.scale(x2, y2)
  163. # incremental error line algorithm
  164. ex = abs(x2 - x1)
  165. ey = -abs(y2 - y1)
  166. dx = +1 if x1 < x2 else -1
  167. dy = +1 if y1 < y2 else -1
  168. e = ex + ey
  169. while True:
  170. if x1 >= 0 and x1 < self.width and y1 >= 0 and y1 < self.height:
  171. self.grid[x1 + y1*self.width] = (color, char)
  172. e2 = 2*e
  173. if x1 == x2 and y1 == y2:
  174. break
  175. if e2 > ey:
  176. e += ey
  177. x1 += dx
  178. if x1 == x2 and y1 == y2:
  179. break
  180. if e2 < ex:
  181. e += ex
  182. y1 += dy
  183. if x2 >= 0 and x2 < self.width and y2 >= 0 and y2 < self.height:
  184. self.grid[x2 + y2*self.width] = (color, char)
  185. def plot(self, coords, *,
  186. color=COLORS[0],
  187. char=True,
  188. line_char=True):
  189. # draw lines
  190. if line_char:
  191. for (x1, y1), (x2, y2) in zip(coords, coords[1:]):
  192. if y1 is not None and y2 is not None:
  193. self.line(x1, y1, x2, y2,
  194. color=color,
  195. char=line_char)
  196. # draw points
  197. if char and (not line_char or char is not True):
  198. for x, y in coords:
  199. if y is not None:
  200. self.point(x, y,
  201. color=color,
  202. char=char)
  203. def draw(self, row, *,
  204. dots=False,
  205. braille=False,
  206. color=False,
  207. **_):
  208. # scale if needed
  209. if braille:
  210. xscale, yscale = 2, 4
  211. elif dots:
  212. xscale, yscale = 1, 2
  213. else:
  214. xscale, yscale = 1, 1
  215. y = self.height//yscale-1 - row
  216. row_ = []
  217. for x in range(self.width//xscale):
  218. best_f = ''
  219. best_c = False
  220. # encode into a byte
  221. b = 0
  222. for i in range(xscale*yscale):
  223. f, c = self.grid[x*xscale+(xscale-1-(i%xscale))
  224. + (y*yscale+(i//xscale))*self.width]
  225. if c:
  226. b |= 1 << i
  227. if f:
  228. best_f = f
  229. if c and c is not True:
  230. best_c = c
  231. # use byte to lookup character
  232. if b:
  233. if best_c:
  234. c = best_c
  235. elif braille:
  236. c = CHARS_BRAILLE[b]
  237. else:
  238. c = CHARS_DOTS[b]
  239. else:
  240. c = ' '
  241. # color?
  242. if b and color and best_f:
  243. c = '\x1b[%sm%s\x1b[m' % (best_f, c)
  244. # draw axis in blank spaces
  245. if not b:
  246. zx, zy = self.scale(0, 0)
  247. if x == zx // xscale and y == zy // yscale:
  248. c = '+'
  249. elif x == zx // xscale and y == 0:
  250. c = 'v'
  251. elif x == zx // xscale and y == self.height//yscale-1:
  252. c = '^'
  253. elif y == zy // yscale and x == 0:
  254. c = '<'
  255. elif y == zy // yscale and x == self.width//xscale-1:
  256. c = '>'
  257. elif x == zx // xscale:
  258. c = '|'
  259. elif y == zy // yscale:
  260. c = '-'
  261. row_.append(c)
  262. return ''.join(row_)
  263. def collect(csv_paths, renames=[]):
  264. # collect results from CSV files
  265. paths = []
  266. for path in csv_paths:
  267. if os.path.isdir(path):
  268. path = path + '/*.csv'
  269. for path in glob.glob(path):
  270. paths.append(path)
  271. results = []
  272. for path in paths:
  273. try:
  274. with openio(path) as f:
  275. reader = csv.DictReader(f, restval='')
  276. for r in reader:
  277. results.append(r)
  278. except FileNotFoundError:
  279. pass
  280. if renames:
  281. for r in results:
  282. # make a copy so renames can overlap
  283. r_ = {}
  284. for new_k, old_k in renames:
  285. if old_k in r:
  286. r_[new_k] = r[old_k]
  287. r.update(r_)
  288. return results
  289. def dataset(results, x=None, y=None, define=[]):
  290. # organize by 'by', x, and y
  291. dataset = {}
  292. i = 0
  293. for r in results:
  294. # filter results by matching defines
  295. if not all(k in r and r[k] in vs for k, vs in define):
  296. continue
  297. # find xs
  298. if x is not None:
  299. if x not in r:
  300. continue
  301. try:
  302. x_ = dat(r[x])
  303. except ValueError:
  304. continue
  305. else:
  306. x_ = i
  307. i += 1
  308. # find ys
  309. if y is not None:
  310. if y not in r:
  311. y_ = None
  312. else:
  313. try:
  314. y_ = dat(r[y])
  315. except ValueError:
  316. y_ = None
  317. else:
  318. y_ = None
  319. if y_ is not None:
  320. dataset[x_] = y_ + dataset.get(x_, 0)
  321. else:
  322. dataset[x_] = y_ or dataset.get(x_, None)
  323. return dataset
  324. def datasets(results, by=None, x=None, y=None, define=[]):
  325. # filter results by matching defines
  326. results_ = []
  327. for r in results:
  328. if all(k in r and r[k] in vs for k, vs in define):
  329. results_.append(r)
  330. results = results_
  331. # if y not specified, try to guess from data
  332. if y is None:
  333. y = co.OrderedDict()
  334. for r in results:
  335. for k, v in r.items():
  336. if by is not None and k in by:
  337. continue
  338. if y.get(k, True):
  339. try:
  340. dat(v)
  341. y[k] = True
  342. except ValueError:
  343. y[k] = False
  344. y = list(k for k,v in y.items() if v)
  345. if by is not None:
  346. # find all 'by' values
  347. ks = set()
  348. for r in results:
  349. ks.add(tuple(r.get(k, '') for k in by))
  350. ks = sorted(ks)
  351. # collect all datasets
  352. datasets = co.OrderedDict()
  353. for ks_ in (ks if by is not None else [()]):
  354. for x_ in (x if x is not None else [None]):
  355. for y_ in y:
  356. # hide x/y if there is only one field
  357. k_x = x_ if len(x or []) > 1 else ''
  358. k_y = y_ if len(y or []) > 1 else ''
  359. datasets[ks_ + (k_x, k_y)] = dataset(
  360. results,
  361. x_,
  362. y_,
  363. [(by_, k_) for by_, k_ in zip(by, ks_)]
  364. if by is not None else [])
  365. return datasets
  366. def main(csv_paths, *,
  367. by=None,
  368. x=None,
  369. y=None,
  370. define=[],
  371. xlim=None,
  372. ylim=None,
  373. width=None,
  374. height=None,
  375. color=False,
  376. braille=False,
  377. colors=None,
  378. chars=None,
  379. line_chars=None,
  380. no_lines=False,
  381. legend=None,
  382. keep_open=False,
  383. sleep=None,
  384. **args):
  385. # figure out what color should be
  386. if color == 'auto':
  387. color = sys.stdout.isatty()
  388. elif color == 'always':
  389. color = True
  390. else:
  391. color = False
  392. # allow shortened ranges
  393. if xlim is not None and len(xlim) == 1:
  394. xlim = (0, xlim[0])
  395. if ylim is not None and len(ylim) == 1:
  396. ylim = (0, ylim[0])
  397. # separate out renames
  398. renames = [k.split('=', 1)
  399. for k in it.chain(by or [], x or [], y or [])
  400. if '=' in k]
  401. if by is not None:
  402. by = [k.split('=', 1)[0] for k in by]
  403. if x is not None:
  404. x = [k.split('=', 1)[0] for k in x]
  405. if y is not None:
  406. y = [k.split('=', 1)[0] for k in y]
  407. def draw(f):
  408. def writeln(s=''):
  409. f.write(s)
  410. f.write('\n')
  411. f.writeln = writeln
  412. # first collect results from CSV files
  413. results = collect(csv_paths, renames)
  414. # then extract the requested datasets
  415. datasets_ = datasets(results, by, x, y, define)
  416. # what colors to use?
  417. if colors is not None:
  418. colors_ = colors
  419. else:
  420. colors_ = COLORS
  421. if chars is not None:
  422. chars_ = chars
  423. else:
  424. chars_ = [True]
  425. if line_chars is not None:
  426. line_chars_ = line_chars
  427. elif not no_lines:
  428. line_chars_ = [True]
  429. else:
  430. line_chars_ = [False]
  431. # build legend?
  432. legend_width = 0
  433. if legend:
  434. legend_ = []
  435. for i, k in enumerate(datasets_.keys()):
  436. label = '%s%s' % (
  437. '%s ' % chars_[i % len(chars_)]
  438. if chars is not None
  439. else '%s ' % line_chars_[i % len(line_chars_)]
  440. if line_chars is not None
  441. else '',
  442. ','.join(k_ for k_ in k if k_))
  443. if label:
  444. legend_.append(label)
  445. legend_width = max(legend_width, len(label)+1)
  446. # find xlim/ylim
  447. if xlim is not None:
  448. xlim_ = xlim
  449. else:
  450. xlim_ = (
  451. min(it.chain([0], (k
  452. for r in datasets_.values()
  453. for k, v in r.items()
  454. if v is not None))),
  455. max(it.chain([0], (k
  456. for r in datasets_.values()
  457. for k, v in r.items()
  458. if v is not None))))
  459. if ylim is not None:
  460. ylim_ = ylim
  461. else:
  462. ylim_ = (
  463. min(it.chain([0], (v
  464. for r in datasets_.values()
  465. for _, v in r.items()
  466. if v is not None))),
  467. max(it.chain([0], (v
  468. for r in datasets_.values()
  469. for _, v in r.items()
  470. if v is not None))))
  471. # figure out our plot size
  472. if width is not None:
  473. width_ = width
  474. else:
  475. width_ = shutil.get_terminal_size((80, 8))[0]
  476. # make space for units
  477. width_ -= 5
  478. # make space for legend
  479. if legend in {'left', 'right'} and legend_:
  480. width_ -= legend_width
  481. # limit a bit
  482. width_ = max(2*4, width_)
  483. if height is not None:
  484. height_ = height
  485. else:
  486. height_ = shutil.get_terminal_size((80, 8))[1]
  487. # make space for shell prompt
  488. if not keep_open:
  489. height_ -= 1
  490. # make space for units
  491. height_ -= 1
  492. # make space for legend
  493. if legend in {'above', 'below'} and legend_:
  494. legend_cols = min(len(legend_), max(1, width_//legend_width))
  495. height_ -= (len(legend_)+legend_cols-1) // legend_cols
  496. # limit a bit
  497. height_ = max(2, height_)
  498. # create a plot and draw our coordinates
  499. plot = Plot(
  500. # scale if we're printing with dots or braille
  501. 2*width_ if line_chars is None and braille else width_,
  502. 4*height_ if line_chars is None and braille
  503. else 2*height_ if line_chars is None
  504. else height_,
  505. xlim=xlim_,
  506. ylim=ylim_,
  507. **args)
  508. for i, (k, dataset) in enumerate(datasets_.items()):
  509. plot.plot(
  510. sorted((x,y) for x,y in dataset.items()),
  511. color=colors_[i % len(colors_)],
  512. char=chars_[i % len(chars_)],
  513. line_char=line_chars_[i % len(line_chars_)])
  514. # draw legend=above?
  515. if legend == 'above' and legend_:
  516. for i in range(0, len(legend_), legend_cols):
  517. f.writeln('%4s %*s%s' % (
  518. '',
  519. max(width_ - sum(len(label)+1
  520. for label in legend_[i:i+legend_cols]),
  521. 0) // 2,
  522. '',
  523. ' '.join('%s%s%s' % (
  524. '\x1b[%sm' % colors_[j % len(colors_)] if color else '',
  525. legend_[j],
  526. '\x1b[m' if color else '')
  527. for j in range(i, min(i+legend_cols, len(legend_))))))
  528. for row in range(height_):
  529. f.writeln('%s%4s %s%s' % (
  530. # draw legend=left?
  531. ('%s%-*s %s' % (
  532. '\x1b[%sm' % colors_[row % len(colors_)] if color else '',
  533. legend_width-1,
  534. legend_[row] if row < len(legend_) else '',
  535. '\x1b[m' if color else ''))
  536. if legend == 'left' and legend_ else '',
  537. # draw plot
  538. si(ylim_[0], 4) if row == height_-1
  539. else si(ylim_[1], 4) if row == 0
  540. else '',
  541. plot.draw(row,
  542. braille=line_chars is None and braille,
  543. dots=line_chars is None and not braille,
  544. color=color,
  545. **args),
  546. # draw legend=right?
  547. (' %s%s%s' % (
  548. '\x1b[%sm' % colors_[row % len(colors_)] if color else '',
  549. legend_[row] if row < len(legend_) else '',
  550. '\x1b[m' if color else ''))
  551. if legend == 'right' and legend_ else ''))
  552. f.writeln('%*s %-4s%*s%4s' % (
  553. 4 + (legend_width if legend == 'left' and legend_ else 0),
  554. '',
  555. si(xlim_[0], 4),
  556. width_ - 2*4,
  557. '',
  558. si(xlim_[1], 4)))
  559. # draw legend=below?
  560. if legend == 'below' and legend_:
  561. for i in range(0, len(legend_), legend_cols):
  562. f.writeln('%4s %*s%s' % (
  563. '',
  564. max(width_ - sum(len(label)+1
  565. for label in legend_[i:i+legend_cols]),
  566. 0) // 2,
  567. '',
  568. ' '.join('%s%s%s' % (
  569. '\x1b[%sm' % colors_[j % len(colors_)] if color else '',
  570. legend_[j],
  571. '\x1b[m' if color else '')
  572. for j in range(i, min(i+legend_cols, len(legend_))))))
  573. last_lines = 1
  574. def redraw():
  575. nonlocal last_lines
  576. canvas = io.StringIO()
  577. draw(canvas)
  578. canvas = canvas.getvalue().splitlines()
  579. # give ourself a canvas
  580. while last_lines < len(canvas):
  581. sys.stdout.write('\n')
  582. last_lines += 1
  583. for i, line in enumerate(canvas):
  584. jump = len(canvas)-1-i
  585. # move cursor, clear line, disable/reenable line wrapping
  586. sys.stdout.write('\r')
  587. if jump > 0:
  588. sys.stdout.write('\x1b[%dA' % jump)
  589. sys.stdout.write('\x1b[K')
  590. sys.stdout.write('\x1b[?7l')
  591. sys.stdout.write(line)
  592. sys.stdout.write('\x1b[?7h')
  593. if jump > 0:
  594. sys.stdout.write('\x1b[%dB' % jump)
  595. sys.stdout.flush()
  596. if keep_open:
  597. try:
  598. while True:
  599. redraw()
  600. # don't just flood open calls
  601. time.sleep(sleep or 0.1)
  602. except KeyboardInterrupt:
  603. pass
  604. redraw()
  605. sys.stdout.write('\n')
  606. else:
  607. draw(sys.stdout)
  608. if __name__ == "__main__":
  609. import sys
  610. import argparse
  611. parser = argparse.ArgumentParser(
  612. description="Plot CSV files in terminal.")
  613. parser.add_argument(
  614. 'csv_paths',
  615. nargs='*',
  616. default=CSV_PATHS,
  617. help="Description of where to find *.csv files. May be a directory "
  618. "or list of paths. Defaults to %r." % CSV_PATHS)
  619. parser.add_argument(
  620. '-b', '--by',
  621. type=lambda x: [x.strip() for x in x.split(',')],
  622. help="Fields to render as separate plots. All other fields will be "
  623. "summed as needed. Can rename fields with new_name=old_name.")
  624. parser.add_argument(
  625. '-x',
  626. type=lambda x: [x.strip() for x in x.split(',')],
  627. help="Fields to use for the x-axis. Can rename fields with "
  628. "new_name=old_name.")
  629. parser.add_argument(
  630. '-y',
  631. type=lambda x: [x.strip() for x in x.split(',')],
  632. help="Fields to use for the y-axis. Can rename fields with "
  633. "new_name=old_name.")
  634. parser.add_argument(
  635. '-D', '--define',
  636. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  637. action='append',
  638. help="Only include rows where this field is this value. May include "
  639. "comma-separated options.")
  640. parser.add_argument(
  641. '--color',
  642. choices=['never', 'always', 'auto'],
  643. default='auto',
  644. help="When to use terminal colors. Defaults to 'auto'.")
  645. parser.add_argument(
  646. '--braille',
  647. action='store_true',
  648. help="Use unicode braille characters. Note that braille characters "
  649. "sometimes suffer from inconsistent widths.")
  650. parser.add_argument(
  651. '--colors',
  652. type=lambda x: x.split(','),
  653. help="Colors to use.")
  654. parser.add_argument(
  655. '--chars',
  656. help="Characters to use for points.")
  657. parser.add_argument(
  658. '--line-chars',
  659. help="Characters to use for lines.")
  660. parser.add_argument(
  661. '-L', '--no-lines',
  662. action='store_true',
  663. help="Only draw the data points.")
  664. parser.add_argument(
  665. '-W', '--width',
  666. type=lambda x: int(x, 0),
  667. help="Width in columns. A width of 0 indicates no limit. Defaults "
  668. "to terminal width or 80.")
  669. parser.add_argument(
  670. '-H', '--height',
  671. type=lambda x: int(x, 0),
  672. help="Height in rows. Defaults to terminal height or 8.")
  673. parser.add_argument(
  674. '-X', '--xlim',
  675. type=lambda x: tuple(dat(x) if x else None for x in x.split(',')),
  676. help="Range for the x-axis.")
  677. parser.add_argument(
  678. '-Y', '--ylim',
  679. type=lambda x: tuple(dat(x) if x else None for x in x.split(',')),
  680. help="Range for the y-axis.")
  681. parser.add_argument(
  682. '--xlog',
  683. action='store_true',
  684. help="Use a logarithmic x-axis.")
  685. parser.add_argument(
  686. '--ylog',
  687. action='store_true',
  688. help="Use a logarithmic y-axis.")
  689. parser.add_argument(
  690. '-l', '--legend',
  691. choices=['above', 'below', 'left', 'right'],
  692. help="Place a legend here.")
  693. parser.add_argument(
  694. '-k', '--keep-open',
  695. action='store_true',
  696. help="Continue to open and redraw the CSV files in a loop.")
  697. parser.add_argument(
  698. '-s', '--sleep',
  699. type=float,
  700. help="Time in seconds to sleep between redraws when running with -k. "
  701. "Defaults to 0.01.")
  702. sys.exit(main(**{k: v
  703. for k, v in vars(parser.parse_intermixed_args()).items()
  704. if v is not None}))