-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #70 from Carifio24/volume
Add exporter for volume viewer
- Loading branch information
Showing
23 changed files
with
633 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import re | ||
|
||
from glue.config import settings | ||
from glue_plotly.common import DEFAULT_FONT | ||
|
||
|
||
def dimensions(viewer_state): | ||
# when vispy viewer is in "native aspect ratio" mode, scale axes size by data | ||
if viewer_state.native_aspect: | ||
width = viewer_state.x_max - viewer_state.x_min | ||
height = viewer_state.y_max - viewer_state.y_min | ||
depth = viewer_state.z_max - viewer_state.z_min | ||
|
||
# otherwise, set all axes to be equal size | ||
else: | ||
width = 1200 # this 1200 size is arbitrary, could change to any width; just need to scale rest accordingly | ||
height = 1200 | ||
depth = 1200 | ||
|
||
return [width, height, depth] | ||
|
||
|
||
def projection_type(viewer_state): | ||
return "perspective" if viewer_state.perspective_view else "orthographic" | ||
|
||
|
||
def axis(viewer_state, ax): | ||
title = getattr(viewer_state, f'{ax}_att').label | ||
range = [getattr(viewer_state, f'{ax}_min'), getattr(viewer_state, f'{ax}_max')] | ||
return dict( | ||
title=title, | ||
titlefont=dict( | ||
family=DEFAULT_FONT, | ||
size=20, | ||
color=settings.FOREGROUND_COLOR | ||
), | ||
backgroundcolor=settings.BACKGROUND_COLOR, | ||
showspikes=False, | ||
linecolor=settings.FOREGROUND_COLOR, | ||
tickcolor=settings.FOREGROUND_COLOR, | ||
zeroline=False, | ||
mirror=True, | ||
ticks='outside', | ||
showline=True, | ||
showgrid=False, | ||
showticklabels=True, | ||
tickfont=dict( | ||
family=DEFAULT_FONT, | ||
size=12, | ||
color=settings.FOREGROUND_COLOR), | ||
range=range, | ||
type='linear', | ||
rangemode='normal', | ||
visible=viewer_state.visible_axes | ||
) | ||
|
||
|
||
def bbox_mask(viewer_state, x, y, z): | ||
return (x >= viewer_state.x_min) & (x <= viewer_state.x_max) & \ | ||
(y >= viewer_state.y_min) & (y <= viewer_state.y_max) & \ | ||
(z >= viewer_state.z_min) & (z <= viewer_state.z_max) | ||
|
||
|
||
def clipped_data(viewer_state, layer_state): | ||
x = layer_state.layer[viewer_state.x_att] | ||
y = layer_state.layer[viewer_state.y_att] | ||
z = layer_state.layer[viewer_state.z_att] | ||
|
||
# Plotly doesn't show anything outside the bounding box | ||
mask = bbox_mask(viewer_state, x, y, z) | ||
|
||
return x[mask], y[mask], z[mask], mask | ||
|
||
|
||
def plotly_up_from_vispy(vispy_up): | ||
regex = re.compile("(\\+|-)(x|y|z)") | ||
up = {"x": 0, "y": 0, "z": 0} | ||
m = regex.match(vispy_up) | ||
if m is not None and len(m.groups()) == 2: | ||
sign = 1 if m.group(1) == "+" else -1 | ||
up[m.group(2)] = sign | ||
return up | ||
|
||
|
||
def layout_config(viewer_state): | ||
width, height, depth = dimensions(viewer_state) | ||
return dict( | ||
margin=dict(r=50, l=50, b=50, t=50), # noqa | ||
width=1200, | ||
paper_bgcolor=settings.BACKGROUND_COLOR, | ||
scene=dict( | ||
xaxis=axis(viewer_state, 'x'), | ||
yaxis=axis(viewer_state, 'y'), | ||
zaxis=axis(viewer_state, 'z'), | ||
camera=dict( | ||
projection=dict( | ||
type=projection_type(viewer_state) | ||
), | ||
# Currently there's no way to change this in glue | ||
up=plotly_up_from_vispy("+z") | ||
), | ||
aspectratio=dict(x=1 * viewer_state.x_stretch, | ||
y=height / width * viewer_state.y_stretch, | ||
z=depth / width * viewer_state.z_stretch), | ||
aspectmode='manual' | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
from glue_plotly.utils import rgba_components | ||
from numpy import linspace, meshgrid, nan_to_num, nanmin | ||
|
||
from glue.core import BaseData | ||
from glue.core.state_objects import State | ||
from glue.core.subset_group import GroupedSubset | ||
|
||
from glue_plotly.common import color_info | ||
from glue_plotly.common.base_3d import bbox_mask | ||
|
||
import plotly.graph_objects as go | ||
|
||
|
||
def positions(bounds): | ||
# The viewer bounds are in reverse order | ||
coord_arrays = [linspace(b[0], b[1], num=b[2]) for b in reversed(bounds)] | ||
return meshgrid(*coord_arrays) | ||
|
||
|
||
def parent_layer(viewer_or_state, subset): | ||
data = subset.data | ||
for layer in viewer_or_state.layers: | ||
if layer.layer is data: | ||
return layer | ||
return None | ||
|
||
|
||
def values(viewer_state, layer_state, bounds, precomputed=None): | ||
subset_layer = isinstance(layer_state.layer, GroupedSubset) | ||
parent = layer_state.layer.data if subset_layer else layer_state.layer | ||
parent_label = parent.label | ||
if precomputed is not None and parent_label in precomputed: | ||
data = precomputed[parent_label] | ||
else: | ||
data = parent.compute_fixed_resolution_buffer( | ||
target_data=viewer_state.reference_data, | ||
bounds=bounds, | ||
target_cid=layer_state.attribute | ||
) | ||
|
||
if subset_layer: | ||
subcube = parent.compute_fixed_resolution_buffer( | ||
target_data=viewer_state.reference_data, | ||
bounds=bounds, | ||
subset_state=layer_state.layer.subset_state | ||
) | ||
values = subcube * data | ||
else: | ||
values = data | ||
|
||
# This accounts for two transformations: the fact that the viewer bounds are in reverse order, | ||
# plus a need to change R -> L handedness for Plotly | ||
values = values.transpose(1, 2, 0) | ||
min_value = nanmin(values) | ||
replacement = min_value - 1 | ||
replaced = nan_to_num(values, replacement) | ||
return replaced | ||
|
||
|
||
def colorscale(layer_state, size=10): | ||
color = color_info(layer_state) | ||
r, g, b, a = rgba_components(color) | ||
fractions = [(i / size) ** 0.25 for i in range(size + 1)] | ||
return [f"rgba({f*r},{f*g},{f*b},{f*a})" for f in fractions] | ||
|
||
|
||
def opacity_scale(layer_state): | ||
return [[0, 0], [1, 1]] | ||
|
||
|
||
def isomin_for_layer(viewer_or_state, layer): | ||
if isinstance(layer.layer, GroupedSubset): | ||
parent = parent_layer(viewer_or_state, layer.layer) | ||
if parent is not None: | ||
parent_state = parent if isinstance(parent, State) else parent.state | ||
return parent_state.vmin | ||
|
||
state = layer if isinstance(layer, State) else layer | ||
return state.vmin | ||
|
||
|
||
def isomax_for_layer(viewer_or_state, layer): | ||
if isinstance(layer.layer, GroupedSubset): | ||
parent = parent_layer(viewer_or_state, layer.layer) | ||
if parent is not None: | ||
parent_state = parent if isinstance(parent, State) else parent.state | ||
return parent_state.vmax | ||
|
||
state = layer if isinstance(layer, State) else layer | ||
return state.vmax | ||
|
||
|
||
def traces_for_layer(viewer_state, layer_state, bounds, | ||
isosurface_count=5, add_data_label=True): | ||
|
||
xyz = positions(bounds) | ||
mask = bbox_mask(viewer_state, *xyz) | ||
clipped_xyz = [c[mask] for c in xyz] | ||
clipped_values = values(viewer_state, layer_state, bounds)[mask] | ||
name = layer_state.layer.label | ||
if add_data_label and not isinstance(layer_state.layer, BaseData): | ||
name += " ({0})".format(layer_state.layer.data.label) | ||
|
||
return [go.Volume( | ||
name=name, | ||
hoverinfo="skip", | ||
hovertext=None, | ||
x=clipped_xyz[0], | ||
y=clipped_xyz[1], | ||
z=clipped_xyz[2], | ||
value=clipped_values, | ||
colorscale=colorscale(layer_state), | ||
opacityscale=opacity_scale(layer_state), | ||
isomin=isomin_for_layer(viewer_state, layer_state), | ||
isomax=isomax_for_layer(viewer_state, layer_state), | ||
opacity=layer_state.alpha, | ||
surface_count=isosurface_count, | ||
showscale=False | ||
)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.