├── .gitignore ├── LICENSE ├── README.md ├── figures ├── dense-lines.png └── example.png ├── pydlc ├── __init__.py └── _plot.py ├── setup.cfg ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Charles L. Bérubé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![DOI](https://zenodo.org/badge/374140211.svg)](https://zenodo.org/badge/latestdoi/374140211) 2 | 3 | 4 | # PyDLC - Density Line Charts with Python 5 | Python implementation of the Density Line Chart [(Moritz & Fisher, 2018)](https://arxiv.org/abs/1808.06019) to visualize time series collections. 6 | 7 |

8 | 9 |

10 | 11 | ## Installation 12 | 13 | ### Python Package Index 14 | 15 | ```console 16 | $ pip install pydlc 17 | ``` 18 | 19 | ### Requirements 20 | - [numpy](numpy.org/) 21 | - [matplotlib](matplotlib.org/) 22 | 23 | 24 | ## Usage 25 | 26 | ### Example 27 | The following example shows how to import and use the `dense_lines` plotting function. 28 | ```python 29 | import numpy as np 30 | import matplotlib.pyplot as plt 31 | from pydlc import dense_lines 32 | 33 | # Generate random synthetic time series 34 | x = np.linspace(0, 90, 25) 35 | ys = [] 36 | for _ in range(10000): 37 | ys.append(np.random.randn(1)*np.exp(-x/100)) 38 | 39 | # Plot here 40 | fig, axs = plt.subplots(1, 2, figsize=(8, 3), sharey=True, sharex=True) 41 | axs[0].plot(x, np.array(ys).T, lw=1) # this is slow and cluttered 42 | axs[0].set_title('Line Chart') 43 | im = dense_lines(ys, x=x, ax=axs[1], cmap='magma') # this is fast and clean 44 | axs[1].set_title('Density Lines Chart') 45 | fig.colorbar(im) 46 | fig.tight_layout() 47 | plt.show() 48 | ``` 49 | 50 | ### Arguments 51 | - ys (`list` of `1darray`): The lines to plot. Can also be 52 | passed as a `2darray`. 53 | - x (`1darray`, optional): The x values corresponding to 54 | the data passed with `ys`. If not provided, `range(0, len(ys))` 55 | is used. 56 | - ax (`matplotlib axes`, optional): The axes to plot on. If not 57 | provided a new figure will be created. 58 | - ny (`int`, optional): The vertical grid size. Higher values 59 | yield a smoother density estimation. Default: 100. 60 | - y_pad (`float`, optional): The padding fraction to set the 61 | grid limits past the data values. Must be greater than 0. 62 | Default: 0.01. 63 | - normalize (`bool`, optional): Normalize the plot so the density 64 | is between 0 and 1. Default: True. 65 | - **kwargs: Arbitrary keyword arguments to pass to `plt.imshow()`. 66 | 67 | ## Limitations 68 | - All series to be included in the density estimation and passed in the `ys` argument must have the same length. 69 | - The vertical grid size can be adjusted with the `ny` parameter. Higher values of `ny` yield a smoother density visualization. However, the horizontal grid size is currently limited to the same size as the input sequences and there is no parameter to adjust it (yet). 70 | 71 | ## Algorithm 72 | This graphical abstract explains the algorithm ([source](https://idl.cs.washington.edu/papers/dense-lines/)). 73 |

74 | 75 |

76 | -------------------------------------------------------------------------------- /figures/dense-lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clberube/pydlc/6f7f669e331ed3167d57dcbf1d4e00379eef406e/figures/dense-lines.png -------------------------------------------------------------------------------- /figures/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clberube/pydlc/6f7f669e331ed3167d57dcbf1d4e00379eef406e/figures/example.png -------------------------------------------------------------------------------- /pydlc/__init__.py: -------------------------------------------------------------------------------- 1 | # @Author: charles.berube@polymtl.ca 2 | # @Date: 2021-06-05 10:06:42 3 | # @Last modified by: charles.berube@polymtl.ca 4 | # @Last modified time: 2021-06-05 10:06:07 5 | 6 | 7 | from ._plot import dense_lines 8 | -------------------------------------------------------------------------------- /pydlc/_plot.py: -------------------------------------------------------------------------------- 1 | # @Author: charles 2 | # @Date: 2021-06-05 09:06:98 3 | # @Email: charles.berube@polymtl.ca 4 | # @Last modified by: charles 5 | # @Last modified time: 2021-06-05 11:06:20 6 | 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | 11 | 12 | def dense_lines(ys, x=None, ax=None, ny=100, y_lim=None, y_pad=0.01, 13 | normalize=True, **kwargs): 14 | """Returns a Density Line Chart. 15 | 16 | Args: 17 | ys (:obj:`list` of :obj:`1darray`): The lines to plot. Can also be 18 | passed as a `2darray`. 19 | x (:obj:`1darray`, optional): The x values corresponding to 20 | the data passed with `ys`. If not provided, range(0, len(ys)) 21 | is used. 22 | ax (:obj:`matplotlib axes`, optional): The axes to plot on. If not 23 | provided a new figure will be created. 24 | ny (:obj:`int`, optional): The vertical grid size. Higher values 25 | yield a smoother density estimation. Lower values may yield a 26 | pixelated result. Default: 100. 27 | y_pad (:obj:`float`, optional): The padding fraction to establish the 28 | grid limits past the data values. Must be greater than 0. 29 | Default: 0.01 (1%). 30 | normalize (:obj:`bool`, optional): Normalize the plot so the density 31 | is between 0 and 1. Default: True. 32 | **kwargs: Arbitrary keyword arguments to pass to plt.imshow(). 33 | 34 | Returns: 35 | A plt.imshow() object. 36 | 37 | """ 38 | if ax is None: 39 | ax = plt.gca() 40 | 41 | if isinstance(ys, list): 42 | ys = np.array(ys) 43 | 44 | assert isinstance(ys, np.ndarray), ( 45 | "`ys` must be a list of 1D arrays or a 2D array") 46 | 47 | assert y_pad > 0, ( 48 | "`y_pad` must be greater than 0") 49 | 50 | if x is None: 51 | x = np.arange(ys.shape[1]) 52 | 53 | kwargs.setdefault('aspect', 'auto') 54 | kwargs.setdefault('origin', 'lower') 55 | 56 | nx = x.shape[0] 57 | x_range = np.arange(nx) 58 | 59 | if y_lim is None: 60 | y_pad *= (ys.max() - ys.min()) 61 | y_grid = np.linspace(ys.min()-y_pad, ys.max()+y_pad, ny) 62 | else: 63 | y_grid = np.linspace(y_lim[0], y_lim[1], ny) 64 | 65 | x_grid = np.linspace(x.min(), x.max(), nx) 66 | 67 | grid = np.zeros((ny, nx)) 68 | indices = np.searchsorted(y_grid, ys) - 1 69 | 70 | for idx in indices: 71 | grid[idx, x_range] += 1 72 | 73 | if normalize: 74 | grid /= grid.max() 75 | 76 | extent = (x_grid.min(), x_grid.max(), y_grid.min(), y_grid.max()) 77 | img = ax.imshow(grid, extent=extent, **kwargs) 78 | return img 79 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # @Author: charles 2 | # @Date: 2021-06-06 09:06:59 3 | # @Email: charles.berube@polymtl.ca 4 | # @Last modified by: charles 5 | # @Last modified time: 2021-06-06 09:06:98 6 | 7 | 8 | from distutils.core import setup 9 | setup( 10 | name = 'pydlc', # How you named your package folder (MyLib) 11 | packages = ['pydlc'], # Chose the same as "name" 12 | version = '0.2', # Start with a small number and increase it with every change you make 13 | license='MIT', # Chose a license from here: https://help.github.com/articles/licensing-a-repository 14 | description = 'Python implementation of the Density Line Chart (Moritz & Fisher, 2018) to visualize large collections of time series.', # Give a short description about your library 15 | author = 'Charles L. Bérubé', # Type in your name 16 | author_email = 'charles.berube@polymtl.ca', # Type in your E-Mail 17 | url = 'https://github.com/clberube/pydlc', # Provide either the link to your github or to your website 18 | download_url = 'https://github.com/clberube/pydlc/archive/refs/tags/v0.2.tar.gz', # I explain this later on 19 | keywords = ['data visualization', 'density', 'time series'], # Keywords that define your package best 20 | install_requires=[ # I get to this in a second 21 | 'numpy', 22 | 'matplotlib', 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package 26 | 'Intended Audience :: Science/Research', # Define that your audience are developers 27 | 'Topic :: Software Development :: Build Tools', 28 | 'License :: OSI Approved :: MIT License', # Again, pick a license 29 | 'Programming Language :: Python :: 3', #Specify which pyhton versions that you want to support 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # @Author: charles 2 | # @Date: 2021-06-05 11:06:90 3 | # @Email: charles.berube@polymtl.ca 4 | # @Last modified by: charles 5 | # @Last modified time: 2021-06-05 11:06:27 6 | 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from pydlc import dense_lines 11 | 12 | 13 | if __name__ == "__main__": 14 | # Generate random synthetic time series 15 | x = np.linspace(0, 90, 25) 16 | ys = [] 17 | for _ in range(10000): 18 | ys.append(np.random.randn(1)*np.exp(-x/100)) 19 | 20 | # Plot here 21 | fig, axs = plt.subplots(1, 2, figsize=(8, 3), sharey=True, sharex=True) 22 | axs[0].plot(x, np.array(ys).T, lw=1) # this is slow and cluttered 23 | axs[0].set_title('Line Chart') 24 | im = dense_lines(ys, x=x, ax=axs[1], cmap='magma') # this is fast and clean 25 | axs[1].set_title('Density Lines Chart') 26 | fig.colorbar(im) 27 | fig.tight_layout() 28 | plt.savefig('./figures/example.png', dpi=144, bbox_inches='tight') 29 | plt.show() 30 | --------------------------------------------------------------------------------