Repository: bluesky/bluesky (python)

Issue:
LiveGrid needs to accept a title kwarg
<!--- Provide a general summary of the issue in the Title above -->

## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
We should be able to provide a title of a plot generated by LiveGrid`.

## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
Not currently possible without a bulky workaround.

## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
Requested by @andrewmkiss at SRX.

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
`xf05id1-ws2` at NSLS-II SRX. See https://github.com/NSLS-II-SRX/profile_collection/pull/11 for details.

Pull request:
Expose API to set title on LiveGrid plots
Add a keyword argument `title=""` to `LiveGrid.__init__` that allows overriding the default title generated from the scan number.

## Motivation and Context

Fixes #1396 

## How Has This Been Tested?

```
from ophyd.sim import det4, motor1, motor2

from bluesky import RunEngine
from bluesky.callbacks.mpl_plotting import LiveGrid
from bluesky.plans import grid_scan

RE = RunEngine()
RE(
    grid_scan([det4], motor1, -3, 3, 6, motor2, -5, 5, 10, False),
    LiveGrid((6, 10), "det4"),
)
```

![notitle](https://github.com/bluesky/bluesky/assets/15689569/987c0aab-7539-4c91-ada0-219b6f40318e)

```
from ophyd.sim import det4, motor1, motor2

from bluesky import RunEngine
from bluesky.callbacks.mpl_plotting import LiveGrid
from bluesky.plans import grid_scan

RE = RunEngine()
RE(
    grid_scan([det4], motor1, -3, 3, 6, motor2, -5, 5, 10, False),
    LiveGrid((6, 10), "det4", title="MY PLOT"),
)
```

![title](https://github.com/bluesky/bluesky/assets/15689569/858f1b13-3ffa-4340-bd29-b8cecdde034e)

```
from ophyd.sim import det4, motor1, motor2

from bluesky import RunEngine
from bluesky.callbacks.mpl_plotting import LiveGrid
from bluesky.plans import grid_scan

RE = RunEngine()
RE(
    grid_scan([det4], motor1, -3, 3, 6, motor2, -5, 5, 10, False),
    LiveGrid((6, 10), "det4", title=""),
```

![emptytitle](https://github.com/bluesky/bluesky/assets/15689569/f3cceb97-7c04-49ea-a78f-027e585dda00)


Patch:
diff --git a/src/bluesky/callbacks/mpl_plotting.py b/src/bluesky/callbacks/mpl_plotting.py
index 4c98c3a3..27641e9b 100644
--- a/src/bluesky/callbacks/mpl_plotting.py
+++ b/src/bluesky/callbacks/mpl_plotting.py
@@ -450,6 +450,10 @@ class LiveGrid(QtAwareCallback):
         Defines the positive direction of the y axis, takes the values 'up'
         (default) or 'down'.
 
+    title : string, optional
+        Override title of plot. If None (default), title is generated from the scan
+        ID. Set to empty string to remove title.
+
     See Also
     --------
     :class:`bluesky.callbacks.mpl_plotting.LiveScatter`.
@@ -469,6 +473,7 @@ class LiveGrid(QtAwareCallback):
         ax=None,
         x_positive="right",
         y_positive="up",
+        title=None,
         **kwargs,
     ):
         super().__init__(**kwargs)
@@ -478,7 +483,7 @@ class LiveGrid(QtAwareCallback):
         def setup():
             # Run this code in start() so that it runs on the correct thread.
             nonlocal raster_shape, I, clim, cmap, xlabel, ylabel, extent  # noqa: E741
-            nonlocal aspect, ax, x_positive, y_positive, kwargs
+            nonlocal aspect, ax, x_positive, y_positive, title, kwargs
             with self.__setup_lock:
                 if self.__setup_event.is_set():
                     return
@@ -506,6 +511,7 @@ class LiveGrid(QtAwareCallback):
             self.aspect = aspect
             self.x_positive = x_positive
             self.y_positive = y_positive
+            self.title = title
 
         self.__setup = setup
 
@@ -548,7 +554,11 @@ class LiveGrid(QtAwareCallback):
             raise ValueError('y_positive must be either "up" or "down"')
 
         self.im = im
-        self.ax.set_title("scan {uid} [{sid}]".format(sid=doc["scan_id"], uid=doc["uid"][:6]))
+        self.ax.set_title(
+            self.title
+            if self.title is not None
+            else "scan {uid} [{sid}]".format(sid=doc["scan_id"], uid=doc["uid"][:6])
+        )
         self.snaking = doc.get("snaking", (False, False))
 
         cb = self.ax.figure.colorbar(im, ax=self.ax)


Test patch:
diff --git a/src/bluesky/tests/test_callbacks.py b/src/bluesky/tests/test_callbacks.py
index 471264ac..734033a9 100644
--- a/src/bluesky/tests/test_callbacks.py
+++ b/src/bluesky/tests/test_callbacks.py
@@ -366,6 +366,15 @@ def test_live_grid(RE, hw):
         RE(grid_scan([hw.det4], hw.motor1, -3, 3, 6, hw.motor2, -5, 5, 10, False), LiveRaster((6, 10), "det4"))
 
 
+def test_live_grid_title(RE, hw):
+    hw.motor1.delay = 0
+    hw.motor2.delay = 0
+    RE(
+        grid_scan([hw.det4], hw.motor1, -3, 3, 6, hw.motor2, -5, 5, 10, False),
+        LiveGrid((6, 10), "det4", title="Det 4 Grid Plot"),
+    )
+
+
 def test_live_scatter(RE, hw):
     RE(
         grid_scan([hw.det5], hw.jittery_motor1, -3, 3, 6, hw.jittery_motor2, -5, 5, 10, False),