Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b818f88
fix: correct copyright year to 2026 in LICENSE and README
symoon9 Apr 26, 2026
0125184
Upate README.md
symoon9 Apr 26, 2026
0b7f050
docs: add screenshot and demo.ipynb link to README, simplify palette …
symoon9 Apr 26, 2026
9aed27c
docs: add screenshot and updated demo notebook
symoon9 Apr 26, 2026
c764e13
feat: add Copy and Save buttons to the figure preview toolbar
symoon9 Apr 26, 2026
4005df8
fix: move Copy/Save buttons into render_out for cross-environment com…
symoon9 Apr 26, 2026
c044095
fix: use closest()/querySelector() for Copy/Save buttons; add logo to…
symoon9 Apr 26, 2026
a5da421
Upload logo png
symoon9 Apr 26, 2026
bb7c2ac
fix: replace HTML onclick buttons with ipywidgets.Button for VSCode c…
symoon9 Apr 26, 2026
aee8d4b
feat: Save Figure panel, legend title, UI polish (closes #20, #21, #2…
symoon9 Apr 26, 2026
7a3c0d1
fix: compact slider readout, add _mk_slider helper, rename save section
symoon9 Apr 26, 2026
a675b19
fix: toggle btn light-blue tint, palette count badge, spines label size
symoon9 Apr 26, 2026
eb5f619
fix: compact slider readout, add _mk_slider helper, rename save section
symoon9 Apr 27, 2026
b96af73
fix: correct CSS selector for per-btn (.mpl-per-{pid} button, not nes…
symoon9 Apr 27, 2026
fbdeb82
fix: toggle btn light-blue tint, palette count badge, spines label size
symoon9 Apr 27, 2026
126a980
fix: CSS typos, legend layout order, per-btn style; update README
symoon9 Apr 27, 2026
0bdf8bf
Rplace logo png
symoon9 Apr 27, 2026
49a2f38
Update README
symoon9 Apr 27, 2026
eee0da9
Update README
symoon9 Apr 27, 2026
1592faa
Update images in README
symoon9 Apr 27, 2026
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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Seo-Yoon Moon
Copyright (c) 2026 Seo-Yoon Moon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
61 changes: 17 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
# mplstudio

[![PyPI version](https://img.shields.io/pypi/v/mplstudio)](https://pypi.org/project/mplstudio/)
[![Python](https://img.shields.io/pypi/pyversions/mplstudio)](https://pypi.org/project/mplstudio/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![CI](https://github.com/symoon9/mplstudio/actions/workflows/ci.yml/badge.svg)](https://github.com/symoon9/mplstudio/actions/workflows/ci.yml)

An interactive GUI for styling matplotlib figures directly in Jupyter.
An **interactive GUI** for **styling matplotlib figures** directly in Jupyter.

Adjust colors, fonts, axes, legends, and more in real time without touching your plot code.
Adjust colors, fonts, axes, legends, and more in real time **without touching your plot code.**

---
![mplstudio control panel](docs/screenshot_readme.png)

## Installation

```bash
pip install mplstudio
```

Requires Python 3.9+, Jupyter Notebook or JupyterLab, and matplotlib ≥ 3.5.
Requires `Python 3.9+`, Jupyter Notebook or JupyterLab, and `matplotlib ≥ 3.5`.

---

## Quick Start

Expand All @@ -35,9 +33,8 @@ ax.legend()
mplstudio.studio(fig)
```

This displays an interactive control panel below your figure with live preview.
For detailed usage examples, see [`examples/demo.ipynb`](examples/demo.ipynb).

---

## API Reference

Expand Down Expand Up @@ -66,57 +63,35 @@ Return a sorted list of all valid section names.
```python
mplstudio.available_sections()
# ['alpha', 'axes', 'colors', 'figure_size', 'grid_spines',
# 'legend', 'palette_suggestions', 'typography']
# 'legend', 'palette_suggestions', 'save', 'typography']
```

---

## Sections
## Available Sections

| Section | Controls |
|---------|----------|
| `figure_size` | Width and height sliders |
| `typography` | Font size for all elements or individually (title, labels, ticks, legend) |
| `colors` | Palette picker, manual per-series color pickers, smart CIELAB palette, colormap selector, background color |
| `colors` | Palette picker (with color count), manual per-series color pickers, smart CIELAB palette, colormap selector, background color |
| `alpha` | Global opacity slider + per-series opacity |
| `axes` | Title, x/y axis labels, x/y limits — supports multi-axis figures |
| `legend` | Location dropdown, legend entry names, bbox position |
| `grid_spines` | Grid on/off, spine style (box / left-bottom / none) |
| `legend` | Legend title, collapsible series label editor, location dropdown, bbox position |
| `grid_spines` | Grid toggle, spine style (Box / 2-Side / None) |
| `palette_suggestions` | Colorblind-safe palette recommendations filtered by use case and background |
| `save` | Save figure with custom filename, path, DPI, and format (png, jpg, pdf, svg, eps) |

---

## Palette Utilities

mplstudio ships a curated palette library and color science tools you can use independently of the GUI.

```python
from mplstudio import get_palette, smart_palette, recommend, palette_names

# List all available palettes
palette_names()

# Get colors from a named palette
colors = get_palette("Okabe-Ito") # colorblind-safe, 8 colors
colors = get_palette("Tableau 10") # familiar defaults

# Generate N maximally distinct colors using CIELAB ΔE greedy selection
colors = smart_palette(6) # always a superset of smart_palette(5)

# Find palettes matching criteria
suggestions = recommend(
n_colors=5,
colorblind_safe=True,
use_case="categorical", # "categorical" | "sequential" | "diverging"
background="light", # "light" | "dark"
top_k=3,
)
for p in suggestions:
print(p["name"], p["colors"])
```
mplstudio ships a curated palette library and color science tools you can use independently of the GUI: `get_palette`, `smart_palette`, `recommend`, `palette_names`, and `delta_e`. See [`examples/demo.ipynb`](examples/demo.ipynb) for usage examples.

### Available Palettes

Palettes for categorical, and continuous variables (sequential and diverging color maps).

Following table shows palettes for **categorical** values. For **continuous** variables, mplstudio uses matplotlib's built-in colormaps. See the [matplotlib colormap reference](https://matplotlib.org/stable/gallery/color/colormap_reference.html) for the full list.

| Palette | Colors | Tags |
|---------|--------|------|
| Okabe-Ito | 8 | colorblind-safe |
Expand All @@ -137,7 +112,6 @@ for p in suggestions:
| Pastel | 6 | light background |
| High Contrast | 5 | light background |

---

## Requirements

Expand All @@ -147,8 +121,7 @@ for p in suggestions:
- ipykernel ≥ 6.0
- Jupyter Notebook or JupyterLab

---

## License

[MIT](LICENSE) © 2024 Seo-Yoon Moon
[MIT](LICENSE) © 2026 Seo-Yoon Moon
Binary file added docs/mplstudio_logo_square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_readme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 30 additions & 22 deletions examples/demo.ipynb

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion mplstudio/style/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
set_legend_position,
set_legend_labels,
set_legend_bbox,
set_legend_title,
LEGEND_LOCATIONS,
)
from ._grid_spines import set_grid, set_spine_style, SPINE_STYLES
Expand All @@ -43,7 +44,7 @@
"set_title", "set_xlabel", "set_ylabel", "set_xlim", "set_ylim",
# legend
"set_legend_position", "set_legend_labels", "set_legend_bbox",
"LEGEND_LOCATIONS",
"set_legend_title", "LEGEND_LOCATIONS",
# grid & spines
"set_grid", "set_spine_style", "SPINE_STYLES",
]
10 changes: 5 additions & 5 deletions mplstudio/style/_grid_spines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from matplotlib.figure import Figure


SPINE_STYLES = ["box", "left-bottom", "none"]
SPINE_STYLES = ["Box", "2-Side", "None"]


def set_grid(fig: Figure, visible: bool, alpha: float = 0.3) -> None:
Expand All @@ -16,16 +16,16 @@ def set_grid(fig: Figure, visible: bool, alpha: float = 0.3) -> None:


def set_spine_style(fig: Figure, style: str) -> None:
"""style: 'box' | 'left-bottom' | 'none'"""
"""style: 'Box' | '2-Side' | 'None '"""
for ax in fig.axes:
if style == "box":
if style == "Box":
for spine in ax.spines.values():
spine.set_visible(True)
elif style == "left-bottom":
elif style == "2-Side":
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_visible(True)
ax.spines["bottom"].set_visible(True)
elif style == "none":
elif style == "None":
for spine in ax.spines.values():
spine.set_visible(False)
8 changes: 8 additions & 0 deletions mplstudio/style/_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ def set_legend_bbox(fig: Figure, x: float, y: float, ax_idx: int = 0) -> None:
legend = fig.axes[ax_idx].get_legend()
if legend:
legend.set_bbox_to_anchor((x, y))


def set_legend_title(fig: Figure, title: str, ax_idx: int = 0) -> None:
if ax_idx >= len(fig.axes):
return
legend = fig.axes[ax_idx].get_legend()
if legend:
legend.set_title(title)
2 changes: 1 addition & 1 deletion mplstudio/widget/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

_KNOWN_SECTIONS = frozenset({
"figure_size", "typography", "colors", "alpha",
"axes", "legend", "grid_spines", "palette_suggestions",
"axes", "legend", "grid_spines", "palette_suggestions", "save",
})

_GITHUB_ISSUES = "https://github.com/symoon9/mplstudio/issues"
16 changes: 16 additions & 0 deletions mplstudio/widget/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def _per_btn(label: str, pid: str) -> tuple[widgets.Button, widgets.VBox, list]:
btn = widgets.Button(description=f"{label} ▾", button_style="",
layout=widgets.Layout(width="100%"))
btn.add_class(f"mpl-per-{pid}")
btn.add_class(f"mpl-per-btn")

def _toggle(_):
flag[0] = not flag[0]
Expand All @@ -37,6 +38,21 @@ def _toggle(_):
return btn, box, flag


def _mk_slider(cls, description: str, dw: str = "82px", **kw) -> widgets.Widget:
"""Return a themed Int/FloatSlider with consistent style.

Args:
cls: widgets.IntSlider or widgets.FloatSlider
description: label text
dw: description_width (CSS string)
**kw: forwarded to the slider constructor
"""
kw.setdefault("layout", widgets.Layout(width="100%"))
kw.setdefault("continuous_update", False)
kw.setdefault("readout", True)
return cls(description=description, style={"description_width": dw}, **kw)


def _lim_sliders(lo_init: float, hi_init: float):
"""Return (lo_sl, hi_sl) FloatSlider pair for xlim/ylim blocks.

Expand Down
16 changes: 9 additions & 7 deletions mplstudio/widget/_sections/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .._ctx import _PanelCtx
from .._helpers import _section, _swatches_div, _build_palette_preview, _build_colormap_gradient
from ... import style as S
from ...palettes import palette_names, SEQUENTIAL_CMAPS, DIVERGING_CMAPS, smart_palette
from ...palettes import palette_names, get_palette, SEQUENTIAL_CMAPS, DIVERGING_CMAPS, smart_palette


def build(ctx: _PanelCtx) -> widgets.VBox:
Expand All @@ -20,23 +20,25 @@ def build(ctx: _PanelCtx) -> widgets.VBox:
"mixed": f"mixed · {n_series} categorical + colormap",
}[plot_type]
type_badge = widgets.HTML(
f"<span style='font-size:0.78em;color:#888'>Detected: {_type_label}</span>")
f"<span style='font-size:0.9em;color:#888'>Detected: {_type_label}</span>")

cat_box_children: list[widgets.Widget] = []

if plot_type in ("categorical", "mixed"):
color_mode = widgets.ToggleButtons(
options=["Palette", "Manual", "Smart"], value="Palette",
description="Mode:", style={"button_width": "62px"},
layout=widgets.Layout(width="100%", margin="0 0 4px 0"))
layout=widgets.Layout(width="100%", margin="4px 4px"))

_pal_names = palette_names()
_pal_options = [(f"{n} ({len(get_palette(n))})", n) for n in _pal_names]
palette_select = widgets.Dropdown(
options=palette_names(), description="Palette",
options=_pal_options, description="Palette",
style={"description_width": "58px"},
layout=widgets.Layout(width="100%"))
palette_preview_w = _build_palette_preview(palette_names()[0])
layout=widgets.Layout(width="95%"))
palette_preview_w = _build_palette_preview(_pal_names[0])
palette_col = widgets.VBox([palette_select, palette_preview_w],
layout=widgets.Layout(width="100%"))
layout=widgets.Layout(width="95%"))

line_colors = S.get_line_colors(ctx.fig)
line_labels = S.get_line_labels(ctx.fig)
Expand Down
14 changes: 5 additions & 9 deletions mplstudio/widget/_sections/figure_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
import ipywidgets as widgets

from .._ctx import _PanelCtx
from .._helpers import _section
from .._helpers import _section, _mk_slider
from ... import style as S


def build(ctx: _PanelCtx) -> widgets.VBox:
w_init, h_init = ctx.fig.get_size_inches()
fig_width = widgets.FloatSlider(
value=w_init, min=2, max=25, step=0.5, description="Width (in)",
style={"description_width": "82px"},
layout=widgets.Layout(width="100%"), continuous_update=False)
fig_height = widgets.FloatSlider(
value=h_init, min=2, max=25, step=0.5, description="Height (in)",
style={"description_width": "82px"},
layout=widgets.Layout(width="100%"), continuous_update=False)
fig_width = _mk_slider(widgets.FloatSlider, "Width (in)", "82px",
value=w_init, min=2, max=25, step=0.5)
fig_height = _mk_slider(widgets.FloatSlider, "Height (in)", "82px",
value=h_init, min=2, max=25, step=0.5)

def _on_size(_):
S.set_figure_size(ctx.fig, fig_width.value, fig_height.value)
Expand Down
18 changes: 13 additions & 5 deletions mplstudio/widget/_sections/grid_spines.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@


def build(ctx: _PanelCtx) -> widgets.VBox:
grid_toggle = widgets.Checkbox(value=False, description="Show grid")
grid_toggle = widgets.ToggleButtons(
options=["Off", "On"], value="Off",
description="Grid",
style={"description_width": "32px", "button_width": "52px"},
layout=widgets.Layout(width="100%"))

spine_header = widgets.HTML(
"<span >Spines</span>")
spine_style = widgets.ToggleButtons(
options=S.SPINE_STYLES, value="box",
description="Spines:", style={"button_width": "84px"},
options=S.SPINE_STYLES, value=S.SPINE_STYLES[0],
description="",
style={"button_width": "74px"},
layout=widgets.Layout(width="100%"))

def _on_grid(_):
S.set_grid(ctx.fig, grid_toggle.value)
S.set_grid(ctx.fig, grid_toggle.value == "On")
ctx.refresh()

def _on_spine(_):
Expand All @@ -26,4 +34,4 @@ def _on_spine(_):

grid_toggle.observe(_on_grid, names="value")
spine_style.observe(_on_spine, names="value")
return _section("Grid & Spines", ctx.pid, grid_toggle, spine_style)
return _section("Grid & Spines", ctx.pid, grid_toggle, spine_header, spine_style)
Loading
Loading