kroitor / asciichart

Nice-looking lightweight console ASCII line charts ╭┈╯ for NodeJS, browsers and terminal, no dependencies
MIT License
1.84k stars 94 forks source link

Live Updating #57

Closed tudorelu closed 3 years ago

tudorelu commented 3 years ago

Hi man,

This is an awesome lib! I'm wondering if you could give some hints as to how you made the live-updating example work (looking to do it in python).

awesome live ascii chart

kroitor commented 3 years ago

@tudorelu hey! ) You might want to check this issue: https://github.com/kroitor/asciichart/issues/39

Basically, that gif is compiled of screenshots taken once every minute on a redrawing chart. Automating those screenshots is an OS-specific task, but should not be a problem. The chart series has to be kept constant in length, since that defines the width of the graph. When the underlying exchange has a new fresh candle, the most recent candles are redrawn, then a screenshot is taken. Then I leave it working for a few hours, it generates a number of screenshots, one every minute. Then i compile them into a gif and speed them up a thousand times to make it "look fast". Check the timestamp in the upper right corner of the gif – it is not in real-time. In other words, that gif runs 1000x the real speed of OHLCV updates from the exchanges.

If you redraw it in real-time from 1m-OHLCV candles, then your FPS is 1/60 (one frame per minute), which is very slow – you won't see any "smooth animation". Therefore, in reality, an OHLCV chart is a low-fps chart. You may want to pick a different source of data (like the current tip of the orderbook), that will indeed update very frequently, so you may even want to manually limit the upper boundary on fps to avoid eating too much cpu for redrawing the frames.

Let me know if the above explanation does not help.

tudorelu commented 3 years ago

Nice, thanks for the explanation! It's pretty clear.

I was trying to figure out how to create the 'redrawing chart' in python - as opposed to simply printing the plot every 1 minute. I discovered curses, the python TUI tool, and made it work for plots using the default color without much hassle. Had to put in some effort to make it work with multiple colors, but now I have what I needed.

One thing I noticed - when I resize my terminal window, my 'app' crashed. It might be because of curses though.

kroitor commented 3 years ago

@tudorelu you can also do the same without using curses: https://stackoverflow.com/questions/17309749/node-js-console-log-is-it-possible-to-update-a-line-rather-than-create-a-new-l by moving your cursor back up a few lines, effectively overwriting the entire chart on each frame, that makes it appear animated.

MetaMmodern commented 2 years ago

@kroitor hi there. You'd better add the above description to the readme to explicitly say that this gif is not part of this library. It misleads actually.

kroitor commented 2 years ago

@MetaMmodern hi! That gif was built using a very basic shell script with this lib, i'll add instructions on how to make one.

crbyxwpzfl commented 8 months ago

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 ```py 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

nicholaswmin commented 1 month ago

I don't understand what the problem is here.

I just clear the terminal with console.clear() and draw it again; isn't that the essence of a redraw cycle?
I might be misunderstanding the questions here.

Nice one amigo, love it 💯

It certainly needs the ability to set label height (perhaps width too) to avoid those tricks I'm doing, other than that this is really nice.

import asciichart from 'asciichart'

// Max Y-axis value you expect.
// I'll plot numbers between 1-30, so 100 looks good for me
let MAX_Y = 100
let MARGIN_FACTOR = 1.3 // a bit of margin
let UPDATE_MILLIS = 100 // how often to redraw

// plot MAX_Y first; tricks it into zooming
// out to something more reasonable so you dont
// french kiss ur monitor
let points = [MAX_Y]

setInterval(() => {
  // Restart all over again, we ran out of window width
  if (points.length > Math.floor((process.stdout.columns / MARGIN_FACTOR)))
    points = [MAX_Y] // same trick as above

  // Just plot a linearly increasin random float
  points.push(Math.random() * points.length)

  // Clear window
  console.clear()

  // Redraw
  console.log (asciichart.plot([points], {
    offset:  2,
    height: process.stdout.rows / MARGIN_FACTOR,
    colors: [asciichart.green]
  }))
}, UPDATE_MILLIS)

Kapture 2024-05-26 at 08 25 28