bliplot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bliplot-0.1.0/PKG-INFO +132 -0
- bliplot-0.1.0/README.md +122 -0
- bliplot-0.1.0/pyproject.toml +16 -0
- bliplot-0.1.0/src/bliplot/__init__.py +98 -0
- bliplot-0.1.0/src/bliplot/py.typed +0 -0
bliplot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bliplot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal text-based plotting utility
|
|
5
|
+
Author: abdallah-azzouni
|
|
6
|
+
Author-email: abdallah-azzouni <xara25az@gmail.com>
|
|
7
|
+
Requires-Dist: numpy>=2.5.0
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# bliplot
|
|
12
|
+
|
|
13
|
+
Stupidly simple terminal graphs. One function with zero config needed and sub-character resolution.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from bliplot import plot
|
|
17
|
+
print(plot([1, 4, 2, 8, 5, 7, 3, 9, 6]))
|
|
18
|
+
```
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
That's it. That's the whole API.
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
Every terminal-plotting library makes you learn its DSL before you can see a
|
|
26
|
+
single number. `bliplot` is one function that takes a list and gives you back
|
|
27
|
+
a string. Print it, log it, pipe it, stream it... It doesn't care. It's yours !
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install bliplot
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or, if you're using [uv](https://github.com/astral-sh/uv):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv add bliplot
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import math
|
|
45
|
+
from bliplot import plot
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_waveform(x: float) -> float:
|
|
49
|
+
return math.sin(x / 10) * math.cos(x / 5) * 10
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
data_points = [generate_waveform(x) for x in range(100)]
|
|
53
|
+
|
|
54
|
+
graph = plot(data_points, width=70, height=15, color="CYAN")
|
|
55
|
+
print(graph)
|
|
56
|
+
```
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
### Live / streaming graphs
|
|
60
|
+
|
|
61
|
+
Because `plot()` just returns a string, you can call it in a loop for a
|
|
62
|
+
live-updating graph. No special "live mode" needed:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import math
|
|
66
|
+
import time
|
|
67
|
+
from bliplot import plot
|
|
68
|
+
|
|
69
|
+
CLEAR = "\033[H\033[J"
|
|
70
|
+
WIDTH = 80
|
|
71
|
+
HEIGHT = 15
|
|
72
|
+
|
|
73
|
+
# We run this for 200 frames of animation
|
|
74
|
+
for t in range(200):
|
|
75
|
+
lst = []
|
|
76
|
+
for x in range(WIDTH):
|
|
77
|
+
# Moving the x offset by t * 0.2 causes the wave to travel left to right
|
|
78
|
+
x_moving = x - (t * 0.2)
|
|
79
|
+
|
|
80
|
+
# Base wave + high frequency noise
|
|
81
|
+
wave = math.sin(x_moving * 0.2) * math.cos(x_moving * 0.05)
|
|
82
|
+
noise = 0.2 * math.sin(x_moving * 1.5)
|
|
83
|
+
|
|
84
|
+
# Center envelope to keep the edges clean
|
|
85
|
+
envelope = math.exp(-((x - (WIDTH / 2)) / (WIDTH / 3)) ** 2)
|
|
86
|
+
|
|
87
|
+
lst.append((wave + noise) * envelope)
|
|
88
|
+
|
|
89
|
+
# Overwrite the screen
|
|
90
|
+
print(
|
|
91
|
+
CLEAR + plot(lst, width=WIDTH, height=HEIGHT, color="GREEN"), end="", flush=True
|
|
92
|
+
)
|
|
93
|
+
time.sleep(0.04) # ~25 frames per second
|
|
94
|
+
```
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
The clear-screen escape redraws the graph in place instead of scrolling a
|
|
98
|
+
new one down the terminal on every iteration.
|
|
99
|
+
|
|
100
|
+
### Parameters
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
plot(lst: list, width: int = None, height: int = None, color: str = None) -> str
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Param | Type | Default | Description |
|
|
107
|
+
|----------|--------|---------------------|------------------------------------------------------------|
|
|
108
|
+
| `lst` | list | — | Y values to plot. X values are inferred from index. |
|
|
109
|
+
| `width` | int | terminal width | Graph width in characters. |
|
|
110
|
+
| `height` | int | terminal height | Graph height in characters. |
|
|
111
|
+
| `color` | str | `None` (renders as white) | One of `RESET`, `GREEN`, `CYAN`, `YELLOW`, `RED`, `MAGENTA`, `BLUE`, `WHITE`, `BLACK`. |
|
|
112
|
+
|
|
113
|
+
Returns a string, ready to `print()`.
|
|
114
|
+
|
|
115
|
+
`NaN`, `+Inf`, and `-Inf` values in `lst` are treated as `0`.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## How it works
|
|
119
|
+
|
|
120
|
+
Terminal graphs are usually blocky because each character cell can only be
|
|
121
|
+
"on" or "off" so you lose all the resolution between one row of blocks and
|
|
122
|
+
the next. `bliplot` fixes this by spliting each value into an integer part and a fractional
|
|
123
|
+
remainder. The integer part decides how many full blocks (`█`) to stack.
|
|
124
|
+
The remainder picks *which* of the 8 partial block glyphs
|
|
125
|
+
(`▁▂▃▄▅▆▇█`) caps the column, giving you roughly 8x the vertical resolution.
|
|
126
|
+
|
|
127
|
+
Negative values use the upper-eighth block set (`▔🮂🮃▀🮄🮅🮆█`) so bars
|
|
128
|
+
growing downward from the midline look correct rather than upside-down.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
bliplot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# bliplot
|
|
2
|
+
|
|
3
|
+
Stupidly simple terminal graphs. One function with zero config needed and sub-character resolution.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from bliplot import plot
|
|
7
|
+
print(plot([1, 4, 2, 8, 5, 7, 3, 9, 6]))
|
|
8
|
+
```
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
That's it. That's the whole API.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Every terminal-plotting library makes you learn its DSL before you can see a
|
|
16
|
+
single number. `bliplot` is one function that takes a list and gives you back
|
|
17
|
+
a string. Print it, log it, pipe it, stream it... It doesn't care. It's yours !
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install bliplot
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
or, if you're using [uv](https://github.com/astral-sh/uv):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add bliplot
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import math
|
|
35
|
+
from bliplot import plot
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_waveform(x: float) -> float:
|
|
39
|
+
return math.sin(x / 10) * math.cos(x / 5) * 10
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
data_points = [generate_waveform(x) for x in range(100)]
|
|
43
|
+
|
|
44
|
+
graph = plot(data_points, width=70, height=15, color="CYAN")
|
|
45
|
+
print(graph)
|
|
46
|
+
```
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
### Live / streaming graphs
|
|
50
|
+
|
|
51
|
+
Because `plot()` just returns a string, you can call it in a loop for a
|
|
52
|
+
live-updating graph. No special "live mode" needed:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import math
|
|
56
|
+
import time
|
|
57
|
+
from bliplot import plot
|
|
58
|
+
|
|
59
|
+
CLEAR = "\033[H\033[J"
|
|
60
|
+
WIDTH = 80
|
|
61
|
+
HEIGHT = 15
|
|
62
|
+
|
|
63
|
+
# We run this for 200 frames of animation
|
|
64
|
+
for t in range(200):
|
|
65
|
+
lst = []
|
|
66
|
+
for x in range(WIDTH):
|
|
67
|
+
# Moving the x offset by t * 0.2 causes the wave to travel left to right
|
|
68
|
+
x_moving = x - (t * 0.2)
|
|
69
|
+
|
|
70
|
+
# Base wave + high frequency noise
|
|
71
|
+
wave = math.sin(x_moving * 0.2) * math.cos(x_moving * 0.05)
|
|
72
|
+
noise = 0.2 * math.sin(x_moving * 1.5)
|
|
73
|
+
|
|
74
|
+
# Center envelope to keep the edges clean
|
|
75
|
+
envelope = math.exp(-((x - (WIDTH / 2)) / (WIDTH / 3)) ** 2)
|
|
76
|
+
|
|
77
|
+
lst.append((wave + noise) * envelope)
|
|
78
|
+
|
|
79
|
+
# Overwrite the screen
|
|
80
|
+
print(
|
|
81
|
+
CLEAR + plot(lst, width=WIDTH, height=HEIGHT, color="GREEN"), end="", flush=True
|
|
82
|
+
)
|
|
83
|
+
time.sleep(0.04) # ~25 frames per second
|
|
84
|
+
```
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
The clear-screen escape redraws the graph in place instead of scrolling a
|
|
88
|
+
new one down the terminal on every iteration.
|
|
89
|
+
|
|
90
|
+
### Parameters
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
plot(lst: list, width: int = None, height: int = None, color: str = None) -> str
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Param | Type | Default | Description |
|
|
97
|
+
|----------|--------|---------------------|------------------------------------------------------------|
|
|
98
|
+
| `lst` | list | — | Y values to plot. X values are inferred from index. |
|
|
99
|
+
| `width` | int | terminal width | Graph width in characters. |
|
|
100
|
+
| `height` | int | terminal height | Graph height in characters. |
|
|
101
|
+
| `color` | str | `None` (renders as white) | One of `RESET`, `GREEN`, `CYAN`, `YELLOW`, `RED`, `MAGENTA`, `BLUE`, `WHITE`, `BLACK`. |
|
|
102
|
+
|
|
103
|
+
Returns a string, ready to `print()`.
|
|
104
|
+
|
|
105
|
+
`NaN`, `+Inf`, and `-Inf` values in `lst` are treated as `0`.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
Terminal graphs are usually blocky because each character cell can only be
|
|
111
|
+
"on" or "off" so you lose all the resolution between one row of blocks and
|
|
112
|
+
the next. `bliplot` fixes this by spliting each value into an integer part and a fractional
|
|
113
|
+
remainder. The integer part decides how many full blocks (`█`) to stack.
|
|
114
|
+
The remainder picks *which* of the 8 partial block glyphs
|
|
115
|
+
(`▁▂▃▄▅▆▇█`) caps the column, giving you roughly 8x the vertical resolution.
|
|
116
|
+
|
|
117
|
+
Negative values use the upper-eighth block set (`▔🮂🮃▀🮄🮅🮆█`) so bars
|
|
118
|
+
growing downward from the midline look correct rather than upside-down.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bliplot"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Terminal text-based plotting utility"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "abdallah-azzouni", email = "xara25az@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"numpy>=2.5.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def plot(lst: list, width: int = None, height: int = None, color: str = None):
|
|
6
|
+
"""_summary_
|
|
7
|
+
Renders a list of numbers as a text-based graph in the terminal.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
lst (list): The Y values to be plotted. The X values are inferred from the index of the list.
|
|
12
|
+
width (int, optional): The width of the graph in characters. If None, it will use the terminal width.
|
|
13
|
+
height (int, optional): The height of the graph in characters. If None, it will use the terminal height.
|
|
14
|
+
color (str, optional): The color of the graph. Can be one of "RESET", "GREEN", "CYAN", "YELLOW", "RED", "MAGENTA", "BLUE", "WHITE", or "BLACK". If None, it will default to white.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
_type_: The graph as a string, ready to be printed to the terminal.
|
|
18
|
+
|
|
19
|
+
Note:
|
|
20
|
+
Non-finite values (NaN, +Inf, -Inf) in lst are treated as 0.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
term = shutil.get_terminal_size()
|
|
25
|
+
w = width if width is not None else term.columns - 1
|
|
26
|
+
h = height if height is not None else term.lines - 1
|
|
27
|
+
COLORS = {
|
|
28
|
+
"RESET": "\033[0m",
|
|
29
|
+
"GREEN": "\033[32m",
|
|
30
|
+
"CYAN": "\033[36m",
|
|
31
|
+
"YELLOW": "\033[33m",
|
|
32
|
+
"RED": "\033[31m",
|
|
33
|
+
"MAGENTA": "\033[35m",
|
|
34
|
+
"BLUE": "\033[34m",
|
|
35
|
+
"WHITE": "\033[37m",
|
|
36
|
+
"BLACK": "\033[30m",
|
|
37
|
+
}
|
|
38
|
+
color = COLORS.get(
|
|
39
|
+
color,
|
|
40
|
+
COLORS["WHITE"],
|
|
41
|
+
)
|
|
42
|
+
BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
|
|
43
|
+
IBLOCKS = ["▔", "🮂", "🮃", "▀", "🮄", "🮅", "🮆", "█"]
|
|
44
|
+
middle_point = "·"
|
|
45
|
+
|
|
46
|
+
if len(lst) == 0:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
x_scale = np.linspace(0, len(lst) - 1, w)
|
|
50
|
+
x_new = np.interp(x_scale, np.arange(len(lst)), lst)
|
|
51
|
+
x_new = np.nan_to_num(x_new, nan=0.0, posinf=0.0, neginf=0.0)
|
|
52
|
+
|
|
53
|
+
has_positive = bool(np.any(x_new > 0))
|
|
54
|
+
has_negative = bool(np.any(x_new < 0))
|
|
55
|
+
|
|
56
|
+
if has_negative and has_positive:
|
|
57
|
+
midpoint = (h - 1) // 2
|
|
58
|
+
scale_range = midpoint
|
|
59
|
+
elif has_positive:
|
|
60
|
+
midpoint = h - 1
|
|
61
|
+
scale_range = midpoint
|
|
62
|
+
else:
|
|
63
|
+
midpoint = 0
|
|
64
|
+
scale_range = h - 1
|
|
65
|
+
|
|
66
|
+
grid = np.empty((h, w), dtype=object)
|
|
67
|
+
grid.fill(" ")
|
|
68
|
+
|
|
69
|
+
max_abs = np.max(np.abs(x_new))
|
|
70
|
+
normalized = (
|
|
71
|
+
midpoint + (x_new / max_abs) * scale_range
|
|
72
|
+
if max_abs != 0
|
|
73
|
+
else np.full_like(x_new, midpoint)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
grid[midpoint, :] = middle_point
|
|
77
|
+
|
|
78
|
+
for col in range(w):
|
|
79
|
+
val = normalized[col]
|
|
80
|
+
remainder = val % 1
|
|
81
|
+
if val > midpoint:
|
|
82
|
+
solid_height = int(val - midpoint)
|
|
83
|
+
solid_row = midpoint - solid_height
|
|
84
|
+
grid[solid_row:midpoint, col] = BLOCKS[-1]
|
|
85
|
+
if solid_row > 0:
|
|
86
|
+
grid[solid_row - 1, col] = BLOCKS[
|
|
87
|
+
int(remainder * len(BLOCKS)) % len(BLOCKS)
|
|
88
|
+
]
|
|
89
|
+
elif val < midpoint:
|
|
90
|
+
solid_height = int(midpoint - val)
|
|
91
|
+
solid_row = midpoint + solid_height
|
|
92
|
+
grid[midpoint:solid_row, col] = IBLOCKS[-1]
|
|
93
|
+
if solid_row < h:
|
|
94
|
+
grid[solid_row, col] = IBLOCKS[
|
|
95
|
+
int((1 - remainder) * len(IBLOCKS)) % len(IBLOCKS)
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return f"{color}{chr(10).join(''.join(row) for row in grid)}{COLORS['RESET']}"
|
|
File without changes
|