├── README.md ├── example.png ├── nim.cfg ├── plotter.nimble └── src ├── plotter.nim └── xresources.nim /README.md: -------------------------------------------------------------------------------- 1 | # Plotter 2 | 3 | Simple little tool which does what it says on the tin, it plots stuff. If you 4 | have a program which writes out lines of numbers and you want to draw these 5 | values in a nice plot then this is the tool for you! The tool will read from 6 | standard input and write out a nice plot scrolling left to right. The plot is 7 | drawn using the Unicode Braille characters, and each column of values is given 8 | a colour chosen from your X resources (your terminal colours). Values in the 9 | input can be separated by either whitespace, a comma, or both. The plot also 10 | scales to fill your entire terminal, and shows you the lowest and highest value 11 | of your data. 12 | 13 | ![Example of plot](example.png) 14 | 15 | The above image was gotten from running a program which received data from an 16 | Arduino reading some analog data. The program running on the Arduino writes out 17 | four values along with the numbers `100,-100` to clamp the graph. 18 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PMunch/plotter/1319b0ad652c342b66c85ec0413854391aed94bf/example.png -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | -------------------------------------------------------------------------------- /plotter.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Peter Munch-Ellingsen" 5 | description = "Simple tool to plot input piped to it" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["plotter"] 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.6.10" 14 | requires "drawille" 15 | requires "ansiparse" 16 | -------------------------------------------------------------------------------- /src/plotter.nim: -------------------------------------------------------------------------------- 1 | import std / [terminal, math, strutils, locks, enumerate, sequtils, atomics] 2 | import unicode except align 3 | import drawille, ansiparse 4 | import xresources 5 | 6 | type 7 | RollingWindow[T] = object 8 | window: seq[T] 9 | index: int 10 | Frame* = object 11 | highest*, lowest*: float 12 | frame*: string 13 | 14 | proc initRollingWindow[T](size: int, default = default(T)): RollingWindow[T] = 15 | result.window = newSeqWith[T](size, default) 16 | 17 | proc add[T](x: var RollingWindow[T], y: T) = 18 | x.window[x.index] = y 19 | x.index = (x.index + 1) mod x.window.len 20 | 21 | proc len(x: RollingWindow): int = x.window.len 22 | 23 | proc min[T](x: RollingWindow[T]): T = 24 | result = T.high 25 | for i in 0..x.window.high: 26 | result = min(result, x.window[i]) 27 | 28 | proc max[T](x: RollingWindow[T]): T = 29 | for i in 0..x.window.high: 30 | result = max(result, x.window[i]) 31 | 32 | proc `[]`[T](x: RollingWindow[T], index: int): T = 33 | x.window[(x.index + index) mod x.window.len] 34 | 35 | proc ansiSubStr(x: string, strip: int): string = 36 | let ansiData = x.parseAnsi() 37 | var 38 | toStrip = strip 39 | res: seq[AnsiData] 40 | for dat in ansiData: 41 | if dat.kind == String: 42 | if dat.str.runeLen <= toStrip: 43 | toStrip -= dat.str.runeLen 44 | elif toStrip <= 0: 45 | res.add dat 46 | else: 47 | res.add AnsiData(kind: String, str: dat.str.runeSubStr(toStrip)) 48 | toStrip = 0 49 | else: 50 | res.add dat 51 | res.add AnsiData(kind: CSI, parameters: "0", intermediate: "", final: 'm') 52 | res.toString 53 | 54 | let colours = loadColours().colours 55 | 56 | var 57 | shouldQuit: Atomic[bool] 58 | datas: seq[RollingWindow[float]] 59 | frameLock: Lock 60 | latestFrame {.guard: frameLock.}: Frame 61 | (width, height) = terminalSize() 62 | 63 | initLock(frameLock) 64 | height -= 2 65 | var c = newColourCanvas(width, height) 66 | echo c 67 | 68 | proc drawer() {.thread.} = 69 | while not shouldQuit.load: 70 | stdout.hideCursor() 71 | var msg: Frame 72 | withLock(frameLock): 73 | {.cast(gcsafe).}: 74 | msg = deepCopy(latestFrame) 75 | let 76 | lowest = msg.lowest 77 | highest = msg.highest 78 | graph = msg.frame 79 | echo "\e[A\e[K".repeat(height + 2) 80 | let 81 | lowlabel = lowest.formatFloat(precision = -1) 82 | highlabel = highest.formatFloat(precision = -1) 83 | columnWidth = max(lowlabel.len, highlabel.len) + 1 84 | var i = 0 85 | for line in graph.splitLines: 86 | if i == 0: 87 | stdout.write (highLabel & "|").align(columnWidth), line.ansiSubStr(columnWidth) 88 | elif i == height - 1: 89 | stdout.write (lowLabel & "|").align(columnWidth), line.ansiSubStr(columnWidth) 90 | elif highest > 0 and lowest < 0 and i == ((highest / (highest - lowest)) * height.float).int: 91 | stdout.write "0|".align(columnWidth), line.ansiSubStr(columnWidth) 92 | else: 93 | stdout.write "|".align(columnWidth), line.ansiSubStr(columnWidth) 94 | inc i 95 | stdout.flushFile() 96 | stdout.showCursor() 97 | 98 | var drawerThread: Thread[void] 99 | createThread(drawerThread, drawer) 100 | 101 | proc ctrlc() {.noconv.} = 102 | shouldQuit.store true 103 | drawerThread.joinThread 104 | deinitLock(frameLock) 105 | quit() 106 | 107 | setControlCHook(ctrlc) 108 | 109 | while true: 110 | var 111 | lowest = float.high 112 | highest = 0.0 113 | for data in datas: 114 | lowest = min(lowest, data.min) 115 | highest = max(highest, data.max) 116 | let range = highest - lowest 117 | for i in 0..<(width*2): 118 | if range != 0: 119 | for j, data in datas: 120 | if data[i] != data[i]: continue # data[i] is NaN 121 | c.set(i, (height - 1)*4 - (((data[i] - lowest) / range) * (height - 1).float * 4).int, colours[j mod colours.len]) 122 | 123 | withLock(frameLock): 124 | {.cast(gcsafe).}: 125 | latestFrame = Frame(lowest: lowest, highest: highest, frame: $c) 126 | 127 | c.clear() 128 | let input = try: stdin.readLine except EOFError: break 129 | for i, data in enumerate input.split(Whitespace + {','}): 130 | while datas.high < i: 131 | datas.add initRollingWindow[float](width*2, NaN) 132 | datas[i].add(try: data.parseFloat() except: NaN) 133 | 134 | ctrlc() 135 | -------------------------------------------------------------------------------- /src/xresources.nim: -------------------------------------------------------------------------------- 1 | import osproc, strutils 2 | import drawille 3 | 4 | type 5 | XResources* = object 6 | background*: Colour 7 | colours*: array[16, Colour] 8 | cursorColour*: Colour 9 | foreground*: Colour 10 | 11 | proc parseColour(hex: string): Colour = 12 | result.red = parseHexInt(hex[1..2]).uint8 13 | result.green = parseHexInt(hex[3..4]).uint8 14 | result.blue = parseHexInt(hex[5..6]).uint8 15 | 16 | proc loadColours*(): XResources = 17 | # TODO: Create a nicer fallback for Windows machines and machines without xrdb 18 | for line in execProcess("xrdb -query").splitLines: 19 | let pair = line.split ":\t" 20 | if pair.len != 2: continue 21 | let 22 | key = pair[0] 23 | value = pair[1] 24 | case key: 25 | of "*.background": result.background = parseColour(value) 26 | of "*.color0": result.colours[0] = parseColour(value) 27 | of "*.color1": result.colours[1] = parseColour(value) 28 | of "*.color10": result.colours[10] = parseColour(value) 29 | of "*.color11": result.colours[11] = parseColour(value) 30 | of "*.color12": result.colours[12] = parseColour(value) 31 | of "*.color13": result.colours[13] = parseColour(value) 32 | of "*.color14": result.colours[14] = parseColour(value) 33 | of "*.color15": result.colours[15] = parseColour(value) 34 | of "*.color2": result.colours[2] = parseColour(value) 35 | of "*.color3": result.colours[3] = parseColour(value) 36 | of "*.color4": result.colours[4] = parseColour(value) 37 | of "*.color5": result.colours[5] = parseColour(value) 38 | of "*.color6": result.colours[6] = parseColour(value) 39 | of "*.color7": result.colours[7] = parseColour(value) 40 | of "*.color8": result.colours[8] = parseColour(value) 41 | of "*.color9": result.colours[9] = parseColour(value) 42 | of "*.cursorColor": result.cursorColour = parseColour(value) 43 | of "*.foreground": result.foreground = parseColour(value) 44 | else: discard 45 | 46 | when isMainModule: 47 | echo loadColours() 48 | 49 | --------------------------------------------------------------------------------