plotille 6.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of plotille might be problematic. Click here for more details.
- plotille/__init__.py +41 -0
- plotille/_canvas.py +443 -0
- plotille/_cmaps.py +124 -0
- plotille/_cmaps_data.py +1601 -0
- plotille/_colors.py +379 -0
- plotille/_data_metadata.py +103 -0
- plotille/_dots.py +202 -0
- plotille/_figure.py +982 -0
- plotille/_figure_data.py +295 -0
- plotille/_graphs.py +373 -0
- plotille/_input_formatter.py +251 -0
- plotille/_util.py +92 -0
- plotille/data.py +100 -0
- plotille-6.0.0.dist-info/METADATA +644 -0
- plotille-6.0.0.dist-info/RECORD +16 -0
- plotille-6.0.0.dist-info/WHEEL +4 -0
plotille/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2017 - 2025 Tammo Ippen, tammo.ippen@posteo.de
|
|
4
|
+
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from ._canvas import Canvas
|
|
24
|
+
from ._cmaps import Colormap, ListedColormap
|
|
25
|
+
from ._colors import color, hsl
|
|
26
|
+
from ._figure import Figure
|
|
27
|
+
from ._graphs import hist, hist_aggregated, histogram, plot, scatter
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Canvas",
|
|
31
|
+
"Colormap",
|
|
32
|
+
"Figure",
|
|
33
|
+
"ListedColormap",
|
|
34
|
+
"color",
|
|
35
|
+
"hist",
|
|
36
|
+
"hist_aggregated",
|
|
37
|
+
"histogram",
|
|
38
|
+
"hsl",
|
|
39
|
+
"plot",
|
|
40
|
+
"scatter",
|
|
41
|
+
]
|
plotille/_canvas.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2017 - 2025 Tammo Ippen, tammo.ippen@posteo.de
|
|
4
|
+
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from typing import Any, Union
|
|
26
|
+
|
|
27
|
+
from ._colors import MAX_RGB, RGB_VALUES, ColorDefinition, RGB_t, rgb2byte
|
|
28
|
+
from ._dots import Dots
|
|
29
|
+
from ._util import roundeven
|
|
30
|
+
|
|
31
|
+
DotCoord = int
|
|
32
|
+
RefCoord = Union[float, int]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Canvas:
|
|
36
|
+
"""A canvas object for plotting braille dots
|
|
37
|
+
|
|
38
|
+
A Canvas object has a `width` x `height` characters large canvas, in which it
|
|
39
|
+
can plot indivitual braille point, lines out of braille points, rectangles,...
|
|
40
|
+
Since a full braille character has 2 x 4 dots (⣿), the canvas has `width` * 2,
|
|
41
|
+
`height` * 4 dots to plot into in total.
|
|
42
|
+
|
|
43
|
+
It maintains two coordinate systems: a reference system with the limits (xmin, ymin)
|
|
44
|
+
in the lower left corner to (xmax, ymax) in the upper right corner is transformed
|
|
45
|
+
into the canvas discrete, i.e. dots, coordinate system (0, 0) to (`width` * 2,
|
|
46
|
+
`height` * 4). It does so transparently to clients of the Canvas, i.e. all plotting
|
|
47
|
+
functions only accept coordinates in the reference system. If the coordinates are
|
|
48
|
+
outside the reference system, they are not plotted.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
width: DotCoord,
|
|
54
|
+
height: DotCoord,
|
|
55
|
+
xmin: RefCoord = 0,
|
|
56
|
+
ymin: RefCoord = 0,
|
|
57
|
+
xmax: RefCoord = 1,
|
|
58
|
+
ymax: RefCoord = 1,
|
|
59
|
+
background: ColorDefinition = None,
|
|
60
|
+
**color_kwargs: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initiate a Canvas object
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
width: int The number of characters for the width (columns) of
|
|
66
|
+
the canvas.
|
|
67
|
+
height: int The number of characters for the hight (rows) of the
|
|
68
|
+
canvas.
|
|
69
|
+
xmin, ymin: float Lower left corner of reference system.
|
|
70
|
+
xmax, ymax: float Upper right corner of reference system.
|
|
71
|
+
background: multiple Background color of the canvas.
|
|
72
|
+
**color_kwargs: More arguments to the color-function.
|
|
73
|
+
See `plotille.color()`.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Canvas object
|
|
77
|
+
"""
|
|
78
|
+
assert isinstance(width, int), "`width` has to be of type `int`"
|
|
79
|
+
assert isinstance(height, int), "`height` has to be of type `int`"
|
|
80
|
+
assert width > 0, "`width` has to be greater than 0"
|
|
81
|
+
assert height > 0, "`height` has to be greater than 0"
|
|
82
|
+
assert isinstance(xmin, (int, float))
|
|
83
|
+
assert isinstance(xmax, (int, float))
|
|
84
|
+
assert isinstance(ymin, (int, float))
|
|
85
|
+
assert isinstance(ymax, (int, float))
|
|
86
|
+
assert xmin < xmax, f"xmin ({xmin}) has to be smaller than xmax ({xmax})"
|
|
87
|
+
assert ymin < ymax, f"ymin ({ymin}) has to be smaller than ymax ({ymax})"
|
|
88
|
+
|
|
89
|
+
# characters in X / Y direction
|
|
90
|
+
self._width = width
|
|
91
|
+
self._height = height
|
|
92
|
+
# the X / Y limits of the canvas, i.e. (0, 0) in canvas is (xmin,ymin) and
|
|
93
|
+
# (width-1, height-1) in canvas is (xmax, ymax)
|
|
94
|
+
self._xmin = xmin
|
|
95
|
+
self._xmax = xmax
|
|
96
|
+
self._ymin = ymin
|
|
97
|
+
self._ymax = ymax
|
|
98
|
+
# value of x/y between one point
|
|
99
|
+
self._x_delta_pt = abs((xmax - xmin) / (width * 2))
|
|
100
|
+
self._y_delta_pt = abs((ymax - ymin) / (height * 4))
|
|
101
|
+
# the canvas to print in
|
|
102
|
+
self._color_mode = color_kwargs.get("mode", "names")
|
|
103
|
+
self._canvas = [
|
|
104
|
+
[Dots(bg=background, **color_kwargs) for j_ in range(width)]
|
|
105
|
+
for i_ in range(height)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
return f"Canvas(width={self.width}, height={self.height}, xmin={self.xmin}, ymin={self.ymin}, xmax={self.xmax}, ymax={self.ymax})"
|
|
110
|
+
|
|
111
|
+
def __repr__(self) -> str:
|
|
112
|
+
return self.__str__()
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def width(self) -> int:
|
|
116
|
+
"""Number of characters in X direction"""
|
|
117
|
+
return self._width
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def height(self) -> int:
|
|
121
|
+
"""Number of characters in Y direction"""
|
|
122
|
+
return self._height
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def xmin(self) -> RefCoord:
|
|
126
|
+
"""Get xmin coordinate of reference coordinate system [including]."""
|
|
127
|
+
return self._xmin
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def ymin(self) -> RefCoord:
|
|
131
|
+
"""Get ymin coordinate of reference coordinate system [including]."""
|
|
132
|
+
return self._ymin
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def xmax(self) -> RefCoord:
|
|
136
|
+
"""Get xmax coordinate of reference coordinate system [excluding]."""
|
|
137
|
+
return self._xmax
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def xmax_inside(self) -> float:
|
|
141
|
+
"Get max x-coordinate of reference coordinate system still inside the canvas."
|
|
142
|
+
return self.xmin + (self.width * 2 - 1) * self._x_delta_pt
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def ymax(self) -> RefCoord:
|
|
146
|
+
"""Get ymax coordinate of reference coordinate system [excluding]."""
|
|
147
|
+
return self._ymax
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def ymax_inside(self) -> float:
|
|
151
|
+
"Get max y-coordinate of reference coordinate system still inside the canvas."
|
|
152
|
+
return self.ymin + (self.height * 4 - 1) * self._y_delta_pt
|
|
153
|
+
|
|
154
|
+
def _transform_x(self, x: RefCoord) -> DotCoord:
|
|
155
|
+
return int(roundeven((x - self.xmin) / self._x_delta_pt))
|
|
156
|
+
|
|
157
|
+
def _transform_y(self, y: RefCoord) -> DotCoord:
|
|
158
|
+
return int(roundeven((y - self.ymin) / self._y_delta_pt))
|
|
159
|
+
|
|
160
|
+
def _set(
|
|
161
|
+
self,
|
|
162
|
+
x_idx: int,
|
|
163
|
+
y_idx: int,
|
|
164
|
+
set_: bool = True,
|
|
165
|
+
color: ColorDefinition = None,
|
|
166
|
+
marker: str | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Put a dot into the canvas at (x_idx, y_idx) [canvas coordinate system]
|
|
169
|
+
|
|
170
|
+
Parameters:
|
|
171
|
+
x: int x-coordinate on canvas.
|
|
172
|
+
y: int y-coordinate on canvas.
|
|
173
|
+
set_: bool Whether to plot or remove the point.
|
|
174
|
+
color: multiple Color of the point.
|
|
175
|
+
marker: str Instead of braille dots set a marker char.
|
|
176
|
+
"""
|
|
177
|
+
x_c, x_p = x_idx // 2, x_idx % 2
|
|
178
|
+
y_c, y_p = y_idx // 4, y_idx % 4
|
|
179
|
+
|
|
180
|
+
if 0 <= x_c < self.width and 0 <= y_c < self.height:
|
|
181
|
+
self._canvas[y_c][x_c].update(x_p, y_p, set_, marker)
|
|
182
|
+
if color:
|
|
183
|
+
if set_:
|
|
184
|
+
self._canvas[y_c][x_c].fg = color
|
|
185
|
+
elif color == self._canvas[y_c][x_c].fg:
|
|
186
|
+
self._canvas[y_c][x_c].fg = None
|
|
187
|
+
|
|
188
|
+
def dots_between(
|
|
189
|
+
self, x0: RefCoord, y0: RefCoord, x1: RefCoord, y1: RefCoord
|
|
190
|
+
) -> tuple[DotCoord, DotCoord]:
|
|
191
|
+
"""Number of dots between (x0, y0) and (x1, y1).
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
x0, y0: float Point 0
|
|
195
|
+
x1, y1: float Point 1
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
(int, int): dots in (x, y) direction
|
|
199
|
+
"""
|
|
200
|
+
x0_idx = self._transform_x(x0)
|
|
201
|
+
y0_idx = self._transform_y(y0)
|
|
202
|
+
x1_idx = self._transform_x(x1)
|
|
203
|
+
y1_idx = self._transform_y(y1)
|
|
204
|
+
|
|
205
|
+
return x1_idx - x0_idx, y1_idx - y0_idx
|
|
206
|
+
|
|
207
|
+
def text(
|
|
208
|
+
self,
|
|
209
|
+
x: RefCoord,
|
|
210
|
+
y: RefCoord,
|
|
211
|
+
text: str,
|
|
212
|
+
set_: bool = True,
|
|
213
|
+
color: ColorDefinition = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Put some text into the canvas at (x, y) [reference coordinate system]
|
|
216
|
+
|
|
217
|
+
Parameters:
|
|
218
|
+
x: float x-coordinate on reference system.
|
|
219
|
+
y: float y-coordinate on reference system.
|
|
220
|
+
set_: bool Whether to set the text or clear the characters.
|
|
221
|
+
text: str The text to add.
|
|
222
|
+
color: multiple Color of the point.
|
|
223
|
+
"""
|
|
224
|
+
x_idx = self._transform_x(x) // 2
|
|
225
|
+
y_idx = self._transform_y(y) // 4
|
|
226
|
+
|
|
227
|
+
for idx in range(self.width - x_idx):
|
|
228
|
+
if text is None or len(text) <= idx:
|
|
229
|
+
break
|
|
230
|
+
val: str | None = text[idx]
|
|
231
|
+
if not set_:
|
|
232
|
+
val = None
|
|
233
|
+
self._canvas[y_idx][x_idx + idx].marker = val
|
|
234
|
+
if color:
|
|
235
|
+
if set_:
|
|
236
|
+
self._canvas[y_idx][x_idx + idx].fg = color
|
|
237
|
+
elif color == self._canvas[y_idx][x_idx + idx].fg:
|
|
238
|
+
self._canvas[y_idx][x_idx + idx].fg = None
|
|
239
|
+
|
|
240
|
+
def point(
|
|
241
|
+
self,
|
|
242
|
+
x: RefCoord,
|
|
243
|
+
y: RefCoord,
|
|
244
|
+
set_: bool = True,
|
|
245
|
+
color: ColorDefinition = None,
|
|
246
|
+
marker: str | None = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Put a point into the canvas at (x, y) [reference coordinate system]
|
|
249
|
+
|
|
250
|
+
Parameters:
|
|
251
|
+
x: float x-coordinate on reference system.
|
|
252
|
+
y: float y-coordinate on reference system.
|
|
253
|
+
set_: bool Whether to plot or remove the point.
|
|
254
|
+
color: multiple Color of the point.
|
|
255
|
+
marker: str Instead of braille dots set a marker char.
|
|
256
|
+
"""
|
|
257
|
+
x_idx = self._transform_x(x)
|
|
258
|
+
y_idx = self._transform_y(y)
|
|
259
|
+
self._set(x_idx, y_idx, set_, color, marker)
|
|
260
|
+
|
|
261
|
+
def fill_char(self, x: RefCoord, y: RefCoord, set_: bool = True) -> None:
|
|
262
|
+
"""Fill the complete character at the point (x, y) [reference coordinate system]
|
|
263
|
+
|
|
264
|
+
Parameters:
|
|
265
|
+
x: float x-coordinate on reference system.
|
|
266
|
+
y: float y-coordinate on reference system.
|
|
267
|
+
set_: bool Whether to plot or remove the point.
|
|
268
|
+
"""
|
|
269
|
+
x_idx = self._transform_x(x)
|
|
270
|
+
y_idx = self._transform_y(y)
|
|
271
|
+
|
|
272
|
+
x_c = x_idx // 2
|
|
273
|
+
y_c = y_idx // 4
|
|
274
|
+
|
|
275
|
+
if set_:
|
|
276
|
+
self._canvas[y_c][x_c].fill()
|
|
277
|
+
else:
|
|
278
|
+
self._canvas[y_c][x_c].clear()
|
|
279
|
+
|
|
280
|
+
def line(
|
|
281
|
+
self,
|
|
282
|
+
x0: RefCoord,
|
|
283
|
+
y0: RefCoord,
|
|
284
|
+
x1: RefCoord,
|
|
285
|
+
y1: RefCoord,
|
|
286
|
+
set_: bool = True,
|
|
287
|
+
color: ColorDefinition = None,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Plot line between point (x0, y0) and (x1, y1) [reference coordinate system].
|
|
290
|
+
|
|
291
|
+
Parameters:
|
|
292
|
+
x0, y0: float Point 0
|
|
293
|
+
x1, y1: float Point 1
|
|
294
|
+
set_: bool Whether to plot or remove the line.
|
|
295
|
+
color: multiple Color of the line.
|
|
296
|
+
"""
|
|
297
|
+
x0_idx = self._transform_x(x0)
|
|
298
|
+
y0_idx = self._transform_y(y0)
|
|
299
|
+
self._set(x0_idx, y0_idx, set_, color)
|
|
300
|
+
|
|
301
|
+
x1_idx = self._transform_x(x1)
|
|
302
|
+
y1_idx = self._transform_y(y1)
|
|
303
|
+
self._set(x1_idx, y1_idx, set_, color)
|
|
304
|
+
|
|
305
|
+
x_diff = x1_idx - x0_idx
|
|
306
|
+
y_diff = y1_idx - y0_idx
|
|
307
|
+
steps = max(abs(x_diff), abs(y_diff))
|
|
308
|
+
for i in range(1, steps):
|
|
309
|
+
xb = x0_idx + int(roundeven(x_diff / steps * i))
|
|
310
|
+
yb = y0_idx + int(roundeven(y_diff / steps * i))
|
|
311
|
+
self._set(xb, yb, set_, color)
|
|
312
|
+
|
|
313
|
+
def rect(
|
|
314
|
+
self,
|
|
315
|
+
xmin: RefCoord,
|
|
316
|
+
ymin: RefCoord,
|
|
317
|
+
xmax: RefCoord,
|
|
318
|
+
ymax: RefCoord,
|
|
319
|
+
set_: bool = True,
|
|
320
|
+
color: ColorDefinition = None,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Plot rectangle with bbox (xmin, ymin) and (xmax, ymax).
|
|
323
|
+
|
|
324
|
+
In the reference coordinate system.
|
|
325
|
+
|
|
326
|
+
Parameters:
|
|
327
|
+
xmin, ymin: float Lower left corner of rectangle.
|
|
328
|
+
xmax, ymax: float Upper right corner of rectangle.
|
|
329
|
+
set_: bool Whether to plot or remove the rect.
|
|
330
|
+
color: multiple Color of the rect.
|
|
331
|
+
"""
|
|
332
|
+
assert xmin <= xmax
|
|
333
|
+
assert ymin <= ymax
|
|
334
|
+
self.line(xmin, ymin, xmin, ymax, set_, color)
|
|
335
|
+
self.line(xmin, ymax, xmax, ymax, set_, color)
|
|
336
|
+
self.line(xmax, ymax, xmax, ymin, set_, color)
|
|
337
|
+
self.line(xmax, ymin, xmin, ymin, set_, color)
|
|
338
|
+
|
|
339
|
+
def braille_image(
|
|
340
|
+
self,
|
|
341
|
+
pixels: Sequence[int],
|
|
342
|
+
threshold: int = 127,
|
|
343
|
+
inverse: bool = False,
|
|
344
|
+
color: ColorDefinition = None,
|
|
345
|
+
set_: bool = True,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Print an image using braille dots into the canvas.
|
|
348
|
+
|
|
349
|
+
The pixels and braille dots in the canvas are a 1-to-1 mapping, hence
|
|
350
|
+
a 80 x 80 pixel image will need a 40 x 20 canvas.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
from PIL import Image
|
|
354
|
+
import plotille as plt
|
|
355
|
+
|
|
356
|
+
img = Image.open("/path/to/image")
|
|
357
|
+
img = img.convert('L')
|
|
358
|
+
img = img.resize((80, 80))
|
|
359
|
+
cvs = plt.Canvas(40, 20)
|
|
360
|
+
cvs.braille_image(img.getdata(), 125)
|
|
361
|
+
print(cvs.plot())
|
|
362
|
+
|
|
363
|
+
Parameters:
|
|
364
|
+
pixels: list[number] All pixels of the image in one list.
|
|
365
|
+
threshold: float All pixels above this threshold will be
|
|
366
|
+
drawn.
|
|
367
|
+
inverse: bool Whether to invert the image.
|
|
368
|
+
color: multiple Color of the point.
|
|
369
|
+
set_: bool Whether to plot or remove the dots.
|
|
370
|
+
"""
|
|
371
|
+
assert len(pixels) == self.width * 2 * self.height * 4
|
|
372
|
+
row_size = self.width * 2
|
|
373
|
+
|
|
374
|
+
for idx, value in enumerate(pixels):
|
|
375
|
+
do_dot = value >= threshold
|
|
376
|
+
if inverse:
|
|
377
|
+
do_dot = not do_dot
|
|
378
|
+
if not do_dot:
|
|
379
|
+
continue
|
|
380
|
+
y = self.height * 4 - idx // row_size - 1
|
|
381
|
+
x = idx % row_size
|
|
382
|
+
|
|
383
|
+
self._set(x, y, color=color, set_=set_)
|
|
384
|
+
|
|
385
|
+
def image(self, pixels: Sequence[RGB_t | None], set_: bool = True) -> None:
|
|
386
|
+
"""Print an image using background colors into the canvas.
|
|
387
|
+
|
|
388
|
+
The pixels of the image and the characters in the canvas are a
|
|
389
|
+
1-to-1 mapping, hence a 80 x 80 image will need a 80 x 80 canvas.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
from PIL import Image
|
|
393
|
+
import plotille as plt
|
|
394
|
+
|
|
395
|
+
img = Image.open("/path/to/image")
|
|
396
|
+
img = img.convert('RGB')
|
|
397
|
+
img = img.resize((40, 40))
|
|
398
|
+
cvs = plt.Canvas(40, 40, mode='rgb')
|
|
399
|
+
cvs.image(img.getdata())
|
|
400
|
+
print(cvs.plot())
|
|
401
|
+
|
|
402
|
+
Parameters:
|
|
403
|
+
pixels: list[(R,G,B)] All pixels of the image in one list.
|
|
404
|
+
set_: bool Whether to plot or remove the background
|
|
405
|
+
colors.
|
|
406
|
+
"""
|
|
407
|
+
assert len(pixels) == self.width * self.height
|
|
408
|
+
|
|
409
|
+
for idx, values in enumerate(pixels):
|
|
410
|
+
if values is None:
|
|
411
|
+
continue
|
|
412
|
+
# RGB
|
|
413
|
+
assert len(values) == RGB_VALUES
|
|
414
|
+
assert all(0 <= v <= MAX_RGB for v in values)
|
|
415
|
+
|
|
416
|
+
y = self.height - idx // self.width - 1
|
|
417
|
+
x = idx % self.width
|
|
418
|
+
|
|
419
|
+
color_value: ColorDefinition
|
|
420
|
+
if set_ is False:
|
|
421
|
+
color_value = None
|
|
422
|
+
elif self._color_mode == "rgb":
|
|
423
|
+
color_value = values
|
|
424
|
+
elif self._color_mode == "byte":
|
|
425
|
+
color_value = rgb2byte(*values)
|
|
426
|
+
else:
|
|
427
|
+
raise NotImplementedError(
|
|
428
|
+
"Only color_modes rgb and byte are supported."
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
self._canvas[y][x].bg = color_value
|
|
432
|
+
|
|
433
|
+
def plot(self, linesep: str = os.linesep) -> str:
|
|
434
|
+
"""Transform canvas into `print`-able string
|
|
435
|
+
|
|
436
|
+
Parameters:
|
|
437
|
+
linesep: str The requested line separator. default: os.linesep
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
unicode: The canvas as a string.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
return linesep.join("".join(map(str, row)) for row in reversed(self._canvas))
|
plotille/_cmaps.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2017 - 2025 Tammo Ippen, tammo.ippen@posteo.de
|
|
4
|
+
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
|
|
26
|
+
from . import _cmaps_data
|
|
27
|
+
|
|
28
|
+
Number = float | int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Colormap:
|
|
32
|
+
"""
|
|
33
|
+
Baseclass for all scalar to RGB mappings.
|
|
34
|
+
|
|
35
|
+
Typically, Colormap instances are used to convert data values (floats)
|
|
36
|
+
from the interval `[0, 1]` to the RGB color that the respective
|
|
37
|
+
Colormap represents. Scaling the data into the `[0, 1]` interval is
|
|
38
|
+
responsibility of the caller.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str, lookup_table: Sequence[Sequence[float]]) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
name : str
|
|
46
|
+
The name of the colormap.
|
|
47
|
+
N : int
|
|
48
|
+
The number of rgb quantization levels.
|
|
49
|
+
"""
|
|
50
|
+
self.name: str = name
|
|
51
|
+
self._lookup_table: Sequence[Sequence[float]] = lookup_table
|
|
52
|
+
self.bad: Sequence[Number] | None = None
|
|
53
|
+
self.over: Sequence[Number] | None = None
|
|
54
|
+
self.under: Sequence[Number] | None = None
|
|
55
|
+
|
|
56
|
+
def __call__(
|
|
57
|
+
self, X: Number | Sequence[Number]
|
|
58
|
+
) -> Sequence[Number] | list[Sequence[Number] | None] | None:
|
|
59
|
+
"""
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
X : float or iterable of floats
|
|
63
|
+
The data value(s) to convert to RGB.
|
|
64
|
+
For floats, X should be in the interval `[0.0, 1.0]` to
|
|
65
|
+
return the RGB values `X*100` percent along the Colormap line.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
Tuple of RGB values if X is scalar, otherwise an array of
|
|
70
|
+
RGB values with a shape of `X.shape + (3, )`.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
return [self._process_value(x) for x in X] # type: ignore [union-attr]
|
|
74
|
+
except TypeError:
|
|
75
|
+
# not iterable
|
|
76
|
+
assert isinstance(X, (int, float))
|
|
77
|
+
return self._process_value(X)
|
|
78
|
+
|
|
79
|
+
def _process_value(self, x: Number) -> Sequence[Number] | None:
|
|
80
|
+
if not isinstance(x, (int, float)) or math.isnan(x) or math.isinf(x):
|
|
81
|
+
return self.bad
|
|
82
|
+
if x < 0:
|
|
83
|
+
return self.under
|
|
84
|
+
if x > 1:
|
|
85
|
+
return self.over
|
|
86
|
+
idx = round(x * (len(self._lookup_table) - 1))
|
|
87
|
+
return self._lookup_table[idx]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ListedColormap(Colormap):
|
|
91
|
+
def __init__(self, name: str, colors: Sequence[Sequence[int]]) -> None:
|
|
92
|
+
super().__init__(name, lookup_table=colors)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_relative(
|
|
96
|
+
cls, name: str, colors: Sequence[Sequence[float]]
|
|
97
|
+
) -> "ListedColormap":
|
|
98
|
+
return cls(
|
|
99
|
+
name,
|
|
100
|
+
[(round(255 * r), round(255 * g), round(255 * b)) for r, g, b in colors],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Always generate a new cmap, such that you can override bad / over under values easily.
|
|
105
|
+
cmaps = {}
|
|
106
|
+
cmaps["magma"] = lambda: ListedColormap.from_relative("magma", _cmaps_data.magma_data)
|
|
107
|
+
cmaps["inferno"] = lambda: ListedColormap.from_relative(
|
|
108
|
+
"inferno", _cmaps_data.inferno_data
|
|
109
|
+
)
|
|
110
|
+
cmaps["plasma"] = lambda: ListedColormap.from_relative(
|
|
111
|
+
"plasma", _cmaps_data.plasma_data
|
|
112
|
+
)
|
|
113
|
+
cmaps["viridis"] = lambda: ListedColormap.from_relative(
|
|
114
|
+
"viridis", _cmaps_data.viridis_data
|
|
115
|
+
)
|
|
116
|
+
cmaps["jet"] = lambda: ListedColormap.from_relative("jet", _cmaps_data.jet_data)
|
|
117
|
+
cmaps["copper"] = lambda: ListedColormap.from_relative(
|
|
118
|
+
"copper", _cmaps_data.copper_data
|
|
119
|
+
)
|
|
120
|
+
cmaps["gray"] = lambda: ListedColormap(
|
|
121
|
+
name="gray", colors=[(idx, idx, idx) for idx in range(256)]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# for more, have a look at https://matplotlib.org/stable/tutorials/colors/colormaps.html
|