Skip to content

Commit cd38a3f

Browse files
authored
add iplt.text (#246)
* init text * `eval_xy` caching and param excluding * text docstring * text docs * fix param excluder when None passed * fix linking
1 parent 9c48292 commit cd38a3f

File tree

6 files changed

+281
-5
lines changed

6 files changed

+281
-5
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "ddddb250-faff-496c-8f0e-6935196ec14a",
6+
"metadata": {},
7+
"source": [
8+
"# Text and Annotations\n",
9+
"\n",
10+
"\n",
11+
"```{note}\n",
12+
"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",
13+
"```"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"id": "ec2f6996-5f39-4009-a94d-e3a3fda108d9",
20+
"metadata": {
21+
"tags": []
22+
},
23+
"outputs": [],
24+
"source": [
25+
"%matplotlib ipympl\n",
26+
"import matplotlib.pyplot as plt\n",
27+
"import numpy as np\n",
28+
"\n",
29+
"from mpl_interactions import ipyplot as iplt"
30+
]
31+
},
32+
{
33+
"cell_type": "markdown",
34+
"id": "692989d9-07f1-4969-81d8-fc330f0aa5d4",
35+
"metadata": {},
36+
"source": [
37+
"## Working with text strings.\n",
38+
"\n",
39+
"There are two ways to dynamically update text strings in mpl-interactions.\n",
40+
"1. Use a function to return a string\n",
41+
"2. Use a named string formatting\n",
42+
"\n",
43+
"\n",
44+
"You can also combine these and have your function return a string that then gets formatted.\n",
45+
"\n",
46+
"\n",
47+
"In the example below the `xlabel` is generated using a function and the `title` is generated using the formatting approach."
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": null,
53+
"id": "d97390af-872e-42f5-a939-03b734b1cf4f",
54+
"metadata": {
55+
"tags": []
56+
},
57+
"outputs": [],
58+
"source": [
59+
"fig, ax = plt.subplots()\n",
60+
"\n",
61+
"x = np.linspace(0, np.pi, 100)\n",
62+
"\n",
63+
"\n",
64+
"def y(x, volts, tau):\n",
65+
" return np.sin(x * tau) * volts\n",
66+
"\n",
67+
"\n",
68+
"ctrls = iplt.plot(x, y, volts=(0.5, 10), tau=(1, 10, 100))\n",
69+
"\n",
70+
"\n",
71+
"def xlabel_func(tau):\n",
72+
" # you can do arbitrary python here to make a more\n",
73+
" # complicated string\n",
74+
" return f\"Time with a max tau of {np.round(tau, 3)}\"\n",
75+
"\n",
76+
"\n",
77+
"with ctrls[\"tau\"]:\n",
78+
" iplt.xlabel(xlabel_func)\n",
79+
"with ctrls:\n",
80+
" # directly using string formatting\n",
81+
" # the formatting is performed in the update\n",
82+
" iplt.title(title=\"The voltage is {volts:.2f}\")"
83+
]
84+
},
85+
{
86+
"cell_type": "markdown",
87+
"id": "1e152d5d-6c6f-4e87-b5d6-f6755f4bed17",
88+
"metadata": {},
89+
"source": [
90+
"## Arbitrarily placed text\n",
91+
"\n",
92+
"For this you can use {func}`.interactive_text`. Currently `plt.annotation` is not supported. \n"
93+
]
94+
},
95+
{
96+
"cell_type": "code",
97+
"execution_count": null,
98+
"id": "1ccdd8c4-91fa-440a-9a2d-dbea694ee92d",
99+
"metadata": {
100+
"tags": []
101+
},
102+
"outputs": [],
103+
"source": [
104+
"fig, ax = plt.subplots()\n",
105+
"\n",
106+
"theta = np.linspace(0, 2 * np.pi, 100)\n",
107+
"\n",
108+
"\n",
109+
"def gen_string(theta):\n",
110+
" return f\"angle = {np.round(np.rad2deg(theta))}\"\n",
111+
"\n",
112+
"\n",
113+
"def fx(theta):\n",
114+
" return np.cos(theta)\n",
115+
"\n",
116+
"\n",
117+
"def fy(x, theta):\n",
118+
" return np.sin(theta)\n",
119+
"\n",
120+
"\n",
121+
"ctrls = iplt.text(fx, fy, gen_string, theta=theta)\n",
122+
"ax.set_xlim([-1.25, 1.25])\n",
123+
"_ = ax.set_ylim([-1.25, 1.25])"
124+
]
125+
},
126+
{
127+
"cell_type": "markdown",
128+
"id": "e2aafbc0-7958-410e-a3b8-ce0a8a75ef30",
129+
"metadata": {
130+
"jp-MarkdownHeadingCollapsed": true,
131+
"tags": []
132+
},
133+
"source": [
134+
"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"
135+
]
136+
},
137+
{
138+
"cell_type": "code",
139+
"execution_count": null,
140+
"id": "8eb7da4d-3e48-4b28-85a8-080ff85eee0d",
141+
"metadata": {
142+
"tags": []
143+
},
144+
"outputs": [],
145+
"source": [
146+
"fig, ax = plt.subplots()\n",
147+
"ctrls = iplt.text((0, 1, 100), (0.25, 1, 100), \"{x:.2f}, {y:.2f}\")"
148+
]
149+
}
150+
],
151+
"metadata": {
152+
"kernelspec": {
153+
"display_name": "Python 3 (ipykernel)",
154+
"language": "python",
155+
"name": "python3"
156+
},
157+
"language_info": {
158+
"codemirror_mode": {
159+
"name": "ipython",
160+
"version": 3
161+
},
162+
"file_extension": ".py",
163+
"mimetype": "text/x-python",
164+
"name": "python",
165+
"nbconvert_exporter": "python",
166+
"pygments_lexer": "ipython3",
167+
"version": "3.9.9"
168+
}
169+
},
170+
"nbformat": 4,
171+
"nbformat_minor": 5
172+
}

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ examples/custom-callbacks.ipynb
101101
examples/animations.ipynb
102102
examples/range-sliders.ipynb
103103
examples/scalar-arguments.ipynb
104+
examples/text-annotations.ipynb
104105
examples/tidbits.md
105106
```
106107

mpl_interactions/controller.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,12 @@ def excluder(params, except_=None):
467467
Parameters
468468
----------
469469
params : dict
470-
except : str
470+
except : str or list[str]
471471
"""
472-
return {k: v for k, v in params.items() if k not in added_kwargs or k == except_}
472+
if isinstance(except_, str) or except_ is None:
473+
except_ = [except_]
474+
475+
return {k: v for k, v in params.items() if k not in added_kwargs or k in except_}
473476

474477
return excluder
475478

mpl_interactions/helpers.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,29 +238,38 @@ def f(params):
238238
def eval_xy(x_, y_, params, cache=None):
239239
"""
240240
for when y requires x as an argument and either, neither or both
241-
of x and y may be a function.
241+
of x and y may be a function. This will automatically do the param exclusion
242+
for 'x' and 'y'.
242243
243244
Returns
244245
-------
245246
x, y
246247
as numpy arrays
247248
"""
248-
if isinstance(x_, Callable):
249+
if "x" in params:
250+
# passed as a scalar with a slider
251+
x = params["x"]
252+
elif isinstance(x_, Callable):
249253
if cache is not None:
250254
if x_ in cache:
251255
x = cache[x_]
252256
else:
253257
x = x_(**params)
258+
cache[x_] = x
254259
else:
255260
x = x_(**params)
256261
else:
257262
x = x_
258-
if isinstance(y_, Callable):
263+
if "y" in params:
264+
# passed a scalar with a slider
265+
y = params["y"]
266+
elif isinstance(y_, Callable):
259267
if cache is not None:
260268
if y_ in cache:
261269
y = cache[y_]
262270
else:
263271
y = y_(x, **params)
272+
cache[y_] = y
264273
else:
265274
y = y_(x, **params)
266275
else:

mpl_interactions/ipyplot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .pyplot import interactive_imshow as imshow
55
from .pyplot import interactive_plot as plot
66
from .pyplot import interactive_scatter as scatter
7+
from .pyplot import interactive_text as text
78
from .pyplot import interactive_title as title
89
from .pyplot import interactive_xlabel as xlabel
910
from .pyplot import interactive_ylabel as ylabel

mpl_interactions/pyplot.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"interactive_title",
4343
"interactive_xlabel",
4444
"interactive_ylabel",
45+
"interactive_text",
4546
]
4647

4748

@@ -1247,3 +1248,92 @@ def update(params, indices, cache):
12471248
**text_kwargs,
12481249
)
12491250
return controls
1251+
1252+
1253+
def interactive_text(
1254+
x,
1255+
y,
1256+
s,
1257+
fontdict=None,
1258+
controls=None,
1259+
ax=None,
1260+
*,
1261+
slider_formats=None,
1262+
display_controls=True,
1263+
play_buttons=False,
1264+
force_ipywidgets=False,
1265+
**kwargs,
1266+
):
1267+
"""
1268+
Create a text object that will update interactively.
1269+
kwargs for `matplotlib.text.Text` will be passed through, other kwargs will be used to create interactive controls.
1270+
1271+
.. note::
1272+
1273+
fontdict properties are currently static - see https://github.com/ianhi/mpl-interactions/issues/247
1274+
1275+
1276+
Parameters
1277+
----------
1278+
x, y : float or function
1279+
The text position.
1280+
s : str or function
1281+
The text. Can either be static text, a function returning a string or
1282+
can include {} style formatting. e.g. 'The voltage is {volts:.2f}'
1283+
fontdict : dict[str]
1284+
Passed through to the Text object. Currently not dynamically updateable. See
1285+
https://github.com/ianhi/mpl-interactions/issues/247
1286+
controls : mpl_interactions.controller.Controls
1287+
An existing controls object if you want to tie multiple plot elements to the same set of
1288+
controls
1289+
ax : matplotlib axis, optional
1290+
The axis on which to plot. If none the current axis will be used.
1291+
play_buttons : bool or str or dict, optional
1292+
Whether to attach an ipywidgets.Play widget to any sliders that get created.
1293+
If a boolean it will apply to all kwargs, if a dictionary you choose which sliders you
1294+
want to attach play buttons too.
1295+
1296+
- None: no sliders
1297+
- True: sliders on the lft
1298+
- False: no sliders
1299+
- 'left': sliders on the left
1300+
- 'right': sliders on the right
1301+
1302+
force_ipywidgets : boolean
1303+
If True ipywidgets will always be used, even if not using the ipympl backend.
1304+
If False the function will try to detect if it is ok to use ipywidgets
1305+
If ipywidgets are not used the function will fall back on matplotlib widgets
1306+
1307+
Returns
1308+
-------
1309+
controls
1310+
"""
1311+
ipympl = notebook_backend()
1312+
fig, ax = gogogo_figure(ipympl, ax)
1313+
ipympl or force_ipywidgets
1314+
slider_formats = create_slider_format_dict(slider_formats)
1315+
1316+
kwargs, text_kwargs = kwarg_popper(kwargs, Text_kwargs_list)
1317+
funcs, extra_ctrls, param_excluder = prep_scalars(kwargs, x=x, y=y)
1318+
x = funcs["x"]
1319+
y = funcs["y"]
1320+
controls, params = gogogo_controls(
1321+
kwargs, controls, display_controls, slider_formats, play_buttons, extra_ctrls
1322+
)
1323+
1324+
def update(params, indices, cache):
1325+
x_, y_ = eval_xy(x, y, param_excluder(params, ["x", "y"]), cache)
1326+
text.set_x(x_)
1327+
text.set_y(y_)
1328+
text.set_text(callable_else_value_no_cast(s, params, cache).format(**params))
1329+
1330+
controls._register_function(update, fig, params)
1331+
x_, y_ = eval_xy(x, y, param_excluder(params, ["x", "y"]))
1332+
text = ax.text(
1333+
x_,
1334+
y_,
1335+
callable_else_value_no_cast(s, params).format(**params),
1336+
fontdict=fontdict,
1337+
**text_kwargs,
1338+
)
1339+
return controls

0 commit comments

Comments
 (0)