Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,13 @@ ultraplot/_version.py

# Nox build directories
.nox/*

# Generated docs and draw.io assets. The edited diagram remains a repository asset.
tools/cheatsheet/assets/
docs/_static/plot_types/
tools/cheatsheet/ultraplot_cheatsheet.png
tools/cheatsheet/ultraplot_cheatsheet-code.png
tools/cheatsheet/ultraplot_cheatsheet-reference.png
tools/cheatsheet/ultraplot_cheatsheet-beginner.png
tools/cheatsheet/ultraplot_cheatsheet-intermediate.png
tools/cheatsheet/ultraplot_cheatsheet-advanced.png
2 changes: 2 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ version: 2
# Set the OS and build tools
build:
os: ubuntu-22.04
apt_packages:
- xvfb
tools:
python: "mambaforge-latest"

Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Cheatsheet sources and exports are repository assets, not pip distribution files.
prune tools/cheatsheet
137 changes: 137 additions & 0 deletions docs/_ext/drawio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from __future__ import annotations

import base64
import html
import json
from pathlib import Path
import urllib.parse
import xml.etree.ElementTree as ET
import zlib

from docutils import nodes
from docutils.parsers.rst import Directive, directives


def _decode_diagram(diagram: ET.Element) -> str:
"""Return an mxGraphModel XML string from a <diagram> element."""

# Uncompressed draw.io files can contain mxGraphModel directly.
graph = diagram.find("mxGraphModel")
if graph is not None:
return ET.tostring(graph, encoding="unicode")

# Normal compressed draw.io representation:
# base64 -> raw DEFLATE -> URL decode
encoded = (diagram.text or "").strip()

if not encoded:
raise ValueError("Draw.io diagram contains no data")

compressed = base64.b64decode(encoded)
inflated = zlib.decompress(compressed, -15).decode("utf-8")

return urllib.parse.unquote(inflated)


def _get_page(path: Path, selector: str | None) -> str:
root = ET.parse(path).getroot()
diagrams = root.findall("diagram")

if not diagrams:
raise ValueError(f"No diagrams found in {path}")

# Default to first page.
if selector is None:
return _decode_diagram(diagrams[0])

# Numeric selector = page index.
try:
index = int(selector)
except ValueError:
index = None

if index is not None:
try:
return _decode_diagram(diagrams[index])
except IndexError:
raise ValueError(
f"Page index {index} does not exist in {path}"
) from None

# Otherwise treat selector as page name.
for diagram in diagrams:
if diagram.get("name") == selector:
return _decode_diagram(diagram)

names = [d.get("name", "<unnamed>") for d in diagrams]
raise ValueError(
f"Page {selector!r} not found in {path}. "
f"Available pages: {', '.join(names)}"
)


class DrawioDirective(Directive):
required_arguments = 1
has_content = False

option_spec = {
"page": directives.unchanged,
"class": directives.class_option,
}

def run(self):
env = self.state.document.settings.env

# Resolve relative to the .rst file containing the directive.
source = Path(env.doc2path(env.docname)).resolve()
path = (source.parent / self.arguments[0]).resolve()

if not path.exists():
raise self.error(f"Draw.io file not found: {path}")

# Tell Sphinx that changes to the .drawio file should rebuild this page.
env.note_dependency(str(path))

try:
xml = _get_page(path, self.options.get("page"))
except (ET.ParseError, ValueError) as exc:
raise self.error(str(exc)) from exc

config = {
"xml": xml,
"resize": True,
"fit": True,
"nav": False,
"lightbox": False,
"toolbar": "",
}

classes = ["mxgraph", *self.options.get("class", [])]

# data-mxgraph is an HTML attribute, so escape the complete JSON string.
payload = html.escape(
json.dumps(config, separators=(",", ":")),
quote=True,
)

markup = (
f'<div class="{" ".join(classes)}" '
f'data-mxgraph="{payload}"></div>'
)

return [nodes.raw("", markup, format="html")]


def setup(app):
app.add_directive("drawio", DrawioDirective)

# Official diagrams.net viewer.
app.add_js_file(
"https://viewer.diagrams.net/js/viewer-static.min.js"
)

return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
34 changes: 34 additions & 0 deletions docs/_scripts/build_plot_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""
Regenerate the visual plot-type index before a documentation build.

Run from ``conf.py`` the same way ``fetch_releases.py`` is: the page and its
thumbnails are generated artefacts, so a clean checkout builds them rather than
carrying 60-odd PNGs in the repository. Rendering is skipped when the icons are
already present, so a local rebuild costs nothing.
"""

import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
GENERATOR = os.path.join(ROOT, "tools", "cheatsheet")

sys.path.insert(0, GENERATOR)


def main():
try:
import docs_index
except ImportError as error: # the tools folder is not shipped in sdists
print(f"plot-type index skipped: {error}")
return
try:
docs_index.main()
except Exception as error: # never fail the docs build over a thumbnail
print(f"plot-type index skipped: {type(error).__name__}: {error}")


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions docs/cheatsheet.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Cheat Sheet
===========

They say don't change a winning team. With UltraPlot we use Matplotlib's object orientated syntax, and
add to it essential quality of life improvements to make high quality publication ready plots.
To highlight the capabilities in a bird's eye view, we have included a few Matplotlib style cheat sheets below.


================
General Overview
================

.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 1

=================
Every day recipes
=================
.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 2


===============
Quick Reference
===============
.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 3

=========
Beginners
=========
.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 4

============
Intermediate
============
.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 5


======
Expert
======
.. drawio:: ../tools/cheatsheet/ultraplot_cheatsheet.drawio
:page: 6

9 changes: 9 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def __getattr__(self, name):
}
if not FAST_PREVIEW:
run([sys.executable, "_scripts/fetch_releases.py"], check=False)
# Visual plot-type index: thumbnails plus the page that arranges them.
run([sys.executable, "_scripts/build_plot_types.py"], check=False)

# Docs theme selector. Default to Shibuya, but keep env override for A/B checks.
DOCS_THEME = os.environ.get("UPLT_DOCS_THEME", "shibuya").strip().lower()
Expand Down Expand Up @@ -244,9 +246,16 @@ def _reset_ultraplot(gallery_conf, fname):
"sphinx_automodapi.automodapi", # fork of automodapi
"sphinx_copybutton", # add copy button to code
"_ext.notoc",
"_ext.drawio",
"nbsphinx", # parse rst books
"sphinx_gallery.gen_gallery",
]

drawio_headless = "auto"
drawio_builder_export_format = {
"html": "svg",
}

if not FAST_PREVIEW:
extensions.append("sphinx_sitemap")
if HAVE_ULTRAPLOT_THEME_EXT:
Expand Down
1 change: 0 additions & 1 deletion docs/examples/plot_types/05_box_violin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,4 @@
axs[1].format(title="Violin plot", xlabel="Distribution", ylabel="Value")

axs.format(suptitle="Statistical distributions")
uplt.show(block=1)
fig.show()
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ For details, see the full :doc:`User guide <usage>` and
usage
recipes
gallery/index
cheatsheet

.. toctree::
:maxdepth: 1
:caption: Guides
:hidden:

basics
plot_types
subplots
cartesian
data_aware
Expand Down
Loading