asciichart
asciichart copied to clipboard
New features
I like this simple, easy to use package that I use for node statistic clis. I'm missing some nice to have options though:
Multiple series Ability to see more than one series on the same chart
Colors
Ability to affect colors to lines. npm's colors
may be used there
Max width Some charts can be way larger than a terminal. Detecting the terminal length and sub-setting the series sent may be a good way to tackle this issue
Hi, @arkihillel !
Thanks for your feedback!
Multiple series Colors
These two points I'm going to do hopefully soon (need a few days free of other tasks to do it). Also, I highly recommend you to take a look at these awesome packages by @xpl:
- https://github.com/xpl/ansicolor
- https://github.com/xpl/ololog
- https://github.com/xpl/as-table
- https://github.com/xpl/printable-characters
Max width
This cannot be done robustly for all terminals, so it's a little beyond the scope of this small library (I'm trying to keep it simple). But I did use the ansicolor and blessed together with asciichart to render a dashboard and a chart inside it, like this one:
You can use any curses-like library to do the same. Alignment, center, full-width, full-height, etc... So blessed takes care of that, and can crop or scroll the "inside" content according to your rules. It also detects resizes, mouse events and much more. Hope this answers your questions.
Yes - multiple series (with different colours) would be tops!!
@kroitor I'm using this package (awesome, thank you!) and would like to try having 2 lines in the same chart. I ended up looking at the packages you suggested but didn't understand how to use them for this purpose. Can you elaborate, please?
@theBliz it's a bit tricky, since drawing two lines requires changing the algorithm a bit, to find proper min/max values. After that you can plot each line separately using the same min/max range and then merge the two chart layers one over another skipping the whitespaces for top layer "opacity". I hope to add that functionality to the lib soon.
+1 vote for the multi-line chart! Adding color would also be pretty neat.
just hid this since I saw a better issue to leave this https://github.com/kroitor/asciichart/issues/57#issuecomment-1766948289
hi just wanted to stop to say this plotter is really really cool!! here is a little example for what i use your plotter for.
I try to use it as a live serial plotter for sensor data on a rp2040. To scroll the plot I a.pop(0)
for any value above a certain len(a)
and wipe the lines of the previous plot.
# wipe lines
for i in range(-1,len(result)): # range from -1 to scroll plot to the top or 0 to keep plot at position
print('\033[1A', end='\x1b[2K')
this is really usable for smaller scales
https://github.com/kroitor/asciichart/assets/60987359/1d4810c5-469b-49f2-8279-b221595d3743
full python
full python
from __future__ import division
from math import ceil, floor, isnan
black = "\033[30m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
blue = "\033[34m"
magenta = "\033[35m"
cyan = "\033[36m"
lightgray = "\033[37m"
default = "\033[39m"
darkgray = "\033[90m"
lightred = "\033[91m"
lightgreen = "\033[92m"
lightyellow = "\033[93m"
lightblue = "\033[94m"
lightmagenta = "\033[95m"
lightcyan = "\033[96m"
white = "\033[97m"
reset = "\033[0m"
__all__ = [
'plot', 'black', 'red',
'green', 'yellow', 'blue',
'magenta', 'cyan', 'lightgray',
'default', 'darkgray', 'lightred',
'lightgreen', 'lightyellow', 'lightblue',
'lightmagenta', 'lightcyan', 'white', 'reset',
]
# Python 3.2 has math.isfinite, which could have been used, but to support older
# versions, this little helper is shorter than having to keep doing not isnan(),
# plus the double-negative of "not is not a number" is confusing, so this should
# help with readability.
def _isnum(n):
return not isnan(n)
def colored(char, color):
if not color:
return char
else:
return color + char + reset
def plot(series, cfg=None):
if len(series) == 0:
return ''
if not isinstance(series[0], list):
if all(isnan(n) for n in series):
return ''
else:
series = [series]
cfg = cfg or {}
colors = cfg.get('colors', [None])
minimum = cfg.get('min', min(filter(_isnum, [j for i in series for j in i])))
maximum = cfg.get('max', max(filter(_isnum, [j for i in series for j in i])))
default_symbols = ['┼', '┤', '╶', '╴', '─', '╰', '╭', '╮', '╯', '│']
symbols = cfg.get('symbols', default_symbols)
if minimum > maximum:
raise ValueError('The min value cannot exceed the max value.')
interval = maximum - minimum
offset = cfg.get('offset', 3)
height = cfg.get('height', interval)
ratio = height / interval if interval > 0 else 1
min2 = int(floor(minimum * ratio))
max2 = int(ceil(maximum * ratio))
def clamp(n):
return min(max(n, minimum), maximum)
def scaled(y):
return int(round(clamp(y) * ratio) - min2)
rows = max2 - min2
width = 0
for i in range(0, len(series)):
width = max(width, len(series[i]))
width += offset
placeholder = cfg.get('format', '{:8.2f} ')
result = [[' '] * width for i in range(rows + 1)]
# axis and labels
for y in range(min2, max2 + 1):
label = placeholder.format(maximum - ((y - min2) * interval / (rows if rows else 1)))
result[y - min2][max(offset - len(label), 0)] = label
result[y - min2][offset - 1] = symbols[0] if y == 0 else symbols[1] # zero tick mark
# first value is a tick mark across the y-axis
d0 = series[0][0]
if _isnum(d0):
result[rows - scaled(d0)][offset - 1] = symbols[0]
for i in range(0, len(series)):
color = colors[i % len(colors)]
# plot the line
for x in range(0, len(series[i]) - 1):
d0 = series[i][x + 0]
d1 = series[i][x + 1]
if isnan(d0) and isnan(d1):
continue
if isnan(d0) and _isnum(d1):
result[rows - scaled(d1)][x + offset] = colored(symbols[2], color)
continue
if _isnum(d0) and isnan(d1):
result[rows - scaled(d0)][x + offset] = colored(symbols[3], color)
continue
y0 = scaled(d0)
y1 = scaled(d1)
if y0 == y1:
result[rows - y0][x + offset] = colored(symbols[4], color)
continue
result[rows - y1][x + offset] = colored(symbols[5], color) if y0 > y1 else colored(symbols[6], color)
result[rows - y0][x + offset] = colored(symbols[7], color) if y0 > y1 else colored(symbols[8], color)
start = min(y0, y1) + 1
end = max(y0, y1)
for y in range(start, end):
result[rows - y][x + offset] = colored(symbols[9], color)
#return '\n'.join([''.join(row).rstrip() for row in result])
print(f"{chr(10).join([''.join(row).rstrip() for row in result])}")
# whipe lines
for i in range(-1,len(result)): # plus -1 to scroll plot to the top or 0 to keep plot at position
print('\033[1A', end='\x1b[2K')
from math import cos
from math import sin
from math import pi
import time
x = []; a = []; b = []; c = []
i = 1
width = 90
while True:
time.sleep(0.05)
a.append(7 * round(sin(i * ((pi * 4) / width)), 2))
b.append(7 * round(cos(i * ((pi * 4) / width)), 2))
c.append(7 * round(-sin(i * ((pi * 4) / width)), 2))
i += 1
if len(a) >= 200: # x aspect
a.pop(0); b.pop(0); c.pop(0)
plot([a, b, c], {'min': -8, 'max': 8, 'height': 30, 'format': '{:8.0f}', 'colors': [blue, lightcyan, lightmagenta]} )
but for bigger scales it starts blinking. this is about the limit for me
1080/300aspect
https://github.com/kroitor/asciichart/assets/60987359/88b0ce81-f077-4f9a-8ff1-47bfcf79a1f0
your implementation with blessed is way nicer though
...
You can use any curses-like library to do the same. Alignment, center, full-width, full-height, etc... So blessed takes care of that, and can crop or scroll the "inside" content according to your rules. It also detects resizes, mouse events and much more. Hope this answers your questions.
Originally posted by @kroitor in https://github.com/kroitor/asciichart/issues/3#issuecomment-338340713
anyways just thought I leave this here as a simple way for live plots or scrolling plots. perhaps someone has tips to make this better