Some days you just need...
If you have Python3 installed...
Here's a python script to plot the price of cryptocurrencies directly on the command line.
This a single python script, which depends on the requests, pandas, and numpy packages.
It uses the Symbols for Legacy Computing
unicode block for plotting directly inside the terminal emulator. Most
terminals based on the Gnome terminal support these characters.
I make no apologies for the garish color scheme, which is based on what I imagine people in the 1980s imagined the future would look like. You can edit the ANSI color codes in the source to suit.
This pulls price data from the Kraken exchange, but it is not affiliated with Kraken in any way. It's intended as a minimal demo of interfacing with Kraken's REST API, as well as a demonstration of the possibilities of semigraphics characters in modern terminal emulators.
To changes colors, styles, defaults, etc, edit the python source code. This is provided as a self-contained demonstration of calling Kraken's REST API and plotting using unicode characters in the command line. The expectation is that users will modify/incorporate parts of this into their own script.

Quasicrystals are aperiodic spatial tilings, and 2D quasicrystals have been used for centuries in Islamic art. I find these patterns beautiful, and I think more people should know how to render them.
You can render 2D quasicrystals as a sum of plane waves. They can also be constructed using aperiodic tilings (e.g. 4-fold Ammann-Beenker tiles and 5-fold Penrose Tiles), and there are fractal constructions that recursively subdivide a set of tiles using a smaller set of the same tiles (e.g. 5- and 7-fold tilings). Many more constructions can be found on the Tilings Encyclopedia
This post focuses on the "cut and project" construction, which is easy to implement in code. The routines shown here can be found in this iPython notebook on Github.
The simplest way to render crisp, geometric quasicrystals is the "cut and project" construction. This views the quasicrystal as a two-dimensional projection of a planar slice through a N-dimensional hyper-lattice. Let's implement this algorithm.
First, we need to define the idea of a hyperlattice. This is a N-dimensional generalization of a cubic crystal lattice. It tiles ND space using ND hypercubes. We can represent the center of each hypercube (hypercell? hyperpixel?) as a N-D vector of integers
$$
\mathbf q_0 = [ x_1, ... x_N ],\,\,x_i\in\mathbb Z
$$
The associated N-cube, then, includes all points on a unit hypercube $\left[-\tfrac 1 2, \tfrac 1 2\right]^N$ offset to location $\mathbf q_0$:
$$
\mathbf q = \left[-\tfrac 1 2, \tfrac 1 2\right]^N + \mathbf q_0
$$
To construct the quasicrystal, we cut through this ND lattice with a 2D plane. To get a aperiodic slice, this plane must not align with any periodic direction in the hyperlattice.
A good choice is to project each direction of the hyperlattice onto an evenly-spaced set of basis directions, centered at the origin. We define a 2D $(s,t)$ plane $\mathcal P$ in terms of two N-D basis vectors, $\mathbf u, \mathbf v \in \mathbb R^N$.
$$
\begin{aligned}
\mathbf p &= \mathbf u \cdot s + \mathbf v \cdot t,\,\,s,t\in\mathbb R
\\
\mathbf u &= [ \cos(\theta_0), \dots , \cos(\theta_{N-1}) ]
\\
\mathbf v &= [ \sin(\theta_0), \dots , \sin(\theta_{N-1}) ]
\\
\theta_i &= \frac {\pi\cdot i}{N}
\end{aligned}
$$
To render the quasicrystal, cut the N-D lattice along this plane, and project the exposed surface of this cut isometrically onto the 2D (s,t) plane $\mathcal P$. I've searched for an elegant algorithm for this to no avail. It seems like there should be some generalization of e.g. Bresenham's line algorithm to finding the hypercubes (hyperpixels?) associated with the cut. But, inelegant solutions work: it suffices to find the set of N-cubes in the ND lattice that intersect this plane.
$$
\mathcal Q = \left\{ \mathbf q_0 \bigm| \mathcal P \cap \mathbf q\ne \varnothing \right\}
$$
An easy to code (but terrible) way to find points in $\mathcal Q$ is by a brute-force search: Check lots of points in the s,t plane, and quantize them to the nearest vertex $\mathbf q_0$ of the hyper-lattice:
$$
\mathcal Q = \left\{ \mathbf q_0 \bigm| \exists (s,t)\in\mathbb R^2 \text{ s.t. } \left \lfloor \mathbf u s + \mathbf v t \right \rceil = \mathbf q_0 \right\},
$$
where $\lfloor\cdot\rceil$ denotes rounding to the nearest integer vector. This is massively inefficient: many $(s,t)$ points map to the
same lattice point $\mathbf q$, and it will miss points if the grid
resolution is too coarse. But, for small renderings and run-once code,
it does the job. Here it is Python3 code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from scipy.spatial.distance import squareform, pdist from pylab import * from numpy import * N = 4 # Dimension of crystal θ = linspace(0,pi,N+1)[1:] # Arrange basis vectors evenly on [0,π) A = array([cos(θ),sin(θ)]) # Convert from complex notation L = 501 # Number of grid points to search over dx = 5 # Size of square portion of plane to check # Generate the search grid x = linspace(-dx,dx,L) xy = array(meshgrid(x,x)).reshape(2,L*L) # Collapse all grid points to the nearest hyper-cell p = unique(np.round(A.T@xy),axis=1) def plotpoints(p): u = pinv(A).T@p D = squareform(pdist(p.T)) e = {tuple(sorted(e)) for e in zip(*where(abs(D-1)<1e-6))} xy = concatenate([[u[:,a],u[:,b],[NaN,NaN]] for a,b in e]) plot(*xy.T,lw=0.5,color='k') axis("equal") axis("off") plotpoints(p) |
![]() |
| Figure 1: rendered output of a rhobic tiling for the N=4 quasicrystal, computed by brute-force search of the cut-and-project approach. |
| White | #f1f0e9 |
| Black | #44525c |
| Rust | #eb7a59 |
| Ochre | #eea300 |
| Azure | #5aa0df |
| Turquoise | #00bac9 |
Surprisingly, when I ran this through a color-blindness simulator , some colors become even more distinct, so it is fairly color-blind friendly. I added colors to expand the pallet, to handle gradients and plots with extra lines.
| Yellow | #efcd2b |
| Moss | #77ae64 |
| Mauve | #b56ab6 |
| Magenta | #cc79a7 |
| Violet | #8d5ccd |
| Indigo | #606ec3 |
| Viridian | #11be8d |
| Chartreuse | #b59f1a |
These are less distinct, and not all combinations are color-blind friendly. Here's a bit of Python to define and tests these colors:
from pylab import * from matplotlib import patchesWHITE = mpl.colors.to_rgb('#f1f0e9') RUST = mpl.colors.to_rgb('#eb7a59') OCHRE = mpl.colors.to_rgb('#eea300') AZURE = mpl.colors.to_rgb('#5aa0df') TURQUOISE = mpl.colors.to_rgb('#00bac9') BLACK = mpl.colors.to_rgb('#44525c') YELLOW = mpl.colors.to_rgb('#efcd2b') INDIGO = mpl.colors.to_rgb('#606ec3') VIOLET = mpl.colors.to_rgb('#8d5ccd') MAUVE = mpl.colors.to_rgb('#b56ab6') MAGENTA = mpl.colors.to_rgb('#cc79a7') CHARTREUSE = mpl.colors.to_rgb('#b59f1a') MOSS = mpl.colors.to_rgb('#77ae64') VIRIDIAN = mpl.colors.to_rgb('#11be8d')
GATHER = [WHITE,RUST,OCHRE,AZURE,TURQUOISE,BLACK] COLORS = [BLACK,WHITE,YELLOW,OCHRE,CHARTREUSE,MOSS,VIRIDIAN,TURQUOISE,AZURE,INDIGO,VIOLET,MAUVE,MAGENTA,RUST] CYCLE = [BLACK,RUST,AZURE,OCHRE,TURQUOISE,MAUVE,YELLOW,INDIGO] mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=CYCLE)
def test_panel(COLORS): ax = gca() NCOLORS = len(COLORS) axis('off') xlim(0,NCOLORS) ylim(NCOLORS,0) for j in range(NCOLORS): for i in range(NCOLORS): ax.add_patch(patches.Rectangle((i,j),1,1,linewidth=1, edgecolor='none',facecolor=COLORS[i])) ax.add_patch(patches.Rectangle((i+.25,j+.25),.5,.5,linewidth=1, edgecolor='none',facecolor=COLORS[j])) axis('equal')
figure(figsize=(6,3),dpi=200) subplots_adjust(0,0,1,1,0.1,0) subplot(111) test_panel(COLORS) savefig('colorblind_test_panel.png',bbox_inches='tight',pad_inches=0) </pre></div>
Two squences form decent color maps. They aren't perceptually uniform, but pass the color blindness simulator tests. Adding violet, mauve, and moss, to the original Gather pallet creates a hue wheel. This one flunks color-blindness test (see Crameri et al. ). Here they are in python:
riley2 = matplotlib.colors.LinearSegmentedColormap.from_list('riley2', [INDIGO,VIOLET,MAUVE,MAGENTA,RUST,OCHRE])riley3 = matplotlib.colors.LinearSegmentedColormap.from_list('riley3', [OCHRE,CHARTREUSE,MOSS,VIRIDIAN,TURQUOISE,AZURE])
# Smoothed out mauve, violet, rust, ochre, moss, turquoise, azure, in a loop hues = ['#8c62cc', '#9560c8', '#9e62c3', '#a765be', '#b068b6', '#bb6caa', '#c66f98', '#d27384', '#dd7670', '#e67c5c', '#ea8348', '#ec8b34', '#ec9421', '#e99c13', '#dea212', '#c9a620', '#b0a934', '#96ab4a', '#7cae60', '#62b076', '#48b38c', '#2eb5a2', '#1ab7b6', '#12b6c5', '#1cb2ce', '#2dadd4', '#41a7d8', '#529fdb', '#6194db', '#6d86d8', '#7977d4', '#836ad0'] riley = matplotlib.colors.LinearSegmentedColormap.from_list('riley',hues)
# Make new maps behave like native Matplotlib maps plt.register_cmap(name='riley2',cmap=riley2) plt.register_cmap(name='riley3',cmap=riley3) plt.register_cmap(name='riley' ,cmap=riley )
# Show as figure figure(figsize=(5,1),dpi=300) subplot(311) imshow([linspace(0,1,256)],cmap='riley2',aspect='auto') axis('off'); tight_layout() subplot(312) imshow([linspace(0,1,256)],cmap='riley3',aspect='auto') axis('off'); tight_layout() subplot(313) imshow([linspace(0,1,256)],cmap='riley',aspect='auto') axis('off'); tight_layout() subplots_adjust(hspace=0.3) savefig('moremaps.png') </pre></div>