matthewplotlib

matthewplotlib.core

The character grid underneath every plot, and the glyphs that fill it.

A plot is ultimately a rectangle of coloured unicode characters. This module provides that rectangle, along with the routines that turn numeric data into characters dense enough to draw with. The plot types in matthewplotlib.plots are built on top of it.

The grid:

  • CharArray: A grid of unicode codepoints with optional foreground and background colors. Supports composition (stacking, layering, padding), rendering to ANSI strings---including differential updates that repaint only the cells that changed between frames---and rendering to images and animated gifs using an embedded pixel font.

Drawing characters, each packing several data points into one character cell:

@dataclasses.dataclass
class CharArray:

A grid of possibly-coloured characters comprising a plot. For internal use.

Fields:

  • codes: uint32[h,w]. Unicode code point for the character.
  • fg: bool[h,w]. Whether to use a custom foreground color.
  • fg_rgb: uint8[h,w,3]. (If fg) RGB for custom foreground color.
  • bg: bool[h,w]. Whether to use a custom background color.
  • bg_rgb: uint8[h,w,3]. (If bg) RGB for custom background color.
CharArray( codes: NDArray, fg: NDArray, fg_rgb: NDArray, bg: NDArray, bg_rgb: NDArray)
codes: numpy._typing._array_like.NDArray
fg: numpy._typing._array_like.NDArray
fg_rgb: numpy._typing._array_like.NDArray
bg: numpy._typing._array_like.NDArray
bg_rgb: numpy._typing._array_like.NDArray
height: int
width: int
@staticmethod
def from_codes( codes: NDArray, fgcolor: ColorLike | None, bgcolor: ColorLike | None) -> CharArray:
@staticmethod
def from_size( height: int, width: int, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None) -> CharArray:
def pad( self: Self, above: int = 0, below: int = 0, left: int = 0, right: int = 0, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None) -> CharArray:
@staticmethod
def map( f: Callable[[list[NDArray]], NDArray], charss: list[CharArray]) -> CharArray:
def isblank(self: Self) -> NDArray:

True where the character has no visible content.

def isnonblank(self: Self) -> NDArray:

True where the character has visible content.

def to_ansi_str(self: Self) -> str:

Render a CharArray as a sequence of characters and ANSI control codes (merging codes where possible).

def to_ansi_diff_str(self: Self, prev: CharArray) -> str:

Render the minimal ANSI sequence that updates a terminal already showing prev so that it shows self instead, repainting only the cells that differ.

This is the differential counterpart to to_ansi_str: where redrawing a whole frame re-emits every cell, this jumps the cursor to just the changed cells. For an animation whose frames are mostly stable, that is dramatically fewer bytes down the wire.

Cursor contract (the sequence is shaped for a plain print):

  • On entry, the cursor is assumed to be at column 0 on the line immediately below where prev was rendered -- exactly where it sits after printing prev.
  • On exit, the cursor is left at column 0 on the last row of the plot, so the newline print appends carries it to the line below, ready for the next frame to diff against this one in the same way.

In other words the sequence is incomplete on its own: it expects a trailing newline, just as to_ansi_str does. Write it with print(...), not print(..., end="").

Assumes prev was rendered starting at column 0 and that all glyphs are single-width (the standard animated-plot layout).

The two need not be the same size, and only the difference is sent in that case too. Cells outside the region prev covered are always painted, since nothing is on screen there to keep. Rows and columns prev covered but self does not are erased. Growing taller overwrites the rows immediately below the plot: they have to be written into, and a string cannot push them out of the way without knowing where it sits on the screen. Growing shorter leaves those rows blank rather than closing the gap, for the same reason.

Note that an unchanged frame does not return "": it returns a bare cursor-up, so that the newline print appends still lands the cursor where the contract promises rather than a row lower.

def to_plain_str(self: Self) -> str:

Render a CharArray as a sequence of characters without colour.

def to_rgba_array(self: Self, bgcolor: ColorLike | None = None) -> numpy.ndarray:

Convert a CharArray to an RGBA image array

def to_bit_array(self: Self) -> numpy.ndarray:

Convert a CharArray to an bitmap image array

def ords(chrs):

Convert a string or list of characters to a list of unicode code points.

BRAILLE_MAP = array([[0, 3], [1, 4], [2, 5], [6, 7]], dtype=uint8)
def unicode_braille_array( dots: NDArray, dotc: NDArray | None = None, dotw: NDArray | None = None, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None) -> CharArray:

Turns a H by W array of dots into a h=ceil(H/4) by w=ceil(W/2) array of braille Unicode characters.

Inputs:

  • dots: bool[H, W]. Array of booleans or counts. Dots are placed where this array contains nonzero.
  • dotc: optional uint8[H, W, RGB]. Array of colours to use for the fg of each dot. Where multiple dots are coloured within one one character, mixes the colours according to dotw.
  • dotw: optional float[H, W]. Weights for combining colors when multiple dots occur in one cell. If not provided, combine uniformly. If dotc is not provided, this is not used.
  • fgcolor: optional ColorLike. Foreground color used for all braille characters. Overrides dotc if both are provided.
  • bgcolor: optional ColorLike. Background color used for all characters.

Returns:

  • chars: CharArray. An array of Braille characters with h rows and w columns.

An illustrated example, not including colour combination, is as follows:

Start with an array. Assume height is divisible by 4 and width divisible by
2, otherwise pad with 0s until that is the case.
    ____
   [1  0] 0  1  0  1  1  1  1  0  1  0  0  0  0  1  0  0  0  0  0  1  1  0
   [1  0] 0  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 .-[1  0] 0  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 | [1__0] 0  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 |  1  1  1  1  0  1  1  1  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 |  1  0  0  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 |  1  0  0  1  0  1  0  0  0  0  1  0  0  0  0  1  0  0  0  0  1  0  0  1
 |  1  0  0  1  0  1  1  1  1  0  1  1  1  1  0  1  1  1  1  0  0  1  1  0
 |
 | take each 4x2 subarray and ...
 |                                                               braille
 | identify the 4x2 bits with the                                unicode
 | eight numbered braille dots:                                  start pt
 |                                                               |
 |  (dot 1) 1 0 (dot 4)     convert to                           v
 `> (dot 2) 1 0 (dot 5) -----------------> 0 b 0 1 0 0 0 1 1 1 + 0x2800 -.
    (dot 3) 1 0 (dot 6)    braille code        | | | | | | | |           |
    (dot 7) 1 0 (dot 8)                    dot 8 7 6 5 4 3 2 1           |
                                                                         |
  convert the braille code to a unicode character and collate into array |
 .-----------------------------------------------------------------------'
 |  '''
 `->⡇⢸⢸⠉⠁⡇⠀⢸⠀⠀⡎⢱  (Note: this function returns a CharArray, use
    ⡏⢹⢸⣉⡁⣇⣀⢸⣀⡀⢇⡸  .to_plain_str() to get a string.)
    '''
PARTIAL_BLOCKS_ROW = [32, 9615, 9614, 9613, 9612, 9611, 9610, 9609, 9608]
def unicode_bar( proportion: float, width: int, height: int = 1, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None) -> CharArray:

Generates a Unicode progress bar as a list of characters.

This function creates a fixed-width left-to-right bar using Unicode block elements to represent the proportion rounded down to nearest 1/8th of a block.

Inputs:

  • proportion: float. The fraction of the bar to fill. Should be between 0.0 and 1.0 inclusive.
  • width: int (positive). The width of the full bar in characters.
  • height: int (positive, default 1). The number of rows that the bar takes up.
  • fgcolor: optional ColorLike. Foreground color used for the progress bar characters.
  • bgcolor: optional ColorLike. Background color used for the progress bar remainder.

Returns:

  • chars: CharArray A character array representing the bar.

Examples:

>>> unicode_bar(0.5, 10).to_plain_str()
'█████     '
>>> unicode_bar(0.625, 10).to_plain_str()
'██████▎   '

PARTIAL_BLOCKS_COL = [32, 9601, 9602, 9603, 9604, 9605, 9606, 9607, 9608]
def unicode_col( proportion: float, height: int, width: int = 1, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None) -> CharArray:

Generates a Unicode progress column as a list of characters.

This function creates a fixed-height column using Unicode block elements to
represent a proportion rounded down to nearest 1/8th of a block. The list
goes from the top of the bar to the bottom, but the bar grows from the
bottom towards the top.

Inputs:

* proportion: float.
    The fraction of the column to fill. Should be between 0.0 and 1.0
    inclusive.
* height: int (positive).
    The height of the full bar in characters.
* width: int (positive, default 1).
    The number of columns that the bar takes up.
* fgcolor: optional ColorLike.
    Foreground color used for the progress bar characters.
* bgcolor: optional ColorLike.
    Background color used for the progress bar remainder.

Returns:

* chars: CharArray
    A char array representing the column.

Examples:
    >>> unicode_col(0.5, 3).to_plain_str()
    ' 
▄
█'

class BoxStyle(builtins.str, enum.Enum):

A string enum defining preset styles for the border plot.

Each style is a string of eight characters representing the border elements.

Available Styles:

  • LIGHT: A standard, single-line border.
  • HEAVY: A thicker, bold border.
  • DOUBLE: A double-line border.
  • DASHED: A dashed single-line border.
  • BLANK: An invisible border (easily add 1-width padding).
  • ROUND: A single-line border with rounded corners.
  • BUMPER: A single-line border with corners made of blocks.
  • BLOCK1: A blocky border with half-width left and right walls.
  • BLOCK2: A uniform blocky border.
  • TIGER1: A stripy block border.
  • TIGER2: An alternative stripy block border.
  • LIGHTX: A light border with axis ticks.
  • HEAVYX: A heavy border with axis ticks.
  • LOWERX: A partial border with axis ticks.

Demo:

┌──────┐ ┏━━━━━━┓ ╔══════╗ ┌╌╌╌╌╌╌┐ ⡤⠤⠤⠤⠤⠤⠤⢤ ╭──────╮
│LIGHT │ ┃HEAVY ┃ ║DOUBLE║ ┊DASHED┊ ⡇DOTTED⢸ │ROUND │
└──────┘ ┗━━━━━━┛ ╚══════╝ └╌╌╌╌╌╌┘ ⠓⠒⠒⠒⠒⠒⠒⠚ ╰──────╯
         ▛──────▜ ▛▀▀▀▀▀▀▜ █▀▀▀▀▀▀█ ▞▝▝▝▝▝▝▝ ▘▘▘▘▘▘▘▚
 BLANK   │BUMPER│ ▌BLOCK1▐ █BLOCK2█ ▖TIGER1▝ ▘TIGER2▗
         ▙──────▟ ▙▄▄▄▄▄▄▟ █▄▄▄▄▄▄█ ▖▖▖▖▖▖▖▞ ▚▗▗▗▗▗▗▗
┬──────┐ ┲━━━━━━┓ ╷        
│LIGHTX│ ┃HEAVYX┃ │LOWERX  
┼──────┤ ╄━━━━━━┩ ┼──────╴ 

TODO:

  • It might make sense to consider borders with two characters on the left and right sides of the contents. Would open up new design possibilities.
LIGHT = <BoxStyle.LIGHT: '┌─┐││└─┘'>
HEAVY = <BoxStyle.HEAVY: '┏━┓┃┃┗━┛'>
DOUBLE = <BoxStyle.DOUBLE: '╔═╗║║╚═╝'>
DASHED = <BoxStyle.DASHED: '┌╌┐┊┊└╌┘'>
DOTTED = <BoxStyle.DOTTED: '⡤⠤⢤⢸⡇⠓⠒⠚'>
ROUND = <BoxStyle.ROUND: '╭─╮││╰─╯'>
BLANK = <BoxStyle.BLANK: ' '>
BUMPER = <BoxStyle.BUMPER: '▛─▜││▙─▟'>
BLOCK1 = <BoxStyle.BLOCK1: '▛▀▜▐▌▙▄▟'>
BLOCK2 = <BoxStyle.BLOCK2: '█▀████▄█'>
TIGER1 = <BoxStyle.TIGER1: '▞▝▝▝▖▖▖▞'>
TIGER2 = <BoxStyle.TIGER2: '▘▘▚▘▘▚▗▗'>
LIGHTX = <BoxStyle.LIGHTX: '┬─┐││┼─┤'>
HEAVYX = <BoxStyle.HEAVYX: '┲━┓┃┃╄━┩'>
LOWERX = <BoxStyle.LOWERX: '╷ │┼─╴'>
def unicode_box( chars: CharArray, style: BoxStyle, fgcolor: ColorLike | None = None, bgcolor: ColorLike | None = None, title: str = '') -> CharArray:

Wrap a character array in an outline of box drawing characters.

def unicode_image(image: NDArray) -> CharArray:

Convert an RGB image into an array of coloured Unicode half-block characters representing the pixels of the image.

Inputs:

  • image: u8[h, w, rgb]. The pixels of the image.

Returns:

  • chars: CharArray[ceil(h/2), w]. The array of coloured half-block characters. If the image has odd height, the bottom half of the final row is set to the default background colour.