diff --git a/docs/_ext/notoc.py b/docs/_ext/notoc.py new file mode 100644 index 000000000..ccdb8e474 --- /dev/null +++ b/docs/_ext/notoc.py @@ -0,0 +1,21 @@ +from docutils.parsers.rst import Directive +from docutils import nodes + + +class NoTocDirective(Directive): + has_content = False + + def run(self): + # Create a raw HTML node to add the no-right-toc class to body + html = '' + return [nodes.raw("", html, format="html")] + + +def setup(app): + app.add_directive("notoc", NoTocDirective) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..d6a77e4f2 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,188 @@ +.grid-item-card .card-img-top { + height: 100%; + object-fit: cover; + width: 100%; + background-color: slategrey; +} + +/* Make all cards with this class use flexbox for vertical layout */ +.card-with-bottom-text { + display: flex !important; + flex-direction: column !important; + height: 100% !important; +} + +/* Style the card content areas */ +.card-with-bottom-text .sd-card-body { + display: flex !important; + flex-direction: column !important; + flex-grow: 1 !important; +} + +/* Make images not grow or shrink */ +.card-with-bottom-text img { + flex-shrink: 0 !important; + margin-bottom: 0.5rem !important; +} + +/* Push the last paragraph to the bottom */ +.card-with-bottom-text .sd-card-body > p:last-child { + margin-top: auto !important; + padding-top: 0.5rem !important; + text-align: center !important; +} + +.img-container img { + object-fit: cover; + width: 100%; + height: 100%; +} + +.right-toc { + position: fixed; + top: 90px; + right: 20px; + width: 280px; + font-size: 0.9em; + max-height: calc(100vh - 150px); + background-color: #f8f9fa; + z-index: 100; + border-radius: 6px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + border-left: 3px solid #2980b9; +} + +.right-toc-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 15px; + border-bottom: 1px solid #e1e4e5; +} + +.right-toc-title { + font-weight: 600; + font-size: 1.1em; + color: #2980b9; +} + +.right-toc-buttons { + display: flex; + align-items: center; +} + +.right-toc-toggle-btn { + background: none; + border: none; + color: #2980b9; + font-size: 16px; + cursor: pointer; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + padding: 0; + transition: background-color 0.2s; +} + +.right-toc-toggle-btn:hover { + background-color: rgba(41, 128, 185, 0.1); +} + +.right-toc-content { + padding: 15px 15px 15px 20px; + overflow-y: auto; + max-height: calc(100vh - 200px); +} + +.right-toc-list { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +.right-toc-link { + display: block; + padding: 5px 0; + text-decoration: none; + color: #404040; + border-radius: 4px; + transition: all 0.2s ease; + margin-bottom: 3px; +} + +.right-toc-link:hover { + background-color: rgba(41, 128, 185, 0.1); + padding-left: 5px; + color: #2980b9; +} + +.right-toc-level-h1 { + font-weight: 600; + font-size: 1em; +} + +.right-toc-level-h2 { + padding-left: 1.2em; + font-size: 0.95em; +} + +.right-toc-level-h3 { + padding-left: 2.4em; + font-size: 0.9em; + color: #606060; +} + +/* Active TOC item highlighting */ +.right-toc-link.active { + background-color: rgba(41, 128, 185, 0.15); + color: #2980b9; + font-weight: 500; + padding-left: 5px; +} + +/* Collapsed state */ +.right-toc-collapsed { + width: auto; + border-left-width: 0; +} + +.right-toc-collapsed .right-toc-header { + border-bottom: none; + padding: 8px 12px; +} + +/* Scrollbar styling */ +.right-toc-content::-webkit-scrollbar { + width: 5px; +} + +.right-toc-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.right-toc-content::-webkit-scrollbar-thumb { + background: #cdcdcd; + border-radius: 10px; +} + +.right-toc-content::-webkit-scrollbar-thumb:hover { + background: #9e9e9e; +} + +/* Responsive adjustments */ +@media screen and (max-width: 1200px) { + .right-toc { + width: 230px; + } +} + +@media screen and (max-width: 992px) { + .right-toc { + display: none; /* Hide on smaller screens */ + } +} diff --git a/docs/_static/custom.js b/docs/_static/custom.js new file mode 100644 index 000000000..ef54f6847 --- /dev/null +++ b/docs/_static/custom.js @@ -0,0 +1,175 @@ +document.addEventListener("DOMContentLoaded", function () { + // Check if current page has opted out of the TOC + if (document.body.classList.contains("no-right-toc")) { + return; + } + + const content = document.querySelector(".rst-content"); + if (!content) return; + + // Find all headers in the main content + const headers = Array.from( + content.querySelectorAll("h1:not(.document-title), h2, h3"), + ).filter((header) => !header.classList.contains("no-toc")); + + // Only create TOC if there are headers + if (headers.length === 0) return; + + // Create TOC container + const toc = document.createElement("div"); + toc.className = "right-toc"; + toc.innerHTML = + '
' + + '
On This Page
' + + '
' + + '' + + "
" + + '
'; + + const tocList = toc.querySelector(".right-toc-list"); + const tocContent = toc.querySelector(".right-toc-content"); + const tocToggleBtn = toc.querySelector( + ".right-toc-toggle-btn", + ); + + // Set up the toggle button + tocToggleBtn.addEventListener("click", function () { + if (tocContent.style.display === "none") { + tocContent.style.display = "block"; + tocToggleBtn.textContent = "−"; + toc.classList.remove("right-toc-collapsed"); + localStorage.setItem("tocVisible", "true"); + } else { + tocContent.style.display = "none"; + tocToggleBtn.textContent = "+"; + toc.classList.add("right-toc-collapsed"); + localStorage.setItem("tocVisible", "false"); + } + }); + + // Check saved state + if (localStorage.getItem("tocVisible") === "false") { + tocContent.style.display = "none"; + tocToggleBtn.textContent = "+"; + toc.classList.add("right-toc-collapsed"); + } + + // Track used IDs to avoid duplicates + const usedIds = new Set(); + + // Get all existing IDs in the document + document.querySelectorAll("[id]").forEach((el) => { + usedIds.add(el.id); + }); + + // Generate unique IDs for headers that need them + headers.forEach((header, index) => { + // If header already has a unique ID, use that + if (header.id && !usedIds.has(header.id)) { + usedIds.add(header.id); + return; + } + + // Create a slug from the header text + let headerText = header.textContent || ""; + + // Clean the text (remove icons and special characters) + headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); + headerText = headerText.replace(/\s*[¶§#†‡]\s*$/, ""); + headerText = headerText.trim(); + + let slug = headerText + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/--+/g, "-") + .trim(); + + // Make sure slug is not empty + if (!slug) { + slug = "section"; + } + + // Ensure the ID is unique + let uniqueId = slug; + let counter = 1; + + while (usedIds.has(uniqueId)) { + uniqueId = `${slug}-${counter}`; + counter++; + } + + // Set the unique ID and add to our tracking set + header.id = uniqueId; + usedIds.add(uniqueId); + }); + + // Add entries for each header + headers.forEach((header) => { + const item = document.createElement("li"); + const link = document.createElement("a"); + + link.href = "#" + header.id; + + // Get clean text without icons + let headerText = header.textContent || ""; + headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); + headerText = headerText.replace(/\s*[¶§#†‡]\s*$/, ""); + + link.textContent = headerText.trim(); + link.className = + "right-toc-link right-toc-level-" + + header.tagName.toLowerCase(); + + item.appendChild(link); + tocList.appendChild(item); + }); + + // Add TOC to page + document.body.appendChild(toc); + + // Add active link highlighting + const tocLinks = document.querySelectorAll(".right-toc-link"); + const headerElements = Array.from(headers); + + if (tocLinks.length > 0 && headerElements.length > 0) { + // Highlight the current section on scroll + window.addEventListener( + "scroll", + debounce(function () { + let currentSection = null; + let smallestDistanceFromTop = Infinity; + + headerElements.forEach((header) => { + const distance = Math.abs( + header.getBoundingClientRect().top, + ); + if (distance < smallestDistanceFromTop) { + smallestDistanceFromTop = distance; + currentSection = header.id; + } + }); + + tocLinks.forEach((link) => { + link.classList.remove("active"); + if ( + link.getAttribute("href") === `#${currentSection}` + ) { + link.classList.add("active"); + } + }); + }, 100), + ); + } +}); + +// Debounce function to limit scroll event firing +function debounce(func, wait) { + let timeout; + return function () { + const context = this; + const args = arguments; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} diff --git a/docs/_static/example_plots/cartesian_example.png b/docs/_static/example_plots/cartesian_example.png new file mode 100644 index 000000000..eb6eb3ce8 Binary files /dev/null and b/docs/_static/example_plots/cartesian_example.png differ diff --git a/docs/_static/example_plots/colorbars_legends_example.png b/docs/_static/example_plots/colorbars_legends_example.png new file mode 100644 index 000000000..99ebb5986 Binary files /dev/null and b/docs/_static/example_plots/colorbars_legends_example.png differ diff --git a/docs/_static/example_plots/colormaps_example.png b/docs/_static/example_plots/colormaps_example.png new file mode 100644 index 000000000..b8f741d02 Binary files /dev/null and b/docs/_static/example_plots/colormaps_example.png differ diff --git a/docs/_static/example_plots/panels_example.png b/docs/_static/example_plots/panels_example.png new file mode 100644 index 000000000..ab2b6b6be Binary files /dev/null and b/docs/_static/example_plots/panels_example.png differ diff --git a/docs/_static/example_plots/projection_example.png b/docs/_static/example_plots/projection_example.png new file mode 100644 index 000000000..7f63b2ce4 Binary files /dev/null and b/docs/_static/example_plots/projection_example.png differ diff --git a/docs/_static/example_plots/subplot_example.png b/docs/_static/example_plots/subplot_example.png new file mode 100644 index 000000000..a40d169f2 Binary files /dev/null and b/docs/_static/example_plots/subplot_example.png differ diff --git a/docs/conf.py b/docs/conf.py index cad4db09c..467583e9f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -131,6 +131,7 @@ def __getattr__(self, name): extensions = [ "matplotlib.sphinxext.plot_directive", # see: https://matplotlib.org/sampledoc/extensions.html # noqa: E501 "sphinx.ext.autodoc", # include documentation from docstrings + "sphinx_design", "sphinx.ext.doctest", # >>> examples "sphinx.ext.extlinks", # for :pr:, :issue:, :commit: "sphinx.ext.autosectionlabel", # use :ref:`Heading` for any heading @@ -144,6 +145,7 @@ def __getattr__(self, name): "sphinx_automodapi.automodapi", # fork of automodapi "sphinx_rtd_light_dark", # use custom theme "sphinx_copybutton", # add copy button to code + "_ext.notoc", "nbsphinx", # parse rst books ] @@ -288,6 +290,8 @@ def __getattr__(self, name): # Add jupytext support to nbsphinx nbsphinx_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:percent"}]} +nbsphinx_execute = "auto" + # The name of the Pygments (syntax highlighting) style to use. # The light-dark theme toggler overloads this, but set default anyway pygments_style = "none" @@ -311,6 +315,10 @@ def __getattr__(self, name): "collapse_navigation": True, "navigation_depth": 4, "prev_next_buttons_location": "bottom", # top and bottom + "includehidden": True, + "titles_only": True, + "display_toc": True, + "sticky_navigation": True, } # Add any paths that contain custom static files (such as style sheets) here, @@ -338,6 +346,13 @@ def __getattr__(self, name): htmlhelp_basename = "ultraplotdoc" +html_css_files = [ + "custom.css", +] +html_js_files = [ + "custom.js", +] + # -- Options for LaTeX output ------------------------------------------------ latex_elements = { diff --git a/docs/index.rst b/docs/index.rst index bd8004e10..0926e1cff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,21 +1,122 @@ -.. - UltraPlot documentation master file, created by - sphinx-quickstart on Wed Feb 20 01:31:20 2019. - You can adapt this file completely to your liking, but - it should at least contain the root `toctree` directive. +.. notoc:: +.. image:: _static/logo_long.png + :align: center -======= -UltraPlot -======= +**UltraPlot** is a succinct wrapper around `matplotlib `__ +for creating **beautiful, publication-quality graphics** with ease. -A succinct `matplotlib `__ wrapper -for making beautiful, publication-quality graphics. This project -is `published on GitHub `__ and can -be cited using its `Zenodo DOI `__. +🚀 **Key Features** | Create More, Code Less +################### +✔ **Simplified Subplot Management** – Create multi-panel plots effortlessly. +🎨 **Smart Aesthetics** – Optimized colormaps, fonts, and styles out of the box. + +📊 **Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. + +📌 **Get Started** → :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` + +-------------------------------------- + +**📖 User Guide** +################# +A preview of what UltraPlot can do. For more see the sidebar! + +.. grid:: 3 + :gutter: 2 + + .. grid-item-card:: + :link: subplots.html + :shadow: md + :class-card: card-with-bottom-text + + **Subplots & Layouts** + ^^^ + + .. image:: _static/example_plots/subplot_example.png + :align: center + + Create complex multi-panel layouts effortlessly. + + .. grid-item-card:: + :link: cartesian.html + :shadow: md + :class-card: card-with-bottom-text + + **Cartesian Plots** + ^^^ + + .. image:: _static/example_plots/cartesian_example.png + :align: center + + .. container:: bottom-aligned-text + + Easily generate clean, well-formatted plots. + + .. grid-item-card:: + :link: projections.html + :shadow: md + :class-card: card-with-bottom-text + + **Projections & Maps** + ^^^ + + .. image:: _static/example_plots/projection_example.png + :align: center + + .. container:: bottom-aligned-text + Built-in support for projections and geographic plots. + + .. grid-item-card:: + :link: colorbars_legends.html + :shadow: md + :class-card: card-with-bottom-text + + **Colorbars & Legends** + ^^^ + + .. image:: _static/example_plots/colorbars_legends_example.png + :align: center + + Customize legends and colorbars with ease. + + .. grid-item-card:: + :link: insets_panels.html + :shadow: md + :class-card: card-with-bottom-text + + **Insets & Panels** + ^^^ + + .. image:: _static/example_plots/panels_example.png + :align: center + + Add inset plots and panel-based layouts. + + .. grid-item-card:: + :link: colormaps.html + :shadow: md + :class-card: card-with-bottom-text + + **Colormaps & Cycles** + ^^^ + + .. image:: _static/example_plots/colormaps_example.png + :align: center + + Use prebuilt colormaps and define your own color cycles. + + +**📚 Reference & More** +####################### +For more details, check the full :doc:`User guide ` and :doc:`API Reference `. + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`glossary` .. toctree:: :maxdepth: 1 :caption: Getting Started + :hidden: install why @@ -24,6 +125,7 @@ be cited using its `Zenodo DOI `__. .. toctree:: :maxdepth: 1 :caption: User Guide + :hidden: basics subplots @@ -43,16 +145,10 @@ be cited using its `Zenodo DOI `__. .. toctree:: :maxdepth: 1 :caption: Reference + :hidden: api external-links whats_new contributing authors - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`glossary` diff --git a/environment.yml b/environment.yml index 214a9e873..3674d5563 100644 --- a/environment.yml +++ b/environment.yml @@ -24,5 +24,6 @@ dependencies: - sphinx-rtd-theme - basemap >=1.4.1 - pre-commit + - sphinx-design - pip: - git+https://github.com/ultraplot/UltraTheme.git