Skip to content

Commit 75ce4da

Browse files
committed
refactoring unicode
1 parent fa8e05a commit 75ce4da

File tree

7 files changed

+187
-162
lines changed

7 files changed

+187
-162
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,14 @@ Braille art:
281281

282282
TODO: Checklist of specific interesting target features that are and are not
283283
implemented.
284+
285+
Other Python plotting libraries, most of which offer some level of
286+
interactivity that there are no plans to replicate.
287+
288+
* Matplotlib https://github.com/matplotlib/matplotlib
289+
* Seaborn https://github.com/mwaskom/seaborn
290+
* Plotly.py https://github.com/plotly/plotly.py
291+
* Pygal https://github.com/Kozea/pygal
292+
* Bokeh https://github.com/bokeh/bokeh
293+
* Altair https://github.com/vega/altair
294+
* Declarative API

examples/hilbert_curve.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
data = data.astype(bool)
88
plot = mp.hilbert(
99
data=data,
10-
dotcolor=(1.,1.,1.),
11-
bgcolor=(0.,0.,0.),
12-
nullcolor=(.1,.1,.1),
10+
color=(1.,1.,1.),
1311
)
1412

1513
print("printing plot...")

examples/lissajous.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
DIMENSION = 10_000
8-
NUM_STEPS = 10_000
8+
NUM_STEPS = 1_000
99

1010
# sample some brownian motion
1111
print("sample some high-dimensional brownian motion...")
@@ -17,25 +17,28 @@
1717
time = np.arange(NUM_STEPS)
1818

1919
# construct visualisation
20-
plot = (
21-
mp.text("BROWNIAN MOTION")
22-
/ mp.text("first two dimensions:")
23-
/ mp.scatter(traj[:, :2], height=20, width=75, color=(.5,.5,.5))
24-
/ mp.text("principal components:")
25-
/ mp.text("PC1, PC2, PC3 over time")
26-
/ (
27-
mp.scatter(np.c_[time, proj[:,0]], width=75, color="red")
28-
@ mp.scatter(np.c_[time, proj[:,1]], width=75, color="green")
29-
@ mp.scatter(np.c_[time, proj[:,2]], width=75, color="blue")
30-
)
31-
/ (
20+
plot = mp.vstack(
21+
mp.text("BROWNIAN MOTION"),
22+
mp.text("first two dimensions"),
23+
mp.scatter(
24+
traj[:, :2],
25+
color=(.5,.5,.5),
26+
height=20,
27+
width=75,
28+
),
29+
mp.text("principal components 1, 2, 3 over time"),
30+
mp.scatter(np.c_[time, proj[:,0]], color="red", width=75)
31+
@ mp.scatter(np.c_[time, proj[:,1]], color="green", width=75)
32+
@ mp.scatter(np.c_[time, proj[:,2]], color="blue", width=75),
33+
mp.text("principal components 1, 2, 3 paired"),
34+
mp.hstack(
3235
mp.text("PC1 v PC2")
33-
/ mp.scatter(proj[:, (0,1)], color="yellow", width=25)
36+
/ mp.scatter(proj[:, (0,1)], color="yellow", width=25)
3437
+ mp.text("PC1 v PC3")
35-
/ mp.scatter(proj[:, (0,2)], color="magenta", width=25)
38+
/ mp.scatter(proj[:, (0,2)], color="magenta", width=25)
3639
+ mp.text("PC2 v PC3")
37-
/ mp.scatter(proj[:, (1,2)], color="cyan", width=25)
38-
)
40+
/ mp.scatter(proj[:, (1,2)], color="cyan", width=25)
41+
),
3942
)
4043

4144
print("printing plot...")

images/hilbert_curve.png

-579 Bytes
Loading

images/lissajous.png

-531 Bytes
Loading

matthewplotlib/core.py

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
import enum
1212
import dataclasses
13+
1314
import numpy as np
15+
import einops
1416

1517
from typing import Self
16-
from numpy.typing import ArrayLike
18+
from numpy.typing import NDArray, ArrayLike
1719

1820
from matthewplotlib.colors import Color
1921
from matthewplotlib.unscii16 import bitmap
@@ -52,7 +54,7 @@ def bg_(self: Self) -> Color | None:
5254
* If c happens to be '█' or '▟', then it is more effective to return
5355
the foreground color.
5456
* If c happens to be '▀' or '▄', then it is more effective to return a
55-
mixture of the two colours, presuming there are two colours to mix.
57+
mixture of the two colours (when there are two colours to mix).
5658
5759
TODO:
5860
@@ -141,23 +143,25 @@ def to_rgba_array(
141143
], dtype=np.uint8)
142144

143145

144-
def braille_encode(
145-
a: ArrayLike, # bool[4h, 2w]
146-
) -> np.ndarray: # -> uint16[h, w]
146+
def unicode_braille_array(
147+
dots: ArrayLike, # bool[4h, 2w]
148+
color: Color | None = None,
149+
) -> list[list[Char]]:
147150
"""
148151
Turns a HxW array of booleans into a (H//4)x(W//2) array of braille
149152
binary codes.
150153
151154
Inputs:
152155
153-
* a: bool[4h, 2w].
156+
* dots: bool[4h, 2w].
154157
Array of booleans, height divisible by 4 and width divisible by 2.
158+
* color: optional Color.
159+
Foreground color used for braille characters.
155160
156161
Returns:
157162
158-
* bits: uint16[h, w].
159-
An array of braille unicode code points. The unicode characters will
160-
have a dot in the corresponding places where `a` is True.
163+
* array: list[list[Char]].
164+
A nested list of Braille characters with H rows and W columns.
161165
162166
An illustrated example is as follows:
163167
```
@@ -185,26 +189,31 @@ def braille_encode(
185189
convert the braille code to a unicode character and collate into array |
186190
.-----------------------------------------------------------------------'
187191
| '''
188-
`->⡇⢸⢸⠉⠁⡇⠀⢸⠀⠀⡎⢱ (Note: this function returns codepoints, use `chr()`
189-
⡏⢹⢸⣉⡁⣇⣀⢸⣀⡀⢇⡸ to convert these into braille characters for printing.)
192+
`->⡇⢸⢸⠉⠁⡇⠀⢸⠀⠀⡎⢱ (Note: this function returns a nested list of Chars
193+
⡏⢹⢸⣉⡁⣇⣀⢸⣀⡀⢇⡸ rather than a string.)
190194
'''
191195
```
192196
"""
193197
# process input
194-
array = np.asarray(a, dtype=bool)
195-
H, W = array.shape
198+
dots_ = np.asarray(dots, dtype=bool)
199+
H, W = dots_.shape
196200
h, w = H // 4, W // 2
197201

198202
# create a view that chunks it into 4x2 cells
199-
cells = array.reshape(h, 4, w, 2)
203+
cells = dots_.reshape(h, 4, w, 2)
200204

201205
# convert each bit in each cell into a mask and combine into code array
202206
masks = np.left_shift(cells, BRAILLE_MAP.reshape(1,4,1,2), dtype=np.uint16)
203207
codes = np.bitwise_or.reduce(masks, axis=(1,3))
204208

205-
# unicode braille block starts at 0x2800
206-
unicodes = 0x2800 + codes
207-
return unicodes
209+
# convert code array into Char array
210+
array = [
211+
[
212+
Char(chr(0x2800+code), fg=color) if code else BLANK
213+
for code in row
214+
] for row in codes
215+
]
216+
return array
208217

209218

210219
# # #
@@ -323,7 +332,7 @@ def unicode_col(
323332

324333

325334
# # #
326-
# BOX DRAWING
335+
# UNICODE BOX DRAWING
327336

328337

329338
class BoxStyle(str, enum.Enum):
@@ -360,9 +369,9 @@ class BoxStyle(str, enum.Enum):
360369
▛──────▜ ▛▀▀▀▀▀▀▜ █▀▀▀▀▀▀█ ▞▝▝▝▝▝▝▝ ▘▘▘▘▘▘▘▚
361370
BLANK │BUMPER│ ▌BLOCK1▐ █BLOCK2█ ▖TIGER1▝ ▘TIGER2▗
362371
▙──────▟ ▙▄▄▄▄▄▄▟ █▄▄▄▄▄▄█ ▖▖▖▖▖▖▖▞ ▚▗▗▗▗▗▗▗
363-
┬──────┐ ┲━━━━━━┓ ╔══════╗
364-
│LIGHTX│ ┃HEAVYX┃ ║DOUBLX║ │LOWERX
365-
┼──────┤ ╄━━━━━━┩ ╚══════╝ ┼──────╴
372+
┬──────┐ ┲━━━━━━┓ ╷
373+
│LIGHTX│ ┃HEAVYX┃ │LOWERX
374+
┼──────┤ ╄━━━━━━┩ ┼──────╴
366375
```
367376
368377
TODO:
@@ -386,68 +395,124 @@ class BoxStyle(str, enum.Enum):
386395
HEAVYX = "┲━┓┃┃╄━┩"
387396
LOWERX = "╷ │┼─╴"
388397

389-
390398
@property
391-
def nw(self) -> str:
399+
def _nw(self) -> str:
392400
"""Northwest corner symbol."""
393401
return self[0]
394-
395402

396403
@property
397-
def n(self) -> str:
404+
def _n(self) -> str:
398405
"""North edge symbol."""
399406
return self[1]
400-
401407

402408
@property
403-
def ne(self) -> str:
409+
def _ne(self) -> str:
404410
"""Norteast corner symbol."""
405411
return self[2]
406-
407412

408413
@property
409-
def e(self) -> str:
414+
def _e(self) -> str:
410415
"""East edge symbol."""
411416
return self[3]
412-
413417

414418
@property
415-
def w(self) -> str:
419+
def _w(self) -> str:
416420
"""West edge symbol."""
417421
return self[4]
418-
419422

420423
@property
421-
def sw(self) -> str:
424+
def _sw(self) -> str:
422425
"""Southwest corner symbol."""
423426
return self[5]
424-
425427

426428
@property
427-
def s(self) -> str:
429+
def _s(self) -> str:
428430
"""South edge symbol."""
429431
return self[6]
430-
431432

432433
@property
433-
def se(self) -> str:
434+
def _se(self) -> str:
434435
"""Southeast corner symbol."""
435436
return self[7]
436437

437438

439+
def unicode_box(
440+
array: list[list[Char]],
441+
style: BoxStyle,
442+
color: Color | None = None,
443+
) -> list[list[Char]]:
444+
"""
445+
Wrap a character array in an outline of box drawing characters.
446+
"""
447+
# prepare characters
448+
nw = Char(style._nw, fg=color)
449+
n = Char(style._n, fg=color)
450+
ne = Char(style._ne, fg=color)
451+
w = Char(style._w, fg=color)
452+
e = Char(style._e, fg=color)
453+
sw = Char(style._sw, fg=color)
454+
s = Char(style._s, fg=color)
455+
se = Char(style._se, fg=color)
456+
# assemble box
457+
width = len(array[0])
458+
array = [
459+
[nw, *[n] * width, ne],
460+
*[[w, *row, e] for row in array],
461+
[sw, *[s] * width, se],
462+
]
463+
return array
464+
465+
466+
# # #
467+
# UNICODE HALF-BLOCK IMAGE
468+
469+
470+
def unicode_image(
471+
image: NDArray, # u8[h, w, rgb] or float[h, w, rgb]
472+
) -> list[list[Char]]:
473+
h, _w, _3 = image.shape
474+
475+
if h % 2 == 1:
476+
final_row = image[-1]
477+
image = image[:-1]
478+
else:
479+
final_row = None
480+
481+
stacked = einops.rearrange(
482+
image,
483+
'(h fgbg) w c -> h w fgbg c',
484+
fgbg=2,
485+
)
486+
array = [
487+
[
488+
Char(c="▀", fg=Color.parse(fg), bg=Color.parse(bg))
489+
for fg, bg in row
490+
]
491+
for row in stacked
492+
]
493+
494+
if final_row is not None:
495+
array.append([
496+
Char(c="▀", fg=Color.parse(fg), bg=None)
497+
for fg in final_row
498+
])
499+
500+
return array
501+
502+
438503
# # #
439504
# 3D projection
440505

441506

442507
def project3(
443-
xyz: np.ndarray, # float[n, 3]
444-
camera_position: np.ndarray = np.array([0., 0., 2.]), # float[3]
445-
camera_target: np.ndarray = np.zeros(3), # float[3]
446-
scene_up: np.ndarray = np.array([0.,1.,0.]), # float[3]
508+
xyz: np.ndarray, # float[n, 3]
509+
camera_position: np.ndarray = np.array([0., 0., 2.]), # float[3]
510+
camera_target: np.ndarray = np.zeros(3), # float[3]
511+
scene_up: np.ndarray = np.array([0.,1.,0.]), # float[3]
447512
fov_degrees: float = 90.0,
448513
) -> tuple[
449-
np.ndarray, # float[n, 2]
450-
np.ndarray, # bool[n]
514+
np.ndarray, # float[n, 2]
515+
np.ndarray, # bool[n]
451516
]:
452517
"""
453518
Project a 3d point cloud into two dimensions based on a given camera

0 commit comments

Comments
 (0)