Skip to content

Commit

Permalink
add iplt.text (#246)
Browse files Browse the repository at this point in the history
* init text

* `eval_xy` caching and param excluding

* text docstring

* text docs

* fix param excluder when None passed

* fix linking
  • Loading branch information
ianhi authored Jun 23, 2022
1 parent 9c48292 commit cd38a3f
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 5 deletions.
172 changes: 172 additions & 0 deletions docs/examples/text-annotations.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ddddb250-faff-496c-8f0e-6935196ec14a",
"metadata": {},
"source": [
"# Text and Annotations\n",
"\n",
"\n",
"```{note}\n",
"Support for modifying text is not complete as none of the function implemented support updating `fontdict` or other text properties like size and color. However, the core functionality is there to place text, change it's position, or change what it reads. see https://github.com/ianhi/mpl-interactions/issues/247 for updates.\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ec2f6996-5f39-4009-a94d-e3a3fda108d9",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"%matplotlib ipympl\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from mpl_interactions import ipyplot as iplt"
]
},
{
"cell_type": "markdown",
"id": "692989d9-07f1-4969-81d8-fc330f0aa5d4",
"metadata": {},
"source": [
"## Working with text strings.\n",
"\n",
"There are two ways to dynamically update text strings in mpl-interactions.\n",
"1. Use a function to return a string\n",
"2. Use a named string formatting\n",
"\n",
"\n",
"You can also combine these and have your function return a string that then gets formatted.\n",
"\n",
"\n",
"In the example below the `xlabel` is generated using a function and the `title` is generated using the formatting approach."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d97390af-872e-42f5-a939-03b734b1cf4f",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"\n",
"x = np.linspace(0, np.pi, 100)\n",
"\n",
"\n",
"def y(x, volts, tau):\n",
" return np.sin(x * tau) * volts\n",
"\n",
"\n",
"ctrls = iplt.plot(x, y, volts=(0.5, 10), tau=(1, 10, 100))\n",
"\n",
"\n",
"def xlabel_func(tau):\n",
" # you can do arbitrary python here to make a more\n",
" # complicated string\n",
" return f\"Time with a max tau of {np.round(tau, 3)}\"\n",
"\n",
"\n",
"with ctrls[\"tau\"]:\n",
" iplt.xlabel(xlabel_func)\n",
"with ctrls:\n",
" # directly using string formatting\n",
" # the formatting is performed in the update\n",
" iplt.title(title=\"The voltage is {volts:.2f}\")"
]
},
{
"cell_type": "markdown",
"id": "1e152d5d-6c6f-4e87-b5d6-f6755f4bed17",
"metadata": {},
"source": [
"## Arbitrarily placed text\n",
"\n",
"For this you can use {func}`.interactive_text`. Currently `plt.annotation` is not supported. \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1ccdd8c4-91fa-440a-9a2d-dbea694ee92d",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"\n",
"theta = np.linspace(0, 2 * np.pi, 100)\n",
"\n",
"\n",
"def gen_string(theta):\n",
" return f\"angle = {np.round(np.rad2deg(theta))}\"\n",
"\n",
"\n",
"def fx(theta):\n",
" return np.cos(theta)\n",
"\n",
"\n",
"def fy(x, theta):\n",
" return np.sin(theta)\n",
"\n",
"\n",
"ctrls = iplt.text(fx, fy, gen_string, theta=theta)\n",
"ax.set_xlim([-1.25, 1.25])\n",
"_ = ax.set_ylim([-1.25, 1.25])"
]
},
{
"cell_type": "markdown",
"id": "e2aafbc0-7958-410e-a3b8-ce0a8a75ef30",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
"Since the `x` and `y` positions are scalars you can also do nifty things like directly define them by a slider shorthand in the function.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8eb7da4d-3e48-4b28-85a8-080ff85eee0d",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"ctrls = iplt.text((0, 1, 100), (0.25, 1, 100), \"{x:.2f}, {y:.2f}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ examples/custom-callbacks.ipynb
examples/animations.ipynb
examples/range-sliders.ipynb
examples/scalar-arguments.ipynb
examples/text-annotations.ipynb
examples/tidbits.md
```

Expand Down
7 changes: 5 additions & 2 deletions mpl_interactions/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,12 @@ def excluder(params, except_=None):
Parameters
----------
params : dict
except : str
except : str or list[str]
"""
return {k: v for k, v in params.items() if k not in added_kwargs or k == except_}
if isinstance(except_, str) or except_ is None:
except_ = [except_]

return {k: v for k, v in params.items() if k not in added_kwargs or k in except_}

return excluder

Expand Down
15 changes: 12 additions & 3 deletions mpl_interactions/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,29 +238,38 @@ def f(params):
def eval_xy(x_, y_, params, cache=None):
"""
for when y requires x as an argument and either, neither or both
of x and y may be a function.
of x and y may be a function. This will automatically do the param exclusion
for 'x' and 'y'.
Returns
-------
x, y
as numpy arrays
"""
if isinstance(x_, Callable):
if "x" in params:
# passed as a scalar with a slider
x = params["x"]
elif isinstance(x_, Callable):
if cache is not None:
if x_ in cache:
x = cache[x_]
else:
x = x_(**params)
cache[x_] = x
else:
x = x_(**params)
else:
x = x_
if isinstance(y_, Callable):
if "y" in params:
# passed a scalar with a slider
y = params["y"]
elif isinstance(y_, Callable):
if cache is not None:
if y_ in cache:
y = cache[y_]
else:
y = y_(x, **params)
cache[y_] = y
else:
y = y_(x, **params)
else:
Expand Down
1 change: 1 addition & 0 deletions mpl_interactions/ipyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .pyplot import interactive_imshow as imshow
from .pyplot import interactive_plot as plot
from .pyplot import interactive_scatter as scatter
from .pyplot import interactive_text as text
from .pyplot import interactive_title as title
from .pyplot import interactive_xlabel as xlabel
from .pyplot import interactive_ylabel as ylabel
90 changes: 90 additions & 0 deletions mpl_interactions/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"interactive_title",
"interactive_xlabel",
"interactive_ylabel",
"interactive_text",
]


Expand Down Expand Up @@ -1247,3 +1248,92 @@ def update(params, indices, cache):
**text_kwargs,
)
return controls


def interactive_text(
x,
y,
s,
fontdict=None,
controls=None,
ax=None,
*,
slider_formats=None,
display_controls=True,
play_buttons=False,
force_ipywidgets=False,
**kwargs,
):
"""
Create a text object that will update interactively.
kwargs for `matplotlib.text.Text` will be passed through, other kwargs will be used to create interactive controls.
.. note::
fontdict properties are currently static - see https://github.com/ianhi/mpl-interactions/issues/247
Parameters
----------
x, y : float or function
The text position.
s : str or function
The text. Can either be static text, a function returning a string or
can include {} style formatting. e.g. 'The voltage is {volts:.2f}'
fontdict : dict[str]
Passed through to the Text object. Currently not dynamically updateable. See
https://github.com/ianhi/mpl-interactions/issues/247
controls : mpl_interactions.controller.Controls
An existing controls object if you want to tie multiple plot elements to the same set of
controls
ax : matplotlib axis, optional
The axis on which to plot. If none the current axis will be used.
play_buttons : bool or str or dict, optional
Whether to attach an ipywidgets.Play widget to any sliders that get created.
If a boolean it will apply to all kwargs, if a dictionary you choose which sliders you
want to attach play buttons too.
- None: no sliders
- True: sliders on the lft
- False: no sliders
- 'left': sliders on the left
- 'right': sliders on the right
force_ipywidgets : boolean
If True ipywidgets will always be used, even if not using the ipympl backend.
If False the function will try to detect if it is ok to use ipywidgets
If ipywidgets are not used the function will fall back on matplotlib widgets
Returns
-------
controls
"""
ipympl = notebook_backend()
fig, ax = gogogo_figure(ipympl, ax)
ipympl or force_ipywidgets
slider_formats = create_slider_format_dict(slider_formats)

kwargs, text_kwargs = kwarg_popper(kwargs, Text_kwargs_list)
funcs, extra_ctrls, param_excluder = prep_scalars(kwargs, x=x, y=y)
x = funcs["x"]
y = funcs["y"]
controls, params = gogogo_controls(
kwargs, controls, display_controls, slider_formats, play_buttons, extra_ctrls
)

def update(params, indices, cache):
x_, y_ = eval_xy(x, y, param_excluder(params, ["x", "y"]), cache)
text.set_x(x_)
text.set_y(y_)
text.set_text(callable_else_value_no_cast(s, params, cache).format(**params))

controls._register_function(update, fig, params)
x_, y_ = eval_xy(x, y, param_excluder(params, ["x", "y"]))
text = ax.text(
x_,
y_,
callable_else_value_no_cast(s, params).format(**params),
fontdict=fontdict,
**text_kwargs,
)
return controls

0 comments on commit cd38a3f

Please sign in to comment.