asciichart icon indicating copy to clipboard operation
asciichart copied to clipboard

New features

Open arkihillel opened this issue 7 years ago • 6 comments

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

arkihillel avatar Oct 20 '17 13:10 arkihillel

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:

preview

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.

kroitor avatar Oct 20 '17 22:10 kroitor

Yes - multiple series (with different colours) would be tops!!

joergd avatar Dec 18 '17 16:12 joergd

@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 avatar Jan 23 '20 15:01 theBliz

@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.

kroitor avatar Jan 23 '20 16:01 kroitor

+1 vote for the multi-line chart! Adding color would also be pretty neat.

el3ment avatar Apr 15 '20 21:04 el3ment

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

crbyxwpzfl avatar Oct 17 '23 09:10 crbyxwpzfl