Skip to content
Merged

Docs #35

Show file tree
Hide file tree
Changes from all commits
Commits
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
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,132 @@ This code is distributed under MIT license.
* Use `black .` to autoformat.
* Use `flake8 .` to lint.
* Use `pytest .` to run the tests.
* Use `python update_docs_in_readme.py` to update the readme when needed.

On every PR, an app with the same name as your branch is deployed to the Dash
playground instance so that you can change whether your changes did not break
the package.


## Reference

### The VolumeSlicer class

**class `VolumeSlicer(app, volume, *, spacing=None, origin=None, axis=0, reverse_y=True, scene_id=None, color=None, thumbnail=True)`**

A slicer object to show 3D image data in Dash. Upon
instantiation one can provide the following parameters:

* `app` (`dash.Dash`): the Dash application instance.
* `volume` (`ndarray`): the 3D numpy array to slice through. The dimensions
are assumed to be in zyx order. If this is not the case, you can
use `np.swapaxes` to make it so.
* `spacing` (tuple of `float`): The distance between voxels for each
dimension (zyx).The spacing and origin are applied to make the slice
drawn in "scene space" rather than "voxel space".
* `origin` (tuple of `float`): The offset for each dimension (zyx).
* `axis` (`int`): the dimension to slice in. Default 0.
* `reverse_y` (`bool`): Whether to reverse the y-axis, so that the origin of
the slice is in the top-left, rather than bottom-left. Default True.
Note: setting this to False affects performance, see #12.
* `scene_id` (`str`): the scene that this slicer is part of. Slicers
that have the same scene-id show each-other's positions with
line indicators. By default this is derived from `id(volume)`.
* `color` (`str`): the color for this slicer. By default the color is
red, green, or blue, depending on the axis. Set to empty string
for "no color".
* `thumbnail` (`int` or `bool`): preferred size of low-resolution data to be
uploaded to the client. If `False`, the full-resolution data are
uploaded client-side. If `True` (default), a default value of 32 is
used.

Note that this is not a Dash component. The components that make
up the slicer (and which must be present in the layout) are:
`slicer.graph`, `slicer.slider`, and `slicer.stores`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`slicer.graph`, `slicer.slider`, and `slicer.stores`.
`slicer.graph`, `slicer.slider` (optional in layout if several slicers are used), and `slicer.stores`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it is not optional, because we have it as input and output to our callbacks.


**method `VolumeSlicer.create_overlay_data(mask, color=None)`**

Given a 3D mask array and an index, create an object that
can be used as output for `slicer.overlay_data`. The color
can be a hex color or an rgb/rgba tuple. Alternatively, color
can be a list of such colors, defining a colormap.

**property `VolumeSlicer.axis`** (`int`): The axis at which the slicer is slicing.

**property `VolumeSlicer.graph`**: The `dcc.Graph` for this slicer. Use `graph.figure` to access the
Plotly Figure object.

**property `VolumeSlicer.nslices`** (`int`): The number of slices for this slicer.

**property `VolumeSlicer.overlay_data`**: A `dcc.Store` containing the overlay data. The form of this
data is considered an implementation detail; users are expected to use
`create_overlay_data` to create it.

**property `VolumeSlicer.scene_id`** (`str`): The id of the "virtual scene" for this slicer. Slicers that have
the same scene_id show each-other's positions.

**property `VolumeSlicer.slider`**: The `dcc.Slider` to change the index for this slicer. If you
don't want to use the slider, wrap it in a div with style
`display: none`.

**property `VolumeSlicer.state`**: A `dcc.Store` representing the current state of the slicer (present
in slicer.stores). Its data is a dict with the fields:

* "index": the integer slice index.
* "index_changed": a bool indicating whether the index changed since last time.
* "xrange": the view range (2 floats) in the x-dimension (2D).
* "yrange": the view range (2 floats) in the y-dimension (2D).
* "zpos": the float position aling the axis, in scene coordinates.
* "axis": the axis (int) for this slicer.
* "color": the color (str) for this slicer.

The id of the store is a dictionary so it can be used in a
pattern matching Input. Its field are: context, scene, name.
Where scene is the scene_id and name is "state".

**property `VolumeSlicer.stores`**: A list of `dcc.Store` objects that the slicer needs to work.
These must be added to the app layout.



### Reacting to slicer state

It is possible to get notified of updates to slicer position and
view ranges. To get this for all slicers with a specific scene_id, create
a [pattern matching input](https://dash.plotly.com/pattern-matching-callbacks)
like this:
```py
Input({"scene": scene_id, "context": ALL, "name": "state"})
```

See the `state` property for details.


### Setting slicer positions

To programatically set the position of the slicer, create a `dcc.Store` with
a dictionary-id that has the following fields:

* 'context': a unique name for this store.
* 'scene': the scene_id of the slicer objects to set the position for.
* 'name': 'setpos'

The value in the store must be an 3-element tuple (x, y, z) in scene coordinates.
To apply the position for one dimension only, use e.g `(None, None, x)`.


### Performance tips

There tends to be a lot of interaction in an application that contains
slicer objects. Therefore, performance matters to realize a smooth user
experience. Here are some tips to help with that:

* Most importantly, when running the server in debug mode, consider setting
`dev_tools_props_check=False`.
* Also consider creating the `Dash` application with `update_title=None`.
* Setting `reverse_y` to False negatively affects performance. This will be
fixed in a future version of Plotly/Dash.
* For a smooth experience, avoid triggering unnecessary figure updates.
* When adding a callback that uses the slicer position, use the (rate limited)
`state` store rather than the slider value.

72 changes: 72 additions & 0 deletions dash_slicer/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Utilities to produce simple reference docs (in Markdown) from the source code.
"""

import inspect
import dash_slicer


def dedent(text):
"""Dedent a docstring, removing leading whitespace."""
lines = text.lstrip().splitlines()
min_indent = 9999
for i in range(1, len(lines)):
line1 = lines[i]
line2 = line1.lstrip()
if line2:
indent = len(lines[i]) - len(lines[i].lstrip())
min_indent = min(min_indent, indent)
if min_indent > 16:
min_indent = 0
for i in range(1, len(lines)):
lines[i] = lines[i][min_indent:]
return "\n".join(lines)


def get_reference_docs():
"""Create reference documentation from the source code.
A bit like Sphinx autodoc, but using Markdown, and more basic.
Returns a str in Markdown format.

Note that this function is used to build the Dash Slicer chapter
in the Dash docs.
"""
Comment on lines +26 to +33

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure whether to put his here or in dash-docs. The only real advantage is that we can also add our reference docs to the readme, but perhaps we should just provide a link to https://dash.plotly.com/slicer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts welcome :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a good idea to have it here if in dash-docs you can just call this function.


methods = []
props = []

sig = str(inspect.signature(dash_slicer.VolumeSlicer.__init__)).replace(
"self, ", ""
)
doc = f"**class `VolumeSlicer{sig}`**"
doc += "\n\n" + dedent(dash_slicer.VolumeSlicer.__doc__).rstrip()
methods.append(doc)

for name in dir(dash_slicer.VolumeSlicer):
val = getattr(dash_slicer.VolumeSlicer, name)

if name.startswith("_") or not hasattr(val, "__doc__"):
pass
elif callable(val):
# Method
sig = str(inspect.signature(val)).replace("self, ", "")
doc = f"**method `VolumeSlicer.{name}{sig}`**"
doc += "\n\n" + dedent(val.__doc__).rstrip()
methods.append(doc)
else:
# Property
doc = f"**property `VolumeSlicer.{name}`**"
try:
typ = val.fget.__annotations__["return"].__name__
doc += f" (`{typ}`)"
except (AttributeError, KeyError):
pass
doc += ": " + dedent(val.__doc__).rstrip()
props.append(doc)

parts = []
parts.append("### The VolumeSlicer class")
parts += methods
parts += props
parts.append(dash_slicer.slicer.__doc__)
return "\n\n".join(parts)
Loading