├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.md ├── calmap └── __init__.py ├── doc ├── .gitignore ├── Makefile ├── conf.py ├── index.rst ├── requirements.txt └── sphinxext │ └── plot_directive.py ├── setup.py └── tests ├── conftest.py ├── requirements.txt └── test_calmap.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | .cache 5 | *.egg-info 6 | .sonarlint -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: pip 4 | python: 5 | - '3.6' 6 | - '3.7' 7 | - '3.8' 8 | install: 9 | - pip install -r tests/requirements.txt -r doc/requirements.txt 10 | - pip install -e . 11 | script: 12 | - pytest --cov=calmap 13 | - pushd doc && make html && popd 14 | after_success: 15 | - coveralls 16 | deploy: 17 | provider: pypi 18 | distributions: sdist bdist_wheel --universal 19 | docs_dir: doc/.build/html 20 | user: marvint 21 | password: ${PYPI_PASSWORD} 22 | skip_cleanup: true 23 | on: 24 | tags: true 25 | python: '3.7' 26 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Calmap was written by Martijn Vermaat and is now maintained by Marvin Thielk. 2 | 3 | - Martijn Vermaat 4 | - Marvin Thielk 5 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 by Martijn Vermaat and contributors (see AUTHORS.rst 2 | for details). 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst CHANGES.rst LICENSE.rst README.rst 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calendar heatmaps from Pandas time series data 2 | ------------------------------------------------ 3 | [![PyPI version](https://badge.fury.io/py/calmap.svg)](https://badge.fury.io/py/calmap) 4 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/calmap.svg) 5 | [![Build Status](https://travis-ci.com/MarvinT/calmap.svg?branch=master)](https://travis-ci.com/MarvinT/calmap) 6 | [![Coverage Status](https://coveralls.io/repos/github/MarvinT/calmap/badge.svg?branch=master)](https://coveralls.io/github/MarvinT/calmap?branch=master) 7 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 9 | [![Downloads](https://pepy.tech/badge/calmap)](https://pepy.tech/project/calmap) 10 | 11 | Plot [Pandas](http://pandas.pydata.org/) time series data sampled by day in 12 | a heatmap per calendar year, similar to GitHub's contributions plot, using 13 | [matplotlib](http://matplotlib.org/). 14 | 15 | ![alt text](https://pythonhosted.org/calmap/_images/index-2.png "Example calendar heatmap") 16 | 17 | ## Usage 18 | -------- 19 | 20 | See the [documentation](https://pythonhosted.org/calmap). 21 | 22 | 23 | ## Installation 24 | --------------- 25 | 26 | To install the latest release via PyPI using pip:: 27 | 28 | pip install calmap 29 | -------------------------------------------------------------------------------- /calmap/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calendar heatmaps from Pandas time series data. 3 | 4 | Plot Pandas time series data sampled by day in a heatmap per calendar year, 5 | similar to GitHub's contributions calendar. 6 | """ 7 | 8 | 9 | from __future__ import unicode_literals 10 | 11 | import calendar 12 | import datetime 13 | 14 | from matplotlib.colors import ColorConverter, ListedColormap 15 | import matplotlib.pyplot as plt 16 | import numpy as np 17 | import pandas as pd 18 | from distutils.version import StrictVersion 19 | from dateutil.relativedelta import relativedelta 20 | from matplotlib.patches import Polygon 21 | 22 | __version_info__ = ("0", "0", "11") 23 | __date__ = "22 Nov 2018" 24 | 25 | 26 | __version__ = ".".join(__version_info__) 27 | __author__ = "Marvin Thielk; Martijn Vermaat" 28 | __contact__ = "marvin.thielk@gmail.com, martijn@vermaat.name" 29 | __homepage__ = "https://github.com/MarvinT/calmap" 30 | 31 | _pandas_18 = StrictVersion(pd.__version__) >= StrictVersion("0.18") 32 | 33 | 34 | def yearplot( 35 | data, 36 | year=None, 37 | how="sum", 38 | vmin=None, 39 | vmax=None, 40 | cmap="Reds", 41 | fillcolor="whitesmoke", 42 | linewidth=1, 43 | linecolor=None, 44 | daylabels=calendar.day_abbr[:], 45 | dayticks=True, 46 | monthlabels=calendar.month_abbr[1:], 47 | monthticks=True, 48 | monthly_border=False, 49 | ax=None, 50 | **kwargs 51 | ): 52 | """ 53 | Plot one year from a timeseries as a calendar heatmap. 54 | 55 | Parameters 56 | ---------- 57 | data : Series 58 | Data for the plot. Must be indexed by a DatetimeIndex. 59 | year : integer 60 | Only data indexed by this year will be plotted. If `None`, the first 61 | year for which there is data will be plotted. 62 | how : string 63 | Method for resampling data by day. If `None`, assume data is already 64 | sampled by day and don't resample. Otherwise, this is passed to Pandas 65 | `Series.resample` (pandas < 0.18) or `pandas.agg` (pandas >= 0.18). 66 | vmin : float 67 | Min Values to anchor the colormap. If `None`, min and max are used after 68 | resampling data by day. 69 | vmax : float 70 | Max Values to anchor the colormap. If `None`, min and max are used after 71 | resampling data by day. 72 | cmap : matplotlib colormap name or object 73 | The mapping from data values to color space. 74 | fillcolor : matplotlib color 75 | Color to use for days without data. 76 | linewidth : float 77 | Width of the lines that will divide each day. 78 | linecolor : color 79 | Color of the lines that will divide each day. If `None`, the axes 80 | background color is used, or 'white' if it is transparent. 81 | daylabels : list 82 | Strings to use as labels for days, must be of length 7. 83 | dayticks : list or int or bool 84 | If `True`, label all days. If `False`, don't label days. If a list, 85 | only label days with these indices. If an integer, label every n day. 86 | monthlabels : list 87 | Strings to use as labels for months, must be of length 12. 88 | monthticks : list or int or bool 89 | If `True`, label all months. If `False`, don't label months. If a 90 | list, only label months with these indices. If an integer, label every 91 | n month. 92 | monthly_border : bool 93 | Draw black border for each month. Default: False. 94 | ax : matplotlib Axes 95 | Axes in which to draw the plot, otherwise use the currently-active 96 | Axes. 97 | kwargs : other keyword arguments 98 | All other keyword arguments are passed to matplotlib `ax.pcolormesh`. 99 | 100 | Returns 101 | ------- 102 | ax : matplotlib Axes 103 | Axes object with the calendar heatmap. 104 | 105 | Examples 106 | -------- 107 | 108 | By default, `yearplot` plots the first year and sums the values per day: 109 | 110 | .. plot:: 111 | :context: close-figs 112 | 113 | calmap.yearplot(events) 114 | 115 | We can choose which year is plotted with the `year` keyword argment: 116 | 117 | .. plot:: 118 | :context: close-figs 119 | 120 | calmap.yearplot(events, year=2015) 121 | 122 | The appearance can be changed by using another colormap. Here we also use 123 | a darker fill color for days without data and remove the lines: 124 | 125 | .. plot:: 126 | :context: close-figs 127 | 128 | calmap.yearplot(events, cmap='YlGn', fillcolor='grey', 129 | linewidth=0) 130 | 131 | The axis tick labels can look a bit crowded. We can ask to draw only every 132 | nth label, or explicitely supply the label indices. The labels themselves 133 | can also be customized: 134 | 135 | .. plot:: 136 | :context: close-figs 137 | 138 | calmap.yearplot(events, monthticks=3, daylabels='MTWTFSS', 139 | dayticks=[0, 2, 4, 6]) 140 | 141 | """ 142 | if year is None: 143 | year = data.index.sort_values()[0].year 144 | 145 | if how is None: 146 | # Assume already sampled by day. 147 | by_day = data 148 | else: 149 | # Sample by day. 150 | if _pandas_18: 151 | by_day = data.groupby(level=0).agg(how).squeeze() 152 | else: 153 | by_day = data.resample("D", how=how) 154 | 155 | # Min and max per day. 156 | if vmin is None: 157 | vmin = by_day.min() 158 | if vmax is None: 159 | vmax = by_day.max() 160 | 161 | if ax is None: 162 | ax = plt.gca() 163 | 164 | if linecolor is None: 165 | # Unfortunately, linecolor cannot be transparent, as it is drawn on 166 | # top of the heatmap cells. Therefore it is only possible to mimic 167 | # transparent lines by setting them to the axes background color. This 168 | # of course won't work when the axes itself has a transparent 169 | # background so in that case we default to white which will usually be 170 | # the figure or canvas background color. 171 | linecolor = ax.get_facecolor() 172 | if ColorConverter().to_rgba(linecolor)[-1] == 0: 173 | linecolor = "white" 174 | 175 | # Filter on year. 176 | by_day = by_day[str(year)] 177 | 178 | # Add missing days. 179 | by_day = by_day.reindex( 180 | pd.date_range(start=str(year), end=str(year + 1), freq="D")[:-1] 181 | ) 182 | 183 | # Create data frame we can pivot later. 184 | by_day = pd.DataFrame( 185 | { 186 | "data": by_day, 187 | "fill": 1, 188 | "day": by_day.index.dayofweek, 189 | "week": by_day.index.isocalendar().week, 190 | } 191 | ) 192 | 193 | # There may be some days assigned to previous year's last week or 194 | # next year's first week. We create new week numbers for them so 195 | # the ordering stays intact and week/day pairs unique. 196 | by_day.loc[(by_day.index.month == 1) & (by_day.week > 50), "week"] = 0 197 | by_day.loc[(by_day.index.month == 12) & (by_day.week < 10), "week"] = ( 198 | by_day.week.max() + 1 199 | ) 200 | 201 | # Pivot data on day and week and mask NaN days. (we can also mask the days with 0 counts) 202 | plot_data = by_day.pivot(index="day", columns="week", values="data").values[::-1] 203 | plot_data = np.ma.masked_where(np.isnan(plot_data), plot_data) 204 | 205 | # Do the same for all days of the year, not just those we have data for. 206 | fill_data = by_day.pivot(index="day", columns="week", values="fill").values[::-1] 207 | fill_data = np.ma.masked_where(np.isnan(fill_data), fill_data) 208 | 209 | # Draw background of heatmap for all days of the year with fillcolor. 210 | ax.pcolormesh(fill_data, vmin=0, vmax=1, cmap=ListedColormap([fillcolor])) 211 | 212 | # Draw heatmap. 213 | kwargs["linewidth"] = linewidth 214 | kwargs["edgecolors"] = linecolor 215 | ax.pcolormesh(plot_data, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs) 216 | 217 | # Limit heatmap to our data. 218 | ax.set(xlim=(0, plot_data.shape[1]), ylim=(0, plot_data.shape[0])) 219 | 220 | # Square cells. 221 | ax.set_aspect("equal") 222 | 223 | # Remove spines and ticks. 224 | for side in ("top", "right", "left", "bottom"): 225 | ax.spines[side].set_visible(False) 226 | ax.xaxis.set_tick_params(which="both", length=0) 227 | ax.yaxis.set_tick_params(which="both", length=0) 228 | 229 | # Get indices for monthlabels. 230 | if monthticks is True: 231 | monthticks = range(len(monthlabels)) 232 | elif monthticks is False: 233 | monthticks = [] 234 | elif isinstance(monthticks, int): 235 | monthticks = range(len(monthlabels))[monthticks // 2 :: monthticks] 236 | 237 | # Get indices for daylabels. 238 | if dayticks is True: 239 | dayticks = range(len(daylabels)) 240 | elif dayticks is False: 241 | dayticks = [] 242 | elif isinstance(dayticks, int): 243 | dayticks = range(len(daylabels))[dayticks // 2 :: dayticks] 244 | 245 | ax.set_xlabel("") 246 | timestamps = [] 247 | 248 | # Month borders 249 | xticks, labels = [], [] 250 | for month in range(1, 13): 251 | first = datetime.datetime(year, month, 1) 252 | last = first + relativedelta(months=1, days=-1) 253 | # Monday on top 254 | y0 = 6 - first.weekday() 255 | y1 = 6 - last.weekday() 256 | start = datetime.datetime(year, 1, 1).weekday() 257 | x0 = (int(first.strftime("%j")) + start - 1) // 7 258 | x1 = (int(last.strftime("%j")) + start - 1) // 7 259 | P = [ 260 | (x0, y0 + 1), 261 | (x0, 0), 262 | (x1, 0), 263 | (x1, y1), 264 | (x1 + 1, y1), 265 | (x1 + 1, 7), 266 | (x0 + 1, 7), 267 | (x0 + 1, y0 + 1), 268 | ] 269 | 270 | xticks.append(x0 + (x1 - x0 + 1) / 2) 271 | labels.append(first.strftime("%b")) 272 | if monthly_border: 273 | poly = Polygon( 274 | P, 275 | edgecolor="black", 276 | facecolor="None", 277 | linewidth=1, 278 | zorder=20, 279 | clip_on=False, 280 | ) 281 | ax.add_artist(poly) 282 | 283 | ax.set_xticks(xticks) 284 | ax.set_xticklabels(labels) 285 | ax.set_ylabel("") 286 | ax.yaxis.set_ticks_position("right") 287 | ax.set_yticks([6 - i + 0.5 for i in dayticks]) 288 | ax.set_yticklabels( 289 | [daylabels[i] for i in dayticks], rotation="horizontal", va="center" 290 | ) 291 | 292 | return ax 293 | 294 | 295 | def calendarplot( 296 | data, 297 | how="sum", 298 | yearlabels=True, 299 | yearascending=True, 300 | ncols=1, 301 | yearlabel_kws=None, 302 | subplot_kws=None, 303 | gridspec_kws=None, 304 | fig_kws=None, 305 | fig_suptitle=None, 306 | vmin=None, 307 | vmax=None, 308 | **kwargs 309 | ): 310 | """ 311 | Plot a timeseries as a calendar heatmap. 312 | 313 | Parameters 314 | ---------- 315 | data : Series 316 | Data for the plot. Must be indexed by a DatetimeIndex. 317 | how : string 318 | Method for resampling data by day. If `None`, assume data is already 319 | sampled by day and don't resample. Otherwise, this is passed to Pandas 320 | `Series.resample`. 321 | yearlabels : bool 322 | Whether or not to draw the year for each subplot. 323 | yearascending : bool 324 | Sort the calendar in ascending or descending order. 325 | ncols: int 326 | Number of columns passed to `subplots` call. 327 | yearlabel_kws : dict 328 | Keyword arguments passed to the matplotlib `set_ylabel` call which is 329 | used to draw the year for each subplot. 330 | subplot_kws : dict 331 | Keyword arguments passed to the matplotlib `add_subplot` call used to 332 | create each subplot. 333 | gridspec_kws : dict 334 | Keyword arguments passed to the matplotlib `GridSpec` constructor used 335 | to create the grid the subplots are placed on. 336 | fig_kws : dict 337 | Keyword arguments passed to the matplotlib `figure` call. 338 | fig_suptitle : string 339 | Title for the entire figure.. 340 | vmin : float 341 | Min Values to anchor the colormap. If `None`, min and max are used after 342 | resampling data by day. 343 | vmax : float 344 | Max Values to anchor the colormap. If `None`, min and max are used after 345 | resampling data by day. 346 | kwargs : other keyword arguments 347 | All other keyword arguments are passed to `yearplot`. 348 | 349 | Returns 350 | ------- 351 | fig, axes : matplotlib Figure and Axes 352 | Tuple where `fig` is the matplotlib Figure object `axes` is an array 353 | of matplotlib Axes objects with the calendar heatmaps, one per year. 354 | 355 | Examples 356 | -------- 357 | 358 | With `calendarplot` we can plot several years in one figure: 359 | 360 | .. plot:: 361 | :context: close-figs 362 | 363 | calmap.calendarplot(events) 364 | 365 | """ 366 | yearlabel_kws = yearlabel_kws or {} 367 | subplot_kws = subplot_kws or {} 368 | gridspec_kws = gridspec_kws or {} 369 | fig_kws = fig_kws or {} 370 | 371 | years = np.unique(data.index.year) 372 | if not yearascending: 373 | years = years[::-1] 374 | 375 | if ncols == 1: 376 | nrows = len(years) 377 | else: 378 | import math 379 | nrows = math.ceil(len(years) / ncols) 380 | 381 | fig, axes = plt.subplots( 382 | nrows=nrows, 383 | ncols=ncols, 384 | squeeze=False, 385 | subplot_kw=subplot_kws, 386 | gridspec_kw=gridspec_kws, 387 | **fig_kws 388 | ) 389 | axes = axes.flatten() 390 | plt.suptitle(fig_suptitle) 391 | # We explicitely resample by day only once. This is an optimization. 392 | if how is None: 393 | by_day = data 394 | else: 395 | if _pandas_18: 396 | by_day = data.groupby(level=0).agg(how).squeeze() 397 | else: 398 | by_day = data.resample("D", how=how) 399 | 400 | ylabel_kws = dict( 401 | fontsize=32, 402 | color=kwargs.get("fillcolor", "whitesmoke"), 403 | fontweight="bold", 404 | fontname="Arial", 405 | ha="center", 406 | ) 407 | ylabel_kws.update(yearlabel_kws) 408 | 409 | max_weeks = 0 410 | 411 | for year, ax in zip(years, axes): 412 | yearplot(by_day, year=year, how=None, ax=ax, **kwargs) 413 | max_weeks = max(max_weeks, ax.get_xlim()[1]) 414 | 415 | if yearlabels: 416 | ax.set_ylabel(str(year), **ylabel_kws) 417 | 418 | # If we have multiple columns, make sure any extra axes are removed 419 | if ncols != 1: 420 | for ax in axes[len(years):]: 421 | ax.set_axis_off() 422 | 423 | # In a leap year it might happen that we have 54 weeks (e.g., 2012). 424 | # Here we make sure the width is consistent over all years. 425 | for ax in axes: 426 | ax.set_xlim(0, max_weeks) 427 | 428 | # Make the axes look good. 429 | plt.tight_layout() 430 | 431 | return fig, axes 432 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = .build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Calmap.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Calmap.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Calmap" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Calmap" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Calmap documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Nov 26 16:51:51 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import matplotlib as mpl 18 | 19 | mpl.use("Agg") 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.abspath("..")) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | import calmap 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | # The plot_directive extension is a modification of the one bundled with 36 | # matplotlib, adding a fig.tight_layout() call and setting bbox_inches 37 | # to 'tight' on the savefig call. 38 | sys.path.insert(0, os.path.abspath("sphinxext")) 39 | extensions = ["plot_directive", "sphinx.ext.autodoc", "sphinx.ext.viewcode", "numpydoc"] 40 | 41 | # Configuration for plot_directive. 42 | plot_include_source = True 43 | plot_formats = [("png", 90)] 44 | plot_html_show_formats = False 45 | plot_html_show_source_link = False 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = [".templates"] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = ".rst" 54 | 55 | # The encoding of source files. 56 | # source_encoding = 'utf-8-sig' 57 | 58 | # The master toctree document. 59 | master_doc = "index" 60 | 61 | # General information about the project. 62 | project = u"Calmap" 63 | copyright = u"2015, %s" % calmap.__author__ 64 | author = calmap.__author__ 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | version = ".".join(calmap.__version__.split(".")[:2]) 72 | # The full version, including alpha/beta/rc tags. 73 | release = calmap.__version__ 74 | 75 | # The language for content autogenerated by Sphinx. Refer to documentation 76 | # for a list of supported languages. 77 | # 78 | # This is also used if you do content translation via gettext catalogs. 79 | # Usually you set "language" from the command line for these cases. 80 | language = None 81 | 82 | # There are two options for replacing |today|: either, you set today to some 83 | # non-false value, then it is used: 84 | # today = '' 85 | # Else, today_fmt is used as the format for a strftime call. 86 | # today_fmt = '%B %d, %Y' 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | exclude_patterns = [".build"] 91 | 92 | # The reST default role (used for this markup: `text`) to use for all 93 | # documents. 94 | # default_role = None 95 | 96 | # If true, '()' will be appended to :func: etc. cross-reference text. 97 | # add_function_parentheses = True 98 | 99 | # If true, the current module name will be prepended to all description 100 | # unit titles (such as .. function::). 101 | # add_module_names = True 102 | 103 | # If true, sectionauthor and moduleauthor directives will be shown in the 104 | # output. They are ignored by default. 105 | # show_authors = False 106 | 107 | # The name of the Pygments (syntax highlighting) style to use. 108 | pygments_style = "sphinx" 109 | 110 | # A list of ignored prefixes for module index sorting. 111 | # modindex_common_prefix = [] 112 | 113 | # If true, keep warnings as "system message" paragraphs in the built documents. 114 | # keep_warnings = False 115 | 116 | # If true, `todo` and `todoList` produce output, else they produce nothing. 117 | todo_include_todos = False 118 | 119 | 120 | # -- Options for HTML output ---------------------------------------------- 121 | 122 | # The theme to use for HTML and HTML Help pages. See the documentation for 123 | # a list of builtin themes. 124 | html_theme = "alabaster" 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | html_theme_options = { 130 | "github_banner": True, 131 | "github_user": "MarvinT", 132 | "github_repo": "calmap", 133 | } 134 | 135 | # Add any paths that contain custom themes here, relative to this directory. 136 | # html_theme_path = [] 137 | 138 | # The name for this set of Sphinx documents. If None, it defaults to 139 | # " v documentation". 140 | # html_title = None 141 | 142 | # A shorter title for the navigation bar. Default is the same as html_title. 143 | # html_short_title = None 144 | 145 | # The name of an image file (relative to this directory) to place at the top 146 | # of the sidebar. 147 | # html_logo = None 148 | 149 | # The name of an image file (within the static path) to use as favicon of the 150 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | # html_favicon = None 153 | 154 | # Add any paths that contain custom static files (such as style sheets) here, 155 | # relative to this directory. They are copied after the builtin static files, 156 | # so a file named "default.css" will overwrite the builtin "default.css". 157 | html_static_path = [".static"] 158 | 159 | # Add any extra paths that contain custom files (such as robots.txt or 160 | # .htaccess) here, relative to this directory. These files are copied 161 | # directly to the root of the documentation. 162 | # html_extra_path = [] 163 | 164 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 165 | # using the given strftime format. 166 | # html_last_updated_fmt = '%b %d, %Y' 167 | 168 | # If true, SmartyPants will be used to convert quotes and dashes to 169 | # typographically correct entities. 170 | # html_use_smartypants = True 171 | 172 | # Custom sidebar templates, maps document names to template names. 173 | # html_sidebars = {} 174 | 175 | # Additional templates that should be rendered to pages, maps page names to 176 | # template names. 177 | # html_additional_pages = {} 178 | 179 | # If false, no module index is generated. 180 | # html_domain_indices = True 181 | 182 | # If false, no index is generated. 183 | # html_use_index = True 184 | 185 | # If true, the index is split into individual pages for each letter. 186 | # html_split_index = False 187 | 188 | # If true, links to the reST sources are added to the pages. 189 | # html_show_sourcelink = True 190 | 191 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 192 | # html_show_sphinx = True 193 | 194 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 195 | # html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages will 198 | # contain a tag referring to it. The value of this option must be the 199 | # base URL from which the finished HTML is served. 200 | # html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | # html_file_suffix = None 204 | 205 | # Language to be used for generating the HTML full-text search index. 206 | # Sphinx supports the following languages: 207 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 208 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 209 | # html_search_language = 'en' 210 | 211 | # A dictionary with options for the search language support, empty by default. 212 | # Now only 'ja' uses this config value 213 | # html_search_options = {'type': 'default'} 214 | 215 | # The name of a javascript file (relative to the configuration directory) that 216 | # implements a search results scorer. If empty, the default will be used. 217 | # html_search_scorer = 'scorer.js' 218 | 219 | # Output file base name for HTML help builder. 220 | htmlhelp_basename = "Calmapdoc" 221 | 222 | # -- Options for LaTeX output --------------------------------------------- 223 | 224 | latex_elements = { 225 | # The paper size ('letterpaper' or 'a4paper'). 226 | #'papersize': 'letterpaper', 227 | # The font size ('10pt', '11pt' or '12pt'). 228 | #'pointsize': '10pt', 229 | # Additional stuff for the LaTeX preamble. 230 | #'preamble': '', 231 | # Latex figure (float) alignment 232 | #'figure_align': 'htbp', 233 | } 234 | 235 | # Grouping the document tree into LaTeX files. List of tuples 236 | # (source start file, target name, title, 237 | # author, documentclass [howto, manual, or own class]). 238 | latex_documents = [ 239 | (master_doc, "Calmap.tex", u"Calmap Documentation", u"Martijn Vermaat", "manual") 240 | ] 241 | 242 | # The name of an image file (relative to this directory) to place at the top of 243 | # the title page. 244 | # latex_logo = None 245 | 246 | # For "manual" documents, if this is true, then toplevel headings are parts, 247 | # not chapters. 248 | # latex_use_parts = False 249 | 250 | # If true, show page references after internal links. 251 | # latex_show_pagerefs = False 252 | 253 | # If true, show URL addresses after external links. 254 | # latex_show_urls = False 255 | 256 | # Documents to append as an appendix to all manuals. 257 | # latex_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | # latex_domain_indices = True 261 | 262 | 263 | # -- Options for manual page output --------------------------------------- 264 | 265 | # One entry per manual page. List of tuples 266 | # (source start file, name, description, authors, manual section). 267 | man_pages = [(master_doc, "calmap", u"Calmap Documentation", [author], 1)] 268 | 269 | # If true, show URL addresses after external links. 270 | # man_show_urls = False 271 | 272 | 273 | # -- Options for Texinfo output ------------------------------------------- 274 | 275 | # Grouping the document tree into Texinfo files. List of tuples 276 | # (source start file, target name, title, author, 277 | # dir menu entry, description, category) 278 | texinfo_documents = [ 279 | ( 280 | master_doc, 281 | "Calmap", 282 | u"Calmap Documentation", 283 | author, 284 | "Calmap", 285 | "One line description of project.", 286 | "Miscellaneous", 287 | ) 288 | ] 289 | 290 | # Documents to append as an appendix to all manuals. 291 | # texinfo_appendices = [] 292 | 293 | # If false, no module index is generated. 294 | # texinfo_domain_indices = True 295 | 296 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 297 | # texinfo_show_urls = 'footnote' 298 | 299 | # If true, do not generate a @detailmenu in the "Top" node's menu. 300 | # texinfo_no_detailmenu = False 301 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: calmap 2 | 3 | 4 | Calendar heatmaps from Pandas time series data 5 | ============================================== 6 | 7 | Plot `Pandas `_ time series data sampled by day in 8 | a heatmap per calendar year, similar to GitHub's contributions plot, using 9 | `matplotlib `_. 10 | 11 | 12 | Usage 13 | ----- 14 | 15 | Assume we have some weighted events as a Pandas Series with a 16 | DatetimeIndex. They could be Git commits (with the diff size as weight), 17 | mileage of your runs, or minutes spent on telemarketing phone calls driving 18 | you crazy. 19 | 20 | For illustration purposes we just create 500 events as random float values 21 | assigned to random days over a 700-day period: 22 | 23 | .. plot:: 24 | :context: close-figs 25 | 26 | import numpy as np; np.random.seed(sum(map(ord, 'calmap'))) 27 | import pandas as pd 28 | import calmap 29 | 30 | all_days = pd.date_range('1/15/2014', periods=700, freq='D') 31 | days = np.random.choice(all_days, 500) 32 | events = pd.Series(np.random.randn(len(days)), index=days) 33 | 34 | Using :func:`yearplot`, we can easily plot a heatmap of these events over a 35 | year: 36 | 37 | .. plot:: 38 | :context: close-figs 39 | 40 | calmap.yearplot(events, year=2015) 41 | 42 | Or we can use :func:`calendarplot` to plot all years as subplots into one 43 | figure: 44 | 45 | .. plot:: 46 | :context: close-figs 47 | 48 | calmap.calendarplot(events, monthticks=3, daylabels='MTWTFSS', 49 | dayticks=[0, 2, 4, 6], cmap='YlGn', 50 | fillcolor='grey', linewidth=0, 51 | fig_kws=dict(figsize=(8, 4))) 52 | 53 | See the :ref:`API documentation ` for more information and examples. 54 | 55 | 56 | Installation 57 | ------------ 58 | 59 | To install the latest release via PyPI using pip:: 60 | 61 | pip install calmap 62 | 63 | The latest development version `can be found on GitHub 64 | `_. 65 | 66 | 67 | .. _api: 68 | 69 | API documentation 70 | ----------------- 71 | 72 | .. module:: calmap 73 | 74 | .. autofunction:: yearplot 75 | .. autofunction:: calendarplot 76 | 77 | 78 | Copyright 79 | --------- 80 | 81 | This library is licensed under the MIT License, meaning you can do whatever 82 | you want with it as long as all copies include these license terms. The full 83 | license text can be found in the LICENSE.rst file. See the AUTHORS.rst for for 84 | a complete list of copyright holders. 85 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.16.5 2 | matplotlib>=2.0.0 3 | numpydoc 4 | Sphinx<=1.8.2 5 | -------------------------------------------------------------------------------- /doc/sphinxext/plot_directive.py: -------------------------------------------------------------------------------- 1 | """ 2 | A directive for including a matplotlib plot in a Sphinx document. 3 | 4 | By default, in HTML output, `plot` will include a .png file with a 5 | link to a high-res .png and .pdf. In LaTeX output, it will include a 6 | .pdf. 7 | 8 | The source code for the plot may be included in one of three ways: 9 | 10 | 1. **A path to a source file** as the argument to the directive:: 11 | 12 | .. plot:: path/to/plot.py 13 | 14 | When a path to a source file is given, the content of the 15 | directive may optionally contain a caption for the plot:: 16 | 17 | .. plot:: path/to/plot.py 18 | 19 | This is the caption for the plot 20 | 21 | Additionally, one may specify the name of a function to call (with 22 | no arguments) immediately after importing the module:: 23 | 24 | .. plot:: path/to/plot.py plot_function1 25 | 26 | 2. Included as **inline content** to the directive:: 27 | 28 | .. plot:: 29 | 30 | import matplotlib.pyplot as plt 31 | import matplotlib.image as mpimg 32 | import numpy as np 33 | img = mpimg.imread('_static/stinkbug.png') 34 | imgplot = plt.imshow(img) 35 | 36 | 3. Using **doctest** syntax:: 37 | 38 | .. plot:: 39 | A plotting example: 40 | >>> import matplotlib.pyplot as plt 41 | >>> plt.plot([1,2,3], [4,5,6]) 42 | 43 | Options 44 | ------- 45 | 46 | The ``plot`` directive supports the following options: 47 | 48 | format : {'python', 'doctest'} 49 | Specify the format of the input 50 | 51 | include-source : bool 52 | Whether to display the source code. The default can be changed 53 | using the `plot_include_source` variable in conf.py 54 | 55 | encoding : str 56 | If this source file is in a non-UTF8 or non-ASCII encoding, 57 | the encoding must be specified using the `:encoding:` option. 58 | The encoding will not be inferred using the ``-*- coding -*-`` 59 | metacomment. 60 | 61 | context : bool or str 62 | If provided, the code will be run in the context of all 63 | previous plot directives for which the `:context:` option was 64 | specified. This only applies to inline code plot directives, 65 | not those run from files. If the ``:context: reset`` option is 66 | specified, the context is reset for this and future plots, and 67 | previous figures are closed prior to running the code. 68 | ``:context:close-figs`` keeps the context but closes previous figures 69 | before running the code. 70 | 71 | nofigs : bool 72 | If specified, the code block will be run, but no figures will 73 | be inserted. This is usually useful with the ``:context:`` 74 | option. 75 | 76 | Additionally, this directive supports all of the options of the 77 | `image` directive, except for `target` (since plot will add its own 78 | target). These include `alt`, `height`, `width`, `scale`, `align` and 79 | `class`. 80 | 81 | Configuration options 82 | --------------------- 83 | 84 | The plot directive has the following configuration options: 85 | 86 | plot_include_source 87 | Default value for the include-source option 88 | 89 | plot_html_show_source_link 90 | Whether to show a link to the source in HTML. 91 | 92 | plot_pre_code 93 | Code that should be executed before each plot. 94 | 95 | plot_basedir 96 | Base directory, to which ``plot::`` file names are relative 97 | to. (If None or empty, file names are relative to the 98 | directory where the file containing the directive is.) 99 | 100 | plot_formats 101 | File formats to generate. List of tuples or strings:: 102 | 103 | [(suffix, dpi), suffix, ...] 104 | 105 | that determine the file format and the DPI. For entries whose 106 | DPI was omitted, sensible defaults are chosen. When passing from 107 | the command line through sphinx_build the list should be passed as 108 | suffix:dpi,suffix:dpi, .... 109 | 110 | plot_html_show_formats 111 | Whether to show links to the files in HTML. 112 | 113 | plot_rcparams 114 | A dictionary containing any non-standard rcParams that should 115 | be applied before each plot. 116 | 117 | plot_apply_rcparams 118 | By default, rcParams are applied when `context` option is not used in 119 | a plot directive. This configuration option overrides this behavior 120 | and applies rcParams before each plot. 121 | 122 | plot_working_directory 123 | By default, the working directory will be changed to the directory of 124 | the example, so the code can get at its data files, if any. Also its 125 | path will be added to `sys.path` so it can import any helper modules 126 | sitting beside it. This configuration option can be used to specify 127 | a central directory (also added to `sys.path`) where data files and 128 | helper modules for all code are located. 129 | 130 | plot_template 131 | Provide a customized template for preparing restructured text. 132 | """ 133 | from __future__ import absolute_import, division, print_function, unicode_literals 134 | 135 | import six 136 | from six.moves import xrange 137 | 138 | import sys 139 | import os 140 | import shutil 141 | import io 142 | import re 143 | import textwrap 144 | from os.path import relpath 145 | import traceback 146 | import warnings 147 | 148 | if not six.PY3: 149 | import cStringIO 150 | 151 | from docutils.parsers.rst import directives 152 | from docutils.parsers.rst.directives.images import Image 153 | 154 | align = Image.align 155 | import sphinx 156 | 157 | sphinx_version = sphinx.__version__.split(".") 158 | # The split is necessary for sphinx beta versions where the string is 159 | # '6b1' 160 | sphinx_version = tuple([int(re.split("[^0-9]", x)[0]) for x in sphinx_version[:2]]) 161 | 162 | try: 163 | # Sphinx depends on either Jinja or Jinja2 164 | import jinja2 165 | 166 | def format_template(template, **kw): 167 | return jinja2.Template(template).render(**kw) 168 | 169 | 170 | except ImportError: 171 | import jinja 172 | 173 | def format_template(template, **kw): 174 | return jinja.from_string(template, **kw) 175 | 176 | 177 | import matplotlib 178 | import matplotlib.cbook as cbook 179 | 180 | try: 181 | with warnings.catch_warnings(record=True): 182 | warnings.simplefilter("error", UserWarning) 183 | matplotlib.use("Agg") 184 | except UserWarning: 185 | import matplotlib.pyplot as plt 186 | 187 | plt.switch_backend("Agg") 188 | else: 189 | import matplotlib.pyplot as plt 190 | from matplotlib import _pylab_helpers 191 | 192 | __version__ = 2 193 | 194 | # ------------------------------------------------------------------------------ 195 | # Registration hook 196 | # ------------------------------------------------------------------------------ 197 | 198 | 199 | def plot_directive( 200 | name, 201 | arguments, 202 | options, 203 | content, 204 | lineno, 205 | content_offset, 206 | block_text, 207 | state, 208 | state_machine, 209 | ): 210 | return run(arguments, content, options, state_machine, state, lineno) 211 | 212 | 213 | plot_directive.__doc__ = __doc__ 214 | 215 | 216 | def _option_boolean(arg): 217 | if not arg or not arg.strip(): 218 | # no argument given, assume used as a flag 219 | return True 220 | elif arg.strip().lower() in ("no", "0", "false"): 221 | return False 222 | elif arg.strip().lower() in ("yes", "1", "true"): 223 | return True 224 | else: 225 | raise ValueError('"%s" unknown boolean' % arg) 226 | 227 | 228 | def _option_context(arg): 229 | if arg in [None, "reset", "close-figs"]: 230 | return arg 231 | raise ValueError("argument should be None or 'reset' or 'close-figs'") 232 | 233 | 234 | def _option_format(arg): 235 | return directives.choice(arg, ("python", "doctest")) 236 | 237 | 238 | def _option_align(arg): 239 | return directives.choice( 240 | arg, ("top", "middle", "bottom", "left", "center", "right") 241 | ) 242 | 243 | 244 | def mark_plot_labels(app, document): 245 | """ 246 | To make plots referenceable, we need to move the reference from 247 | the "htmlonly" (or "latexonly") node to the actual figure node 248 | itself. 249 | """ 250 | for name, explicit in six.iteritems(document.nametypes): 251 | if not explicit: 252 | continue 253 | labelid = document.nameids[name] 254 | if labelid is None: 255 | continue 256 | node = document.ids[labelid] 257 | if node.tagname in ("html_only", "latex_only"): 258 | for n in node: 259 | if n.tagname == "figure": 260 | sectname = name 261 | for c in n: 262 | if c.tagname == "caption": 263 | sectname = c.astext() 264 | break 265 | 266 | node["ids"].remove(labelid) 267 | node["names"].remove(name) 268 | n["ids"].append(labelid) 269 | n["names"].append(name) 270 | document.settings.env.labels[name] = ( 271 | document.settings.env.docname, 272 | labelid, 273 | sectname, 274 | ) 275 | break 276 | 277 | 278 | def setup(app): 279 | setup.app = app 280 | setup.config = app.config 281 | setup.confdir = app.confdir 282 | 283 | options = { 284 | "alt": directives.unchanged, 285 | "height": directives.length_or_unitless, 286 | "width": directives.length_or_percentage_or_unitless, 287 | "scale": directives.nonnegative_int, 288 | "align": _option_align, 289 | "class": directives.class_option, 290 | "include-source": _option_boolean, 291 | "format": _option_format, 292 | "context": _option_context, 293 | "nofigs": directives.flag, 294 | "encoding": directives.encoding, 295 | } 296 | 297 | app.add_directive("plot", plot_directive, True, (0, 2, False), **options) 298 | app.add_config_value("plot_pre_code", None, True) 299 | app.add_config_value("plot_include_source", False, True) 300 | app.add_config_value("plot_html_show_source_link", True, True) 301 | app.add_config_value("plot_formats", ["png", "hires.png", "pdf"], True) 302 | app.add_config_value("plot_basedir", None, True) 303 | app.add_config_value("plot_html_show_formats", True, True) 304 | app.add_config_value("plot_rcparams", {}, True) 305 | app.add_config_value("plot_apply_rcparams", False, True) 306 | app.add_config_value("plot_working_directory", None, True) 307 | app.add_config_value("plot_template", None, True) 308 | 309 | app.connect(str("doctree-read"), mark_plot_labels) 310 | 311 | 312 | # ------------------------------------------------------------------------------ 313 | # Doctest handling 314 | # ------------------------------------------------------------------------------ 315 | 316 | 317 | def contains_doctest(text): 318 | try: 319 | # check if it's valid Python as-is 320 | compile(text, "", "exec") 321 | return False 322 | except SyntaxError: 323 | pass 324 | r = re.compile(r"^\s*>>>", re.M) 325 | m = r.search(text) 326 | return bool(m) 327 | 328 | 329 | def unescape_doctest(text): 330 | """ 331 | Extract code from a piece of text, which contains either Python code 332 | or doctests. 333 | 334 | """ 335 | if not contains_doctest(text): 336 | return text 337 | 338 | code = "" 339 | for line in text.split("\n"): 340 | m = re.match(r"^\s*(>>>|\.\.\.) (.*)$", line) 341 | if m: 342 | code += m.group(2) + "\n" 343 | elif line.strip(): 344 | code += "# " + line.strip() + "\n" 345 | else: 346 | code += "\n" 347 | return code 348 | 349 | 350 | def split_code_at_show(text): 351 | """ 352 | Split code at plt.show() 353 | 354 | """ 355 | 356 | parts = [] 357 | is_doctest = contains_doctest(text) 358 | 359 | part = [] 360 | for line in text.split("\n"): 361 | if (not is_doctest and line.strip() == "plt.show()") or ( 362 | is_doctest and line.strip() == ">>> plt.show()" 363 | ): 364 | part.append(line) 365 | parts.append("\n".join(part)) 366 | part = [] 367 | else: 368 | part.append(line) 369 | if "\n".join(part).strip(): 370 | parts.append("\n".join(part)) 371 | return parts 372 | 373 | 374 | def remove_coding(text): 375 | """ 376 | Remove the coding comment, which six.exec_ doesn't like. 377 | """ 378 | sub_re = re.compile("^#\s*-\*-\s*coding:\s*.*-\*-$", flags=re.MULTILINE) 379 | return sub_re.sub("", text) 380 | 381 | 382 | # ------------------------------------------------------------------------------ 383 | # Template 384 | # ------------------------------------------------------------------------------ 385 | 386 | 387 | TEMPLATE = """ 388 | {{ source_code }} 389 | 390 | {{ only_html }} 391 | 392 | {% if source_link or (html_show_formats and not multi_image) %} 393 | ( 394 | {%- if source_link -%} 395 | `Source code <{{ source_link }}>`__ 396 | {%- endif -%} 397 | {%- if html_show_formats and not multi_image -%} 398 | {%- for img in images -%} 399 | {%- for fmt in img.formats -%} 400 | {%- if source_link or not loop.first -%}, {% endif -%} 401 | `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ 402 | {%- endfor -%} 403 | {%- endfor -%} 404 | {%- endif -%} 405 | ) 406 | {% endif %} 407 | 408 | {% for img in images %} 409 | .. figure:: {{ build_dir }}/{{ img.basename }}.png 410 | {% for option in options -%} 411 | {{ option }} 412 | {% endfor %} 413 | 414 | {% if html_show_formats and multi_image -%} 415 | ( 416 | {%- for fmt in img.formats -%} 417 | {%- if not loop.first -%}, {% endif -%} 418 | `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ 419 | {%- endfor -%} 420 | ) 421 | {%- endif -%} 422 | 423 | {{ caption }} 424 | {% endfor %} 425 | 426 | {{ only_latex }} 427 | 428 | {% for img in images %} 429 | {% if 'pdf' in img.formats -%} 430 | .. image:: {{ build_dir }}/{{ img.basename }}.pdf 431 | {% endif -%} 432 | {% endfor %} 433 | 434 | {{ only_texinfo }} 435 | 436 | {% for img in images %} 437 | .. image:: {{ build_dir }}/{{ img.basename }}.png 438 | {% for option in options -%} 439 | {{ option }} 440 | {% endfor %} 441 | 442 | {% endfor %} 443 | 444 | """ 445 | 446 | exception_template = """ 447 | .. htmlonly:: 448 | 449 | [`source code <%(linkdir)s/%(basename)s.py>`__] 450 | 451 | Exception occurred rendering plot. 452 | 453 | """ 454 | 455 | # the context of the plot for all directives specified with the 456 | # :context: option 457 | plot_context = dict() 458 | 459 | 460 | class ImageFile(object): 461 | def __init__(self, basename, dirname): 462 | self.basename = basename 463 | self.dirname = dirname 464 | self.formats = [] 465 | 466 | def filename(self, format): 467 | return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) 468 | 469 | def filenames(self): 470 | return [self.filename(fmt) for fmt in self.formats] 471 | 472 | 473 | def out_of_date(original, derived): 474 | """ 475 | Returns True if derivative is out-of-date wrt original, 476 | both of which are full file paths. 477 | """ 478 | return not os.path.exists(derived) or ( 479 | os.path.exists(original) 480 | and os.stat(derived).st_mtime < os.stat(original).st_mtime 481 | ) 482 | 483 | 484 | class PlotError(RuntimeError): 485 | pass 486 | 487 | 488 | def run_code(code, code_path, ns=None, function_name=None): 489 | """ 490 | Import a Python module from a path, and run the function given by 491 | name, if function_name is not None. 492 | """ 493 | 494 | # Change the working directory to the directory of the example, so 495 | # it can get at its data files, if any. Add its path to sys.path 496 | # so it can import any helper modules sitting beside it. 497 | if six.PY2: 498 | pwd = os.getcwdu() 499 | else: 500 | pwd = os.getcwd() 501 | old_sys_path = list(sys.path) 502 | if setup.config.plot_working_directory is not None: 503 | try: 504 | os.chdir(setup.config.plot_working_directory) 505 | except OSError as err: 506 | raise OSError( 507 | str(err) + "\n`plot_working_directory` option in" 508 | "Sphinx configuration file must be a valid " 509 | "directory path" 510 | ) 511 | except TypeError as err: 512 | raise TypeError( 513 | str(err) + "\n`plot_working_directory` option in " 514 | "Sphinx configuration file must be a string or " 515 | "None" 516 | ) 517 | sys.path.insert(0, setup.config.plot_working_directory) 518 | elif code_path is not None: 519 | dirname = os.path.abspath(os.path.dirname(code_path)) 520 | os.chdir(dirname) 521 | sys.path.insert(0, dirname) 522 | 523 | # Reset sys.argv 524 | old_sys_argv = sys.argv 525 | sys.argv = [code_path] 526 | 527 | # Redirect stdout 528 | stdout = sys.stdout 529 | if six.PY3: 530 | sys.stdout = io.StringIO() 531 | else: 532 | sys.stdout = cStringIO.StringIO() 533 | 534 | # Assign a do-nothing print function to the namespace. There 535 | # doesn't seem to be any other way to provide a way to (not) print 536 | # that works correctly across Python 2 and 3. 537 | def _dummy_print(*arg, **kwarg): 538 | pass 539 | 540 | try: 541 | try: 542 | code = unescape_doctest(code) 543 | if ns is None: 544 | ns = {} 545 | if not ns: 546 | if setup.config.plot_pre_code is None: 547 | six.exec_( 548 | six.text_type( 549 | "import numpy as np\n" 550 | + "from matplotlib import pyplot as plt\n" 551 | ), 552 | ns, 553 | ) 554 | else: 555 | six.exec_(six.text_type(setup.config.plot_pre_code), ns) 556 | ns["print"] = _dummy_print 557 | if "__main__" in code: 558 | six.exec_("__name__ = '__main__'", ns) 559 | code = remove_coding(code) 560 | six.exec_(code, ns) 561 | if function_name is not None: 562 | six.exec_(function_name + "()", ns) 563 | except (Exception, SystemExit) as err: 564 | raise PlotError(traceback.format_exc()) 565 | finally: 566 | os.chdir(pwd) 567 | sys.argv = old_sys_argv 568 | sys.path[:] = old_sys_path 569 | sys.stdout = stdout 570 | return ns 571 | 572 | 573 | def clear_state(plot_rcparams, close=True): 574 | if close: 575 | plt.close("all") 576 | matplotlib.rc_file_defaults() 577 | matplotlib.rcParams.update(plot_rcparams) 578 | 579 | 580 | def render_figures( 581 | code, 582 | code_path, 583 | output_dir, 584 | output_base, 585 | context, 586 | function_name, 587 | config, 588 | context_reset=False, 589 | close_figs=False, 590 | ): 591 | """ 592 | Run a pyplot script and save the low and high res PNGs and a PDF 593 | in *output_dir*. 594 | 595 | Save the images under *output_dir* with file names derived from 596 | *output_base* 597 | """ 598 | # -- Parse format list 599 | default_dpi = {"png": 80, "hires.png": 200, "pdf": 200} 600 | formats = [] 601 | plot_formats = config.plot_formats 602 | if isinstance(plot_formats, six.string_types): 603 | # String Sphinx < 1.3, Split on , to mimic 604 | # Sphinx 1.3 and later. Sphinx 1.3 always 605 | # returns a list. 606 | plot_formats = plot_formats.split(",") 607 | for fmt in plot_formats: 608 | if isinstance(fmt, six.string_types): 609 | if ":" in fmt: 610 | suffix, dpi = fmt.split(":") 611 | formats.append((str(suffix), int(dpi))) 612 | else: 613 | formats.append((fmt, default_dpi.get(fmt, 80))) 614 | elif type(fmt) in (tuple, list) and len(fmt) == 2: 615 | formats.append((str(fmt[0]), int(fmt[1]))) 616 | else: 617 | raise PlotError('invalid image format "%r" in plot_formats' % fmt) 618 | 619 | # -- Try to determine if all images already exist 620 | 621 | code_pieces = split_code_at_show(code) 622 | 623 | # Look for single-figure output files first 624 | all_exists = True 625 | img = ImageFile(output_base, output_dir) 626 | for format, dpi in formats: 627 | if out_of_date(code_path, img.filename(format)): 628 | all_exists = False 629 | break 630 | img.formats.append(format) 631 | 632 | if all_exists: 633 | return [(code, [img])] 634 | 635 | # Then look for multi-figure output files 636 | results = [] 637 | all_exists = True 638 | for i, code_piece in enumerate(code_pieces): 639 | images = [] 640 | for j in xrange(1000): 641 | if len(code_pieces) > 1: 642 | img = ImageFile("%s_%02d_%02d" % (output_base, i, j), output_dir) 643 | else: 644 | img = ImageFile("%s_%02d" % (output_base, j), output_dir) 645 | for format, dpi in formats: 646 | if out_of_date(code_path, img.filename(format)): 647 | all_exists = False 648 | break 649 | img.formats.append(format) 650 | 651 | # assume that if we have one, we have them all 652 | if not all_exists: 653 | all_exists = j > 0 654 | break 655 | images.append(img) 656 | if not all_exists: 657 | break 658 | results.append((code_piece, images)) 659 | 660 | if all_exists: 661 | return results 662 | 663 | # We didn't find the files, so build them 664 | 665 | results = [] 666 | if context: 667 | ns = plot_context 668 | else: 669 | ns = {} 670 | 671 | if context_reset: 672 | clear_state(config.plot_rcparams) 673 | plot_context.clear() 674 | 675 | close_figs = not context or close_figs 676 | 677 | for i, code_piece in enumerate(code_pieces): 678 | 679 | if not context or config.plot_apply_rcparams: 680 | clear_state(config.plot_rcparams, close_figs) 681 | elif close_figs: 682 | plt.close("all") 683 | 684 | run_code(code_piece, code_path, ns, function_name) 685 | 686 | images = [] 687 | fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() 688 | for j, figman in enumerate(fig_managers): 689 | if len(fig_managers) == 1 and len(code_pieces) == 1: 690 | img = ImageFile(output_base, output_dir) 691 | elif len(code_pieces) == 1: 692 | img = ImageFile("%s_%02d" % (output_base, j), output_dir) 693 | else: 694 | img = ImageFile("%s_%02d_%02d" % (output_base, i, j), output_dir) 695 | images.append(img) 696 | for format, dpi in formats: 697 | try: 698 | figman.canvas.figure.tight_layout() 699 | figman.canvas.figure.savefig( 700 | img.filename(format), dpi=dpi, bbox_inches="tight" 701 | ) 702 | except Exception as err: 703 | raise PlotError(traceback.format_exc()) 704 | img.formats.append(format) 705 | 706 | results.append((code_piece, images)) 707 | 708 | if not context or config.plot_apply_rcparams: 709 | clear_state(config.plot_rcparams, close=not context) 710 | 711 | return results 712 | 713 | 714 | def run(arguments, content, options, state_machine, state, lineno): 715 | # The user may provide a filename *or* Python code content, but not both 716 | if arguments and content: 717 | raise RuntimeError("plot:: directive can't have both args and content") 718 | 719 | document = state_machine.document 720 | config = document.settings.env.config 721 | nofigs = "nofigs" in options 722 | 723 | options.setdefault("include-source", config.plot_include_source) 724 | keep_context = "context" in options 725 | context_opt = None if not keep_context else options["context"] 726 | 727 | rst_file = document.attributes["source"] 728 | rst_dir = os.path.dirname(rst_file) 729 | 730 | if len(arguments): 731 | if not config.plot_basedir: 732 | source_file_name = os.path.join( 733 | setup.app.builder.srcdir, directives.uri(arguments[0]) 734 | ) 735 | else: 736 | source_file_name = os.path.join( 737 | setup.confdir, config.plot_basedir, directives.uri(arguments[0]) 738 | ) 739 | 740 | # If there is content, it will be passed as a caption. 741 | caption = "\n".join(content) 742 | 743 | # If the optional function name is provided, use it 744 | if len(arguments) == 2: 745 | function_name = arguments[1] 746 | else: 747 | function_name = None 748 | 749 | with io.open(source_file_name, "r", encoding="utf-8") as fd: 750 | code = fd.read() 751 | output_base = os.path.basename(source_file_name) 752 | else: 753 | source_file_name = rst_file 754 | code = textwrap.dedent("\n".join(map(str, content))) 755 | counter = document.attributes.get("_plot_counter", 0) + 1 756 | document.attributes["_plot_counter"] = counter 757 | base, ext = os.path.splitext(os.path.basename(source_file_name)) 758 | output_base = "%s-%d.py" % (base, counter) 759 | function_name = None 760 | caption = "" 761 | 762 | base, source_ext = os.path.splitext(output_base) 763 | if source_ext in (".py", ".rst", ".txt"): 764 | output_base = base 765 | else: 766 | source_ext = "" 767 | 768 | # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames 769 | output_base = output_base.replace(".", "-") 770 | 771 | # is it in doctest format? 772 | is_doctest = contains_doctest(code) 773 | if "format" in options: 774 | if options["format"] == "python": 775 | is_doctest = False 776 | else: 777 | is_doctest = True 778 | 779 | # determine output directory name fragment 780 | source_rel_name = relpath(source_file_name, setup.confdir) 781 | source_rel_dir = os.path.dirname(source_rel_name) 782 | while source_rel_dir.startswith(os.path.sep): 783 | source_rel_dir = source_rel_dir[1:] 784 | 785 | # build_dir: where to place output files (temporarily) 786 | build_dir = os.path.join( 787 | os.path.dirname(setup.app.doctreedir), "plot_directive", source_rel_dir 788 | ) 789 | # get rid of .. in paths, also changes pathsep 790 | # see note in Python docs for warning about symbolic links on Windows. 791 | # need to compare source and dest paths at end 792 | build_dir = os.path.normpath(build_dir) 793 | 794 | if not os.path.exists(build_dir): 795 | os.makedirs(build_dir) 796 | 797 | # output_dir: final location in the builder's directory 798 | dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, source_rel_dir)) 799 | if not os.path.exists(dest_dir): 800 | os.makedirs(dest_dir) # no problem here for me, but just use built-ins 801 | 802 | # how to link to files from the RST file 803 | dest_dir_link = os.path.join( 804 | relpath(setup.confdir, rst_dir), source_rel_dir 805 | ).replace(os.path.sep, "/") 806 | try: 807 | build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, "/") 808 | except ValueError: 809 | # on Windows, relpath raises ValueError when path and start are on 810 | # different mounts/drives 811 | build_dir_link = build_dir 812 | source_link = dest_dir_link + "/" + output_base + source_ext 813 | 814 | # make figures 815 | try: 816 | results = render_figures( 817 | code, 818 | source_file_name, 819 | build_dir, 820 | output_base, 821 | keep_context, 822 | function_name, 823 | config, 824 | context_reset=context_opt == "reset", 825 | close_figs=context_opt == "close-figs", 826 | ) 827 | errors = [] 828 | except PlotError as err: 829 | reporter = state.memo.reporter 830 | sm = reporter.system_message( 831 | 2, 832 | "Exception occurred in plotting %s\n from %s:\n%s" 833 | % (output_base, source_file_name, err), 834 | line=lineno, 835 | ) 836 | results = [(code, [])] 837 | errors = [sm] 838 | 839 | # Properly indent the caption 840 | caption = "\n".join(" " + line.strip() for line in caption.split("\n")) 841 | 842 | # generate output restructuredtext 843 | total_lines = [] 844 | for j, (code_piece, images) in enumerate(results): 845 | if options["include-source"]: 846 | if is_doctest: 847 | lines = [""] 848 | lines += [row.rstrip() for row in code_piece.split("\n")] 849 | else: 850 | lines = [".. code-block:: python", ""] 851 | lines += [" %s" % row.rstrip() for row in code_piece.split("\n")] 852 | source_code = "\n".join(lines) 853 | else: 854 | source_code = "" 855 | 856 | if nofigs: 857 | images = [] 858 | 859 | opts = [ 860 | ":%s: %s" % (key, val) 861 | for key, val in six.iteritems(options) 862 | if key in ("alt", "height", "width", "scale", "align", "class") 863 | ] 864 | 865 | only_html = ".. only:: html" 866 | only_latex = ".. only:: latex" 867 | only_texinfo = ".. only:: texinfo" 868 | 869 | # Not-None src_link signals the need for a source link in the generated 870 | # html 871 | if j == 0 and config.plot_html_show_source_link: 872 | src_link = source_link 873 | else: 874 | src_link = None 875 | 876 | result = format_template( 877 | config.plot_template or TEMPLATE, 878 | dest_dir=dest_dir_link, 879 | build_dir=build_dir_link, 880 | source_link=src_link, 881 | multi_image=len(images) > 1, 882 | only_html=only_html, 883 | only_latex=only_latex, 884 | only_texinfo=only_texinfo, 885 | options=opts, 886 | images=images, 887 | source_code=source_code, 888 | html_show_formats=config.plot_html_show_formats and not nofigs, 889 | caption=caption, 890 | ) 891 | 892 | total_lines.extend(result.split("\n")) 893 | total_lines.extend("\n") 894 | 895 | if total_lines: 896 | state_machine.insert_input(total_lines, source=source_file_name) 897 | 898 | # copy image files to builder's output directory, if necessary 899 | if not os.path.exists(dest_dir): 900 | cbook.mkdirs(dest_dir) 901 | 902 | for code_piece, images in results: 903 | for img in images: 904 | for fn in img.filenames(): 905 | destimg = os.path.join(dest_dir, os.path.basename(fn)) 906 | if fn != destimg: 907 | shutil.copyfile(fn, destimg) 908 | 909 | # copy script (if necessary) 910 | target_name = os.path.join(dest_dir, output_base + source_ext) 911 | with io.open(target_name, "w", encoding="utf-8") as f: 912 | if source_file_name == rst_file: 913 | code_escaped = unescape_doctest(code) 914 | else: 915 | code_escaped = code 916 | f.write(code_escaped) 917 | 918 | return errors 919 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | install_requires = ["matplotlib", "numpy", "pandas"] 5 | 6 | try: 7 | with open("README.md") as readme: 8 | long_description = readme.read() 9 | except IOError: 10 | long_description = "See https://pypi.python.org/pypi/calmap" 11 | 12 | # This is quite the hack, but we don't want to import our package from here 13 | # since that's recipe for disaster (it might have some uninstalled 14 | # dependencies, or we might import another already installed version). 15 | distmeta = {} 16 | for line in open(os.path.join("calmap", "__init__.py")): 17 | try: 18 | field, value = (x.strip() for x in line.split("=")) 19 | except ValueError: 20 | continue 21 | if field == "__version_info__": 22 | value = value.strip("[]()") 23 | value = ".".join(x.strip(" '\"") for x in value.split(",")) 24 | else: 25 | value = value.strip("'\"") 26 | distmeta[field] = value 27 | 28 | setup( 29 | name="calmap", 30 | version=distmeta["__version_info__"], 31 | description="Calendar heatmaps from Pandas time series data", 32 | long_description=long_description, 33 | author=distmeta["__author__"], 34 | author_email=distmeta["__contact__"], 35 | url=distmeta["__homepage__"], 36 | license="MIT License", 37 | platforms=["any"], 38 | packages=["calmap"], 39 | install_requires=install_requires, 40 | classifiers=[ 41 | "Development Status :: 3 - Alpha", 42 | "Intended Audience :: Developers", 43 | "Intended Audience :: Science/Research", 44 | "Operating System :: OS Independent", 45 | "Programming Language :: Python", 46 | "Programming Language :: Python :: 3", 47 | "Programming Language :: Python :: 3.6", 48 | "Programming Language :: Python :: 3.7", 49 | "Programming Language :: Python :: 3.8", 50 | "Topic :: Scientific/Engineering", 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | 3 | matplotlib.use("agg") 4 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=4.6 2 | coverage 3 | pytest-cov 4 | python-coveralls 5 | -------------------------------------------------------------------------------- /tests/test_calmap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for calmap. 3 | """ 4 | 5 | 6 | from __future__ import unicode_literals 7 | 8 | import numpy as np 9 | 10 | np.random.seed(sum(map(ord, "calmap"))) 11 | import pandas as pd 12 | import pytest 13 | 14 | import calmap 15 | 16 | 17 | @pytest.fixture 18 | def events(): 19 | """ 20 | We create 500 events as random float values assigned to random days over a 21 | 700-day period. 22 | """ 23 | all_days = pd.date_range("1/15/2014", periods=700, freq="D") 24 | days = np.random.choice(all_days, 500) 25 | return pd.Series(np.random.randn(len(days)), index=days) 26 | 27 | 28 | def test_yearplot(events): 29 | """ 30 | By default, `yearplot` plots the first year and sums the values per day. 31 | """ 32 | ax = calmap.yearplot(events) 33 | return ax.figure 34 | 35 | 36 | def test_yearplot_year(events): 37 | """ 38 | We can choose which year is plotted with the `year` keyword argment. 39 | """ 40 | ax = calmap.yearplot(events, year=2015) 41 | return ax.figure 42 | 43 | 44 | def test_yearplot_cmap_fillcolor_linewidth(events): 45 | """ 46 | The appearance can be changed by using another colormap. Here we also use 47 | a darker fill color for days without data and remove the lines. 48 | """ 49 | ax = calmap.yearplot(events, cmap="YlGn", fillcolor="grey", linewidth=0) 50 | return ax.figure 51 | 52 | 53 | def test_yearplot_monthticks_daylabels_dayticks(events): 54 | """ 55 | We can ask to draw only every nth label, or explicitely supply the label 56 | indices. The labels themselves can also be customized. 57 | """ 58 | ax = calmap.yearplot( 59 | events, monthticks=3, daylabels="MTWTFSS", dayticks=[0, 2, 4, 6] 60 | ) 61 | return ax.figure 62 | 63 | 64 | def test_calendarplot(events): 65 | """ 66 | With `calendarplot` we can plot several years in one figure. 67 | """ 68 | fig, axes = calmap.calendarplot(events) 69 | return fig 70 | 71 | def test_calendarplot_columns(events): 72 | """ 73 | We can specify a number of columns. 74 | """ 75 | fig, axes = calmap.calendarplot(events, ncols=2) 76 | return fig 77 | --------------------------------------------------------------------------------