Advertisement
Guest User

Untitled

a guest
Apr 26th, 2017
65
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.42 KB | None | 0 0
  1. #!/usr/bin/env python2
  2. #
  3. # Simple plotter in the shell
  4. #
  5. # Copyright (c) 2016 Christopher Haster
  6. # Distributed under the MIT license
  7.  
  8. """
  9. shplot - Simple plotter in the shell
  10.  
  11. This module provides a simple ascii-art plotter that can be used
  12. in most shells without any graphical setup.
  13.  
  14. import shplot
  15.  
  16. sh = shplot.ShPlot()
  17. sh.plot([i for i in range(10)], color="blue")
  18. sh.plot([i/2 for i in range(10)], color="green")
  19. sh.dump()
  20.  
  21. The shplot library can also be used directly from the shell.
  22.  
  23. ./shplot.py data.json
  24.  
  25. """
  26.  
  27. import sys
  28. import math
  29. import itertools
  30. import json
  31. import contextlib
  32.  
  33.  
  34. # Default plot size as fallback
  35. DEFAULT_WIDTH = 72
  36. DEFAULT_HEIGHT = 20
  37.  
  38. # SI prefixes
  39. PREFIXES = {
  40. 18: 'E',
  41. 15: 'P',
  42. 12: 'T',
  43. 9: 'G',
  44. 6: 'M',
  45. 3: 'k',
  46. 0: '',
  47. -3: 'm',
  48. -6: 'u',
  49. -9: 'n',
  50. -12: 'p',
  51. -15: 'f',
  52. -18: 'a',
  53. }
  54.  
  55. def unitfy(n, u='', width=None):
  56. """ Unit formating, squishes and uses SI prefixes to save space """
  57. if n == 0:
  58. return '0' + u
  59.  
  60. if width:
  61. prec = width - ((n < 0) + 2 + len(u))
  62. else:
  63. prec = 3
  64.  
  65. n = float('%.*g' % (prec, n))
  66. unit = 3*math.floor(math.log(abs(n), 10**3))
  67. return '%.*g%s%s' % (prec, n/(10**unit), PREFIXES[unit], u)
  68.  
  69. # ANSI color codes
  70. COLORS = {
  71. 'black': '\x1b[30m',
  72. 'red': '\x1b[31m',
  73. 'green': '\x1b[32m',
  74. 'yellow': '\x1b[33m',
  75. 'blue': '\x1b[34m',
  76. 'magenta': '\x1b[35m',
  77. 'cyan': '\x1b[36m',
  78. 'white': '\x1b[37m',
  79. 'bright black': '\x1b[30;1m',
  80. 'bright red': '\x1b[31;1m',
  81. 'bright green': '\x1b[32;1m',
  82. 'bright yellow': '\x1b[33;1m',
  83. 'bright blue': '\x1b[34;1m',
  84. 'bright magenta': '\x1b[35;1m',
  85. 'bright cyan': '\x1b[36;1m',
  86. 'bright white': '\x1b[37;1m',
  87. }
  88.  
  89. COLOR_RESET = '\x1b[0m'
  90.  
  91. @contextlib.contextmanager
  92. def color(color, file=sys.stdout):
  93. """ Coloring for file objects """
  94. if file.isatty() and color:
  95. file.write(COLORS[color])
  96. yield
  97. file.write(COLOR_RESET)
  98. else:
  99. yield
  100.  
  101. def line((x1, y1), (x2, y2)):
  102. """ Incremental error algorithm for rasterizing a line """
  103. dx = abs(x2-x1)
  104. dy = abs(y2-y1)
  105. sx = 1 if x1 < x2 else -1
  106. sy = 1 if y1 < y2 else -1
  107. err = dx - dy
  108.  
  109. while True:
  110. yield x1, y1
  111.  
  112. err2 = 2*err
  113.  
  114. if x1 == x2 and y1 == y2:
  115. break
  116.  
  117. if err2 > -dy:
  118. err -= dy
  119. x1 += sx
  120.  
  121. if x1 == x2 and y1 == y2:
  122. break
  123.  
  124. if err2 < dx:
  125. err += dx
  126. y1 += sy
  127.  
  128. yield x2, y2
  129.  
  130. def ttydim(file):
  131. """ Try to get the terminal dimensions, may fail (ie on windows) """
  132. try:
  133. import fcntl, termios, struct
  134. height, width, _, _ = struct.unpack('HHHH',
  135. fcntl.ioctl(file.fileno(), termios.TIOCGWINSZ,
  136. struct.pack('HHHH', 0, 0, 0, 0)))
  137. return width, height
  138. except:
  139. return None
  140.  
  141. def isiter(i):
  142. """ Check if argument is iterable """
  143. return hasattr(i, '__iter__')
  144.  
  145.  
  146. # Shell plotting class
  147. class ShPlot:
  148. def __init__(self, width=None, height=None):
  149. """ Creates a shell plotter """
  150. self._dats = []
  151. self._width = width
  152. self._height = height
  153. self._xmin = None
  154. self._xmax = None
  155. self._ymin = None
  156. self._ymax = None
  157.  
  158. def width(self, width):
  159. """
  160. Set width of plot, defaults to tty width when available
  161. otherwise arbitrarily 72
  162. """
  163. self._width = width
  164.  
  165. def height(self, height):
  166. """
  167. Set height of plot, defaults to ratio of tty width when available
  168. otherwise arbitrarily 20
  169. """
  170. self._height = height
  171.  
  172. def xlim(self, xmin=None, xmax=None):
  173. """
  174. Set the x-limits of the plot, defaults to min/max of platted data
  175. """
  176. if xmax is None and isiter(xmin):
  177. xmin, xmax = xmin
  178.  
  179. self._xmin = xmin
  180. self._xmax = xmax
  181.  
  182. def ylim(self, ymin=None, ymax=None):
  183. """
  184. Set the y-limits of the plot, defaults to min/max of plotted data
  185. """
  186. if ymax is None and isiter(ymin):
  187. ymin, ymax = ymin
  188.  
  189. self._ymin = ymin
  190. self._ymax = ymax
  191.  
  192. def plot(self, x=None, y=None, color=None, chars='oo.'):
  193. """
  194. Plot a set of data, most arguments are optional. Can take
  195. 1-dimensional list, list of tuples or two lists of coordinates.
  196.  
  197. Args:
  198. x: x values, defaults to 'range(0, len(y))'
  199. y: y values
  200. color: terminal color of data, available colors are in
  201. shplot.COLORS, defaults to no color
  202. chars: string of characters to draw data, with four uses:
  203. chars[0]: data point
  204. chars[1]: line interpolated between data points
  205. chars[2]: vertical line under the data point
  206. chars[3]: area under the data point
  207. defaults to 'oo.'
  208. """
  209. if not x and not y:
  210. return
  211. elif not y:
  212. y = x
  213. x = None
  214.  
  215. y = list(y)
  216. if x:
  217. x = list(x)
  218. elif all(map(isiter, y)):
  219. x, y = map(list, zip(*y))
  220. else:
  221. x = range(len(y))
  222.  
  223. self._dats.append({
  224. 'x': map(float, x),
  225. 'y': map(float, y),
  226. 'color': color,
  227. 'chars': chars
  228. })
  229.  
  230. def _generate(self, width, height):
  231. """ Generate 2d map of points """
  232. assert len(self._dats) > 0
  233. m = {}
  234.  
  235. xmin = self._xmin
  236. xmax = self._xmax
  237. ymin = self._ymin
  238. ymax = self._ymax
  239.  
  240. if xmin is None: xmin = min(min(d['x']) for d in self._dats)
  241. if xmax is None: xmax = max(max(d['x']) for d in self._dats)
  242. if ymin is None: ymin = min(min(d['y']) for d in self._dats)
  243. if ymax is None: ymax = max(max(d['y']) for d in self._dats)
  244.  
  245. if xmin == xmax or ymin == ymax:
  246. return m, (xmin, xmax), (ymin, ymax)
  247.  
  248. xscale = lambda x: int((width-1) * ((x-xmin) / (xmax-xmin)))
  249. yscale = lambda y: int((height-1) * ((y-ymin) / (ymax-ymin)))
  250.  
  251. flatten = lambda i: reduce(itertools.chain, i, [])
  252. repeat = itertools.repeat
  253.  
  254. for dat in self._dats:
  255. z0 = zip(map(xscale, dat['x']), map(yscale, dat['y']))
  256. z1 = list(flatten(line(p0, p1) for p0, p1 in zip(z0, z0[1:])))
  257. z2 = flatten(zip(repeat(x), range(0, y)) for x, y in z0)
  258. z3 = flatten(zip(repeat(x), range(0, y)) for x, y in z1)
  259.  
  260. for z, path in enumerate([z0, z1, z2, z3]):
  261. if len(dat['chars']) <= z or dat['chars'][z] == ' ':
  262. continue
  263.  
  264. for x, y in path:
  265. if (x, y) in m and m[(x, y)][0] < z:
  266. continue
  267.  
  268. m[(x, y)] = (z, dat)
  269.  
  270. return m, (xmin, xmax), (ymin, ymax)
  271.  
  272. def dump(self, file=sys.stdout):
  273. """ Dump the plot to a file object, defaults to stdout """
  274. width = self._width or DEFAULT_WIDTH
  275. height = self._height or DEFAULT_HEIGHT
  276.  
  277. if file.isatty() and (not self._width or not self._height):
  278. dim = ttydim(file)
  279. if dim:
  280. width = self._width or min(dim[0]-8, DEFAULT_WIDTH)
  281. height = self._height or width*DEFAULT_HEIGHT/DEFAULT_WIDTH
  282.  
  283. m, (xmin, xmax), (ymin, ymax) = self._generate(width, height)
  284.  
  285. for y in reversed(range(height)):
  286. if y == height-1:
  287. file.write('%-5s^' % ('%4s' % unitfy(ymax, width=5)))
  288. else:
  289. file.write(5*' ' + '|')
  290.  
  291. for x in range(width):
  292. if not (x, y) in m:
  293. file.write(' ')
  294. continue
  295.  
  296. z, dat = m[(x, y)]
  297.  
  298. with color(dat['color'], file):
  299. file.write(dat['chars'][z:z+1])
  300.  
  301. file.write('\n')
  302.  
  303. file.write('%-5s+' % ('%4s' % unitfy(ymin, width=5)))
  304. file.write((width-1)*'-' + '>')
  305. file.write('\n')
  306.  
  307. file.write(5*' ')
  308. file.write('%-5s' % unitfy(xmin, width=5))
  309. file.write((width-9)*' ')
  310. file.write('%5s' % unitfy(xmax, width=5))
  311. file.write('\n')
  312.  
  313. def dumps(self):
  314. """ Dump the plot to a string """
  315. class strfile:
  316. def __init__(self):
  317. self._buffer = []
  318.  
  319. def write(self, data):
  320. self._buffer.append(data)
  321.  
  322. def isatty(self):
  323. return False
  324.  
  325. def __str__(self):
  326. return ''.join(self._buffer)
  327.  
  328. s = strfile()
  329. self.dump(s)
  330. return str(s)
  331.  
  332. # Entry point for standalone program
  333. def main(*args):
  334. if not sys.stdin.isatty():
  335. input = sys.stdin
  336. elif len(args) >= 1:
  337. input = open(args[0], 'r')
  338. else:
  339. sys.stderr.write("Usage: %s <input.json>\n" % sys.argv[0])
  340. sys.exit(1)
  341.  
  342. shplot = ShPlot()
  343. if len(args) >= 2:
  344. shplot.width(int(args[1]))
  345. if len(args) >= 3:
  346. shplot.height(int(args[2]))
  347.  
  348. dats = json.load(input)
  349. if isinstance(dats, dict):
  350. dats = [dats]
  351.  
  352. for dat in dats:
  353. for attr in ['width', 'height', 'xlim', 'ylim']:
  354. if attr in dat:
  355. getattr(shplot, attr)(dat[attr])
  356. del dat[attr]
  357.  
  358. shplot.plot(**dat)
  359.  
  360. shplot.dump(sys.stdout)
  361.  
  362. if __name__ == "__main__":
  363. main(*sys.argv[1:])
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement