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 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
+ ![first example](assets/img1.png)
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
+ ![waveform](assets/wave.png)
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
+ ![live render](assets/live_render.gif)
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
@@ -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
+ ![first example](assets/img1.png)
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
+ ![waveform](assets/wave.png)
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
+ ![live render](assets/live_render.gif)
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