diff --git a/.github/workflows/niceplots.yml b/.github/workflows/niceplots.yml index 0bf5bc7..edb00c9 100644 --- a/.github/workflows/niceplots.yml +++ b/.github/workflows/niceplots.yml @@ -8,7 +8,6 @@ on: tags: - v*.*.* pull_request: - branches: [main] jobs: black: @@ -21,27 +20,64 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9] - numpy-version: ["1.16.6", "1.18.5"] - mpl-version: ["3.2.*", "3.3.*", "3.4.*"] + python-version: ["3.8", "3.9", "3.10"] + numpy-version: ["1.19.*", "1.21.*", "1.24.*"] + mpl-version: ["3.4.*", "3.6.*"] + exclude: + - python-version: "3.9" + numpy-version: "1.19.*" + - python-version: "3.10" + numpy-version: "1.19.*" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install apt dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: fonts-cmu nodejs npm + version: 1.0 + - name: Install node dependencies + run: | + sudo npm install -g odiff-bin + - name: Install Prompt font + run: | + mkdir Prompt + cd Prompt + wget "https://fonts.google.com/download?family=Prompt" -O Prompt.zip + unzip Prompt.zip + mkdir ~/.fonts + cp *.ttf ~/.fonts + - name: Install python dependencies run: | - sudo apt-get install fonts-cmu pip install --upgrade pip wheel pip install numpy==${{ matrix.numpy-version }} pip install matplotlib==${{ matrix.mpl-version }} pip install . - name: Test examples run: | - # this is very janky for now cd examples bash testExamples.sh + - name: Compare against reference images + if: ${{ success() && matrix.python-version == '3.10' && matrix.numpy-version == '1.24.*' && matrix.mpl-version == '3.6.*' }} + run: | + cd examples + bash ImageComparisonTest.sh + - name: Upload examples if failed + uses: actions/upload-artifact@v3 + if: ${{ failure() && matrix.python-version == '3.10' && matrix.numpy-version == '1.24.*' && matrix.mpl-version == '3.6.*' }} + with: + name: Examples + path: examples/ + # - name: Upload new reference images + # if: ${{ github.event_name == 'push' && success() && matrix.python-version == '3.10' && matrix.numpy-version == '1.24.*' && matrix.mpl-version == '3.6.*' }} + # uses: stefanzweifel/git-auto-commit-action@v4 + # with: + # file_pattern: 'examples/ref/*.png' + # commit_message: Update reference images + # --- publish to PyPI pypi: diff --git a/.gitignore b/.gitignore index b99349d..2bed3e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ -# Ignore any pdf's or png's except in the examples folder +# Ignore any figures, except the reference images used for testing *.pdf -!examples/*.pdf *.png -!examples/*.png +!examples/ref/*.png +*.svg .vscode/ +# Ignore generated doc files +doc/_build/ +doc/auto_examples/ + ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ @@ -152,4 +156,4 @@ dmypy.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -*.code-workspace \ No newline at end of file +*.code-workspace diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 6a6dd0f..94986ab 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,9 +9,19 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.10" apt_packages: - fonts-cmu + # Install the fonts for the james style + jobs: + pre_install: + - mkdir Prompt + - cd Prompt + - wget "https://fonts.google.com/download?family=Prompt" -O Prompt.zip + - unzip Prompt.zip + - mkdir ~/.fonts + - cp *.ttf ~/.fonts + - cd .. # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/README.md b/README.md index 68055d0..d99c9be 100644 --- a/README.md +++ b/README.md @@ -7,53 +7,74 @@ [![PyPI - Downloads](https://img.shields.io/pypi/dm/niceplots)](https://pypi.org/project/niceplots/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - - + + + -### How do I install? +## How do I install? -Niceplots can be pip installed directly from PyPI +NicePlots can be pip installed directly from PyPI ```shell pip install niceplots ``` -#### If you want to make changes +### If you want to make changes * Clone this repository, then enter the folder in the command line terminal. * Enter `pip install -e .` within the `niceplots` folder. -#### Font installation (optional) +### Font installation (optional) -Niceplots will try and use the [computer modern bright](https://tug.org/FontCatalogue/computermodernbright/) font for the best looking plots so be sure to install it as a system font if you want to recreate the style of the plots above. -Otherwise, niceplots will still work but revert back to the matplotlib default sans-serif font, DejaVu Sans. +NicePlots styles use fonts that do not ship with most operating systems, so you'll need to install them separately. +If they are not installed, matplotlib will revert back to its default sans-serif font, DejaVu Sans. + +The font used by each style is as follows: +- doumont-light (default niceplots): CMU Bright +- doumont-dark: CMU Bright +- james-dark: Prompt +- james-light: Prompt + +Install the fonts on your system and then delete Matplotlib's font cache, which is located in `~/.cache/matplotlib` by default on most operating systems. +Matplotlib will rebuild the font cache next time it is run and (hopefully) find the new fonts. + +#### CMU Bright (doumont-light and doumont-dark) + +The computer modern bright font can be downloaded from [this link](https://tug.org/FontCatalogue/computermodernbright/). +Alternatively, on Ubuntu, the font can be installed with the following commands: -To install the font on Ubuntu, run the following commands: ``` sudo apt-get update sudo apt-get install fonts-cmu ``` Arch linux users can get the font by installing the `otf-cm-unicode` package from AUR. -If niceplots doesn't recognize the font, it might be necessary to delete Matplotlib's font cache file from its location on your computer, likely in `~/.cache/matplotlib` +#### Prompt (james-dark and james-light) + +The Prompt font can be download from [Google Fonts](https://fonts.google.com/specimen/Prompt). + +## How do I get set up? + +* `import matplotlib.pyplot as plt` and `import niceplots` at the top of a file where you would like to use any function defined in this package. +* Use `plt.style.use(niceplots.get_style())` to set some defaults for nice-looking plots. You can also try passing different styles to `get_style()`, such as NicePlots' `"james-dark"` or any of matplotlib's styles (see the function's documentation for a full list of available NicePlots styles). +* Take advantage of NicePlots' helper functions, including (but not limited to) `adjust_spines`, `horiz_bar`, and `plot_nested_pie`, which are all documented in the [examples gallery](https://mdolab-niceplots.readthedocs-hosted.com/en/latest/auto_examples/index.html). +* Admire your beautiful data. -### How do I get set up? +## Do you have docs? -* Use `import niceplots` at the top of a file where you would like to use any function defined in this package. -* Use `niceplots.setRCParams()` to set some matplotlib defaults for nice looking plots. Set `dark_mode=True` and `set_background_color=True` to make plots with a dark background. -* Use `niceplots.All()` after all the plot commands to apply the niceplot standards on the figure. -* To use the Matlab colormap "parula", execute `from niceplots import parula` then use `parula.parula_map` as your colormap within your plotting script. See the contour plot example code for an example of this. +Sort of, you can find our examples gallery and API documentation [here](https://mdolab-niceplots.readthedocs-hosted.com/en/latest) -### Do you have docs? +## Help, my old NicePlots code doesn't work anymore! -Sort of, you can find our examples gallery and api documentation [here](https://mdolab-niceplots.readthedocs-hosted.com/en/latest) +We made a couple of changes to the API in version 2.0.0, most of them can be fixed with a simple find and replace. +Check the [release notes](https://github.com/mdolab/niceplots/releases/tag/v2.0.0) for more details. -### Contribution guidelines +## Contribution guidelines * Make any changes you see fit. Please fork your own version and submit a pull request. -### Who do I talk to? +## Who do I talk to? * Alasdair Gray, alachris@umich.edu * Eytan Adler, eytana@umich.edu diff --git a/doc/conf.py b/doc/conf.py index b7c0ca2..e5b710f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,4 +1,46 @@ from sphinx_mdolab_theme.config import * +from glob import glob +import shutil +import os +from sphinx_gallery.scrapers import figure_rst +from pathlib import PurePosixPath + + +class svgScraper(object): + """This is a custom scraper for sphinx-gallery that allows us to use the svg files written by our examples. It is + almost entirely copied from the PNGScraper shown at: + https://sphinx-gallery.github.io/dev/advanced.html#example-2-detecting-image-files-on-disk + """ + + def __init__(self): + self.seen = set() + + def __repr__(self): + return "svgScraper" + + def __call__(self, block, block_vars, gallery_conf): + # Find all svg files in the directory of this example. + path_current_example = os.path.dirname(block_vars["src_file"]) + svgs = sorted(glob(os.path.join(path_current_example, "*.svg"))) + + # Get the name of the current example, e.g if the file is called "plot_nested_pie_chart.py", + # then example_name = "nested_pie_chart" + example_name = os.path.splitext(os.path.basename(block_vars["src_file"]))[0].split("plot_")[0] + + # Iterate through svgs, copy them to the sphinx-gallery output directory + image_names = list() + image_path_iterator = block_vars["image_path_iterator"] + for svg in svgs: + if svg not in self.seen and example_name.lower() in svg.lower(): + self.seen |= set(svg) + this_image_path = image_path_iterator.next() + image_path = PurePosixPath(this_image_path) + image_path = image_path.with_suffix(".svg") + image_names.append(image_path) + shutil.move(svg, image_path) + # Use the `figure_rst` helper function to generate rST for image files + return figure_rst(image_names, gallery_conf["src_dir"]) + # -- Project information ----------------------------------------------------- project = "niceplots" @@ -8,4 +50,5 @@ sphinx_gallery_conf = { "examples_dirs": "../examples", # path to your example scripts "gallery_dirs": "auto_examples", # path to where to save gallery generated output + "image_scrapers": (svgScraper(),), } diff --git a/doc/index.rst b/doc/index.rst index 652c114..386e2eb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to niceplots's documentation! +Welcome to NicePlots's documentation! ===================================== diff --git a/doc/requirements.txt b/doc/requirements.txt index 289f2fc..002bfca 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,3 @@ sphinx_mdolab_theme sphinx-gallery -matplotlib>=3.4 \ No newline at end of file +matplotlib>=3.6 diff --git a/examples/ImageComparisonTest.sh b/examples/ImageComparisonTest.sh new file mode 100644 index 0000000..0f59243 --- /dev/null +++ b/examples/ImageComparisonTest.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# This script compares the images generated by the example scripts to a set of reference images + +test_passed=true # Kepp track of whether any tests fail + +# First check that there aren't any images produced by the examples that don't have a reference image +for f in *.png +do + if ! test -f "ref/$f"; then + echo "" + echo "$f doesn't have a reference image to compare to" + test_passed=false + fi +done + +mkdir -p diffs + +# For each image in the ref directory, check that there is a corresponding image in the current directory and then compare them +for f in ref/*.png +do + echo "" + base_name=$(basename ${f}) + + if test -f "$base_name"; then + filename_no_ext="${base_name%.*}" + echo "Comparing $base_name and $f" + odiff --antialiasing --threshold=0.1 --diff-mask $base_name $f diffs/${filename_no_ext}-diff.png + if [ $? -ne 0 ]; then + test_passed=false + fi + else + echo "Couldn't find image to compare against $f" + test_passed=false + fi +done + +echo "" +echo "===========================" +if [ "$test_passed" = true ] ; then + echo "All comparisons passed" + echo "===========================" + exit 0 +else + echo "Some comparisons failed" + echo "===========================" + exit 0 # In future (if we can make the image comparison more robust) we should exit with a non-zero exit code (e.g. 1) +fi diff --git a/examples/ParulaContours.png b/examples/ParulaContours.png deleted file mode 100644 index f03bb9d..0000000 Binary files a/examples/ParulaContours.png and /dev/null differ diff --git a/examples/ParulaContours_dark.png b/examples/ParulaContours_dark.png deleted file mode 100644 index 7e1d2a7..0000000 Binary files a/examples/ParulaContours_dark.png and /dev/null differ diff --git a/examples/ParulaContours_grey.png b/examples/ParulaContours_grey.png deleted file mode 100644 index ece2581..0000000 Binary files a/examples/ParulaContours_grey.png and /dev/null differ diff --git a/examples/ParulaContours_navy.png b/examples/ParulaContours_navy.png deleted file mode 100644 index e90424c..0000000 Binary files a/examples/ParulaContours_navy.png and /dev/null differ diff --git a/examples/bar_chart.png b/examples/bar_chart.png deleted file mode 100644 index 06e2881..0000000 Binary files a/examples/bar_chart.png and /dev/null differ diff --git a/examples/coloredLine.png b/examples/coloredLine.png deleted file mode 100644 index 3b9b2ae..0000000 Binary files a/examples/coloredLine.png and /dev/null differ diff --git a/examples/coloredLine_dark.png b/examples/coloredLine_dark.png deleted file mode 100644 index 3507b68..0000000 Binary files a/examples/coloredLine_dark.png and /dev/null differ diff --git a/examples/defaultPulseResponse.png b/examples/defaultPulseResponse.png deleted file mode 100644 index d6c8fa7..0000000 Binary files a/examples/defaultPulseResponse.png and /dev/null differ diff --git a/examples/niceplotsPulseResponse.png b/examples/niceplotsPulseResponse.png deleted file mode 100644 index c350030..0000000 Binary files a/examples/niceplotsPulseResponse.png and /dev/null differ diff --git a/examples/optProb-hashed.png b/examples/optProb-hashed.png deleted file mode 100644 index fe8e09f..0000000 Binary files a/examples/optProb-hashed.png and /dev/null differ diff --git a/examples/optProb-shaded.png b/examples/optProb-shaded.png deleted file mode 100644 index b6009da..0000000 Binary files a/examples/optProb-shaded.png and /dev/null differ diff --git a/examples/opt_stacks.png b/examples/opt_stacks.png deleted file mode 100644 index 62816b3..0000000 Binary files a/examples/opt_stacks.png and /dev/null differ diff --git a/examples/opt_stacks_more_data.png b/examples/opt_stacks_more_data.png deleted file mode 100644 index 530c793..0000000 Binary files a/examples/opt_stacks_more_data.png and /dev/null differ diff --git a/examples/plot_bar_chart.py b/examples/plot_bar_chart.py index e8faa49..8d5e64d 100644 --- a/examples/plot_bar_chart.py +++ b/examples/plot_bar_chart.py @@ -3,41 +3,23 @@ ========= An example of a bar chart. """ -import random import matplotlib.pyplot as plt import niceplots -niceplots.setRCParams() -try: - # use random words for the example - word_file = "/usr/share/dict/words" - words = open(word_file).read().splitlines() - wl = len(words) - random.seed(100) +header = ["Method", "Time (sec)"] +labels = [ + "Analytic Forward", + "Analytic Adjoint", + "FD Forward Diff.", + "FD Central Diff.", + "FD Backward Diff.", +] +times = [0.00456, 0.00847, 0.0110, 0.0213, 0.011] +nd = 4 - def rw(): - return random.choice(words) +with plt.style.context(niceplots.get_style()): + niceplots.horiz_bar(labels, times, header, nd=nd, size=[7, 0.65]) - header = [rw(), rw()] - n = 15 # number of bars to create - labels = [rw() for i in range(n)] - times = [random.random() * random.randint(0, 1000) for i in range(n)] - nd = 1 - -except FileNotFoundError: # noqa: E722 if user is not on a *nix system - header = ["Method", "Time (sec)"] - labels = [ - "Analytic Forward", - "Analytic Adjoint", - "FD Forward Diff.", - "FD Central Diff.", - "FD Backward Diff.", - ] - times = [0.00456, 0.00847, 0.0110, 0.0213, 0.011] - nd = 4 - -niceplots.horiz_bar(labels, times, header, nd=nd, size=[7, 0.65]) - -plt.savefig("bar_chart.pdf", bbox_inches="tight") -plt.savefig("bar_chart.png", dpi=400, bbox_inches="tight") + plt.savefig("bar_chart.png") + plt.savefig("bar_chart.svg") diff --git a/examples/plot_color_demo.py b/examples/plot_color_demo.py new file mode 100644 index 0000000..2ec9090 --- /dev/null +++ b/examples/plot_color_demo.py @@ -0,0 +1,66 @@ +""" +============================================================================== +NicePlots Style Colors Demo +============================================================================== +This script demonstrates colors available in each style. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +import matplotlib.pyplot as plt +import niceplots +import numpy as np + +# ============================================================================== +# Extension modules +# ============================================================================== + +# Get the style names from NicePlots +styles = niceplots.get_available_styles() + +plt.style.use(niceplots.get_style("james-light")) + +fig, axs = plt.subplots(1, len(styles), figsize=(3 * len(styles), 8)) +axs = axs.flatten() + +for i, style in enumerate(styles): + with plt.style.context(niceplots.get_style(style)): + ax = axs[i] + colors = niceplots.get_colors() + background = colors["Background"] + for key in ["Axis", "Background", "Text", "Label"]: + del colors[key] + + ax.set_title(style, pad=20.0) + + # Fill the background with the style's background color + ax.fill_between([0, 1], [0, 0], [1, 1], color=background) + + # Swatch properties + spacing = 0.05 + y_spacing = spacing * 3 + width = (1 - (len(colors) + 1) * spacing) / len(colors) + + # Plot the swatches + for i_color, color in enumerate(colors.keys()): + y_start = i_color * (spacing + width) + spacing + y_end = width + y_start + ax.fill_between([y_spacing, 1 - y_spacing], [y_start] * 2, [y_end] * 2, color=colors[color]) + ax.text(0.5, (y_start + y_end) / 2, color, va="center", ha="center") + + ax.set_xlim([0, 1]) + ax.set_ylim([0, 1]) + ax.invert_yaxis() + + # Remove the spines and ticks + ax.spines[["top", "bottom", "left", "right"]].set_visible(False) + ax.set_xticks(()) + ax.set_yticks(()) + +fig.savefig("style_color_demo.png") +fig.savefig("style_color_demo.svg") diff --git a/examples/plot_colored_line.py b/examples/plot_colored_line.py index 705070b..69958a4 100644 --- a/examples/plot_colored_line.py +++ b/examples/plot_colored_line.py @@ -1,7 +1,7 @@ """ Colored line plotting ===================== -An example of the plotColoredLine function, plotting the sine and cosine functions, +An example of the plot_colored_line function, plotting the sine and cosine functions, colored by their derivatives """ @@ -21,33 +21,37 @@ # Extension modules # ============================================================================== -niceplots.setRCParams() +plt.style.use(niceplots.get_style()) x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) c = np.cos(x) fig, ax = plt.subplots() -niceplots.plotColoredLine(x, y, c, cmap="coolwarm", fig=fig, ax=ax, addColorBar=True, cRange=None, cBarLabel="$dy/dx$") -niceplots.plotColoredLine(x, c, -y, cmap="coolwarm", fig=fig, ax=ax) +niceplots.plot_colored_line( + x, y, c, cmap="coolwarm", fig=fig, ax=ax, addColorBar=True, cRange=None, cBarLabel="$dy/dx$", clip_on=False +) +niceplots.plot_colored_line(x, c, -y, cmap="coolwarm", fig=fig, ax=ax, clip_on=False) niceplots.adjust_spines(ax) ax.set_xlabel("$x$") ax.set_ylabel("$y$", rotation="horizontal", ha="right") ax.set_xticks(np.linspace(0, 2, 5) * np.pi) ax.set_xticklabels([0, r"$\frac{\pi}{2}$", r"$\pi$", r"$\frac{3\pi}{2}$", r"$2\pi$"]) ax.set_xlim(0, 2 * np.pi) -fig.savefig("coloredLine.png", dpi=400) -fig.savefig("coloredLine.pdf") +fig.savefig("colored_line.png") +fig.savefig("colored_line.svg") # Use a custom norm to specify the colormap range divnorm = TwoSlopeNorm(vmin=-1.0, vcenter=0.8, vmax=1.0) -fig, ax = niceplots.plotColoredLine(x, y, c, cmap="coolwarm", norm=divnorm, addColorBar=False, cBarLabel="$dy/dx$") +fig, ax = niceplots.plot_colored_line( + x, y, c, cmap="coolwarm", norm=divnorm, addColorBar=False, cBarLabel="$dy/dx$", clip_on=False +) niceplots.adjust_spines(ax) ax.set_xlabel("$x$") ax.set_ylabel("$y$", rotation="horizontal", ha="right") ax.set_xticks(np.linspace(0, 2, 5) * np.pi) ax.set_xticklabels([0, r"$\frac{\pi}{2}$", r"$\pi$", r"$\frac{3\pi}{2}$", r"$2\pi$"]) ax.set_xlim(0, 2 * np.pi) -fig.savefig("coloredLineCustomNorm.png", dpi=400) -fig.savefig("coloredLineCustomNorm.pdf") -# plt.show() + +fig.savefig("colored_line_custom_norm.png") +fig.savefig("colored_line_custom_norm.svg") diff --git a/examples/plot_dark_mode.py b/examples/plot_dark_mode.py deleted file mode 100644 index 75a2305..0000000 --- a/examples/plot_dark_mode.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -============================================================================== -Dark mode plotting example -============================================================================== -An example of dark mode usage applied to some of the existing examples. Shows -various usage of the dark_mode and set_dark_background in -:func:`niceplots.utils.setRCParams`. -""" - -# ============================================================================== -# Standard Python modules -# ============================================================================== - -# ============================================================================== -# External Python modules -# ============================================================================== -import numpy as np -import matplotlib.pyplot as plt -import niceplots - -# ============================================================================== -# Extension modules -# ============================================================================== - -# Dark mode with a transparent background, saved to a png -niceplots.setRCParams(dark_mode=True) -# The next line sets the thumbnail for the sphinx gallery in docs -# sphinx_gallery_thumbnail_number = 2 - -x = np.linspace(0, 2 * np.pi, 100) -y = np.sin(x) -c = np.cos(x) - -fig, ax = plt.subplots() -niceplots.plotColoredLine(x, y, c, cmap="coolwarm", fig=fig, ax=ax, addColorBar=True, cRange=None, cBarLabel="$dy/dx$") -niceplots.plotColoredLine(x, c, -y, cmap="coolwarm", fig=fig, ax=ax, addColorBar=True, cRange=None, cBarLabel="$dy/dx$") -niceplots.adjust_spines(ax) -ax.set_xlabel("$x$") -ax.set_ylabel("$y$", rotation="horizontal", ha="right") -ax.set_xticks(np.linspace(0, 2, 5) * np.pi) -ax.set_xticklabels([0, r"$\frac{\pi}{2}$", r"$\pi$", r"$\frac{3\pi}{2}$", r"$2\pi$"]) -ax.set_xlim(0, 2 * np.pi) -fig.savefig("coloredLine_dark.png", transparent=True, dpi=400) - - -# Plot contours with the default and a custom background color -niceColors = niceplots.get_niceColors() - -bkgnd = [True, niceColors["Grey"], "#0c1c38"] # different ways of setting the background via set_dark_background -fname_suffix = ["_dark", "_grey", "_navy"] - -for i, back in enumerate(bkgnd): - niceplots.setRCParams(dark_mode=True, set_dark_background=back) - - def f(x1, x2): - return x1**3 + 2.0 * x1 * x2**2 - x2**3 - 20.0 * x1 - - x1 = x2 = np.linspace(-5, 5, 201) - X1, X2 = np.meshgrid(x1, x2) - minimum = [2.58199, 0] - - q2fig, q2ax = plt.subplots() - q2ax.contour(X1, X2, f(X1, X2), cmap=niceplots.parula.parula_map, levels=40) - q2ax.plot( - minimum[0], - minimum[1], - clip_on=False, - marker="o", - color="w", - markeredgecolor=q2ax.get_facecolor(), - linestyle="", - markersize=12, - ) - q2ax.annotate( - "Local Minimum", - xy=minimum, - xytext=(-5, 10), - textcoords="offset points", - va="bottom", - ha="center", - ) - q2ax.plot( - -minimum[0], - minimum[1], - clip_on=False, - marker="o", - color="w", - markeredgecolor=q2ax.get_facecolor(), - linestyle="", - markersize=12, - ) - q2ax.annotate( - "Local Maximum", - xy=(-minimum[0], 0), - xytext=(0, -10), - textcoords="offset points", - va="top", - ha="center", - ) - niceplots.adjust_spines(q2ax, outward=True) - q2ax.set_xlabel("$x_1$") - q2ax.set_xticks([min(x1), -minimum[0], 0, minimum[0], max(x1)]) - q2ax.set_xlim(left=min(x1), right=max(x1)) - q2ax.set_ylim(bottom=min(x2), top=max(x2)) - q2ax.set_yticks([min(x2), 0, minimum[-1], max(x2)]) - q2ax.set_ylabel("$x_2$", rotation="horizontal", ha="right") - plt.savefig(f"ParulaContours{fname_suffix[i]}.png", dpi=400) diff --git a/examples/plot_default_formatting.py b/examples/plot_default_formatting.py deleted file mode 100644 index 654dadf..0000000 --- a/examples/plot_default_formatting.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Default formatting -================== -A short example to demonstrate the use of niceplots default plot formatting and compre it to matplotlib defaults -""" - -import numpy as np -import matplotlib.pyplot as plt -import niceplots - - -# This function computes the displacement history of an undamped oscilator subjec to to a force pulse of length tp -def MSPulseResponse(t, tp, omega): - x = np.where( - t < tp, - 1.0 - np.cos(omega * t), - (np.cos(omega * tp) - 1.0) * np.cos(omega * t) + np.sin(omega * tp) * np.sin(omega * t), - ) - return x - - -m = 1.0 -k = 100.0 -omega = np.sqrt(k / m) -t = np.linspace(0, 3.0, 1001) -TP = [0.5, 0.8, 1.2, 1.6, 2.0] - -for formatting in ["default", "niceplots"]: - if formatting == "niceplots": - niceplots.setRCParams() - colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] - - fig, axes = plt.subplots(nrows=len(TP), figsize=(12, 16)) - - for i in range(len(TP)): - tp = TP[i] - ax = axes[i] - x = MSPulseResponse(t, tp, omega) - line = ax.plot(t, x, clip_on=False, color=colors[i]) - ax.vlines(tp, -3, 1.0 - np.cos(omega * tp), linestyle="--", color="gray", zorder=0) - ax.set_xticks([0, tp, 3]) - if i == len(TP) - 1: - ax.set_xlabel("t (s)") - ax.set_ylabel(r"$\frac{x(t)}{x_s}$", ha="right", rotation="horizontal") - ax.set_ylim(bottom=-2.0, top=2.0) - if formatting == "niceplots": - niceplots.adjust_spines(ax, outward=True) - - plt.savefig(f"{formatting}PulseResponse.pdf") - plt.savefig(f"{formatting}PulseResponse.png", dpi=400) diff --git a/examples/plot_nested_pie_chart.py b/examples/plot_nested_pie_chart.py index 38ea37c..b93a4ed 100644 --- a/examples/plot_nested_pie_chart.py +++ b/examples/plot_nested_pie_chart.py @@ -4,9 +4,10 @@ An example of a nested pie chart. """ import matplotlib.pyplot as plt -from niceplots import setRCParams, plotNestedPie +import niceplots -setRCParams() +plt.style.use(niceplots.get_style("james-dark")) +colors = niceplots.get_colors() data = { "Pie": { @@ -25,12 +26,8 @@ }, } -# Custom colors -colors = ["#e86492", "#f0a43a", "#56b2f0"] - -fig, ax = plt.subplots(figsize=(13, 8)) -pieObjects = plotNestedPie(data, colors=colors, ax=ax) -ax.set_title("The best pies") +fig, ax = plt.subplots(figsize=(8, 8)) +pieObjects = niceplots.plot_nested_pie(data, colors=list(colors.values()), ax=ax) # Customize one of the wedges... pieObjects["Pizza"]["Cheese"]["wedge"].set_radius(1.1) @@ -40,5 +37,5 @@ pieObjects["Pizza"]["Cheese"]["text"].set_weight("bold") pieObjects["Pizza"]["Cheese"]["text"].set_x(-0.82) -plt.savefig("nested_pie_chart.pdf", bbox_inches="tight") -plt.savefig("nested_pie_chart.png", dpi=400, bbox_inches="tight") +plt.savefig("nested_pie_chart.png") +plt.savefig("nested_pie_chart.svg") diff --git a/examples/plot_optProb.py b/examples/plot_opt_prob.py similarity index 84% rename from examples/plot_optProb.py rename to examples/plot_opt_prob.py index 0f0ce85..88ab35c 100644 --- a/examples/plot_optProb.py +++ b/examples/plot_opt_prob.py @@ -1,7 +1,7 @@ """ Plotting optimization problem ============================= -An example of how to use the :func:`niceplots.utils.plotOptProb` function to plot a constrained 2D optimization problem. +An example of how to use the :func:`niceplots.utils.plot_opt_prob` function to plot a constrained 2D optimization problem. This example plots the 2D Rosenbrock function with a quadratic equality constraint and 3 circular inequality constraints, which has an optimum at (1, 1). """ @@ -20,8 +20,8 @@ # Extension modules # ============================================================================== -niceplots.setRCParams() -niceColors = niceplots.get_niceColors() +plt.style.use(niceplots.get_style()) +colors = niceplots.get_colors() def Rosenbrock(x, y): @@ -51,7 +51,7 @@ def eqCon(x, y): for conStyle in ["shaded", "hashed"]: fig, ax = plt.subplots(figsize=(8, 8)) ax.set_aspect("equal") - niceplots.plotOptProb( + niceplots.plot_opt_prob( Rosenbrock, xRange=[0, 1.5], yRange=[0, 1.5], @@ -72,7 +72,7 @@ def eqCon(x, y): optX, optY, "-o", - c=niceColors["Grey"], + c=colors["Axis"], markeredgecolor="w", linewidth=2.0, markersize=8, @@ -80,6 +80,5 @@ def eqCon(x, y): ) # Save figures - fig.savefig(f"optProb-{conStyle}.png", dpi=400) - fig.savefig(f"optProb-{conStyle}.pdf") -plt.show() + fig.savefig(f"opt_prob-{conStyle}.png") + fig.savefig(f"opt_prob-{conStyle}.svg") diff --git a/examples/plot_parula_contours.py b/examples/plot_parula_contours.py index 0bd9f64..3d9642b 100644 --- a/examples/plot_parula_contours.py +++ b/examples/plot_parula_contours.py @@ -16,8 +16,8 @@ import niceplots -niceplots.setRCParams() -niceColors = niceplots.get_niceColors() +plt.style.use(niceplots.get_style()) +colors = niceplots.get_colors() def f(x1, x2): @@ -28,32 +28,27 @@ def f(x1, x2): X1, X2 = np.meshgrid(x1, x2) minimum = [2.58199, 0] -q2fig, q2ax = plt.subplots() +q2fig, q2ax = plt.subplots(figsize=(8, 5)) q2ax.contour(X1, X2, f(X1, X2), cmap=niceplots.parula.parula_map, levels=40) q2ax.plot( minimum[0], minimum[1], clip_on=False, marker="o", - color=niceColors["Grey"], + color=colors["Axis"], markeredgecolor="w", linestyle="", markersize=12, ) q2ax.annotate( - "Local Minimum", - xy=minimum, - xytext=(-5, 10), - textcoords="offset points", - va="bottom", - ha="center", + "Local Minimum", xy=minimum, xytext=(-5, 10), textcoords="offset points", va="bottom", ha="center", fontsize="small" ) q2ax.plot( -minimum[0], minimum[1], clip_on=False, marker="o", - color=niceColors["Grey"], + color=colors["Axis"], markeredgecolor="w", linestyle="", markersize=12, @@ -65,7 +60,8 @@ def f(x1, x2): textcoords="offset points", va="top", ha="center", - color=niceColors["Grey"], + color=colors["Axis"], + fontsize="small", ) niceplots.adjust_spines(q2ax, outward=True) q2ax.set_xlabel("$x_1$") @@ -74,4 +70,6 @@ def f(x1, x2): q2ax.set_ylim(bottom=min(x2), top=max(x2)) q2ax.set_yticks([min(x2), 0, minimum[-1], max(x2)]) q2ax.set_ylabel("$x_2$", rotation="horizontal", ha="right") -plt.savefig("ParulaContours.png", dpi=400) + +plt.savefig("parula_contours.png") +plt.savefig("parula_contours.svg") diff --git a/examples/plot_stacks.py b/examples/plot_stacks.py index 3f798a9..0a2d282 100644 --- a/examples/plot_stacks.py +++ b/examples/plot_stacks.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt import niceplots -niceplots.setRCParams() +plt.style.use(niceplots.get_style()) # Set the random seed to get consistent results. np.random.seed(314) @@ -37,8 +37,8 @@ # You can set some plot options here as well, like the pad distance for the # y-labels from the y-axes if you have long labels. # This function will save a pdf with the filename given. -niceplots.stacked_plots("Time (s)", time, data[0], figsize=(10, 6), filename="opt_stacks.pdf") -plt.savefig("opt_stacks.png", dpi=400) +f, axarr = niceplots.stacked_plots("Time (s)", time, data[0], figsize=(10, 6), filename="opt_stacks.png") +f.savefig("opt_stacks.svg") # stacked_plots can also accept a list of dicts to plot multiple strains # of data on the same set of plots. @@ -50,6 +50,6 @@ data, figsize=(10, 6), line_scaler=0.5, - filename="opt_stacks_more_data.pdf", + filename="opt_stacks_more_data.png", ) -plt.savefig("opt_stacks_more_data.png", dpi=400) +f.savefig("opt_stacks_more_data.svg") diff --git a/examples/plot_style_demo.py b/examples/plot_style_demo.py new file mode 100644 index 0000000..193bd81 --- /dev/null +++ b/examples/plot_style_demo.py @@ -0,0 +1,94 @@ +""" +============================================================================== +NicePlots Style Demo +============================================================================== +This script demonstrates the styles available in NicePlots and compares them +to the default matplotlib style. The script generates a plot of a Gaussian +distributions with some random "measured" data points and a shaded area +indicating a +/- 1 std dev region. The plot is loosely based on the plot shown +on page 147 of Jean Luc Doumont's "Trees, maps, and theorems" book. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +import matplotlib.pyplot as plt +import niceplots +import numpy as np + +# ============================================================================== +# Extension modules +# ============================================================================== + + +def gaussian(x, mu, sig): + return 1.0 / (np.sqrt(2.0 * np.pi) * sig) * np.exp(-np.power((x - mu) / sig, 2.0) / 2) + + +np.random.seed(0) + +xMin = -3 +xMax = 3 + +# Generate analytic gaussian distribution +xLine = np.linspace(xMin, xMax, 1000) +yLine = gaussian(xLine, 0, 1) + +# Generate some fake "measured" data points with some noise +xRand = np.random.uniform(xMin, xMax, 45) +yRand = gaussian(xRand, 0, 1) + np.random.normal(0, 0.02, len(xRand)) +yRand[yRand < 0] += 0.05 + +# Create a version of the plot with each niceplots style and the default matplotlib style +for formatting in ["default"] + niceplots.get_available_styles(): + with plt.style.context(niceplots.get_style(formatting)): + + # If using a niceplots style, we can get a nice dictionary of the style's colors + if formatting != "default": + colours = niceplots.get_colors() + + fig, ax = plt.subplots() + + # Plotting tip #1, only add axis ticks at important values + ax.set_yticks([0, 0.4]) + ax.set_ylim(bottom=0, top=0.4) + ax.set_xticks([xMin, -1, 0, 1, xMax]) + + # Test out some LaTeX in the axis labels + ax.set_xlabel("Some variable, $x$") + ax.set_ylabel("$\\mathbb{E}(x,\\mu=0, \\sigma=1)$", rotation="horizontal", ha="right") + + # Plot the analytic and measured data, store the returned objects so we can get their colours later + (line,) = plt.plot(xLine, yLine, clip_on=False) + (markers,) = plt.plot(xRand, yRand, "o", clip_on=False) + + # Plotting tip #2, instead of a legend, put colored labels directly next to the data + ax.annotate("Calculated", xy=(-1.2, 0.2), ha="right", va="bottom", color=line.get_color()) + ax.annotate("Measured", xy=(0.7, 0.395), ha="left", va="top", color=markers.get_color()) + + # Plot the shaded area indicating the +/- 1 std dev region. + # If using a niceplots style, we can use the colours dictionary to make the region the same color as the axes, + # otherwise we'll just use gray + if formatting == "default": + fill_color = "gray" + else: + fill_color = colours["Axis"] + + ax.fill_between(xLine, yLine, 0, where=np.abs(xLine) <= 1, facecolor=fill_color, alpha=0.2, zorder=0) + ax.annotate("68.27%", xy=(0, 0.075), ha="center", va="bottom", color=fill_color) + plt.annotate("", xy=(-1, 0.07), xytext=(1.0, 0.07), arrowprops=dict(arrowstyle="<|-|>", color=fill_color)) + + fig.suptitle(f"{formatting} style") + + # Plotting tip #3, use the niceplots adjust_spines function to space the axes out nicely + if formatting != "default": + niceplots.adjust_spines(ax) + else: + plt.tight_layout() + + fig.savefig(f"{formatting}_style_demo.svg") + fig.savefig(f"{formatting}_style_demo.png") diff --git a/examples/ref/bar_chart.png b/examples/ref/bar_chart.png new file mode 100644 index 0000000..c4d734d Binary files /dev/null and b/examples/ref/bar_chart.png differ diff --git a/examples/ref/colored_line.png b/examples/ref/colored_line.png new file mode 100644 index 0000000..077c0a0 Binary files /dev/null and b/examples/ref/colored_line.png differ diff --git a/examples/ref/colored_line_custom_norm.png b/examples/ref/colored_line_custom_norm.png new file mode 100644 index 0000000..1925837 Binary files /dev/null and b/examples/ref/colored_line_custom_norm.png differ diff --git a/examples/ref/default_style_demo.png b/examples/ref/default_style_demo.png new file mode 100644 index 0000000..3b19e5d Binary files /dev/null and b/examples/ref/default_style_demo.png differ diff --git a/examples/ref/doumont-dark_style_demo.png b/examples/ref/doumont-dark_style_demo.png new file mode 100644 index 0000000..1302582 Binary files /dev/null and b/examples/ref/doumont-dark_style_demo.png differ diff --git a/examples/ref/doumont-light_style_demo.png b/examples/ref/doumont-light_style_demo.png new file mode 100644 index 0000000..7cc85ad Binary files /dev/null and b/examples/ref/doumont-light_style_demo.png differ diff --git a/examples/ref/james-dark_style_demo.png b/examples/ref/james-dark_style_demo.png new file mode 100644 index 0000000..9306f5d Binary files /dev/null and b/examples/ref/james-dark_style_demo.png differ diff --git a/examples/ref/james-light_style_demo.png b/examples/ref/james-light_style_demo.png new file mode 100644 index 0000000..15fbb2b Binary files /dev/null and b/examples/ref/james-light_style_demo.png differ diff --git a/examples/ref/nested_pie_chart.png b/examples/ref/nested_pie_chart.png new file mode 100644 index 0000000..3a6459b Binary files /dev/null and b/examples/ref/nested_pie_chart.png differ diff --git a/examples/ref/opt_prob-hashed.png b/examples/ref/opt_prob-hashed.png new file mode 100644 index 0000000..f6dc277 Binary files /dev/null and b/examples/ref/opt_prob-hashed.png differ diff --git a/examples/ref/opt_prob-shaded.png b/examples/ref/opt_prob-shaded.png new file mode 100644 index 0000000..b5ce355 Binary files /dev/null and b/examples/ref/opt_prob-shaded.png differ diff --git a/examples/ref/opt_stacks.png b/examples/ref/opt_stacks.png new file mode 100644 index 0000000..8f65a32 Binary files /dev/null and b/examples/ref/opt_stacks.png differ diff --git a/examples/ref/opt_stacks_more_data.png b/examples/ref/opt_stacks_more_data.png new file mode 100644 index 0000000..d5853db Binary files /dev/null and b/examples/ref/opt_stacks_more_data.png differ diff --git a/examples/ref/parula_contours.png b/examples/ref/parula_contours.png new file mode 100644 index 0000000..20af10c Binary files /dev/null and b/examples/ref/parula_contours.png differ diff --git a/examples/ref/style_color_demo.png b/examples/ref/style_color_demo.png new file mode 100644 index 0000000..b011458 Binary files /dev/null and b/examples/ref/style_color_demo.png differ diff --git a/niceplots/__init__.py b/niceplots/__init__.py index 710a419..818f157 100644 --- a/niceplots/__init__.py +++ b/niceplots/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.3.0" +__version__ = "2.0.0" from .utils import * from .parula import * diff --git a/niceplots/styles/README.md b/niceplots/styles/README.md new file mode 100644 index 0000000..8fe0124 --- /dev/null +++ b/niceplots/styles/README.md @@ -0,0 +1,15 @@ +New styles can be added here by making a new file. + +### File naming + +The filename must end in `.mplstyle`. +Whatever is before `.mplstyle` will be the string used to reference the style when users call `setStyle` or `styleContext`. + +### File contents + +There are only two NicePlots-specific stylesheet requirements: + +1. The color cycle is specified using the `axes.prop_cycle` rcParam. +2. The name of each color in the color cycle is specified in the `keymap.help` rcParam. + +The existing stylesheets can be used as a model. diff --git a/niceplots/styles/doumont-dark.mplstyle b/niceplots/styles/doumont-dark.mplstyle new file mode 100644 index 0000000..f49c34d --- /dev/null +++ b/niceplots/styles/doumont-dark.mplstyle @@ -0,0 +1,55 @@ +# Figure settings +savefig.dpi : 600 +figure.constrained_layout.use : True + + +# Axes settings +axes.spines.top : False +axes.spines.right : False +axes.labelpad : 8.0 +axes.xmargin : 0 +axes.ymargin : 0 + + +# Font settings +font.family : sans-serif +font.sans-serif : CMU Bright +axes.unicode_minus : False +font.size : 18 + +axes.labelweight : bold + +xtick.labelsize : small +ytick.labelsize : small + +text.latex.preamble : r"\usepackage{cmbright}" +mathtext.default: regular +pgf.rcfonts : True + + +# Legend settings +legend.columnspacing : 0.2 +legend.frameon : False + + +# Line settings +lines.linewidth : 2.0 +lines.markeredgewidth : 1.0 + + +# Color settings +axes.prop_cycle : cycler('color', ['e29400ff', '1E90FF', 'E21A1A', '00a650ff', '800000ff', 'ff8f00', '800080ff', '00A6D6', 'ffffffff']) + +# Use this parameter, which (hopefully) nobody will ever use, +# to store text the names of the colors in the color cycle +keymap.help : Yellow, Blue, Red, Green, Maroon, Orange, Purple, Cyan, White + + +lines.markeredgecolor : k +axes.edgecolor : ebebeb +text.color : ebebeb +axes.labelcolor : ebebeb +xtick.color : ebebeb +ytick.color : ebebeb +axes.facecolor : k +figure.facecolor : k diff --git a/niceplots/styles/doumont-light.mplstyle b/niceplots/styles/doumont-light.mplstyle new file mode 100644 index 0000000..34daa08 --- /dev/null +++ b/niceplots/styles/doumont-light.mplstyle @@ -0,0 +1,50 @@ +# Figure settings +savefig.dpi : 600 +figure.constrained_layout.use : True + + +# Axes settings +axes.spines.top : False +axes.spines.right : False +axes.labelpad : 8.0 +axes.xmargin : 0 +axes.ymargin : 0 + + +# Font settings +font.family : sans-serif +font.sans-serif : CMU Bright +axes.unicode_minus : False +font.size : 18 + +axes.labelweight : bold + +xtick.labelsize : small +ytick.labelsize : small + +text.latex.preamble : r"\usepackage{cmbright}" +mathtext.default: regular +pgf.rcfonts : True + + +# Legend settings +legend.columnspacing : 0.2 +legend.frameon : False + + +# Line settings +lines.linewidth : 2.0 +lines.markeredgewidth : 1.0 + +# Color settings +axes.prop_cycle : cycler('color', ['e29400ff', '1E90FF', 'E21A1A', '00a650ff', '800000ff', 'ff8f00', '800080ff', '00A6D6', '000000ff']) + +# Use this parameter, which (hopefully) nobody will ever use, to store text the names of the colors in the color cycle +keymap.help : Yellow, Blue, Red, Green, Maroon, Orange, Purple, Cyan, Black + +lines.markeredgecolor : w +axes.edgecolor : 5a5758ff +text.color : 5a5758ff +axes.labelcolor : 5a5758ff +xtick.color : 5a5758ff +ytick.color : 5a5758ff diff --git a/niceplots/styles/james-dark.mplstyle b/niceplots/styles/james-dark.mplstyle new file mode 100644 index 0000000..c907a88 --- /dev/null +++ b/niceplots/styles/james-dark.mplstyle @@ -0,0 +1,35 @@ +font.family : sans-serif +font.sans-serif : Prompt +font.weight : 300 +axes.unicode_minus : True +font.size : 16 +figure.dpi : 100 +savefig.dpi : 600 +axes.spines.top : False +axes.spines.right : False +mathtext.default: regular + +legend.columnspacing : 0.2 +legend.frameon : False +figure.constrained_layout.use : True + +lines.linewidth : 2.0 + +axes.prop_cycle : cycler('color', ['52a1fa', 'f26f6f', 'd6d6d6', 'c678fa', '78b865', 'e6ad53']) + +# Use this parameter, which (hopefully) nobody will ever use, +# to store text the names of the colors in the color cycle +keymap.help : Blue, Red, White, Purple, Green, Orange + +axes.edgecolor : bfd0e3 +text.color : f5f5f5 +axes.labelcolor : f5f5f5 +axes.labelweight : 500 +xtick.color : bfd0e3 +ytick.color : bfd0e3 +xtick.labelsize : small +ytick.labelsize : small +axes.facecolor : 011d3b +figure.facecolor : 011d3b + +pgf.rcfonts : True diff --git a/niceplots/styles/james-light.mplstyle b/niceplots/styles/james-light.mplstyle new file mode 100644 index 0000000..f1ba0db --- /dev/null +++ b/niceplots/styles/james-light.mplstyle @@ -0,0 +1,35 @@ +font.family : sans-serif +font.sans-serif : Prompt +font.weight : 300 +axes.unicode_minus : True +font.size : 16 +figure.dpi : 100 +savefig.dpi : 600 +axes.spines.top : False +axes.spines.right : False +mathtext.default: regular + +legend.columnspacing : 0.2 +legend.frameon : False +figure.constrained_layout.use : True + +lines.linewidth : 2.0 + +axes.prop_cycle : cycler('color', ['52a1fa', 'f26f6f', '485263', 'ae66de', '3eb051', 'faaa48']) + +# Use this parameter, which (hopefully) nobody will ever use, +# to store text the names of the colors in the color cycle +keymap.help : Blue, Red, Navy, Purple, Green, Orange + +axes.edgecolor : 3b4c66 +text.color : 273345 +axes.labelcolor : 273345 +axes.labelweight : 500 +xtick.color : 3b4c66 +ytick.color : 3b4c66 +xtick.labelsize : small +ytick.labelsize : small +axes.facecolor : none +figure.facecolor : none + +pgf.rcfonts : True diff --git a/niceplots/utils.py b/niceplots/utils.py index 6cb449a..17569d2 100644 --- a/niceplots/utils.py +++ b/niceplots/utils.py @@ -1,127 +1,124 @@ import matplotlib.pyplot as plt import numpy as np -from cycler import cycler from collections import OrderedDict from .parula import parula_map from matplotlib import patheffects from matplotlib.collections import LineCollection import matplotlib.colors as mcolor import warnings +import os -def setRCParams(dark_mode=False, set_dark_background=False): +def get_style(styleName="doumont-light"): """ - Set some defaults for generating nice, Doumont-esque plots. + Get the stylesheet to pass to matplotlib's style setting functions. This function + works both with niceplots styles and matplotlib's built-in styles. Usage examples:: + + import matplotlib.pyplot as plt + import niceplots + + plt.style.use(niceplots.get_style()) + plt.plot([0, 1], [0, 1]) + + # Or you can use it within a context manager + with plt.style.context(niceplots.get_style()): + plt.plot([0, 1], [0, 1]) + + # Also try different styles + plt.style.use(niceplots.get_style("james-dark")) # niceplots james dark style + plt.style.use(niceplots.get_style("default")) # matplotlib default style Parameters ---------- - dark_mode (optional) : bool - If true, sets axes, labels, etc. to white so the plot - can be used on a dark background. - NOTE: Unless you are explicitly saving the plot with - a transparent background (e.g. as a png with - transparent=True), also set the - set_dark_background option. - set_dark_background (optional) : bool or str - If true, sets the axis and figure backgrounds to black. - This option can also be set to a color string, such as - "#aaaaaa", to use a color other than black. + styleName : str, optional + Name of desired style. By default uses doumont-light style. Avaiable styles are: + + - doumont-light: the niceplots style you know and love + - doumont-dark: the dark version of the niceplots style you know and love + - james-dark: a really cool alternative to classic niceplots + - james-light: a version of james with a light background, naturally + + Returns + ------- + str + The style string to be passed to one of matplotlib's style setting functions. """ - plt.rcParams["font.family"] = "sans-serif" - plt.rcParams["font.sans-serif"] = ["CMU Bright"] - plt.rcParams["axes.unicode_minus"] = False - plt.rcParams["font.size"] = 24 - plt.rcParams["figure.dpi"] = 100 - plt.rcParams["figure.figsize"] = [12, 6.75] - plt.rcParams["savefig.dpi"] = 600 - plt.rcParams["axes.spines.top"] = False - plt.rcParams["axes.spines.right"] = False - plt.rcParams["axes.labelpad"] = 8.0 - plt.rcParams["text.latex.preamble"] = r"\usepackage{cmbright}" - - plt.rcParams["legend.columnspacing"] = 0.2 - plt.rcParams["legend.frameon"] = False - plt.rcParams["figure.constrained_layout.use"] = True - - plt.rcParams["patch.edgecolor"] = "w" - - plt.rcParams["axes.spines.top"] = False - plt.rcParams["axes.spines.right"] = False - - plt.rcParams["axes.autolimit_mode"] = "round_numbers" - plt.rcParams["axes.xmargin"] = 0 - plt.rcParams["axes.ymargin"] = 0 - - plt.rcParams["lines.linewidth"] = 2.0 - plt.rcParams["lines.markeredgewidth"] = 1.0 - plt.rcParams["lines.markeredgecolor"] = "w" - - niceColors = get_niceColors() - plt.rcParams["axes.prop_cycle"] = cycler("color", list(niceColors.values())[:-2]) - - # Color for axes, labels, ticks, text, etc. - color = niceColors["Grey"] - if dark_mode: - plt.rcParams["patch.edgecolor"] = "#000000" - color = "#ffffff" # white - - plt.rcParams["axes.edgecolor"] = color - plt.rcParams["text.color"] = color - plt.rcParams["axes.labelcolor"] = color - plt.rcParams["axes.labelweight"] = 200 - plt.rcParams["xtick.color"] = color - plt.rcParams["ytick.color"] = color - - # Set the axis and figure background color if necessary - if isinstance(set_dark_background, bool): - if set_dark_background: - # Set background to black - plt.rcParams["axes.facecolor"] = "#000000" - plt.rcParams["figure.facecolor"] = "#000000" - elif isinstance(set_dark_background, str): - # Set background to set_dark_background - plt.rcParams["axes.facecolor"] = set_dark_background - plt.rcParams["figure.facecolor"] = set_dark_background - plt.rcParams["patch.edgecolor"] = set_dark_background - else: - raise TypeError(f"set_dark_background value of {set_dark_background} is invalid") - - -def get_niceColors(): - # Define an ordered dictionary of some nice Doumont style colors to use as the default color cycle - niceColors = OrderedDict() - niceColors["Yellow"] = "#e29400ff" # '#f8a30dff' - niceColors["Blue"] = "#1E90FF" - niceColors["Red"] = "#E21A1A" - niceColors["Green"] = "#00a650ff" - niceColors["Maroon"] = "#800000ff" - niceColors["Orange"] = "#ff8f00" - niceColors["Purple"] = "#800080ff" - niceColors["Cyan"] = "#00A6D6" - niceColors["Black"] = "#000000ff" - # The 2 colours below are not used in the colour cycle as they are too close to the other colours. - # Grey is kept in the dictionary as it is used as the default axis/tick colour. - # RedOrange is the old Orange, and is kept because I think it looks nice. - niceColors["Grey"] = "#5a5758ff" - niceColors["RedOrange"] = "#E21A1A" - - return niceColors - - -def get_delftColors(): - # Define an ordered dictionary of the official TU Delft colors to use as the default color cycle - delftColors = OrderedDict() - delftColors["Cyan"] = "#00A6D6" # '#f8a30dff' - delftColors["Yellow"] = "#E1C400" - delftColors["Purple"] = "#6D177F" - delftColors["Red"] = "#E21A1A" - delftColors["Green"] = "#A5CA1A" - delftColors["Blue"] = "#1D1C73" - delftColors["Orange"] = "#E64616" - delftColors["Grey"] = "#5a5758ff" - delftColors["Black"] = "#000000ff" - - return delftColors + # If the style is a niceplots style, return the file path + if styleName in get_available_styles(): + curDir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(curDir, "styles", styleName + ".mplstyle") + + # Otherwise assume it's a matplotlib style and just return the style name + return styleName + + +def get_colors(): + """ + Get a dictionary with the colors for the current style. This function + only works when niceplots styles are used (not built-in matplotlib ones). + + Returns + ------- + dict + Dictionary of the colors for the requested style. The keys + are human-readable names and the keys are the hex codes. It + also adds color names from the rcParams that are generally useful: + + - "Axis": axis spine color + - "Background" axis background color + - "Text": default text color + - "Label": axis label color + """ + # Get the color codes and their names from the (hopefully) "special" parameter + color_codes = get_colors_list() + color_names = plt.rcParams["keymap.help"] + + # Ensure that the amount of color names matches the amount of colors + if len(color_codes) != len(color_names): + raise ValueError( + "The colors are not properly named in the stylesheet, please open an issue on GitHub with the details!" + ) + + colors = OrderedDict(zip(color_names, color_codes)) + colors["Axis"] = plt.rcParams["axes.edgecolor"] + colors["Background"] = plt.rcParams["axes.facecolor"] + colors["Text"] = plt.rcParams["text.color"] + colors["Label"] = plt.rcParams["axes.labelcolor"] + + return colors + + +def get_colors_list(): + """ + Get a list with the colors for the current style. This function + works with all matplotlib styles. + + Returns + ------- + list + List of the colors for the requested style. + """ + return plt.rcParams["axes.prop_cycle"].by_key()["color"] + + +def get_available_styles(): + """ + Get a list of the names of styles available. + + Returns + ------- + list + The names of the available styles. + """ + curDir = os.path.dirname(os.path.abspath(__file__)) + styleFilenames = os.listdir(os.path.join(curDir, "styles")) + styles = [] + for s in styleFilenames: + name, ext = os.path.splitext(s) + if ext == ".mplstyle": + styles.append(name) + styles.sort() # alphabetize + return styles def handle_close(evt): @@ -227,10 +224,11 @@ def horiz_bar(labels, times, header, nd=1, size=[5, 0.5], color=None): Axes on which data is plotted """ - # Use niceColours yellow if no colour specified - niceColours = get_niceColors() + # Use the first color if none is specified if color is None: - color = niceColours["Yellow"] + color = get_colors_list()[0] + style_colors = get_colors() + line_color = style_colors["Axis"] # Obtain parameters to size the chart correctly num = len(times) @@ -252,7 +250,7 @@ def horiz_bar(labels, times, header, nd=1, size=[5, 0.5], color=None): for j, (l, t, ax) in enumerate(zip(labels, times, axarr)): # Draw the gray line and singular yellow dot - ax.axhline(y=1, c=niceColours["Grey"], lw=3, zorder=0, alpha=0.5) + ax.axhline(y=1, c=line_color, lw=3, zorder=0, alpha=0.5) ax.scatter([t], [1], c=color, lw=0, s=100, zorder=1, clip_on=False) # Set chart properties @@ -308,7 +306,7 @@ def stacked_plots( data_dict_list = [data_dict_list] if colors is None: - colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + colors = get_colors_list() data_dict = data_dict_list[0] n = len(data_dict) @@ -377,7 +375,7 @@ def stacked_plots( return f, axarr -def plotOptProb( +def plot_opt_prob( obj, xRange, yRange, @@ -471,7 +469,7 @@ def plotOptProb( cmap = parula_map if colors is None: - colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + colors = get_colors_list() nColor = len(colors) # --- Create grid of points for evaluating functions --- @@ -533,7 +531,7 @@ def plotOptProb( return -def plotColoredLine( +def plot_colored_line( x, y, c, cmap=None, fig=None, ax=None, addColorBar=False, cRange=None, cBarLabel=None, norm=None, **kwargs ): """Plot an XY line whose color is determined by some other variable C @@ -611,7 +609,7 @@ def plotColoredLine( return -def plotNestedPie( +def plot_nested_pie( data, colors=None, alphas=None, @@ -641,7 +639,8 @@ def plotNestedPie( colors : str or list of str with hex colors, optional Colors to use for the inner wedges. Can either specify a qualitative matplotlib colormap (it will assume this is the case if a string is specified), or a list of colors specified with hex codes (e.g., "#F4A103"), - by default will use nice colors (niceplots default) + by default will use nice colors (niceplots default). Loops through the colors if more categories than + colors are specified. alphas : iterable of floats at least as long as the max number of subcategories for a given category Transparencies to use to vary the color in the outer categories ax : matplotlib axes object, optional @@ -682,12 +681,13 @@ def plotNestedPie( ax : matplotlib axes object Axis with the colored line. Returned only if no input ax object is specified """ - # If colors is not specified, turn the niceColors into a list of hex colors + # If colors is not specified, turn the style's colors into a list of hex colors if colors is None: - colors = [c for c in get_niceColors().values()] + colors = [c for c in get_colors().values()] # If colors is given as a qualitative matplotlib colormap, turn it into a list of hex colors elif isinstance(colors, str): colors = [mcolor.rgb2hex(plt.colormaps[colors](i)) for i in range(len(data))] + numColors = len(colors) # Go through the colors and only take the color information (not transparency) for i in range(len(colors)): @@ -720,13 +720,13 @@ def plotNestedPie( if alphas is None: alphas = np.linspace(0.75, 0.95, maxSubcat)[-1::-1] - innerColors = [colors[i] for i in range(len(data))] + innerColors = [colors[i % numColors] for i in range(len(data))] outerColors = [] iCat = 0 for catVals in data.values(): numSubcats = len(catVals) for iSubcat in range(numSubcats): - outerColors.append(colors[iCat] + float.hex(alphas[iSubcat])[4:6]) + outerColors.append(colors[iCat % numColors] + float.hex(alphas[iSubcat])[4:6]) iCat += 1 # Nested plot fitting params diff --git a/setup.py b/setup.py index aca5e6b..8242fca 100644 --- a/setup.py +++ b/setup.py @@ -29,4 +29,6 @@ "numpy>=1.16", "matplotlib>=2.2", ], + include_package_data=True, + package_data={"": ["styles/*.mplstyle"]}, )