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/_colors.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
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 colorsys
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
from typing import Final, Literal, Union
|
|
28
|
+
|
|
29
|
+
MAX_RGB: Final = 255
|
|
30
|
+
MAX_HUE: Final = 360
|
|
31
|
+
RGB_VALUES: Final = 3
|
|
32
|
+
|
|
33
|
+
RGB_t = tuple[int, int, int] | Sequence[int]
|
|
34
|
+
ColorDefinition = Union[str, int, "ColorNames", RGB_t, None]
|
|
35
|
+
ColorMode = Literal["names", "byte", "rgb"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ColorNames = Literal[
|
|
39
|
+
"black",
|
|
40
|
+
"red",
|
|
41
|
+
"green",
|
|
42
|
+
"yellow",
|
|
43
|
+
"blue",
|
|
44
|
+
"magenta",
|
|
45
|
+
"cyan",
|
|
46
|
+
"white",
|
|
47
|
+
"bright_black",
|
|
48
|
+
"bright_red",
|
|
49
|
+
"bright_green",
|
|
50
|
+
"bright_yellow",
|
|
51
|
+
"bright_blue",
|
|
52
|
+
"bright_magenta",
|
|
53
|
+
"bright_cyan",
|
|
54
|
+
"bright_white",
|
|
55
|
+
"bright_black_old",
|
|
56
|
+
"bright_red_old",
|
|
57
|
+
"bright_green_old",
|
|
58
|
+
"bright_yellow_old",
|
|
59
|
+
"bright_blue_old",
|
|
60
|
+
"bright_magenta_old",
|
|
61
|
+
"bright_cyan_old",
|
|
62
|
+
"bright_white_old",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def color(
|
|
67
|
+
text: str,
|
|
68
|
+
fg: ColorDefinition = None,
|
|
69
|
+
bg: ColorDefinition = None,
|
|
70
|
+
mode: ColorMode = "names",
|
|
71
|
+
no_color: bool = False,
|
|
72
|
+
full_reset: bool = True,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Surround `text` with control characters for coloring
|
|
75
|
+
|
|
76
|
+
c.f. http://en.wikipedia.org/wiki/ANSI_escape_code
|
|
77
|
+
|
|
78
|
+
There are 3 color modes possible:
|
|
79
|
+
- `names`: corresponds to 3/4 bit encoding; provide colors as lower case
|
|
80
|
+
with underscore names, e.g. 'red', 'bright_green'
|
|
81
|
+
- `byte`: corresponds to 8-bit encoding; provide colors as int ∈ [0, 255];
|
|
82
|
+
compare 256-color lookup table
|
|
83
|
+
- `rgb`: corresponds to 24-bit encoding; provide colors either in 3- or
|
|
84
|
+
6-character hex encoding or provide as a list / tuple with three ints
|
|
85
|
+
(∈ [0, 255] each)
|
|
86
|
+
|
|
87
|
+
With `fg` you can specify the foreground, i.e. text color, and with `bg` you
|
|
88
|
+
specify the background color. The resulting `text` also gets the `RESET` signal
|
|
89
|
+
at the end, s.t. no coloring swaps over to following text!
|
|
90
|
+
|
|
91
|
+
Make sure to set the colors corresponding to the `mode`, otherwise you get
|
|
92
|
+
`ValueErrors`.
|
|
93
|
+
|
|
94
|
+
If you do not want a foreground or background color, leave the corresponding
|
|
95
|
+
parameter `None`. If both are `None`, you get `text` directly.
|
|
96
|
+
|
|
97
|
+
When you stick to mode `names` and only use the none `bright_` versions,
|
|
98
|
+
the color control characters conform to ISO 6429 and the ANSI Escape sequences
|
|
99
|
+
as defined in http://ascii-table.com/ansi-escape-sequences.php.
|
|
100
|
+
|
|
101
|
+
Color names for mode `names` are:
|
|
102
|
+
black red green yellow blue magenta cyan white <- ISO 6429
|
|
103
|
+
bright_black bright_red bright_green bright_yellow
|
|
104
|
+
bright_blue bright_magenta bright_cyan bright_white
|
|
105
|
+
(trying other names will raise ValueError)
|
|
106
|
+
|
|
107
|
+
If you want to use colorama (https://pypi.python.org/pypi/colorama), you should
|
|
108
|
+
also stick to the ISO 6429 colors.
|
|
109
|
+
|
|
110
|
+
The environment variables `NO_COLOR` (https://no-color.org/) and `FORCE_COLOR`
|
|
111
|
+
(only toggle; see https://nodejs.org/api/tty.html#tty_writestream_getcolordepth_env)
|
|
112
|
+
have some influence on color output.
|
|
113
|
+
|
|
114
|
+
If you do not run in a TTY, e.g. pipe to some other program or redirect output
|
|
115
|
+
into a file, color codes are stripped as well.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
text: str Some text to surround.
|
|
119
|
+
fg: multiple Specify the foreground / text color.
|
|
120
|
+
bg: multiple Specify the background color.
|
|
121
|
+
color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'
|
|
122
|
+
no_color: bool Remove color optionally. default=False
|
|
123
|
+
full_reset: bool Reset all codes or only color codes. default=True
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str: `text` enclosed with corresponding coloring controls
|
|
127
|
+
"""
|
|
128
|
+
if fg is None and bg is None:
|
|
129
|
+
return text
|
|
130
|
+
|
|
131
|
+
if no_color or os.environ.get("NO_COLOR"):
|
|
132
|
+
# https://no-color.org/
|
|
133
|
+
return text
|
|
134
|
+
|
|
135
|
+
# similar to https://nodejs.org/api/tty.html#tty_writestream_getcolordepth_env
|
|
136
|
+
# except for only on or of
|
|
137
|
+
force_color = os.environ.get("FORCE_COLOR")
|
|
138
|
+
if force_color:
|
|
139
|
+
force_color = force_color.strip().lower()
|
|
140
|
+
if force_color in ("0", "false", "none"):
|
|
141
|
+
return text
|
|
142
|
+
|
|
143
|
+
if not force_color and not _isatty():
|
|
144
|
+
# only color if tty (not a redirect / pipe)
|
|
145
|
+
return text
|
|
146
|
+
|
|
147
|
+
start = ""
|
|
148
|
+
if mode == "names":
|
|
149
|
+
assert fg is None or isinstance(fg, str)
|
|
150
|
+
assert bg is None or isinstance(bg, str)
|
|
151
|
+
start = _names(fg, bg)
|
|
152
|
+
elif mode == "byte":
|
|
153
|
+
assert fg is None or isinstance(fg, int)
|
|
154
|
+
assert bg is None or isinstance(bg, int)
|
|
155
|
+
start = _byte(fg, bg)
|
|
156
|
+
elif mode == "rgb":
|
|
157
|
+
if isinstance(fg, str):
|
|
158
|
+
fg = _hex2rgb(fg)
|
|
159
|
+
if isinstance(bg, str):
|
|
160
|
+
bg = _hex2rgb(bg)
|
|
161
|
+
|
|
162
|
+
assert fg is None or isinstance(fg, (list, tuple))
|
|
163
|
+
assert bg is None or isinstance(bg, (list, tuple))
|
|
164
|
+
start = _rgb(fg, bg)
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError(f'Invalid mode "{mode}". Use one of "names", "byte" or "rgb".')
|
|
167
|
+
|
|
168
|
+
assert start
|
|
169
|
+
res = start + text
|
|
170
|
+
if full_reset:
|
|
171
|
+
return res + "\x1b[0m"
|
|
172
|
+
else:
|
|
173
|
+
return res + "\x1b[39;49m"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def hsl(hue: float, saturation: float, lightness: float) -> tuple[int, int, int]:
|
|
177
|
+
"""Convert HSL color space into RGB color space.
|
|
178
|
+
|
|
179
|
+
In contrast to colorsys.hls_to_rgb, this works directly in
|
|
180
|
+
360 deg Hue and give RGB values in the range of 0 to 255.
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
hue: float Position in the spectrum. 0 to 360.
|
|
184
|
+
saturation: float Color saturation. 0 to 1.
|
|
185
|
+
lightness: float Color lightness. 0 to 1.
|
|
186
|
+
"""
|
|
187
|
+
assert 0 <= hue <= MAX_HUE
|
|
188
|
+
assert 0 <= saturation <= 1
|
|
189
|
+
assert 0 <= lightness <= 1
|
|
190
|
+
|
|
191
|
+
r, g, b = colorsys.hls_to_rgb(hue / 360.0, lightness, saturation)
|
|
192
|
+
return round(r * 255), round(g * 255), round(b * 255)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def rgb2byte(r: int, g: int, b: int) -> int:
|
|
196
|
+
"""Convert RGB values into an index for the byte color-mode.
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
r: int Red value. Between 0 and 255.
|
|
200
|
+
g: int Green value. Between 0 and 255.
|
|
201
|
+
b: int Blue value. Between 0 and 255.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
idx: int Index of approximate color in the byte color-mode.
|
|
205
|
+
"""
|
|
206
|
+
assert 0 <= r <= MAX_RGB
|
|
207
|
+
assert 0 <= g <= MAX_RGB
|
|
208
|
+
assert 0 <= b <= MAX_RGB
|
|
209
|
+
|
|
210
|
+
if r == g == b < 244:
|
|
211
|
+
# gray:
|
|
212
|
+
gray_idx = _value_to_index(min(238, r), off=8, steps=10)
|
|
213
|
+
return gray_idx + 232
|
|
214
|
+
|
|
215
|
+
# here we also have some gray values ...
|
|
216
|
+
r_idx = _value_to_index(r)
|
|
217
|
+
g_idx = _value_to_index(g)
|
|
218
|
+
b_idx = _value_to_index(b)
|
|
219
|
+
|
|
220
|
+
return 16 + 36 * r_idx + 6 * g_idx + b_idx
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _value_to_index(v: int, off: int = 55, steps: int = 40) -> int:
|
|
224
|
+
idx = (v - off) / steps
|
|
225
|
+
if idx < 0:
|
|
226
|
+
return 0
|
|
227
|
+
return round(idx)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _isatty() -> bool:
|
|
231
|
+
return sys.stdout.isatty()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _names(fg: str | None, bg: str | None) -> str:
|
|
235
|
+
"""3/4 bit encoding part
|
|
236
|
+
|
|
237
|
+
c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#3.2F4_bit
|
|
238
|
+
|
|
239
|
+
Parameters:
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
if not (fg is None or fg in _FOREGROUNDS):
|
|
243
|
+
raise ValueError(f'Invalid color name fg = "{fg}"')
|
|
244
|
+
if not (bg is None or bg in _BACKGROUNDS):
|
|
245
|
+
raise ValueError(f'Invalid color name bg = "{bg}"')
|
|
246
|
+
|
|
247
|
+
fg_ = _FOREGROUNDS.get(fg, "")
|
|
248
|
+
bg_ = _BACKGROUNDS.get(bg, "")
|
|
249
|
+
|
|
250
|
+
return _join_codes(fg_, bg_)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _byte(fg: int | None, bg: int | None) -> str:
|
|
254
|
+
"""8-bite encoding part
|
|
255
|
+
|
|
256
|
+
c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
|
|
257
|
+
"""
|
|
258
|
+
if not (fg is None or (isinstance(fg, int) and 0 <= fg <= MAX_RGB)):
|
|
259
|
+
raise ValueError(f"Invalid fg = {fg}. Allowed int in [0, 255].")
|
|
260
|
+
if not (bg is None or (isinstance(bg, int) and 0 <= bg <= MAX_RGB)):
|
|
261
|
+
raise ValueError(f"Invalid bg = {bg}. Allowed int in [0, 255].")
|
|
262
|
+
|
|
263
|
+
fg_ = ""
|
|
264
|
+
if fg is not None:
|
|
265
|
+
fg_ = "38;5;" + str(fg)
|
|
266
|
+
bg_ = ""
|
|
267
|
+
if bg is not None:
|
|
268
|
+
bg_ = "48;5;" + str(bg)
|
|
269
|
+
|
|
270
|
+
return _join_codes(fg_, bg_)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _hex2rgb(h: str) -> tuple[int, int, int]:
|
|
274
|
+
"""Transform rgb hex representation into rgb tuple of ints representation"""
|
|
275
|
+
assert isinstance(h, str)
|
|
276
|
+
if h.lower().startswith("0x"):
|
|
277
|
+
h = h[2:]
|
|
278
|
+
if len(h) == 3:
|
|
279
|
+
return (int(h[0] * 2, base=16), int(h[1] * 2, base=16), int(h[2] * 2, base=16))
|
|
280
|
+
if len(h) == 6:
|
|
281
|
+
return (int(h[0:2], base=16), int(h[2:4], base=16), int(h[4:6], base=16))
|
|
282
|
+
|
|
283
|
+
raise ValueError("Invalid hex RGB value.")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _rgb(fg: Sequence[int] | None, bg: Sequence[int] | None) -> str:
|
|
287
|
+
"""24-bit encoding part
|
|
288
|
+
|
|
289
|
+
c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit
|
|
290
|
+
"""
|
|
291
|
+
if not (
|
|
292
|
+
fg is None
|
|
293
|
+
or (
|
|
294
|
+
(isinstance(fg, (list, tuple)) and len(fg) == RGB_VALUES)
|
|
295
|
+
and all(0 <= f <= MAX_RGB for f in fg)
|
|
296
|
+
)
|
|
297
|
+
):
|
|
298
|
+
raise ValueError(f"Foreground fg either None or 3-tuple: {fg}")
|
|
299
|
+
if not (
|
|
300
|
+
bg is None
|
|
301
|
+
or (
|
|
302
|
+
(isinstance(bg, (list, tuple)) and len(bg) == RGB_VALUES)
|
|
303
|
+
and all(0 <= b <= MAX_RGB for b in bg)
|
|
304
|
+
)
|
|
305
|
+
):
|
|
306
|
+
raise ValueError(f"Foreground fg either None or 3-tuple: {bg}")
|
|
307
|
+
|
|
308
|
+
fg_ = ""
|
|
309
|
+
if fg is not None:
|
|
310
|
+
fg_ = "38;2;" + ";".join(map(str, fg))
|
|
311
|
+
bg_ = ""
|
|
312
|
+
if bg is not None:
|
|
313
|
+
bg_ = "48;2;" + ";".join(map(str, bg))
|
|
314
|
+
|
|
315
|
+
return _join_codes(fg_, bg_)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _join_codes(fg: str, bg: str) -> str:
|
|
319
|
+
"""Join `fg` and `bg` with ; and surround with correct esc sequence."""
|
|
320
|
+
colors = ";".join(filter(lambda c: len(c) > 0, (fg, bg)))
|
|
321
|
+
if colors:
|
|
322
|
+
return "\x1b[" + colors + "m"
|
|
323
|
+
|
|
324
|
+
return ""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
_BACKGROUNDS: dict[str | None, str] = {
|
|
328
|
+
"black": "40",
|
|
329
|
+
"red": "41",
|
|
330
|
+
"green": "42",
|
|
331
|
+
"yellow": "43",
|
|
332
|
+
"blue": "44",
|
|
333
|
+
"magenta": "45",
|
|
334
|
+
"cyan": "46",
|
|
335
|
+
"white": "47",
|
|
336
|
+
"bright_black": "100",
|
|
337
|
+
"bright_red": "101",
|
|
338
|
+
"bright_green": "102",
|
|
339
|
+
"bright_yellow": "103",
|
|
340
|
+
"bright_blue": "104",
|
|
341
|
+
"bright_magenta": "105",
|
|
342
|
+
"bright_cyan": "106",
|
|
343
|
+
"bright_white": "107",
|
|
344
|
+
"bright_black_old": "1;40",
|
|
345
|
+
"bright_red_old": "1;41",
|
|
346
|
+
"bright_green_old": "1;42",
|
|
347
|
+
"bright_yellow_old": "1;43",
|
|
348
|
+
"bright_blue_old": "1;44",
|
|
349
|
+
"bright_magenta_old": "1;45",
|
|
350
|
+
"bright_cyan_old": "1;46",
|
|
351
|
+
"bright_white_old": "1;47",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_FOREGROUNDS: dict[str | None, str] = {
|
|
355
|
+
"black": "30",
|
|
356
|
+
"red": "31",
|
|
357
|
+
"green": "32",
|
|
358
|
+
"yellow": "33",
|
|
359
|
+
"blue": "34",
|
|
360
|
+
"magenta": "35",
|
|
361
|
+
"cyan": "36",
|
|
362
|
+
"white": "37",
|
|
363
|
+
"bright_black": "90",
|
|
364
|
+
"bright_red": "91",
|
|
365
|
+
"bright_green": "92",
|
|
366
|
+
"bright_yellow": "93",
|
|
367
|
+
"bright_blue": "94",
|
|
368
|
+
"bright_magenta": "95",
|
|
369
|
+
"bright_cyan": "96",
|
|
370
|
+
"bright_white": "97",
|
|
371
|
+
"bright_black_old": "1;30",
|
|
372
|
+
"bright_red_old": "1;31",
|
|
373
|
+
"bright_green_old": "1;32",
|
|
374
|
+
"bright_yellow_old": "1;33",
|
|
375
|
+
"bright_blue_old": "1;34",
|
|
376
|
+
"bright_magenta_old": "1;35",
|
|
377
|
+
"bright_cyan_old": "1;36",
|
|
378
|
+
"bright_white_old": "1;37",
|
|
379
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Metadata tracking for data type conversions.
|
|
2
|
+
|
|
3
|
+
When we normalize datetime values to float (timestamps), we need to remember
|
|
4
|
+
that they were originally datetimes so we can format them correctly later.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from datetime import datetime, tzinfo
|
|
9
|
+
from typing import Any, final
|
|
10
|
+
|
|
11
|
+
from ._util import DataValue
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
class DataMetadata:
|
|
16
|
+
"""Tracks whether data was originally datetime and timezone info.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
is_datetime: True if the original data was datetime-like
|
|
20
|
+
timezone: The timezone if datetime was timezone-aware, else None
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, is_datetime: bool, timezone: tzinfo | None = None) -> None:
|
|
24
|
+
self.is_datetime = is_datetime
|
|
25
|
+
self.timezone = timezone
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_value(cls, value: Any) -> "DataMetadata":
|
|
29
|
+
"""Create metadata from a single value.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value: Any value (datetime, numeric, etc.)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DataMetadata instance
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(value, datetime):
|
|
38
|
+
return cls(is_datetime=True, timezone=value.tzinfo)
|
|
39
|
+
# For numeric types, numpy datetime64, etc.
|
|
40
|
+
# Check if it has a dtype attribute (numpy)
|
|
41
|
+
if hasattr(value, "dtype") and "datetime" in str(value.dtype):
|
|
42
|
+
# numpy datetime64 - these don't have timezone in the same way
|
|
43
|
+
return cls(is_datetime=True, timezone=None)
|
|
44
|
+
return cls(is_datetime=False, timezone=None)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_sequence(cls, sequence: Sequence[Any]) -> "DataMetadata":
|
|
48
|
+
"""Create metadata from a sequence of values.
|
|
49
|
+
|
|
50
|
+
All values in the sequence should have the same type.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
sequence: Sequence of values
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
DataMetadata instance
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If sequence contains mixed timezones
|
|
60
|
+
"""
|
|
61
|
+
if len(sequence) == 0:
|
|
62
|
+
return cls(is_datetime=False, timezone=None)
|
|
63
|
+
|
|
64
|
+
metadatas = [cls.from_value(v) for v in sequence]
|
|
65
|
+
datetime_flags = {m.is_datetime for m in metadatas}
|
|
66
|
+
|
|
67
|
+
if len(datetime_flags) > 1:
|
|
68
|
+
raise ValueError("Cannot mix numeric and datetime values.")
|
|
69
|
+
|
|
70
|
+
if not metadatas[0].is_datetime:
|
|
71
|
+
return DataMetadata(is_datetime=False, timezone=None)
|
|
72
|
+
|
|
73
|
+
timezones = {m.timezone for m in metadatas}
|
|
74
|
+
has_naive = None in timezones
|
|
75
|
+
has_aware = len(timezones - {None}) > 0
|
|
76
|
+
|
|
77
|
+
if has_naive and has_aware:
|
|
78
|
+
raise ValueError("Cannot mix timezone-naive and timezone-aware datetime.")
|
|
79
|
+
|
|
80
|
+
# Pick first encountered timezone as default
|
|
81
|
+
display_timezone = metadatas[0].timezone
|
|
82
|
+
|
|
83
|
+
return DataMetadata(is_datetime=True, timezone=display_timezone)
|
|
84
|
+
|
|
85
|
+
def convert_for_display(
|
|
86
|
+
self, value: float, tz_override: tzinfo | None = None
|
|
87
|
+
) -> DataValue:
|
|
88
|
+
"""Convert normalized float back to original type for display.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value: Normalized float value (timestamp if datetime)
|
|
92
|
+
tz_override: Optional timezone override for datetime display
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
float for numeric data, datetime for datetime data
|
|
96
|
+
"""
|
|
97
|
+
if not self.is_datetime:
|
|
98
|
+
# if not datetime, we assume we have some numeric value ... no conversion there
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
display_tz = tz_override or self.timezone
|
|
102
|
+
|
|
103
|
+
return datetime.fromtimestamp(value, tz=display_tz)
|
plotille/_dots.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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 typing import Any
|
|
24
|
+
|
|
25
|
+
from ._colors import ColorDefinition, color
|
|
26
|
+
|
|
27
|
+
# I plot upside down, hence the different order
|
|
28
|
+
# fmt: off
|
|
29
|
+
_xy2dot = [
|
|
30
|
+
[1 << 6, 1 << 7],
|
|
31
|
+
[1 << 2, 1 << 5],
|
|
32
|
+
[1 << 1, 1 << 4],
|
|
33
|
+
[1 << 0, 1 << 3],
|
|
34
|
+
]
|
|
35
|
+
# fmt: on
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Dots:
|
|
39
|
+
"""A Dots object is responsible for printing requested braille dots and colors
|
|
40
|
+
|
|
41
|
+
Dot ordering: \u2800 '⠀' - \u28ff '⣿'' Coding according to ISO/TR 11548-1
|
|
42
|
+
|
|
43
|
+
Hence, each dot on or off is 8bit, i.e. 256 possibilities. With dot number
|
|
44
|
+
one being the lsb and 8 is msb:
|
|
45
|
+
|
|
46
|
+
idx: 8 7 6 5 4 3 2 1
|
|
47
|
+
bits: 0 0 0 0 0 0 0 0
|
|
48
|
+
|
|
49
|
+
Ordering of dots:
|
|
50
|
+
|
|
51
|
+
1 4
|
|
52
|
+
2 5
|
|
53
|
+
3 6
|
|
54
|
+
7 8
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
marker: str | None = None,
|
|
60
|
+
fg: ColorDefinition = None,
|
|
61
|
+
bg: ColorDefinition = None,
|
|
62
|
+
**color_kwargs: Any,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Create a Dots object
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
dots: List[int] With set dots to on; ∈ 1 - 8
|
|
68
|
+
marker: str Set a marker instead of braille dots.
|
|
69
|
+
fg: str Color of dots
|
|
70
|
+
bg: str Color of background
|
|
71
|
+
**color_kwargs: More arguments to the color-function.
|
|
72
|
+
See `plotille.color()`.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dots
|
|
76
|
+
"""
|
|
77
|
+
assert marker is None or len(marker) == 1
|
|
78
|
+
self._dots = 0
|
|
79
|
+
self._marker = marker
|
|
80
|
+
self.fg = fg
|
|
81
|
+
self.bg = bg
|
|
82
|
+
self._color_kwargs = color_kwargs
|
|
83
|
+
if "mode" not in self._color_kwargs:
|
|
84
|
+
self._color_kwargs["mode"] = "names"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def color_kwargs(self) -> dict[str, Any]:
|
|
88
|
+
return self._color_kwargs
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def dots(self) -> list[int]:
|
|
92
|
+
assert self._dots.bit_length() <= 8
|
|
93
|
+
dots = []
|
|
94
|
+
x = self._dots
|
|
95
|
+
bit = 1
|
|
96
|
+
while x != 0:
|
|
97
|
+
if x & 1 == 1:
|
|
98
|
+
dots.append(bit)
|
|
99
|
+
bit += 1
|
|
100
|
+
x >>= 1
|
|
101
|
+
return sorted(dots)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def marker(self) -> str | None:
|
|
105
|
+
return self._marker
|
|
106
|
+
|
|
107
|
+
@marker.setter
|
|
108
|
+
def marker(self, value: str | None) -> None:
|
|
109
|
+
assert value is None or isinstance(value, str)
|
|
110
|
+
assert value is None or len(value) == 1
|
|
111
|
+
self._marker = value
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
return "Dots(dots={}, marker={}, fg={}, bg={}, color_kwargs={})".format(
|
|
115
|
+
self.dots,
|
|
116
|
+
self.marker,
|
|
117
|
+
self.fg,
|
|
118
|
+
self.bg,
|
|
119
|
+
" ".join(f"{k}: {v}" for k, v in self.color_kwargs.items()),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
if self.marker:
|
|
124
|
+
res = self.marker
|
|
125
|
+
else:
|
|
126
|
+
res = chr(0x2800 + self._dots)
|
|
127
|
+
|
|
128
|
+
return color(res, fg=self.fg, bg=self.bg, **self.color_kwargs)
|
|
129
|
+
|
|
130
|
+
def fill(self) -> None:
|
|
131
|
+
self._dots = 0xFF
|
|
132
|
+
|
|
133
|
+
def clear(self) -> None:
|
|
134
|
+
self._dots = 0
|
|
135
|
+
self.marker = None
|
|
136
|
+
|
|
137
|
+
def update(
|
|
138
|
+
self, x: int, y: int, set_: bool = True, marker: str | None = None
|
|
139
|
+
) -> None:
|
|
140
|
+
"""(Un)Set dot at position x, y, with (0, 0) is top left corner.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
x: int x-coordinate ∈ [0, 1]
|
|
144
|
+
y: int y-coordinate ∈ [0, 1, 2, 3]
|
|
145
|
+
set_: bool True, sets dot, False, removes dot
|
|
146
|
+
marker: str Instead of braille dots set a marker char.
|
|
147
|
+
"""
|
|
148
|
+
assert x in (0, 1)
|
|
149
|
+
assert y in (0, 1, 2, 3)
|
|
150
|
+
|
|
151
|
+
if set_:
|
|
152
|
+
self._dots |= _xy2dot[y][x]
|
|
153
|
+
if marker:
|
|
154
|
+
self.marker = marker
|
|
155
|
+
else:
|
|
156
|
+
self._dots = self._dots & (_xy2dot[y][x] ^ 0xFF)
|
|
157
|
+
self.marker = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def braille_from(dots: list[int]) -> str:
|
|
161
|
+
"""Unicode character for braille with given dots set
|
|
162
|
+
|
|
163
|
+
See https://en.wikipedia.org/wiki/Braille_Patterns#Identifying.2C_naming_and_ordering
|
|
164
|
+
for dot to braille encoding.
|
|
165
|
+
|
|
166
|
+
Parameters:
|
|
167
|
+
dots: List[int] All dots that should be set. Allowed dots are 1,2,3,4,5,6,7,8
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
unicode: braille sign with given dots set. \u2800 - \u28ff
|
|
171
|
+
"""
|
|
172
|
+
bin_code = ["0"] * 8
|
|
173
|
+
for i in dots:
|
|
174
|
+
bin_code[8 - i] = "1"
|
|
175
|
+
|
|
176
|
+
code = 0x2800 + int("".join(bin_code), 2)
|
|
177
|
+
|
|
178
|
+
return chr(code)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def dots_from(braille: str) -> list[int]:
|
|
182
|
+
"""Get set dots from given
|
|
183
|
+
|
|
184
|
+
See https://en.wikipedia.org/wiki/Braille_Patterns#Identifying.2C_naming_and_ordering
|
|
185
|
+
for braille to dot decoding.
|
|
186
|
+
|
|
187
|
+
Parameters:
|
|
188
|
+
braille: unicode Braille character in \u2800 - \u28ff
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List[int]: dots that are set in braille sign
|
|
192
|
+
"""
|
|
193
|
+
assert 0x2800 <= ord(braille) <= 0x28FF
|
|
194
|
+
|
|
195
|
+
code = str(bin(ord(braille) - 0x2800))[2:].rjust(8, "0")
|
|
196
|
+
|
|
197
|
+
dots = []
|
|
198
|
+
for i, c in enumerate(code):
|
|
199
|
+
if c == "1":
|
|
200
|
+
dots += [8 - i]
|
|
201
|
+
|
|
202
|
+
return sorted(dots)
|