Skip to content

Instantly share code, notes, and snippets.

@L3viathan
Created May 30, 2025 15:15
Show Gist options
  • Select an option

  • Save L3viathan/d23ef7953e682447deb0b50eb3057570 to your computer and use it in GitHub Desktop.

Select an option

Save L3viathan/d23ef7953e682447deb0b50eb3057570 to your computer and use it in GitHub Desktop.
Plotting with braille characters
import click
from math import ceil
COLORS = {
0: (0, 0, 0),
1: (133, 153, 0),
2: (42, 161, 152),
3: (38, 139, 210),
4: (108, 113, 196),
5: (211, 54, 130),
6: (220, 50, 47),
}
def limit_numbers(*numbers, digits):
effective_digits = digits - 1 if any(number < 0 for number in numbers) else digits
if any(len(str(int(abs(number)))) > effective_digits for number in numbers):
# scientific notation needed
return [f"{f'{number:e}'[:digits]:>{digits}}" for number in numbers]
if all(abs(number) < 1 for number in numbers):
if any(
len(str(number)) - len(str(number).lstrip("0.")) > (digits - 3)
for number in numbers
):
# too small
return [f"{f'{number:e}'[:digits]:>{digits}}" for number in numbers]
else:
return [f"{str(number)[:digits]:>{digits}}" for number in numbers]
else:
return [f"{str(number)[:digits]:>{digits}}" for number in numbers]
def mark_last(iterable):
empty = object()
last = empty
for element in iterable:
if last is empty:
last = element
continue
yield False, last
last = element
if last is not empty:
yield True, last
# eight pixels as a braille char
class Oxel:
def __init__(self):
self.data = [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
]
def __setitem__(self, x_y, value):
x, y = x_y
self.data[y][x] = value
def __getitem__(self, x_y):
x, y = x_y
return self.data[y][x]
def __str__(self):
maxval = max(max(row) for row in self.data)
r, g, b = COLORS.get(maxval, (255, 255, 0))
char = chr(
0x2800
+ bool(self.data[0][0])
+ bool(self.data[1][0]) * 2
+ bool(self.data[2][0]) * 4
+ bool(self.data[0][1]) * 8
+ bool(self.data[1][1]) * 16
+ bool(self.data[2][1]) * 32
+ bool(self.data[3][0]) * 64
+ bool(self.data[3][1]) * 128
)
return f"\x1b[38;2;{r};{g};{b}m{char}\x1b[0m"
# a grid of oxels
class Grid:
def __init__(self, width, height):
self.data = [
[Oxel() for _ in range(ceil(width/2))]
for _ in range(ceil(height/4))
]
def lines(self):
for line in self.data:
yield "".join(str(oxel) for oxel in line)
def __str__(self):
return "\n".join(self.lines()) + "\n"
def __setitem__(self, x_y, value):
x, y = x_y
oxel = self.data[y // 4][x // 2]
oxel[x % 2, y % 4] = value
def __getitem__(self, x_y):
x, y = x_y
oxel = self.data[y // 4][x // 2]
return oxel[x % 2, y % 4]
# a "virtual" grid, with scaling/floats that uses a Grid as its display
class Graph:
def __init__(self, width, height):
self.width = width
self.height = height
self.dots = []
self.min_x = float("inf")
self.max_x = -float("inf")
self.min_y = float("inf")
self.max_y = -float("inf")
def add_dot(self, x, y):
self.dots.append((x, y))
self.min_x = min(x, self.min_x)
self.min_y = min(y, self.min_y)
self.max_x = max(x, self.max_x)
self.max_y = max(y, self.max_y)
def scale_x(self, value):
return int(
(value - self.min_x) / (self.max_x - self.min_x) * (self.width - 1)
)
def scale_y(self, value):
return int(
(value - self.min_y) / (self.max_y - self.min_y) * (self.height - 1)
)
def make_grid(self):
grid = Grid(self.width, self.height)
for x, y in self.dots:
if x < self.min_x or x > self.max_x:
continue
if y < self.min_y or y > self.max_y:
continue
scaled_x = self.scale_x(x)
scaled_y = self.scale_y(y)
grid[scaled_x, scaled_y] += 1
return grid
def __str__(self):
return str(self.make_grid())
# knows about axes, labels, ticks, ...
class Plot:
def __init__(self, graph, *, xlab=None, ylab=None, ticks_x=None, ticks_y=None):
self.graph = graph
if ticks_x is not None:
ticks_x = abs(graph.scale_x(ticks_x)) - graph.scale_x(0)
else:
ticks_x = 20
if ticks_y is not None:
ticks_y = abs(graph.scale_y(ticks_y)) - graph.scale_y(0)
else:
ticks_y = 20
self.tick_x = ticks_x // 2
self.tick_y = ticks_y // 5
self.xlab = xlab
self.ylab = ylab
def lines(self):
grid = self.graph.make_grid()
if self.ylab:
indent = len(self.ylab)
else:
indent = 0
if self.xlab:
yield (
" " * (indent + 2)
+ f"\x1b[1m{self.xlab:^{self.graph.width // 2}}\x1b[22m"
)
min_x, max_x = limit_numbers(self.graph.min_x, self.graph.max_x, digits=indent)
min_y, max_y = limit_numbers(self.graph.min_y, self.graph.max_y, digits=indent)
yield (
" " * 2
+ min_x
+ f" {max_x:>{self.graph.width // 2}}"
)
yield (
min_y
+ " ┼"
+ (
("─" * (self.tick_x - 1) + "┴")
* ((self.graph.width // (2 * self.tick_x)) + 1)
)[:self.graph.width//2 + 1]
)
for is_last, (i, grid_line) in mark_last(enumerate(grid.lines())):
if self.ylab and i == self.graph.height // 4 // 2:
indentstr = f"\x1b[1m{self.ylab}\x1b[22m"
elif is_last:
indentstr = max_y
else:
indentstr = " " * indent
if i % self.tick_y != (self.tick_y - 1):
yield indentstr + f" │ {grid_line}"
else:
yield indentstr + f" ┤ {grid_line}"
def __str__(self):
return "\n".join(self.lines()) + "\n"
###############################################################################
@click.group()
def cli():
pass
@cli.command()
@click.argument("data")
@click.argument("xlab")
@click.argument("ylab")
@click.option("--min-x", type=float, help="Override minimum X value")
@click.option("--max-x", type=float, help="Override maximum X value")
@click.option("--min-y", type=float, help="Override minimum Y value")
@click.option("--max-y", type=float, help="Override maximum Y value")
@click.option(
"--width",
"-w",
type=int,
help="Width of the graph (excluding axes etc.) in chars.",
)
@click.option(
"--height",
"-h",
type=int,
help="Height of the graph (excluding axes etc.) in chars.",
)
@click.option(
"--ticks-x",
type=float,
)
@click.option(
"--ticks-y",
type=float,
)
def dots(data, xlab, ylab, min_x, max_x, min_y, max_y, width, height, ticks_x, ticks_y):
import os
from csv import DictReader
cols, lines = os.get_terminal_size()
if width is None:
width = int(cols * 1.5)
if height is None:
height = lines * 3
g = Graph(width, height)
with open(data) as f:
r = DictReader(f)
for row in r:
g.add_dot(float(row[xlab]), float(row[ylab]))
if min_x is not None:
g.min_x = min_x
if max_x is not None:
g.max_x = max_x
if min_y is not None:
g.min_y = min_y
if max_y is not None:
g.max_y = max_y
print("scaled ty is", g.scale_y(ticks_y))
print(Plot(g, xlab=xlab, ylab=ylab, ticks_x=ticks_x, ticks_y=ticks_y))
if __name__ == "__main__":
cli()
@L3viathan
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment