diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7e24d20 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..d561c00 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,405 @@ +Metadata-Version: 2.1 +Name: plotille +Version: 3.7.2 +Summary: Plot in the terminal using braille dots. +Home-page: https://github.com/tammoippen/plotille +License: MIT +Keywords: plot,scatter,histogram,terminal,braille,unicode,timeseries +Author: Tammo Ippen +Author-email: tammo.ippen@posteo.de +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Classifier: Environment :: Console +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Scientific/Engineering :: Visualization +Classifier: Topic :: Terminals +Requires-Dist: six (>=1.12,<2.0) +Project-URL: Repository, https://github.com/tammoippen/plotille +Description-Content-Type: text/markdown + +# Plotille + +[![CircleCI](https://circleci.com/gh/tammoippen/plotille.svg?style=svg)](https://circleci.com/gh/tammoippen/plotille) +[![Coverage Status](https://coveralls.io/repos/github/tammoippen/plotille/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/plotille?branch=master) +[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tammoippen/plotille.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tammoippen/plotille/context:python) +[![Tested CPython Versions](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg)](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg) +[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg) +[![PyPi version](https://img.shields.io/pypi/v/plotille.svg)](https://pypi.python.org/pypi/plotille) +[![PyPi license](https://img.shields.io/pypi/l/plotille.svg)](https://pypi.python.org/pypi/plotille) + +Plot, scatter plots and histograms in the terminal using braille dots, with (almost) no dependancies. Plot with color or make complex figures - similar to a very small sibling to matplotlib. Or use the canvas to plot dots and lines 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`, `pendulum.datetime` or `numpy.datetime64` values. Labels are generated respecting the difference of `x_limits` and `y_limits`. + +## 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 recomended way is to use a `Figure`: + +```python +In [12]: plotille.Figure? +Init signature: plotille.Figure() +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: float Define the X limits of the reference coordinate system, + that will be plottered. + y_limits: float Define the Y limits of the reference coordinate system, + that will be plottered. + color_mode: str Define the used color mode. See `plotille.color()`. + with_colors: bool Define, whether to use colors at all. + background: multiple 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)) +``` +![](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) +# create a scatter plot with no interpolation between points +Figure.scatter(self, X, Y, lc=None, label=None) +# create a histogram over X +Figure.histogram(self, X, bins=160, lc=None) +``` + +Other interesting functions are: +```python +# remove all plots 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) +``` + +### Graphing: + +There are some utility functions for fast graphing of single plots. + +#### Plot: +```python +In [4]: plotille.plot? +Signature: +plt.plot( + X, + Y, + width=80, + height=40, + X_label='X', + Y_label='Y', + linesep='\n', + interp='linear', + x_min=None, + x_max=None, + y_min=None, + y_max=None, + lc=None, + bg=None, + color_mode='names', + origin=True, +) +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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + +Returns: + str: plot over `X`, `Y`. + +In [5]: print(plotille.plot(X, np.sin(X), height=30, width=60)) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/plot.png) + +#### Scatter: +```python +In [6]: plotille.scatter? +Signature: +plt.scatter( + X, + Y, + width=80, + height=40, + X_label='X', + Y_label='Y', + linesep='\n', + x_min=None, + x_max=None, + y_min=None, + y_max=None, + lc=None, + bg=None, + color_mode='names', + origin=True, +) +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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + +Returns: + str: scatter plot over `X`, `Y`. + +In [7]: print(plotille.scatter(X, np.sin(X), height=30, width=60)) +``` +![](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, bins=40, width=80, log_scale=False, linesep='\n', lc=None, bg=None, color_mode='names') +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 seperator. default: os.linesep + lc: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str 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))) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/hist.png) + +#### Histogram: + +There is also another more 'usual' histogram function available: +```python +In [10]: plotille.histogram? +Signature: plotille.histogram(X, bins=160, width=80, height=40, X_label='X', Y_label='Counts', linesep='\n', x_min=None, x_max=None, y_min=None, y_max=None, lc=None, bg=None, color_mode='names') +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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str 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))) +``` +![](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, height, xmin=0, ymin=0, xmax=1, ymax=1, background=None, color_mode='names') +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. + hight: 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_mode: str The color-mode for all colors of this canvas; either 'names' (default) + 'rgb' or 'byte'. See `plotille.color()`. + +Returns: + Canvas object +``` + +The most interesting functions are: + +*point:* +```python +Signature: plotille.Canvas.point(self, x, y, set_=True, color=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. +``` + +*line:* +```python +In [14]: plotille.Canvas.line? +Signature: plotille.Canvas.line(self, x0, y0, x1, y1, set_=True, color=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]: Canvas.rect? +Signature: plotille.Canvas.rect(self, xmin, ymin, xmax, ymax, set_=True, color=None) +Docstring: +Plot rectangle with bbox (xmin, ymin) and (xmax, ymax) [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. +``` + +*plot:* +```python +In [16]: Canvas.plot? +Signature: plotille.Canvas.plot(self, x_axis=False, y_axis=False, y_label='Y', x_label='X', linesep='\n') +Docstring: +Transform canvas into `print`-able string + +Parameters: + x_axis: bool Add a X-axis at the bottom. + y_axis: bool Add a Y-axis to the left. + y_label: str Label for Y-axis. max 8 characters. + x_label: str Label for X-axis. + linesep: str The requested line seperator. default: os.linesep + +Returns: + unicode: The cancas 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()) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/house.png) + + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/tammoippen/plotille.svg)](https://starchart.cc/tammoippen/plotille) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a199648 --- /dev/null +++ b/README.md @@ -0,0 +1,375 @@ +# Plotille + +[![CircleCI](https://circleci.com/gh/tammoippen/plotille.svg?style=svg)](https://circleci.com/gh/tammoippen/plotille) +[![Coverage Status](https://coveralls.io/repos/github/tammoippen/plotille/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/plotille?branch=master) +[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tammoippen/plotille.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tammoippen/plotille/context:python) +[![Tested CPython Versions](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg)](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg) +[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg) +[![PyPi version](https://img.shields.io/pypi/v/plotille.svg)](https://pypi.python.org/pypi/plotille) +[![PyPi license](https://img.shields.io/pypi/l/plotille.svg)](https://pypi.python.org/pypi/plotille) + +Plot, scatter plots and histograms in the terminal using braille dots, with (almost) no dependancies. Plot with color or make complex figures - similar to a very small sibling to matplotlib. Or use the canvas to plot dots and lines 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`, `pendulum.datetime` or `numpy.datetime64` values. Labels are generated respecting the difference of `x_limits` and `y_limits`. + +## 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 recomended way is to use a `Figure`: + +```python +In [12]: plotille.Figure? +Init signature: plotille.Figure() +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: float Define the X limits of the reference coordinate system, + that will be plottered. + y_limits: float Define the Y limits of the reference coordinate system, + that will be plottered. + color_mode: str Define the used color mode. See `plotille.color()`. + with_colors: bool Define, whether to use colors at all. + background: multiple 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)) +``` +![](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) +# create a scatter plot with no interpolation between points +Figure.scatter(self, X, Y, lc=None, label=None) +# create a histogram over X +Figure.histogram(self, X, bins=160, lc=None) +``` + +Other interesting functions are: +```python +# remove all plots 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) +``` + +### Graphing: + +There are some utility functions for fast graphing of single plots. + +#### Plot: +```python +In [4]: plotille.plot? +Signature: +plt.plot( + X, + Y, + width=80, + height=40, + X_label='X', + Y_label='Y', + linesep='\n', + interp='linear', + x_min=None, + x_max=None, + y_min=None, + y_max=None, + lc=None, + bg=None, + color_mode='names', + origin=True, +) +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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + +Returns: + str: plot over `X`, `Y`. + +In [5]: print(plotille.plot(X, np.sin(X), height=30, width=60)) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/plot.png) + +#### Scatter: +```python +In [6]: plotille.scatter? +Signature: +plt.scatter( + X, + Y, + width=80, + height=40, + X_label='X', + Y_label='Y', + linesep='\n', + x_min=None, + x_max=None, + y_min=None, + y_max=None, + lc=None, + bg=None, + color_mode='names', + origin=True, +) +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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + +Returns: + str: scatter plot over `X`, `Y`. + +In [7]: print(plotille.scatter(X, np.sin(X), height=30, width=60)) +``` +![](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, bins=40, width=80, log_scale=False, linesep='\n', lc=None, bg=None, color_mode='names') +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 seperator. default: os.linesep + lc: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str 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))) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/hist.png) + +#### Histogram: + +There is also another more 'usual' histogram function available: +```python +In [10]: plotille.histogram? +Signature: plotille.histogram(X, bins=160, width=80, height=40, X_label='X', Y_label='Counts', linesep='\n', x_min=None, x_max=None, y_min=None, y_max=None, lc=None, bg=None, color_mode='names') +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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str 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))) +``` +![](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, height, xmin=0, ymin=0, xmax=1, ymax=1, background=None, color_mode='names') +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. + hight: 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_mode: str The color-mode for all colors of this canvas; either 'names' (default) + 'rgb' or 'byte'. See `plotille.color()`. + +Returns: + Canvas object +``` + +The most interesting functions are: + +*point:* +```python +Signature: plotille.Canvas.point(self, x, y, set_=True, color=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. +``` + +*line:* +```python +In [14]: plotille.Canvas.line? +Signature: plotille.Canvas.line(self, x0, y0, x1, y1, set_=True, color=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]: Canvas.rect? +Signature: plotille.Canvas.rect(self, xmin, ymin, xmax, ymax, set_=True, color=None) +Docstring: +Plot rectangle with bbox (xmin, ymin) and (xmax, ymax) [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. +``` + +*plot:* +```python +In [16]: Canvas.plot? +Signature: plotille.Canvas.plot(self, x_axis=False, y_axis=False, y_label='Y', x_label='X', linesep='\n') +Docstring: +Transform canvas into `print`-able string + +Parameters: + x_axis: bool Add a X-axis at the bottom. + y_axis: bool Add a Y-axis to the left. + y_label: str Label for Y-axis. max 8 characters. + x_label: str Label for X-axis. + linesep: str The requested line seperator. default: os.linesep + +Returns: + unicode: The cancas 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()) +``` +![](https://github.com/tammoippen/plotille/raw/master/imgs/house.png) + + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/tammoippen/plotille.svg)](https://starchart.cc/tammoippen/plotille) diff --git a/plotille/__init__.py b/plotille/__init__.py new file mode 100644 index 0000000..05da48f --- /dev/null +++ b/plotille/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from ._canvas import Canvas +from ._colors import color +from ._figure import Figure +from ._graphs import hist, histogram, plot, scatter + + +__all__ = [ + 'Canvas', + 'color', + 'Figure', + 'hist', + 'histogram', + 'plot', + 'scatter', +] diff --git a/plotille/_canvas.py b/plotille/_canvas.py new file mode 100644 index 0000000..51385f3 --- /dev/null +++ b/plotille/_canvas.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from os import linesep + +import six + +from ._dots import Dots +from ._util import roundeven + + +class Canvas(object): + """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. + """ + def __init__(self, width, height, xmin=0, ymin=0, xmax=1, ymax=1, background=None, color_mode='names'): + """Initiate a Canvas object + + Parameters: + width: int The number of characters for the width (columns) of the canvas. + hight: 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_mode: str The color-mode for all colors of this canvas; either 'names' (default) + 'rgb' or 'byte'. See `plotille.color()`. + + Returns: + Canvas object + """ + assert isinstance(width, int), '`width` has to be of type `int`' + assert isinstance(height, int), '`height` has to be of type `int`' + assert width > 0, '`width` has to be greater than 0' + assert height > 0, '`height` has to be greater than 0' + assert isinstance(xmin, (int, float)) + assert isinstance(xmax, (int, float)) + assert isinstance(ymin, (int, float)) + assert isinstance(ymax, (int, float)) + assert xmin < xmax, 'xmin ({}) has to be smaller than xmax ({})'.format(xmin, xmax) + assert ymin < ymax, 'ymin ({}) has to be smaller than ymax ({})'.format(ymin, ymax) + + # characters in X / Y direction + self._width = width + self._height = height + # the X / Y limits of the canvas, i.e. (0, 0) in canvas is (xmin,ymin) and + # (width-1, height-1) in canvas is (xmax, ymax) + self._xmin = xmin + self._xmax = xmax + self._ymin = ymin + self._ymax = ymax + # value of x/y between one character + self._x_delta = abs((xmax - xmin) / width) + self._y_delta = abs((ymax - ymin) / height) + # value of x/y between one point + self._x_delta_pt = self._x_delta / 2 + self._y_delta_pt = self._y_delta / 4 + # the canvas to print in + self._canvas = [[Dots(bg=background, color_mode=color_mode) for j_ in range(width)] for i_ in range(height)] + + def __str__(self): + return 'Canvas(width={}, height={}, xmin={}, ymin={}, xmax={}, ymax={})'.format( + self.width, self.height, self.xmin, self.ymin, self.xmax, self.ymax, + ) + + def __repr__(self): + return self.__str__() + + @property + def width(self): + """Number of characters in X direction""" + return self._width + + @property + def height(self): + """Number of characters in Y direction""" + return self._height + + @property + def xmin(self): + """Get xmin coordinate of reference coordinate system.""" + return self._xmin + + @property + def ymin(self): + """Get ymin coordinate of reference coordinate system.""" + return self._ymin + + @property + def xmax(self): + """Get xmax coordinate of reference coordinate system.""" + return self._xmax + + @property + def ymax(self): + """Get ymax coordinate of reference coordinate system.""" + return self._ymax + + def _transform_x(self, x): + return int(roundeven((x - self.xmin) / self._x_delta_pt)) + + def _transform_y(self, y): + return int(roundeven((y - self.ymin) / self._y_delta_pt)) + + def _set(self, x_idx, y_idx, set_=True, color=None): + """Put a dot into the canvas at (x_idx, y_idx) [canvas coordinate system] + + Parameters: + x: int x-coordinate on canvas. + y: int y-coordinate on canvas. + set_: bool Whether to plot or remove the point. + color: multiple Color of the point. + """ + x_c, x_p = x_idx // 2, x_idx % 2 + y_c, y_p = y_idx // 4, y_idx % 4 + + if 0 <= x_c < self.width and 0 <= y_c < self.height: + self._canvas[y_c][x_c].update(x_p, y_p, set_) + if color: + self._canvas[y_c][x_c].fg = color + + def dots_between(self, x0, y0, x1, y1): + """Number of dots between (x0, y0) and (x1, y1). + + Parameters: + x0, y0: float Point 0 + x1, y1: float Point 1 + + Returns: + (int, int): dots in (x, y) direction + """ + x0_idx = self._transform_x(x0) + y0_idx = self._transform_y(y0) + x1_idx = self._transform_x(x1) + y1_idx = self._transform_y(y1) + + return x1_idx - x0_idx, y1_idx - y0_idx + + def point(self, x, y, set_=True, color=None): + """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. + """ + x_idx = self._transform_x(x) + y_idx = self._transform_y(y) + self._set(x_idx, y_idx, set_, color) + + def fill_char(self, x, y, set_=True): + """Fill the complete character at the point (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. + """ + x_idx = self._transform_x(x) + y_idx = self._transform_y(y) + + x_c = x_idx // 2 + y_c = y_idx // 4 + + if set_: + self._canvas[y_c][x_c].fill() + else: + self._canvas[y_c][x_c].clear() + + def line(self, x0, y0, x1, y1, set_=True, color=None): + """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. + """ + x0_idx = self._transform_x(x0) + y0_idx = self._transform_y(y0) + self._set(x0_idx, y0_idx, set_, color) + + x1_idx = self._transform_x(x1) + y1_idx = self._transform_y(y1) + self._set(x1_idx, y1_idx, set_, color) + + x_diff = x1_idx - x0_idx + y_diff = y1_idx - y0_idx + steps = max(abs(x_diff), abs(y_diff)) + for i in range(1, steps): + xb = x0_idx + int(roundeven(x_diff / steps * i)) + yb = y0_idx + int(roundeven(y_diff / steps * i)) + self._set(xb, yb, set_, color) + + def rect(self, xmin, ymin, xmax, ymax, set_=True, color=None): + """Plot rectangle with bbox (xmin, ymin) and (xmax, ymax) [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. + """ + assert xmin <= xmax + assert ymin <= ymax + self.line(xmin, ymin, xmin, ymax, set_, color) + self.line(xmin, ymax, xmax, ymax, set_, color) + self.line(xmax, ymax, xmax, ymin, set_, color) + self.line(xmax, ymin, xmin, ymin, set_, color) + + def plot(self, linesep=linesep): + """Transform canvas into `print`-able string + + Parameters: + linesep: str The requested line seperator. default: os.linesep + + Returns: + unicode: The canvas as a string. + """ + + return linesep.join(''.join(map(six.text_type, row)) + for row in reversed(self._canvas)) diff --git a/plotille/_colors.py b/plotille/_colors.py new file mode 100644 index 0000000..3500209 --- /dev/null +++ b/plotille/_colors.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import sys + +import six + + +def color(text, fg=None, bg=None, mode='names', no_color=False): + """Surround `text` with control characters for coloring + + c.f. http://en.wikipedia.org/wiki/ANSI_escape_code + + There are 3 color modes possible: + - `names`: corresponds to 3/4 bit encoding; provide colors as lower case + with underscore names, e.g. 'red', 'bright_green' + - `byte`: corresponds to 8-bit encoding; provide colors as int ∈ [0, 255]; + compare 256-color lookup table + - `rgb`: corresponds to 24-bit encoding; provide colors either in 3- or 6-character + hex encoding or provide as a list / tuple with three ints (∈ [0, 255] each) + + With `fg` you can specify the foreground, i.e. text color, and with `bg` you + specify the background color. The resulting `text` also gets the `RESET` signal + at the end, s.t. no coloring swaps over to following text! + + Make sure to set the colors corresponding to the `mode`, otherwise you get + `ValueErrors`. + + If you do not want a foreground or background color, leave the corresponding + paramter `None`. If both are `None`, you get `text` directly. + + When you stick to mode `names` and only use the none `bright_` versions, + the color control characters conform to ISO 6429 and the ANSI Escape sequences + as defined in http://ascii-table.com/ansi-escape-sequences.php. + + Color names for mode `names` are: + black red green yellow blue magenta cyan white <- ISO 6429 + bright_black bright_red bright_green bright_yellow + bright_blue bright_magenta bright_cyan bright_white + (trying other names will raise ValueError) + + If you want to use colorama (https://pypi.python.org/pypi/colorama), you should + also stick to the ISO 6429 colors. + + Parameters: + text: str Some text to surround. + fg: multiple Specify the foreground / text color. + bg: multiple Specify the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + no_color: bool Remove color optionally. default=False + + Returns: + str: `text` enclosed with corresponding coloring controls + """ + if fg is None and bg is None: + return text + + if not _isatty() or no_color: + # only color if tty (not a redirect / pipe) + return text + + start = '' + if mode == 'names': + start = _names(fg, bg) + elif mode == 'byte': + start = _byte(fg, bg) + elif mode == 'rgb': + if isinstance(fg, six.string_types): + fg = _hex2rgb(fg) + if isinstance(bg, six.string_types): + bg = _hex2rgb(bg) + + start = _rgb(fg, bg) + else: + raise ValueError('Invalid mode "{}". Use one of "names", "byte" or "rgb".'.format(mode)) + + if start: + return start + text + '\x1b[0m' + + # should not be reachable + + +def _isatty(): + return sys.stdout.isatty() + + +def _names(fg, bg): + """3/4 bit encoding part + + c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#3.2F4_bit + + Parameters: + + """ + if not (fg is None or fg in _FOREGROUNDS): + raise ValueError('Invalid color name fg = "{}"'.format(fg)) + if not (bg is None or bg in _BACKGROUNDS): + raise ValueError('Invalid color name bg = "{}"'.format(bg)) + + fg_ = _FOREGROUNDS.get(fg, '') + bg_ = _BACKGROUNDS.get(bg, '') + + return _join_codes(fg_, bg_) + + +def _byte(fg, bg): + """8-bite encoding part + + c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + """ + if not (fg is None or (isinstance(fg, int) and 0 <= fg <= 255)): + raise ValueError('Invalid fg = {}. Allowed int in [0, 255].'.format(fg)) + if not (bg is None or (isinstance(bg, int) and 0 <= bg <= 255)): + raise ValueError('Invalid bg = {}. Allowed int in [0, 255].'.format(bg)) + + fg_ = '' + if fg is not None: + fg_ = '38;5;' + six.text_type(fg) + bg_ = '' + if bg is not None: + bg_ = '48;5;' + six.text_type(bg) + + return _join_codes(fg_, bg_) + + +def _hex2rgb(h): + """Transform rgb hex representation into rgb tuple of ints representation""" + assert isinstance(h, six.string_types) + if h.lower().startswith('0x'): + h = h[2:] + if len(h) == 3: + return (int(h[0] * 2, base=16), int(h[1] * 2, base=16), int(h[2] * 2, base=16)) + if len(h) == 6: + return (int(h[0:2], base=16), int(h[2:4], base=16), int(h[4:6], base=16)) + + raise ValueError('Invalid hex RGB value.') + + +def _rgb(fg, bg): + """24-bit encoding part + + c.f. https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit + """ + if not (fg is None or (isinstance(fg, (list, tuple)) and len(fg) == 3) and all(0 <= f <= 255 for f in fg)): + raise ValueError('Foreground fg either None or 3-tuple: {}'.format(fg)) + if not (bg is None or (isinstance(bg, (list, tuple)) and len(bg) == 3) and all(0 <= b <= 255 for b in bg)): + raise ValueError('Foreground fg either None or 3-tuple: {}'.format(bg)) + + fg_ = '' + if fg is not None: + fg_ = '38;2;' + ';'.join(map(six.text_type, fg)) + bg_ = '' + if bg is not None: + bg_ = '48;2;' + ';'.join(map(six.text_type, bg)) + + return _join_codes(fg_, bg_) + + +def _join_codes(fg, bg): + """Join `fg` and `bg` with ; and surround with correct esc sequence.""" + colors = ';'.join(filter(lambda c: len(c) > 0, (fg, bg))) + if colors: + return '\x1b[' + colors + 'm' + + return '' + + +_BACKGROUNDS = { + 'black': '40', + 'red': '41', + 'green': '42', + 'yellow': '43', + 'blue': '44', + 'magenta': '45', + 'cyan': '46', + 'white': '47', + 'bright_black': '100', + 'bright_red': '101', + 'bright_green': '102', + 'bright_yellow': '103', + 'bright_blue': '104', + 'bright_magenta': '105', + 'bright_cyan': '106', + 'bright_white': '107', +} + +_FOREGROUNDS = { + 'black': '30', + 'red': '31', + 'green': '32', + 'yellow': '33', + 'blue': '34', + 'magenta': '35', + 'cyan': '36', + 'white': '37', + 'bright_black': '1;30', + 'bright_red': '1;31', + 'bright_green': '1;32', + 'bright_yellow': '1;33', + 'bright_blue': '1;34', + 'bright_magenta': '1;35', + 'bright_cyan': '1;36', + 'bright_white': '1;37', +} diff --git a/plotille/_dots.py b/plotille/_dots.py new file mode 100644 index 0000000..f66febf --- /dev/null +++ b/plotille/_dots.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import six + +from ._colors import color + + +class Dots(object): + """A Dots object is responsible for printing requested braille dots and colors + + Dot ordering: \u2800 '⠀' - \u28FF '⣿'' Coding according to ISO/TR 11548-1 + + Hence, each dot on or off is 8bit, i.e. 256 posibilities. With dot number + one being the msb and 8 is lsb: + + idx: 1 2 3 4 5 6 7 8 + bits: 0 0 0 0 0 0 0 0 + + Ordering of dots: + + 1 4 + 2 5 + 3 6 + 7 8 + """ + def __init__(self, dots=None, fg=None, bg=None, color_mode='names'): + """Create a Dots object + + Parameters: + dots: List[int] With set dots to on; ∈ 1 - 8 + fg: str Color of dots + bg: str Color of background + color_mode: str Define the used color mode. See `plotille.color()`. + + Returns: + Dots + """ + if dots is None: + dots = [] + self.dots = dots + self.fg = fg + self.bg = bg + self._mode = color_mode + + @property + def mode(self): + return self._mode + + @property + def dots(self): + return list(self._dots) + + @dots.setter + def dots(self, value): + assert isinstance(value, (list, tuple)) + assert all(map(lambda x: 1 <= x <= 8, value)) + self._dots = list(value) + + def __repr__(self): + return 'Dots(dots={}, fg={}, bg={}, color_mode={})'.format(self.dots, self.fg, self.bg, self.mode) + + def __str__(self): + res = braille_from(self.dots) + + return color(res, fg=self.fg, bg=self.bg, mode=self.mode) + + def fill(self): + self.dots = [1, 2, 3, 4, 5, 6, 7, 8] + + def clear(self): + self.dots = [] + + def update(self, x, y, set_=True): + """(Un)Set dot at position x, y, with (0, 0) is top left corner. + + Parameters: + x: int x-coordinate ∈ [0, 1] + y: int y-coordinate ∈ [0, 1, 2, 3] + set_: bool True, sets dot, False, removes dot + """ + xy2dot = [[7, 8], # I plot upside down, hence the different order + [3, 6], + [2, 5], + [1, 4]] + if set_: + self.dots = sorted(set(self.dots) | {xy2dot[y][x]}) + else: + idx = xy2dot[y][x] + if idx in self._dots: + self._dots.remove(idx) + + +def braille_from(dots): + """Unicode character for braille with given dots set + + See https://en.wikipedia.org/wiki/Braille_Patterns#Identifying.2C_naming_and_ordering + for dot to braille encoding. + + Parameters: + dots: List[int] All dots that should be set. Allowed dots are 1,2,3,4,5,6,7,8 + + Returns: + unicode: braille sign with given dots set. \u2800 - \u28ff + """ + bin_code = ['0'] * 8 + for i in dots: + bin_code[8 - i] = '1' + + code = 0x2800 + int(''.join(bin_code), 2) + + return six.unichr(code) + + +def dots_from(braille): + """Get set dots from given + + See https://en.wikipedia.org/wiki/Braille_Patterns#Identifying.2C_naming_and_ordering + for braille to dot decoding. + + Parameters: + braille: unicode Braille character in \u2800 - \u28ff + + Returns: + List[int]: dots that are set in braille sign + """ + assert 0x2800 <= ord(braille) <= 0x28ff + + code = six.text_type(bin(ord(braille) - 0x2800))[2:].rjust(8, '0') + + dots = [] + for i, c in enumerate(code): + if c == '1': + dots += [8 - i] + + return sorted(dots) diff --git a/plotille/_figure.py b/plotille/_figure.py new file mode 100644 index 0000000..717be86 --- /dev/null +++ b/plotille/_figure.py @@ -0,0 +1,451 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from collections import namedtuple +from datetime import timedelta +from itertools import cycle +import os + +from six.moves import zip + +from ._canvas import Canvas +from ._colors import color +from ._input_formatter import InputFormatter +from ._util import hist, mk_timedelta, timestamp + +# TODO documentation!!! +# TODO tests + + +class Figure(object): + """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: float Define the X limits of the reference coordinate system, + that will be plottered. + y_limits: float Define the Y limits of the reference coordinate system, + that will be plottered. + color_mode: str Define the used color mode. See `plotille.color()`. + with_colors: bool Define, whether to use colors at all. + background: multiple Define the background color. + x_label, y_label: str Define the X / Y axis label. + """ + _COLOR_SEQ = [ + {'names': 'white', 'rgb': (255, 255, 255), 'byte': 0X7}, + {'names': 'red', 'rgb': (255, 0, 0), 'byte': 0x1}, + {'names': 'green', 'rgb': (0, 255, 0), 'byte': 0x2}, + {'names': 'yellow', 'rgb': (255, 255, 0), 'byte': 0x3}, + {'names': 'blue', 'rgb': (0, 0, 255), 'byte': 0x4}, + {'names': 'magenta', 'rgb': (255, 0, 255), 'byte': 0x5}, + {'names': 'cyan', 'rgb': (0, 255, 255), 'byte': 0x6}, + ] + + def __init__(self): + self._color_seq = iter(cycle(Figure._COLOR_SEQ)) + self._width = None + self._height = None + self._x_min = None + self._x_max = None + self._y_min = None + self._y_max = None + self._color_mode = None + self._with_colors = True + self._origin = True + self.linesep = os.linesep + self.background = None + self.x_label = 'X' + self.y_label = 'Y' + self._plots = [] + self._in_fmt = InputFormatter() + + @property + def width(self): + if self._width is not None: + return self._width + return 80 + + @width.setter + def width(self, value): + if not (isinstance(value, int) and value > 0): + raise ValueError('Invalid width: {}'.format(value)) + self._width = value + + @property + def height(self): + if self._height is not None: + return self._height + return 40 + + @height.setter + def height(self, value): + if not (isinstance(value, int) and value > 0): + raise ValueError('Invalid height: {}'.format(value)) + self._height = value + + @property + def color_mode(self): + if self._color_mode is not None: + return self._color_mode + return 'names' + + @color_mode.setter + def color_mode(self, value): + if value not in ('names', 'byte', 'rgb'): + raise ValueError('Only supports: names, byte, rgb!') + if self._plots != []: + raise RuntimeError('Change color mode only, when no plots are prepared.') + self._color_mode = value + + @property + def with_colors(self): + return self._with_colors + + @with_colors.setter + def with_colors(self, value): + if not isinstance(value, bool): + raise ValueError('Only bool allowed: "{}"'.format(value)) + self._with_colors = value + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, value): + if not isinstance(value, bool): + raise ValueError('Invalid origin: {}'.format(value)) + self._origin = value + + def register_label_formatter(self, type_, formatter): + self._in_fmt.register_formatter(type_, formatter) + + def register_float_converter(self, type_, converter): + self._in_fmt.register_converter(type_, converter) + + def x_limits(self): + return self._limits(self._x_min, self._x_max, False) + + def set_x_limits(self, min_=None, max_=None): + self._x_min, self._x_max = self._set_limits(self._x_min, self._x_max, min_, max_) + + def y_limits(self): + return self._limits(self._y_min, self._y_max, True) + + def set_y_limits(self, min_=None, max_=None): + self._y_min, self._y_max = self._set_limits(self._y_min, self._y_max, min_, max_) + + def _set_limits(self, init_min, init_max, min_=None, max_=None): + if min_ is not None and max_ is not None: + if min_ >= max_: + raise ValueError('min_ is larger or equal than max_.') + init_min = min_ + init_max = max_ + elif min_ is not None: + if init_max is not None and min_ >= init_max: + raise ValueError('Previous max is smaller or equal to new min_.') + init_min = min_ + elif max_ is not None: + if init_min is not None and init_min >= max_: + raise ValueError('Previous min is larger or equal to new max_.') + init_max = max_ + else: + init_min = None + init_max = None + + return init_min, init_max + + def _limits(self, low_set, high_set, is_height): + if low_set is not None and high_set is not None: + return low_set, high_set + + low, high = None, None + for p in self._plots: + if is_height: + _min, _max = _limit(p.height_vals()) + else: + _min, _max = _limit(p.width_vals()) + if low is None: + low = _min + high = _max + + low = min(_min, low) + high = max(_max, high) + + return _choose(low, high, low_set, high_set) + + def _y_axis(self, ymin, ymax, label='Y'): + delta = abs(ymax - ymin) + if isinstance(delta, timedelta): + y_delta = mk_timedelta(timestamp(delta) / self.height) + else: + y_delta = delta / self.height + + res = [self._in_fmt.fmt(i * y_delta + ymin, abs(ymax - ymin), chars=10) + ' | ' + for i in range(self.height)] + # add max separately + res += [self._in_fmt.fmt(self.height * y_delta + ymin, abs(ymax - ymin), chars=10) + ' |'] + + ylbl = '({})'.format(label) + ylbl_left = (10 - len(ylbl)) // 2 + ylbl_right = ylbl_left + len(ylbl) % 2 + + res += [' ' * (ylbl_left) + ylbl + ' ' * (ylbl_right) + ' ^'] + return list(reversed(res)) + + def _x_axis(self, xmin, xmax, label='X', with_y_axis=False): + delta = abs(xmax - xmin) + if isinstance(delta, timedelta): + x_delta = mk_timedelta(timestamp(delta) / self.width) + else: + x_delta = delta / self.width + starts = ['', ''] + if with_y_axis: + starts = ['-' * 11 + '|-', ' ' * 11 + '| '] + res = [] + + res += [starts[0] + '|---------' * (self.width // 10) + '|-> (' + label + ')'] + res += [starts[1] + ' '.join(self._in_fmt.fmt(i * 10 * x_delta + xmin, delta, left=True, chars=9) + for i in range(self.width // 10 + 1))] + return res + + def clear(self): + self._plots = [] + + def plot(self, X, Y, lc=None, interp='linear', label=None): + if len(X) > 0: + if lc is None: + lc = next(self._color_seq)[self.color_mode] + self._plots += [Plot.create(X, Y, lc, interp, label)] + + def scatter(self, X, Y, lc=None, label=None): + if len(X) > 0: + if lc is None: + lc = next(self._color_seq)[self.color_mode] + self._plots += [Plot.create(X, Y, lc, None, label)] + + def histogram(self, X, bins=160, lc=None): + if len(X) > 0: + if lc is None: + lc = next(self._color_seq)[self.color_mode] + self._plots += [Histogram.create(X, bins, lc)] + + def show(self, legend=False): + xmin, xmax = self.x_limits() + ymin, ymax = self.y_limits() + if all(isinstance(p, Histogram) for p in self._plots): + ymin = 0 + # create canvas + canvas = Canvas(self.width, self.height, + self._in_fmt.convert(xmin), self._in_fmt.convert(ymin), + self._in_fmt.convert(xmax), self._in_fmt.convert(ymax), + self.background, self.color_mode) + + plot_origin = False + for p in self._plots: + p.write(canvas, self.with_colors, self._in_fmt) + if isinstance(p, Plot): + plot_origin = True + + if self.origin and plot_origin: + # print X / Y origin axis + canvas.line(self._in_fmt.convert(xmin), 0, self._in_fmt.convert(xmax), 0) + canvas.line(0, self._in_fmt.convert(ymin), 0, self._in_fmt.convert(ymax)) + + res = canvas.plot(linesep=self.linesep) + + # add y axis + yaxis = self._y_axis(ymin, ymax, label=self.y_label) + res = ( + yaxis[0] + self.linesep # up arrow + + yaxis[1] + self.linesep # maximum + + self.linesep.join(lbl + line for lbl, line in zip(yaxis[2:], res.split(self.linesep))) + ) + + # add x axis + xaxis = self._x_axis(xmin, xmax, label=self.x_label, with_y_axis=True) + res = ( + res + self.linesep # plot + + self.linesep.join(xaxis) + ) + + if legend: + res += '\n\nLegend:\n-------\n' + res += '\n'.join([ + color('⠤⠤ {}'.format(p.label if p.label is not None else 'Label {}'.format(i)), + fg=p.lc, mode=self.color_mode, no_color=not self.with_colors) + for i, p in enumerate(self._plots) + if isinstance(p, Plot) + ]) + return res + + +class Plot(namedtuple('Plot', ['X', 'Y', 'lc', 'interp', 'label'])): + + @classmethod + def create(cls, X, Y, lc, interp, label): + if len(X) != len(Y): + raise ValueError('X and Y dim have to be the same.') + if interp not in ('linear', None): + raise ValueError('Only "linear" and None are allowed values for `interp`.') + + return cls(X, Y, lc, interp, label) + + def width_vals(self): + return self.X + + def height_vals(self): + return self.Y + + def write(self, canvas, with_colors, in_fmt): + # make point iterators + from_points = zip(map(in_fmt.convert, self.X), map(in_fmt.convert, self.Y)) + to_points = zip(map(in_fmt.convert, self.X), map(in_fmt.convert, self.Y)) + + # remove first point of to_points + (x0, y0) = next(to_points) + + color = self.lc if with_colors else None + + # print first point + canvas.point(x0, y0, color=color) + + # plot other points and lines + for (x0, y0), (x, y) in zip(from_points, to_points): + canvas.point(x, y, color=color) + if self.interp == 'linear': + canvas.line(x0, y0, x, y, color=color) + + +class Histogram(namedtuple('Histogram', ['X', 'bins', 'frequencies', 'buckets', 'lc'])): + @classmethod + def create(cls, X, bins, lc): + frequencies, buckets = hist(X, bins) + + return cls(X, bins, frequencies, buckets, lc) + + def width_vals(self): + return self.X + + def height_vals(self): + return self.frequencies + + def write(self, canvas, with_colors, in_fmt): + # how fat will one bar of the histogram be + x_diff = (canvas.dots_between(in_fmt.convert(self.buckets[0]), 0, + in_fmt.convert(self.buckets[1]), 0)[0] or 1) + bin_size = (in_fmt.convert(self.buckets[1]) - in_fmt.convert(self.buckets[0])) / x_diff + + color = self.lc if with_colors else None + for i in range(self.bins): + # for each bucket + if self.frequencies[i] > 0: + for j in range(x_diff): + # print bar + x_ = in_fmt.convert(self.buckets[i]) + j * bin_size + + if canvas.xmin <= x_ <= canvas.xmax: + canvas.line(x_, 0, + x_, self.frequencies[i], + color=color) + + +def _limit(values): + _min = 0 + _max = 1 + if len(values) > 0: + _min = min(values) + _max = max(values) + + return (_min, _max) + + +def _diff(low, high): + if low == high: + if low == 0: + return 0.5 + else: + return abs(low * 0.1) + else: + delta = abs(high - low) + if isinstance(delta, timedelta): + return mk_timedelta(timestamp(delta) * 0.1) + else: + return delta * 0.1 + + +def _default(low_set, high_set): + if low_set is None and high_set is None: + return 0.0, 1.0 # defaults + + if low_set is None and high_set is not None: + if high_set <= 0: + return high_set - 1, high_set + else: + return 0.0, high_set + + if low_set is not None and high_set is None: + if low_set >= 1: + return low_set, low_set + 1 + else: + return low_set, 1.0 + + # Should never get here! => checked in function before + + +def _choose(low, high, low_set, high_set): + no_data = low is None and high is None + if no_data: + return _default(low_set, high_set) + + else: # some data + if low_set is None and high_set is None: + # no restrictions from user, use low & high + diff = _diff(low, high) + return low - diff, high + diff + + if low_set is None and high_set is not None: + # user sets high end + if high_set < low: + # high is smaller than lowest value + return high_set - 1, high_set + + diff = _diff(low, high_set) + return low - diff, high_set + + if low_set is not None and high_set is None: + # user sets low end + if low_set > high: + # low is larger than highest value + return low_set, low_set + 1 + + diff = _diff(low_set, high) + return low_set, high + diff + + # Should never get here! => checked in function before diff --git a/plotille/_graphs.py b/plotille/_graphs.py new file mode 100644 index 0000000..4ab1c8f --- /dev/null +++ b/plotille/_graphs.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from math import log +import os + +from ._colors import color +from ._figure import Figure +from ._input_formatter import InputFormatter +from ._util import hist as compute_hist + + +def hist(X, bins=40, width=80, log_scale=False, linesep=os.linesep, + lc=None, bg=None, color_mode='names'): + """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 seperator. default: os.linesep + lc: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + + Returns: + str: histogram over `X` from left to right. + """ + def _scale(a): + if log_scale and a > 0: + return log(a) + return a + + ipf = InputFormatter() + h, b = compute_hist(X, bins) + h_max = _scale(max(h)) or 1 + delta = b[-1] - b[0] + + canvas = [' bucket | {} {}'.format('_' * width, 'Total Counts')] + lasts = ['', '⠂', '⠆', '⠇', '⡇', '⡗', '⡷', '⡿'] + for i in range(bins): + hight = int(width * 8 * _scale(h[i]) / h_max) + canvas += ['[{}, {}) | {} {}'.format( + ipf.fmt(b[i], delta=delta, chars=8, left=True), + ipf.fmt(b[i + 1], delta=delta, chars=8, left=False), + color('⣿' * (hight // 8) + lasts[hight % 8], fg=lc, bg=bg, mode=color_mode) + + color('\u2800' * (width - (hight // 8) + int(hight % 8 == 0)), bg=bg, mode=color_mode), + h[i])] + canvas += ['‾' * (2 * 8 + 2 + 3 + width + 12)] + return linesep.join(canvas) + + +def histogram(X, bins=160, width=80, height=40, X_label='X', Y_label='Counts', linesep=os.linesep, + x_min=None, x_max=None, y_min=None, y_max=None, + lc=None, bg=None, color_mode='names'): + """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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + + Returns: + str: histogram over `X`. + """ + fig = Figure() + fig.width = width + fig.height = height + fig.x_label = X_label + fig.y_label = Y_label + fig.linesep = linesep + if x_min is not None: + fig.set_x_limits(min_=x_min) + if x_max is not None: + fig.set_x_limits(max_=x_max) + if y_min is not None: + fig.set_y_limits(min_=y_min) + if y_max is not None: + fig.set_y_limits(max_=y_max) + fig.background = bg + fig.color_mode = color_mode + + if lc is None and bg is None: + fig.with_colors = False + + fig.histogram(X, bins, lc) + + return fig.show() + + +def scatter(X, Y, width=80, height=40, X_label='X', Y_label='Y', linesep=os.linesep, + x_min=None, x_max=None, y_min=None, y_max=None, + lc=None, bg=None, color_mode='names', origin=True): + """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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + + Returns: + str: scatter plot over `X`, `Y`. + """ + return plot(X, Y, width, height, X_label, Y_label, linesep, None, + x_min, x_max, y_min, y_max, lc, bg, color_mode, origin) + + +def plot(X, Y, width=80, height=40, X_label='X', Y_label='Y', linesep=os.linesep, interp='linear', + x_min=None, x_max=None, y_min=None, y_max=None, + lc=None, bg=None, color_mode='names', origin=True): + """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. + hight: 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 seperator. 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: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + + Returns: + str: plot over `X`, `Y`. + """ + fig = Figure() + fig.width = width + fig.height = height + fig.x_label = X_label + fig.y_label = Y_label + fig.linesep = linesep + fig.origin = origin + if x_min is not None: + fig.set_x_limits(min_=x_min) + if x_max is not None: + fig.set_x_limits(max_=x_max) + if y_min is not None: + fig.set_y_limits(min_=y_min) + if y_max is not None: + fig.set_y_limits(max_=y_max) + fig.background = bg + fig.color_mode = color_mode + + if lc is None and bg is None: + fig.with_colors = False + + fig.plot(X, Y, lc, interp) + + return fig.show() diff --git a/plotille/_input_formatter.py b/plotille/_input_formatter.py new file mode 100644 index 0000000..873d100 --- /dev/null +++ b/plotille/_input_formatter.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from collections import OrderedDict +from datetime import date, datetime, time, timedelta +import math + +import six + +from ._util import roundeven, timestamp + + +class InputFormatter(object): + def __init__(self): + self.formatters = OrderedDict() + + self.formatters[float] = _num_formatter + for int_type in six.integer_types: + self.formatters[int_type] = _num_formatter + + self.formatters[date] = _date_formatter + self.formatters[datetime] = _datetime_formatter + + self.converters = OrderedDict() + self.converters[float] = _convert_numbers + for int_type in six.integer_types: + self.converters[int_type] = _convert_numbers + + self.converters[date] = _convert_date + self.converters[datetime] = _convert_datetime + + try: + import numpy as np + + self.converters[np.datetime64] = _convert_np_datetime + self.formatters[np.datetime64] = _np_datetime_formatter + except ImportError: # pragma: nocover + pass + + def register_formatter(self, t, f): + self.formatters[t] = f + + def register_converter(self, t, f): + self.converters[t] = f + + def fmt(self, val, delta, left=False, chars=9): + for t, f in reversed(self.formatters.items()): + if isinstance(val, t): + return f(val, chars=chars, delta=delta, left=left) + + return str(val) + + def convert(self, val): + for t, f in reversed(self.converters.items()): + if isinstance(val, t): + return f(val) + + return val + + +def _np_datetime_formatter(val, chars, delta, left=False): + # assert isinstance(val, np.datetime64) + # assert isinstance(delta, np.timedelta64) + + return _datetime_formatter(val.item(), chars, delta.item(), left) + + +def _date_formatter(val, chars, delta, left=False): + assert isinstance(val, date) + assert isinstance(delta, timedelta) + + val_dt = datetime.combine(val, time.min) + return _datetime_formatter(val_dt, chars, delta, left) + + +def _datetime_formatter(val, chars, delta, left=False): + assert isinstance(val, datetime) + assert isinstance(delta, timedelta) + + if chars < 8: + raise ValueError('Not possible to display value "{}" with {} characters!'.format(val, chars)) + + res = '' + + if delta.days <= 0: + # make time representation + if chars < 15: + res = '{:02d}:{:02d}:{:02d}'.format(val.hour, val.minute, val.second) + else: + res = '{:02d}:{:02d}:{:02d}.{:06d}'.format(val.hour, val.minute, val.second, val.microsecond) + elif 1 <= delta.days <= 10: + # make day / time representation + if chars < 11: + res = '{:02d}T{:02d}:{:02d}'.format(val.day, val.hour, val.minute) + else: + res = '{:02d}T{:02d}:{:02d}:{:02d}'.format(val.day, val.hour, val.minute, val.second) + else: + # make date representation + if chars < 10: + res = '{:02d}-{:02d}-{:02d}'.format(val.year % 100, val.month, val.day) + else: + res = '{:04d}-{:02d}-{:02d}'.format(val.year, val.month, val.day) + + if left: + return res.ljust(chars) + else: + return res.rjust(chars) + + +def _num_formatter(val, chars, delta, left=False): + if not (isinstance(val, six.integer_types) or isinstance(val, float)): + raise ValueError('Only accepting numeric (int/long/float) types, not "{}" of type: {}'.format(val, type(val))) + + if abs(val - roundeven(val)) < 1e-8: # about float (f32) machine precision + val = int(roundeven(val)) + + if isinstance(val, six.integer_types): + return _int_formatter(val, chars, left) + elif isinstance(val, float): + return _float_formatter(val, chars, left) + + +def _float_formatter(val, chars, left=False): + assert isinstance(val, float) + if math.isinf(val): + return str(val).ljust(chars) if left else str(val).rjust(chars) + sign = 1 if val < 0 else 0 + order = 0 if val == 0 else math.log10(abs(val)) + align = '<' if left else '' + + if order >= 0: + # larger than 1 values or smaller than -1 + digits = math.ceil(order) + fractionals = int(max(0, chars - 1 - digits - sign)) + if digits + sign > chars: + return _large_pos(val, chars, left, digits, sign) + + return '{:{}{}.{}f}'.format(val, align, chars, fractionals) + else: + # between -1 and 1 values + order = abs(math.floor(order)) + + if order > 4: # e-04 4 digits + exp_digits = int(max(2, math.ceil(math.log10(order)))) + exp_digits += 2 # the - sign and the e + + return '{:{}{}.{}e}'.format(val, align, chars, chars - exp_digits - 2 - sign) + else: + return '{:{}{}.{}f}'.format(val, align, chars, chars - 2 - sign) + + +def _int_formatter(val, chars, left=False): + assert isinstance(val, six.integer_types) + if val != 0: + sign = 1 if val < 0 else 0 + digits = math.ceil(math.log10(abs(val))) + if digits + sign > chars: + return _large_pos(val, chars, left, digits, sign) + align = '<' if left else '' + return '{:{}{}d}'.format(val, align, chars) + + +def _large_pos(val, chars, left, digits, sign): + align = '<' if left else '' + # exponent is always + and has at least two digits (1.3e+06) + exp_digits = max(2, math.ceil(math.log10(digits))) + exp_digits += 2 # the + sign and the e + front_digits = chars - exp_digits - sign + residual_digits = int(max(0, front_digits - 2)) + if front_digits < 1: + raise ValueError('Not possible to display value "{}" with {} characters!'.format(val, chars)) + return '{:{}{}.{}e}'.format(val, align, chars, residual_digits) + + +def _convert_numbers(v): + assert isinstance(v, float) or isinstance(v, six.integer_types) + return float(v) + + +def _convert_np_datetime(v): + # assert isinstance(v, np.datetime64) + return timestamp(v.item()) + + +def _convert_date(v): + assert isinstance(v, date) + return (v - date.min).days + + +def _convert_datetime(v): + assert isinstance(v, datetime) + return timestamp(v) diff --git a/plotille/_util.py b/plotille/_util.py new file mode 100644 index 0000000..5751c18 --- /dev/null +++ b/plotille/_util.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# The MIT License + +# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from datetime import datetime, timedelta, tzinfo +import math +import time + + +def roundeven(x): + """Round to next even integer number in case of `X.5` + + In Python3 this is the same as `round(x, 0)`, but since Python2 rounds up + in that case and I want consistent behaviour, here is the roundeven function. + + Parameters: + x: float The number to round. + + Returns: + int: floor(x) if x - floor(x) < 0.5 + ceil(x) if x - floor(x) > 0.5 + next even of x if x - floor(x) == 0.5 + """ + if math.isinf(x) or math.isnan(x): + return x # same behaviour as in python2 + x_r = round(x) + if abs(x_r - x) == 0.5: + return int(2.0 * round(x / 2)) + return x_r + + +def _numpy_to_native(x): + # cf. https://numpy.org/doc/stable/reference/generated/numpy.ndarray.item.html + if (' 0 + + xmin = min(X) if len(X) > 0 else 0.0 + xmax = max(X) if len(X) > 0 else 1.0 + if xmin == xmax: + xmin -= 0.5 + xmax += 0.5 + + xmin = _numpy_to_native(xmin) + xmax = _numpy_to_native(xmax) + + delta = xmax - xmin + is_datetime = False + if isinstance(delta, timedelta): + is_datetime = True + delta = timestamp(delta) + + xwidth = delta / bins + + y = [0] * bins + for x in X: + x_ = _numpy_to_native(x) + delta = (x_ - xmin) + if isinstance(delta, timedelta): + delta = timestamp(delta) + x_idx = min(bins - 1, int(delta // xwidth)) + y[x_idx] += 1 + + if is_datetime: + xwidth = mk_timedelta(xwidth) + + return y, [i * xwidth + xmin for i in range(bins + 1)] + + +class _UTC(tzinfo): + """UTC""" + _ZERO = timedelta(0) + + def utcoffset(self, dt): + return self._ZERO + + def tzname(self, dt): + return 'UTC' + + def dst(self, dt): + return self._ZERO + + +_EPOCH = datetime(1970, 1, 1, tzinfo=_UTC()) + + +def timestamp(v): + """Get timestamp of `v` datetime in py2/3.""" + if isinstance(v, datetime): + if v.tzinfo is None: + return time.mktime(v.timetuple()) + v.microsecond / 1e6 + else: + return (v - _EPOCH).total_seconds() + elif isinstance(v, timedelta): + return v.total_seconds() + + +def mk_timedelta(v): + seconds = int(v) + microseconds = int((v - seconds) * 1e6) + return timedelta(seconds=seconds, microseconds=microseconds) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6caf8cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] + +name = "plotille" +version = "3.7.2" +description = "Plot in the terminal using braille dots." +authors = ["Tammo Ippen "] +license = "MIT" + +readme = "README.md" + +repository = "https://github.com/tammoippen/plotille" +homepage = "https://github.com/tammoippen/plotille" + +keywords = ['plot', 'scatter', 'histogram', 'terminal', 'braille', 'unicode', 'timeseries'] + +classifiers = [ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Environment :: Console', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Science/Research', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Scientific/Engineering :: Visualization', + 'Topic :: Terminals' +] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" + +six = "^1.12" + + +[tool.poetry.dev-dependencies] + +coveralls = "^1.5.1" +flake8 = "^3.5.0" +flake8-bugbear = { version = "^18.8.0", python = "^3.5" } +flake8-commas = "^2.0.0" +flake8-comprehensions = "^1.4.1" +flake8-import-order = "^0.18" +flake8-pep3101 = "^1.2.1" +flake8-polyfill = "^1.0.2" +flake8-quotes = "^1.0.0" +funcsigs = { version = "^1.0", python = "~2.7" } +mock = "^2.0" +pendulum = "^2.0" +pep8-naming = "^0.7" +pytest = "^3.7.3" +pytest-cov = "^2.5.1" +pytest-flake8 = "^1.0.2" +pytest-mccabe = "^0.1" +pytest-mock = "^1.10" +pytest-pythonpath = "^0.7.3" +numpy = "^1.15" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..619ba52 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = \ +['plotille'] + +package_data = \ +{'': ['*']} + +install_requires = \ +['six>=1.12,<2.0'] + +setup_kwargs = { + 'name': 'plotille', + 'version': '3.7.2', + 'description': 'Plot in the terminal using braille dots.', + 'long_description': "# Plotille\n\n[![CircleCI](https://circleci.com/gh/tammoippen/plotille.svg?style=svg)](https://circleci.com/gh/tammoippen/plotille)\n[![Coverage Status](https://coveralls.io/repos/github/tammoippen/plotille/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/plotille?branch=master)\n[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tammoippen/plotille.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tammoippen/plotille/context:python)\n[![Tested CPython Versions](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg)](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-brightgreen.svg)\n[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--7.3%2C%203.5--7.0.0%2C%203.6--7.3-brightgreen.svg)\n[![PyPi version](https://img.shields.io/pypi/v/plotille.svg)](https://pypi.python.org/pypi/plotille)\n[![PyPi license](https://img.shields.io/pypi/l/plotille.svg)](https://pypi.python.org/pypi/plotille)\n\nPlot, scatter plots and histograms in the terminal using braille dots, with (almost) no dependancies. Plot with color or make complex figures - similar to a very small sibling to matplotlib. Or use the canvas to plot dots and lines yourself.\n\nInstall:\n\n```sh\npip install plotille\n```\n\nSimilar to other libraries:\n\n* like [drawille](https://github.com/asciimoo/drawille), but focused on graphing – plus X/Y-axis.\n* like [termplot](https://github.com/justnoise/termplot), but with braille (finer dots), left to right histogram and linear interpolation for plotting function.\n* like [termgraph](https://github.com/sgeisler/termgraph) (not on pypi), but very different style.\n* like [terminalplot](https://github.com/kressi/terminalplot), but with braille, X/Y-axis, histogram, linear interpolation.\n\nBasic support for timeseries plotting is provided with release 3.2: for any `X` or `Y` values you can also add `datetime.datetime`, `pendulum.datetime` or `numpy.datetime64` values. Labels are generated respecting the difference of `x_limits` and `y_limits`.\n\n## Documentation\n\n```python\nIn [1]: import plotille\nIn [2]: import numpy as np\nIn [3]: X = np.sort(np.random.normal(size=1000))\n```\n\n### Figure\n\nTo construct plots the recomended way is to use a `Figure`:\n\n```python\nIn [12]: plotille.Figure?\nInit signature: plotille.Figure()\nDocstring:\nFigure class to compose multiple plots.\n\nWithin a Figure you can easily compose many plots, assign labels to plots\nand define the properties of the underlying Canvas. Possible properties that\ncan be defined are:\n\n width, height: int Define the number of characters in X / Y direction\n which are used for plotting.\n x_limits: float Define the X limits of the reference coordinate system,\n that will be plottered.\n y_limits: float Define the Y limits of the reference coordinate system,\n that will be plottered.\n color_mode: str Define the used color mode. See `plotille.color()`.\n with_colors: bool Define, whether to use colors at all.\n background: multiple Define the background color.\n x_label, y_label: str Define the X / Y axis label.\n```\n\nBasically, you create a `Figure`, define the properties and add your plots. Using the `show()` function, the `Figure` generates the plot using a new canvas:\n\n```python\nIn [13] fig = plotille.Figure()\nIn [14] fig.width = 60\nIn [15] fig.height = 30\nIn [16] fig.set_x_limits(min_=-3, max_=3)\nIn [17] fig.set_y_limits(min_=-1, max_=1)\nIn [18] fig.color_mode = 'byte'\nIn [19] fig.plot([-0.5, 1], [-1, 1], lc=25, label='First line')\nIn [20] fig.scatter(X, np.sin(X), lc=100, label='sin')\nIn [21] fig.plot(X, (X+2)**2 , lc=200, label='square')\nIn [22] print(fig.show(legend=True))\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/figure.png)\n\nThe available plotting functions are:\n```python\n# create a plot with linear interpolation between points\nFigure.plot(self, X, Y, lc=None, interp='linear', label=None)\n# create a scatter plot with no interpolation between points\nFigure.scatter(self, X, Y, lc=None, label=None)\n# create a histogram over X\nFigure.histogram(self, X, bins=160, lc=None)\n```\n\nOther interesting functions are:\n```python\n# remove all plots from the figure\nFigure.clear(self)\n# Create a canvas, plot the registered plots and return the string for displaying the plot\nFigure.show(self, legend=False)\n```\n\n### Graphing:\n\nThere are some utility functions for fast graphing of single plots.\n\n#### Plot:\n```python\nIn [4]: plotille.plot?\nSignature:\nplt.plot(\n X,\n Y,\n width=80,\n height=40,\n X_label='X',\n Y_label='Y',\n linesep='\\n',\n interp='linear',\n x_min=None,\n x_max=None,\n y_min=None,\n y_max=None,\n lc=None,\n bg=None,\n color_mode='names',\n origin=True,\n)\nDocstring:\nCreate plot with X , Y values and linear interpolation between points\n\nParameters:\n X: List[float] X values.\n Y: List[float] Y values. X and Y must have the same number of entries.\n width: int The number of characters for the width (columns) of the canvas.\n hight: int The number of characters for the hight (rows) of the canvas.\n X_label: str Label for X-axis.\n Y_label: str Label for Y-axis. max 8 characters.\n linesep: str The requested line seperator. default: os.linesep\n interp: Optional[str] Specify interpolation; values None, 'linear'\n x_min, x_max: float Limits for the displayed X values.\n y_min, y_max: float Limits for the displayed Y values.\n lc: multiple Give the line color.\n bg: multiple Give the background color.\n color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'\n see plotille.color.__docs__\n origin: bool Whether to print the origin. default: True\n\nReturns:\n str: plot over `X`, `Y`.\n\nIn [5]: print(plotille.plot(X, np.sin(X), height=30, width=60))\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/plot.png)\n\n#### Scatter:\n```python\nIn [6]: plotille.scatter?\nSignature:\nplt.scatter(\n X,\n Y,\n width=80,\n height=40,\n X_label='X',\n Y_label='Y',\n linesep='\\n',\n x_min=None,\n x_max=None,\n y_min=None,\n y_max=None,\n lc=None,\n bg=None,\n color_mode='names',\n origin=True,\n)\nDocstring:\nCreate scatter plot with X , Y values\n\nBasically plotting without interpolation:\n `plot(X, Y, ... , interp=None)`\n\nParameters:\n X: List[float] X values.\n Y: List[float] Y values. X and Y must have the same number of entries.\n width: int The number of characters for the width (columns) of the canvas.\n hight: int The number of characters for the hight (rows) of the canvas.\n X_label: str Label for X-axis.\n Y_label: str Label for Y-axis. max 8 characters.\n linesep: str The requested line seperator. default: os.linesep\n x_min, x_max: float Limits for the displayed X values.\n y_min, y_max: float Limits for the displayed Y values.\n lc: multiple Give the line color.\n bg: multiple Give the background color.\n color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'\n see plotille.color.__docs__\n origin: bool Whether to print the origin. default: True\n\nReturns:\n str: scatter plot over `X`, `Y`.\n\nIn [7]: print(plotille.scatter(X, np.sin(X), height=30, width=60))\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/scatter.png)\n\n#### Hist:\n\nInspired 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)).\n```python\nIn [8]: plotille.hist?\nSignature: plotille.hist(X, bins=40, width=80, log_scale=False, linesep='\\n', lc=None, bg=None, color_mode='names')\nDocstring:\nCreate histogram over `X` from left to right\n\nThe values on the left are the center of the bucket, i.e. `(bin[i] + bin[i+1]) / 2`.\nThe values on the right are the total counts of this bucket.\n\nParameters:\n X: List[float] The items to count over.\n bins: int The number of bins to put X entries in (rows).\n width: int The number of characters for the width (columns).\n log_scale: bool Scale the histogram with `log` function.\n linesep: str The requested line seperator. default: os.linesep\n lc: multiple Give the line color.\n bg: multiple Give the background color.\n color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'\n see plotille.color.__docs__\n\nReturns:\n str: histogram over `X` from left to right.\n\nIn [9]: print(plotille.hist(np.random.normal(size=10000)))\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/hist.png)\n\n#### Histogram:\n\nThere is also another more 'usual' histogram function available:\n```python\nIn [10]: plotille.histogram?\nSignature: plotille.histogram(X, bins=160, width=80, height=40, X_label='X', Y_label='Counts', linesep='\\n', x_min=None, x_max=None, y_min=None, y_max=None, lc=None, bg=None, color_mode='names')\nDocstring:\nCreate histogram over `X`\n\nIn contrast to `hist`, this is the more `usual` histogram from bottom\nto up. The X-axis represents the values in `X` and the Y-axis is the\ncorresponding frequency.\n\nParameters:\n X: List[float] The items to count over.\n bins: int The number of bins to put X entries in (columns).\n height: int The number of characters for the height (rows).\n X_label: str Label for X-axis.\n Y_label: str Label for Y-axis. max 8 characters.\n linesep: str The requested line seperator. default: os.linesep\n x_min, x_max: float Limits for the displayed X values.\n y_min, y_max: float Limits for the displayed Y values.\n lc: multiple Give the line color.\n bg: multiple Give the background color.\n color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'\n see plotille.color.__docs__\n\nReturns:\n str: histogram over `X`.\n\nIn [11]: print(plotille.histogram(np.random.normal(size=10000)))\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/histogram.png)\n\n### Canvas:\n\nThe underlying plotting area is modeled as the `Canvas` class:\n```python\nIn [12]: plotille.Canvas?\nInit signature: plotille.Canvas(width, height, xmin=0, ymin=0, xmax=1, ymax=1, background=None, color_mode='names')\nDocstring:\nA canvas object for plotting braille dots\n\nA Canvas object has a `width` x `height` characters large canvas, in which it\ncan plot indivitual braille point, lines out of braille points, rectangles,...\nSince a full braille character has 2 x 4 dots (⣿), the canvas has `width` * 2, `height` * 4\ndots to plot into in total.\n\nIt maintains two coordinate systems: a reference system with the limits (xmin, ymin)\nin the lower left corner to (xmax, ymax) in the upper right corner is transformed\ninto the canvas discrete, i.e. dots, coordinate system (0, 0) to (`width` * 2, `height` * 4).\nIt does so transparently to clients of the Canvas, i.e. all plotting functions\nonly accept coordinates in the reference system. If the coordinates are outside\nthe reference system, they are not plotted.\nInit docstring:\nInitiate a Canvas object\n\nParameters:\n width: int The number of characters for the width (columns) of the canvas.\n hight: int The number of characters for the hight (rows) of the canvas.\n xmin, ymin: float Lower left corner of reference system.\n xmax, ymax: float Upper right corner of reference system.\n background: multiple Background color of the canvas.\n color_mode: str The color-mode for all colors of this canvas; either 'names' (default)\n 'rgb' or 'byte'. See `plotille.color()`.\n\nReturns:\n Canvas object\n```\n\nThe most interesting functions are:\n\n*point:*\n```python\nSignature: plotille.Canvas.point(self, x, y, set_=True, color=None)\nDocstring:\nPut a point into the canvas at (x, y) [reference coordinate system]\n\nParameters:\n x: float x-coordinate on reference system.\n y: float y-coordinate on reference system.\n set_: bool Whether to plot or remove the point.\n color: multiple Color of the point.\n```\n\n*line:*\n```python\nIn [14]: plotille.Canvas.line?\nSignature: plotille.Canvas.line(self, x0, y0, x1, y1, set_=True, color=None)\nDocstring:\nPlot line between point (x0, y0) and (x1, y1) [reference coordinate system].\n\nParameters:\n x0, y0: float Point 0\n x1, y1: float Point 1\n set_: bool Whether to plot or remove the line.\n color: multiple Color of the line.\n```\n\n*rect:*\n```python\nIn [15]: Canvas.rect?\nSignature: plotille.Canvas.rect(self, xmin, ymin, xmax, ymax, set_=True, color=None)\nDocstring:\nPlot rectangle with bbox (xmin, ymin) and (xmax, ymax) [reference coordinate system].\n\nParameters:\n xmin, ymin: float Lower left corner of rectangle.\n xmax, ymax: float Upper right corner of rectangle.\n set_: bool Whether to plot or remove the rect.\n color: multiple Color of the rect.\n```\n\n*plot:*\n```python\nIn [16]: Canvas.plot?\nSignature: plotille.Canvas.plot(self, x_axis=False, y_axis=False, y_label='Y', x_label='X', linesep='\\n')\nDocstring:\nTransform canvas into `print`-able string\n\nParameters:\n x_axis: bool Add a X-axis at the bottom.\n y_axis: bool Add a Y-axis to the left.\n y_label: str Label for Y-axis. max 8 characters.\n x_label: str Label for X-axis.\n linesep: str The requested line seperator. default: os.linesep\n\nReturns:\n unicode: The cancas as a string.\n```\n\nYou can use it for example to plot a house in the terminal:\n```python\nIn [17]: c = Canvas(width=40, height=20)\nIn [18]: c.rect(0.1, 0.1, 0.6, 0.6)\nIn [19]: c.line(0.1, 0.1, 0.6, 0.6)\nIn [20]: c.line(0.1, 0.6, 0.6, 0.1)\nIn [21]: c.line(0.1, 0.6, 0.35, 0.8)\nIn [22]: c.line(0.35, 0.8, 0.6, 0.6)\nIn [23]: print(c.plot())\n```\n![](https://github.com/tammoippen/plotille/raw/master/imgs/house.png)\n\n\n## Stargazers over time\n\n[![Stargazers over time](https://starchart.cc/tammoippen/plotille.svg)](https://starchart.cc/tammoippen/plotille)\n", + 'author': 'Tammo Ippen', + 'author_email': 'tammo.ippen@posteo.de', + 'maintainer': None, + 'maintainer_email': None, + 'url': 'https://github.com/tammoippen/plotille', + 'packages': packages, + 'package_data': package_data, + 'install_requires': install_requires, + 'python_requires': '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', +} + + +setup(**setup_kwargs)