plotille 6.0.5
Plot in the terminal using braille dots.
Requires Python: <4,>=3.10
Keywords: plot, scatter, histogram, terminal, braille, unicode, timeseries

![Hero Plotille](https://github.com/tammoippen/plotille/raw/master/imgs/hero.png)

# Plotille

[![CI](https://github.com/tammoippen/plotille/actions/workflows/CI.yml/badge.svg)](https://github.com/tammoippen/plotille/actions/workflows/CI.yml)
[![codecov](https://codecov.io/gh/tammoippen/plotille/branch/master/graph/badge.svg?token=OGWI832JNM)](https://codecov.io/gh/tammoippen/plotille)
[![Tested CPython Versions](https://img.shields.io/badge/cpython-3.10%2C%203.11%2C%203.12%2C%203.13%2C%203.14-brightgreen.svg)](https://img.shields.io/badge/cpython-3.10%2C%203.11%2C%203.12%2C%203.13%203.14-brightgreen.svg)
[![Tested PyPy Versions](https://img.shields.io/badge/pypy-3.11-brightgreen.svg)](https://img.shields.io/badge/pypy-3.11-brightgreen.svg)
[![PyPi version](https://img.shields.io/pypi/v/plotille.svg)](https://pypi.python.org/pypi/plotille)
[![Downloads](https://pepy.tech/badge/plotille/month)](https://pepy.tech/project/plotille)
[![PyPi license](https://img.shields.io/pypi/l/plotille.svg)](https://pypi.python.org/pypi/plotille)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)

Plots, scatter plots, histograms and heatmaps in the terminal using braille dots, and foreground and background colors - with no dependencies. Make complex figures using the Figure class or make fast and simple plots using graphing function - similar to a very small sibling to matplotlib. Or use the canvas to plot dots, lines and images yourself.

Install:

```sh
pip install plotille
```

Similar to other libraries:

- like [drawille](https://github.com/asciimoo/drawille), but focused on graphing – plus X/Y-axis.
- like [termplot](https://github.com/justnoise/termplot), but with braille (finer dots), left to right histogram and linear interpolation for plotting function.
- like [termgraph](https://github.com/sgeisler/termgraph) (not on pypi), but very different style.
- like [terminalplot](https://github.com/kressi/terminalplot), but with braille, X/Y-axis, histogram, linear interpolation.

Basic support for timeseries plotting is provided with release 3.2: for any `X` or `Y` values you can also add `datetime.datetime`  or `numpy.datetime64` values. Labels are generated respecting the difference of `x_limits` and `y_limits`.

Support for heatmaps using background colors for figures and displaying images binary with braille, or in color with background colors using the canvas - provided with release 4.0

If you are still using python 2.7, please use plotille v4 or before. With v5 I am dropping support for python 2.7, as the effort to maintain the discontinued version is too much.

## Documentation

```python
In [1]: import plotille
In [2]: import numpy as np
In [3]: X = np.sort(np.random.normal(size=1000))
```

### Figure

To construct plots the recommended way is to use a `Figure`:

```python
In [4]: plotille.Figure?
Init signature: plotille.Figure() -> None
Docstring:
Figure class to compose multiple plots.

Within a Figure you can easily compose many plots, assign labels to plots
and define the properties of the underlying Canvas. Possible properties that
can be defined are:

    width, height: int           Define the number of characters in X / Y direction
                                 which are used for plotting.
    x_limits: DataValue          Define the X limits of the reference coordinate system,
                                 that will be plotted.
    y_limits: DataValue          Define the Y limits of the reference coordinate system,
                                 that will be plotted.
    color_mode: str              Define the used color mode. See `plotille.color()`.
    with_colors: bool            Define, whether to use colors at all.
    background: ColorDefinition  Define the background color.
    x_label, y_label: str        Define the X / Y axis label.```

Basically, you create a `Figure`, define the properties and add your plots. Using the `show()` function, the `Figure` generates the plot using a new canvas:

```python
In [13] fig = plotille.Figure()
In [14] fig.width = 60
In [15] fig.height = 30
In [16] fig.set_x_limits(min_=-3, max_=3)
In [17] fig.set_y_limits(min_=-1, max_=1)
In [18] fig.color_mode = 'byte'
In [19] fig.plot([-0.5, 1], [-1, 1], lc=25, label='First line')
In [20] fig.scatter(X, np.sin(X), lc=100, label='sin')
In [21] fig.plot(X, (X+2)**2 , lc=200, label='square')
In [22] print(fig.show(legend=True))
```

![Example figure](https://github.com/tammoippen/plotille/raw/master/imgs/figure.png)

The available plotting functions are:

```python
# create a plot with linear interpolation between points
Figure.plot(self, X, Y, lc=None, interp='linear', label=None, marker=None)
# create a scatter plot with no interpolation between points
Figure.scatter(self, X, Y, lc=None, label=None, marker=None)
# create a histogram over X
Figure.histogram(self, X, bins=160, lc=None)
# print texts at coordinates X, Y
Figure.text(self, X, Y, texts, lc=None)

# The following functions use relative coordinates on the canvas
# i.e. all coordinates are \in [0, 1]
# plot a vertical line at x
Figure.axvline(self, x, ymin=0, ymax=1, lc=None)
# plot a vertical rectangle from (xmin,ymin) to (xmax, ymax).
Figure.axvspan(self, xmin, xmax, ymin=0, ymax=1, lc=None)
# plot a horizontal line at y
Figure.axhline(self, y, xmin=0, xmax=1, lc=None)
# plot a horizontal rectangle from (xmin,ymin) to (xmax, ymax).
Figure.axhspan(self, ymin, ymax, xmin=0, xmax=1, lc=None)

# Display data as an image, i.e. on a 2D regular raster.
Figure.imgshow(self, X, cmap=None)
```

Other interesting functions are:

```python
# remove all plots, texts, spans and images from the figure
Figure.clear(self)
# Create a canvas, plot the registered plots and return the string for displaying the plot
Figure.show(self, legend=False)
```

Please have a look at the [`examples/`](./examples) folder.

### Graphing

There are some utility functions for fast graphing of single plots.

#### Plot

```python
In [4]: plotille.plot?
Signature:
plotille.plot(
    X: Sequence[float | int] | Sequence[datetime.datetime],
    Y: Sequence[float | int] | Sequence[datetime.datetime],
    width: int = 80,
    height: int = 40,
    X_label: str = 'X',
    Y_label: str = 'Y',
    linesep: str = '\n',
    interp: Optional[Literal['linear']] = 'linear',
    x_min: float | int | datetime.datetime | None = None,
    x_max: float | int | datetime.datetime | None = None,
    y_min: float | int | datetime.datetime | None = None,
    y_max: float | int | datetime.datetime | None = None,
    lc: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    bg: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    color_mode: Literal['names', 'byte', 'rgb'] = 'names',
    origin: bool = True,
    marker: str | None = None,
) -> str
Docstring:
Create plot with X , Y values and linear interpolation between points

Parameters:
    X: List[float]         X values.
    Y: List[float]         Y values. X and Y must have the same number of entries.
    width: int             The number of characters for the width (columns) of the
                           canvas.
    height: int            The number of characters for the hight (rows) of the
                           canvas.
    X_label: str           Label for X-axis.
    Y_label: str           Label for Y-axis. max 8 characters.    linesep: str           The requested line separator. default: os.linesep
    linesep: str           The requested line separator. default: os.linesep
    interp: Optional[str]  Specify interpolation; values None, 'linear'
    x_min, x_max: float    Limits for the displayed X values.
    y_min, y_max: float    Limits for the displayed Y values.
    lc: ColorDefinition           Give the line color.
    bg: ColorDefinition           Give the background color.
    color_mode: ColorMode        Specify color input mode; 'names' (default), 'byte' or
                           'rgb' see plotille.color.__docs__
    origin: bool           Whether to print the origin. default: True
    marker: str            Instead of braille dots set a marker char for actual
                           values.

Returns:
    str: plot over `X`, `Y`.

In [5]: print(plotille.plot(X, np.sin(X), height=30, width=60))
```

![Example plot](https://github.com/tammoippen/plotille/raw/master/imgs/plot.png)

#### Scatter

```python
In [6]: plotille.scatter?
Signature:
plotille.scatter(
    X: Sequence[float | int] | Sequence[datetime.datetime],
    Y: Sequence[float | int] | Sequence[datetime.datetime],
    width: int = 80,
    height: int = 40,
    X_label: str = 'X',
    Y_label: str = 'Y',
    linesep: str = '\n',
    x_min: float | int | datetime.datetime | None = None,
    x_max: float | int | datetime.datetime | None = None,
    y_min: float | int | datetime.datetime | None = None,
    y_max: float | int | datetime.datetime | None = None,
    lc: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    bg: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    color_mode: Literal['names', 'byte', 'rgb'] = 'names',
    origin: bool = True,
    marker: str | None = None,
) -> str
Docstring:
Create scatter plot with X , Y values

Basically plotting without interpolation:
    `plot(X, Y, ... , interp=None)`

Parameters:
    X: List[float]       X values.
    Y: List[float]       Y values. X and Y must have the same number of entries.
    width: int           The number of characters for the width (columns) of the
                         canvas.
    height: int          The number of characters for the hight (rows) of the
                         canvas.    X_label: str         Label for X-axis.
    X_label: str         Label for X-axis.
    Y_label: str         Label for Y-axis. max 8 characters.
    linesep: str         The requested line separator. default: os.linesep
    x_min, x_max: float  Limits for the displayed X values.
    y_min, y_max: float  Limits for the displayed Y values.
    lc: ColorDefinition         Give the line color.
    bg: ColorDefinition         Give the background color.
    color_mode: ColorMode      Specify color input mode; 'names' (default), 'byte' or
                         'rgb' see plotille.color.__docs__
    origin: bool         Whether to print the origin. default: True
    marker: str          Instead of braille dots set a marker char.

Returns:
    str: scatter plot over `X`, `Y`.

In [7]: print(plotille.scatter(X, np.sin(X), height=30, width=60))
```

![Example scatter](https://github.com/tammoippen/plotille/raw/master/imgs/scatter.png)

#### Hist

Inspired by [crappyhist](http://kevinastraight.x10host.com/2013/12/28/python-histograms-from-the-console/) (link is gone, but I made a [gist](https://gist.github.com/tammoippen/4474e838e969bf177155231ebba52386)).

```python
In [8]: plotille.hist?
Signature:
plotille.hist(
    X: Sequence[float | int] | Sequence[datetime.datetime],
    bins: int = 40,
    width: int = 80,
    log_scale: bool = False,
    linesep: str = '\n',
    lc: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    bg: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    color_mode: Literal['names', 'byte', 'rgb'] = 'names',
) -> str
Docstring:
Create histogram over `X` from left to right

The values on the left are the center of the bucket, i.e. `(bin[i] + bin[i+1]) / 2`.
The values on the right are the total counts of this bucket.

Parameters:
    X: List[float]       The items to count over.
    bins: int            The number of bins to put X entries in (rows).
    width: int           The number of characters for the width (columns).
    log_scale: bool      Scale the histogram with `log` function.
    linesep: str         The requested line separator. default: os.linesep
    lc: ColorDefinition         Give the line color.
    bg: ColorDefinition         Give the background color.
    color_mode: ColorMode      Specify color input mode; 'names' (default), 'byte' or
                         'rgb' see plotille.color.__docs__

Returns:
    str: histogram over `X` from left to right.

In [9]: print(plotille.hist(np.random.normal(size=10000)))
```

![Example hist](https://github.com/tammoippen/plotille/raw/master/imgs/hist.png)

#### Hist (aggregated)

This function allows you to create a histogram when your data is already aggregated (aka you don't have access to raw values, but you have access to bins and counts for each bin).

This comes handy when working with APIs such as [OpenTelemetry Metrics API](https://opentelemetry-python.readthedocs.io/en/latest/api/metrics.html)
where views such as [ExplicitBucketHistogramAggregation](https://opentelemetry-python.readthedocs.io/en/latest/sdk/metrics.view.html#opentelemetry.sdk.metrics.view.ExplicitBucketHistogramAggregation)
only expose access to aggregated values (counts for each bin / bucket).

```python
In [8]: plotille.hist_aggregated?
Signature:
plotille.hist_aggregated(
    counts: list[int],
    bins: Sequence[float],
    width: int = 80,
    log_scale: bool = False,
    linesep: str = '\n',
    lc: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    bg: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    color_mode: Literal['names', 'byte', 'rgb'] = 'names',
    meta: plotille._data_metadata.DataMetadata | None = None,
) -> str
Docstring:
Create histogram for aggregated data.

Parameters:
    counts: List[int]         Counts for each bucket.
    bins: List[float]         Limits for the bins for the provided counts: limits for
                              bin `i` are `[bins[i], bins[i+1])`.
                              Hence, `len(bins) == len(counts) + 1`.
    width: int                The number of characters for the width (columns).
    log_scale: bool           Scale the histogram with `log` function.
    linesep: str              The requested line separator. default: os.linesep
    lc: ColorDefinition       Give the line color.
    bg: ColorDefinition       Give the background color.
    color_mode: ColorMode     Specify color input mode; 'names' (default), 'byte' or
                              'rgb' see plotille.color.__docs__
    meta: DataMetadata | None For conversion of datetime values.
Returns:
    str: histogram over `X` from left to right.

In [9]: counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
In [10]: bins = [float('-inf'), 10, 50, 100, 200, 300, 500, 800, 1000, 2000, 10000, float('+inf')]
In [11]: print(plotille.hist_aggregated(counts, bins))
```

Keep in mind that there must always be n+1 bins (n is a total number of count values, 11 in the example above).

In this example the first bin is from [-inf, 10) with a count of 1945 and the last bin is from [10000, +inf] with a count of 0.

![Example hist](https://github.com/tammoippen/plotille/raw/master/imgs/hist_aggregated.png)

#### Histogram

There is also another more 'usual' histogram function available:

```python
In [10]: plotille.histogram?
Signature:
plotille.histogram(
    X: Sequence[float | int] | Sequence[datetime.datetime],
    bins: int = 160,
    width: int = 80,
    height: int = 40,
    X_label: str = 'X',
    Y_label: str = 'Counts',
    linesep: str = '\n',
    x_min: float | int | datetime.datetime | None = None,
    x_max: float | int | datetime.datetime | None = None,
    y_min: float | int | datetime.datetime | None = None,
    y_max: float | int | datetime.datetime | None = None,
    lc: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    bg: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    color_mode: Literal['names', 'byte', 'rgb'] = 'names',
) -> str
Docstring:
Create histogram over `X`

In contrast to `hist`, this is the more `usual` histogram from bottom
to up. The X-axis represents the values in `X` and the Y-axis is the
corresponding frequency.

Parameters:
    X: List[float]       The items to count over.
    bins: int            The number of bins to put X entries in (columns).
    height: int          The number of characters for the height (rows).
    X_label: str         Label for X-axis.
    Y_label: str         Label for Y-axis. max 8 characters.
    linesep: str         The requested line separator. default: os.linesep
    x_min, x_max: float  Limits for the displayed X values.
    y_min, y_max: float  Limits for the displayed Y values.
    lc: ColorDefinition         Give the line color.
    bg: ColorDefinition         Give the background color.
    color_mode: ColorMode      Specify color input mode; 'names' (default), 'byte' or
                         'rgb' see plotille.color.__docs__

Returns:
    str: histogram over `X`.

In [11]: print(plotille.histogram(np.random.normal(size=10000)))
```

![Example histogram](https://github.com/tammoippen/plotille/raw/master/imgs/histogram.png)

### Canvas

The underlying plotting area is modeled as the `Canvas` class:

```python
In [12]:  plotille.Canvas?
Init signature:
plotille.Canvas(
    width: int,
    height: int,
    xmin: Union[float, int] = 0,
    ymin: Union[float, int] = 0,
    xmax: Union[float, int] = 1,
    ymax: Union[float, int] = 1,
    background: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    **color_kwargs: Any,
) -> None
Docstring:
A canvas object for plotting braille dots

A Canvas object has a `width` x `height` characters large canvas, in which it
can plot indivitual braille point, lines out of braille points, rectangles,...
Since a full braille character has 2 x 4 dots (⣿), the canvas has `width` * 2,
`height` * 4 dots to plot into in total.

It maintains two coordinate systems: a reference system with the limits (xmin, ymin)
in the lower left corner to (xmax, ymax) in the upper right corner is transformed
into the canvas discrete, i.e. dots, coordinate system (0, 0) to (`width` * 2,
`height` * 4). It does so transparently to clients of the Canvas, i.e. all plotting
functions only accept coordinates in the reference system. If the coordinates are
outside the reference system, they are not plotted.
Init docstring:
Initiate a Canvas object

Parameters:
    width: int            The number of characters for the width (columns) of
                          the canvas.
    height: int           The number of characters for the hight (rows) of the
                          canvas.
    xmin, ymin: float     Lower left corner of reference system.
    xmax, ymax: float     Upper right corner of reference system.
    background: multiple  Background color of the canvas.
    **color_kwargs:       More arguments to the color-function.
                          See `plotille.color()`.

Returns:
    Canvas object
```

The most interesting functions are:

_point:_

```python
In [11]: plotille.Canvas.point?
Signature:
plotille.Canvas.point(
    self,
    x: Union[float, int],
    y: Union[float, int],
    set_: bool = True,
    color: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    marker: str | None = None,
) -> None
Docstring:
Put a point into the canvas at (x, y) [reference coordinate system]

Parameters:
    x: float         x-coordinate on reference system.
    y: float         y-coordinate on reference system.
    set_: bool       Whether to plot or remove the point.
    color: multiple  Color of the point.
    marker: str      Instead of braille dots set a marker char.
```

_line:_

```python
In [14]: plotille.Canvas.line?
Signature:
plotille.Canvas.line(
    self,
    x0: Union[float, int],
    y0: Union[float, int],
    x1: Union[float, int],
    y1: Union[float, int],
    set_: bool = True,
    color: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
) -> None
Docstring:
Plot line between point (x0, y0) and (x1, y1) [reference coordinate system].

Parameters:
    x0, y0: float    Point 0
    x1, y1: float    Point 1
    set_: bool       Whether to plot or remove the line.
    color: multiple  Color of the line.
```

_rect:_

```python
In [15]: plotille.Canvas.rect?
Signature:
plotille.Canvas.rect(
    self,
    xmin: Union[float, int],
    ymin: Union[float, int],
    xmax: Union[float, int],
    ymax: Union[float, int],
    set_: bool = True,
    color: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
) -> None
Docstring:
Plot rectangle with bbox (xmin, ymin) and (xmax, ymax).

In the reference coordinate system.

Parameters:
    xmin, ymin: float  Lower left corner of rectangle.
    xmax, ymax: float  Upper right corner of rectangle.
    set_: bool         Whether to plot or remove the rect.
    color: multiple    Color of the rect.
```

_text:_

```python
In [16]: plotille.Canvas.text?
Signature:
plotille.Canvas.text(
    self,
    x: Union[float, int],
    y: Union[float, int],
    text: str,
    set_: bool = True,
    color: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
) -> None
Docstring:
Put some text into the canvas at (x, y) [reference coordinate system]

Parameters:
    x: float         x-coordinate on reference system.
    y: float         y-coordinate on reference system.
    set_: bool       Whether to set the text or clear the characters.
    text: str        The text to add.
    color: multiple  Color of the point.
```

_braille_image:_

```python
In [17]: plotille.Canvas.braille_image?
Signature:
plotille.Canvas.braille_image(
    self,
    pixels: Sequence[int],
    threshold: int = 127,
    inverse: bool = False,
    color: Union[str, int, ColorNames, tuple[int, int, int], Sequence[int], NoneType] = None,
    set_: bool = True,
) -> None
Docstring:
Print an image using braille dots into the canvas.

The pixels and braille dots in the canvas are a 1-to-1 mapping, hence
a 80 x 80 pixel image will need a 40 x 20 canvas.

Example:
    from PIL import Image
    import plotille as plt

    img = Image.open("/path/to/image")
    img = img.convert('L')
    img = img.resize((80, 80))
    cvs = plt.Canvas(40, 20)
    cvs.braille_image(img.getdata(), 125)
    print(cvs.plot())

Parameters:
    pixels: list[number]  All pixels of the image in one list.
    threshold: float      All pixels above this threshold will be
                          drawn.
    inverse: bool         Whether to invert the image.
    color: multiple       Color of the point.
    set_: bool            Whether to plot or remove the dots.
```

_image:_

```python
In [18]: plotille.Canvas.image?
Signature:
plotille.Canvas.image(
    self,
    pixels: Sequence[tuple[int, int, int] | Sequence[int] | None],
    set_: bool = True,
) -> None
Docstring:
Print an image using background colors into the canvas.

The pixels of the image and the characters in the canvas are a
1-to-1 mapping, hence a 80 x 80 image will need a 80 x 80 canvas.

Example:
    from PIL import Image
    import plotille as plt

    img = Image.open("/path/to/image")
    img = img.convert('RGB')
    img = img.resize((40, 40))
    cvs = plt.Canvas(40, 40, mode='rgb')
    cvs.image(img.getdata())
    print(cvs.plot())

Parameters:
    pixels: list[(R,G,B)]  All pixels of the image in one list.
    set_: bool             Whether to plot or remove the background
                           colors.
```

_plot:_

```python
In [16]: plotille.Canvas.plot?
Signature: plotille.Canvas.plot(self, linesep: str = '\n') -> str
Docstring:
Transform canvas into `print`-able string

Parameters:
    linesep: str  The requested line separator. default: os.linesep

Returns:
    unicode: The canvas as a string.
```

You can use it for example to plot a house in the terminal:

```python
In [17]: c = Canvas(width=40, height=20)
In [18]: c.rect(0.1, 0.1, 0.6, 0.6)
In [19]: c.line(0.1, 0.1, 0.6, 0.6)
In [20]: c.line(0.1, 0.6, 0.6, 0.1)
In [21]: c.line(0.1, 0.6, 0.35, 0.8)
In [22]: c.line(0.35, 0.8, 0.6, 0.6)
In [23]: print(c.plot())
```

![House](https://github.com/tammoippen/plotille/raw/master/imgs/house.png)

Or you could render images with braille dots:

```python
In [24]: img = Image.open('https://github.com/tammoippen/plotille/raw/master/imgs/ich.jpg')
In [25]: img = img.convert('L')
In [26]: img = img.resize((80, 80))
In [27]: cvs = Canvas(40, 20)
In [28]: cvs.braille_image(img.getdata())
In [29]: print(cvs.plot())
```

![Me with dots](https://github.com/tammoippen/plotille/raw/master/imgs/ich-dots.png)

Or you could render images with the background color of characters:

```python
In [24]: img = Image.open('https://github.com/tammoippen/plotille/raw/master/imgs/ich.jpg')
In [25]: img = img.convert('RGB')
In [25]: img = img.resize((80, 40))
In [27]: cvs = Canvas(80, 40, mode="rgb")
In [28]: cvs.image(img.getdata())
In [29]: print(cvs.plot())
```

![Me with chars](https://github.com/tammoippen/plotille/raw/master/imgs/ich-chars.png)

## Stargazers over time

[![Stargazers over time](https://starchart.cc/tammoippen/plotille.svg)](https://starchart.cc/tammoippen/plotille)