Source code for plotly_tools

# plotly_tools.py - Module providing a set of tools for rendering graphs using Plotly
# 
# Author: Stefan Fuertinger [stefan.fuertinger@esi-frankfurt.de]
# Created: August 10 2017
# Last modified: <2017-10-09 14:05:23>

from __future__ import division
import numpy as np
import matplotlib 
import matplotlib.pyplot as plt
from matplotlib.colors import colorConverter, cnames
import plotly.offline as po
import plotly.graph_objs as go
import cmocean
import sys
import h5py
import os

##########################################################################################
[docs]def cmap_plt2js(cmap, cmin=0.0, cmax=1.0, Ncols=None): """ Converts Matplotlib colormaps to JavaScript color-scales Parameters ---------- cmap : Matplotlib colormap A linear segmented colormap (e.g., any member of `plt.cm`). cmin : float Lower cutoff value for truncating the input colormap that satisfies `0 <= cmin < 1`. cmax : float Upper cutoff value for truncating the input colormap that satisfies `0 < cmax <= 1`. Ncols : int Resolution of output color-scale, e.g., if `Ncols = 12`, the output color-scale will contain 12 discrete RGB color increments. Returns ------- cscale : list List of lists containing `Ncols` float-string pairs encoding value-RGB color assignments in increasing order starting with `cscale[0] = [0, 'rgb(n_1,m_1,l_1)']` up to `cscale[Ncols-1] = [1.0, 'rgb(n_Ncols,m_Ncols,l_Ncols)']`, where `n_1,...,n_Ncols`, `m_1,...,m_Ncols` and `l_1,...,l_Ncols` are integers between 0 and 255 (see Examples for details). Notes ----- None Examples -------- The following command converts and down-samples the jet colormap in the range [0.1, 1] to a JavaScript color-scale with 12 components >>> import matplotlib.pyplot as plt >>> cscale = cmap_plt2js(plt.cm.jet,cmin=0.1,Ncols=12) >>> cscale [[0.0, 'rgb(0,0,241)'], [0.090909090909090912, 'rgb(0,56,255)'], [0.18181818181818182, 'rgb(0,140,255)'], [0.27272727272727271, 'rgb(0,224,251)'], [0.36363636363636365, 'rgb(64,255,183)'], [0.45454545454545459, 'rgb(131,255,115)'], [0.54545454545454541, 'rgb(199,255,48)'], [0.63636363636363635, 'rgb(255,222,0)'], [0.72727272727272729, 'rgb(255,145,0)'], [0.81818181818181823, 'rgb(255,67,0)'], [0.90909090909090917, 'rgb(218,0,0)'], [1.0, 'rgb(128,0,0)']] See also -------- None """ # Make sure that our single mandatory input argument makes sense if type(cmap).__name__.find('Colormap') < 0: raise TypeError('Input colormap `cmap` has to be a Matplotlib colormap!') # Check colormap bounds varnames = ["cmin", "cmax"] for var in varnames: scalarcheck(eval(var),var,bounds=[0.0,1.0]) if cmin == 1.0: raise ValueError("Lower cut-off for colormap must be < 1.0!") if cmax == 0.0: raise ValueError("Upper cut-off for colormap must be > 0.0!") # Check colormap resolution if Ncols is None: Ncols = cmap.N else: scalarcheck(Ncols,'Ncols',bounds=[1,np.inf],kind='int') # Create listed colormap and sub-sample/truncate input colormap if wanted new_cmap = plt.matplotlib.colors.ListedColormap(cmap(np.linspace(cmin,cmax,Ncols)), name="truncated_{}".format(cmap.name)) # For JavaScript to understand color specifications, we have to first multiply the [0,1]-normed # color array by 255 (so that we get a colormap with 256 possible values between 0 and 255) and create # a list of value - "rgb(m,n,l)" pairs newcols = new_cmap.colors[:,:-1]*255 cscale = [] for m, dc in enumerate(np.linspace(0.0, 1.0, Ncols)): cscale.append([dc, "rgb("+"".join(str(int(np.round(c)))+"," for c in newcols[m,:])[:-1]+")"]) return cscale
##########################################################################################
[docs]def make_brainsurf(surfname, orientation=True, orientation_lines=True, orientation_labels=True, orientation_lw=2, orientation_lcolor="black", orientation_fsize=18, orientation_fcolor="black", shiny_srf=True, surf_opac = 1.0, surf_color='Gainsboro', view=None): """ Create Plotly graph objects to render brain surfaces Parameters ---------- surfname : string Name of brain surface dataset in associated HDF5 container (access the container with `h5py` and use `h5py.File('brainsurf/brainsurf.h5','r').keys()` to see all available surfaces). orientation : bool Flag to control whether axes are rendered to illustrate the employed coordinate system. Use the `orientation_*` keywords for more fine-grained controls. orientation_lines : bool Flag to control whether axes illustrating the employed coordinate system are rendered (only relevant if `orientation = True`). To prevent standard Cartesian coordinate axes from being drawn on top of the brain's coordinate axes, use the `'scene'` item of the return dictionary in Plotly's `Layout` directive, see Examples below for details. orientation_labels : bool Flag to control whether labels ("Anterior", "Posterior", "Inferior", "Superior", "Left", "Right") highlighting the employed coordinate system are rendered (only relevant if `orientation = True`). orientation_lw : float Line-width of axes illustrating the employed coordinate system (only relevant if both `orientation = True` and `orientation_lines = True`). orientation_lcolor : string String to set the color of axes lines. Check `matplotlib.colors.cnames` for supported colors (only relevant if both `orientation = True` and `orientation_lines = True`). orientation_fsize : int Font size of coordinate system labels (only relevant if both `orientation = True` and `orientation_labels = True`). orientation_fcolor : string String to set the color of axes labels. Check `matplotlib.colors.cnames` for supported colors (only relevant if both `orientation = True` and `orientation_labels = True`). shiny_srf : bool Flag that controls whether the rendered brain surface exhibits good or poor light reflection properties making the surface appear "glossy" (`shiny_srf = True`) or matte. surf_opac : float Sets the opacity of the brain surface using a value between 0.0 (fully transparent) and 1.0 (solid). surf_color : str String to set the color of the brain surface. Check `matplotlib.colors.cnames` for supported colors view : str Camera position. Available options are "Axial", "Sagittal" and "Coronal". If `view` is `None` Plotly's default camera position will be selected. Returns ------- ply_dict : dict A dictionary containing at least the item `'brain'` representing the generated Plotly `Mesh3d` object for rendering the brain surface respecting all provided optional arguments. If `orientation = True` the dictionary additionally contains the items `'orientation_lines'` (a Plotly `Scatter3d` object representing coordinate system axes) and `'orientation_labels'` (a Plotly `Scatter3d` object representing axes labels). If only one of the provided keyword arguments `orientation_lines` or `orientation_labels` was `True`, the output dictionary only contains the respective conform item. If `ply_dict` contains the item `'orientation_lines'` and/or the keyword `view` was not `None`, `ply_dict` further contains the item `'scene'` (a nested dictionary which can be forwarded to Plotly's `Layout` directive to control the visibility of Plotly's default axes and/or the initial camera position, see Examples below for details). Notes ----- None Examples -------- The following command returns a dictionary of Plotly objects to render the `BrainMesh_Ch2withCerebellum` brain as fully opaque glossy surface and sets up the initial camera view point in axial position >>> pyt_dict = pyt.make_brainsurf('BrainMesh_Ch2withCerebellum', view='Axial') The generated objects can be subsequently used to create a HTML file for rendering the surface in a web-browser based on embedded JavaScript code that employs D3.js functionality >>> import plotly.offline as po >>> import plotly.graph_objs as go >>> layout = go.Layout(scene=pyt_dict['scene']) >>> fig = go.Figure(data=[pyt_dict['brain'], pyt_dict['orientation_labels'], pyt_dict['orientation_lines']], layout=layout) >>> po.plot(fig, filename='brain.html') See also -------- Plotly : A data analytics and visualization tool for generating interactive 2D and 3D graphs rendered with D3.js. More information available on its `official website <https://plot.ly/>`_ D3.js : A JavaScript library for visualizing data with HTML, SVG, and CSS. More information available on its `official website <https://d3js.org/>`_ """ # Start by making sure that we have access to the brain surface container (which is assumed to # reside in the sub-directory 'brainsurf' of the local folder) # This beautiful construction creates a string representing the absolute path of this script mypath = os.sep.join(os.path.realpath(__file__).split(os.sep)[0:-1]) h5brain = mypath +os.sep+'brainsurf'+os.sep+'brainsurf.h5' try: h5brainfile = h5py.File(h5brain,'r') except: raise IOError("Could not open brain surface HDF5 container "+h5brain+"!") # Now check if the provided surface file-name makes sense if not isinstance(surfname,(str,unicode)): raise TypeError('Brain surface name has to be a string!') supported = h5brainfile.keys() if surfname not in supported: sp_str = str(supported) sp_str = sp_str.replace('[','') sp_str = sp_str.replace(']','') msg = 'Unavailable surface `'+str(surfname)+\ '`. Available options are: '+sp_str raise ValueError(msg) # Check surface parameters if not isinstance(shiny_srf,bool): raise TypeError('Surface lightning atmosphere has to be provided using a binary True/False flag!') scalarcheck(surf_opac,'surf_opac',bounds=[0.0,1.0]) colorcheck(surf_color,'surf_color') surf_color = surf_color.lower() # Let's see if orientation lines/labels are wanted... for var in [orientation, orientation_lines, orientation_labels]: if not isinstance(var,bool): raise TypeError('Orientation lines/labels are turned on/off using binary True/False flags!') # ... if yes, check orientation line/label parameters if orientation: varnames = ['orientation_lw', 'orientation_fsize'] for var in varnames: scalarcheck(eval(var),var,bounds=[0,np.inf]) varnames = ['orientation_lcolor', 'orientation_fcolor'] for var in varnames: colorcheck(eval(var),var) orientation_lcolor = orientation_lcolor.lower() orientation_fcolor = orientation_fcolor.lower() else: orientation_lines = False orientation_labels = False # Finally, check `view` if view is not None: sp_str = "'Axial', 'Sagittal', 'Coronal'" if not isinstance(view,(str,unicode)): raise TypeError('Brain view must be either `None` or one of '+sp_str+"!") view = view[0].upper()+view[1:] if sp_str.find(view) < 0: raise ValueError("Unavailable view `"+view+"`. Available options are "+sp_str) # Now finally start actually doing something # Extract vertex coordinates and triangle indices from given brain container coords = h5brainfile[surfname]['coord'].value tri = h5brainfile[surfname]['tri'].value h5brainfile.close() # Allocate the return dictionary ply_dict = {} # ========================================================================================== # BRAIN SURFACE MESH # ========================================================================================== # Set surface lightning properties depending on whether we want a shiny "wet" brain or not if shiny_srf: amb = 0.4 dif = 0.8 spec = 1.9 rough = 0.1 fres = 0.2 else: amb = 0.5 dif = 0.6 spec = 1.9 rough = 0.99 fres = 4.99 # Use Plotly's `Mesh3d` to render the surface brain = go.Mesh3d( # Cartesian coordinates of vertices x = coords[:,0], y = coords[:,1], z = coords[:,2], # Corresponding triangle indices i = tri[:,0], j = tri[:,1], k = tri[:,2], # Disable mouse interactions and don't let this show up in the legend hoverinfo = "none", showlegend = False, # Set surface opacity and color opacity = surf_opac, color = "rgb("+"".join(str(int(c))+"," for c in np.array(colorConverter.to_rgb(surf_color))*255)[:-1]+")", # Set lighting properties and position lighting = dict( ambient = amb, diffuse = dif, specular = spec, roughness = rough, fresnel = fres ), lightposition = dict( x = -8*1e2, y = 0, z = 4, ) ) ply_dict['brain'] = brain # ========================================================================================== # ANTERIOR/POSTERIOR/... ORIENTATION LINES # ========================================================================================== # If wanted add some "brain-axes" if orientation: # Extend of orientation lines within Cartesian coordinate system xlo, xhi = (-40, 50) ylo, yhi = (-110, 50) zlo, zhi = (-70, 60) # Draw orientation lines by connecting points in 3D space if orientation_lines: lines = go.Scatter3d( # Order obviously matters a lot here... x = [xlo, xlo, None, xlo, xlo, None, xlo, xhi], y = [ylo, ylo, None, ylo, yhi, None, yhi, yhi], z = [zlo, zhi, None, zlo, zlo, None, zlo, zlo], mode = 'lines', line = dict( width = orientation_lw, color = orientation_lcolor), # Disable mouse interactions and don't let this show up in the legend hoverinfo = "none", showlegend = False ) ply_dict['orientation_lines'] = lines # If orientation lines are rendered, don't show Plotly's standard axes scene = {} for key, value in {"xaxis": "x Axis", "yaxis": "y Axis", "zaxis": "z Axis"}.items(): scene[key] = dict(visible = False) ply_dict['scene'] = scene # Give some spatial orientation via textual clues in 3D space if orientation_labels: pos_info = go.Scatter3d( # Again, order matters here: this has to align with the `text` list below x = [xlo, xlo, xlo, xlo, xlo, xhi], y = [ylo, ylo, ylo-10, yhi+15, yhi, yhi], z = [zlo-10, zhi, zlo, zlo, zlo, zlo], mode = 'text', text = ["Inferior", "Superior", "Posterior", "Anterior", "Left", "Right"], # text = ["<i>"+posit+"</i>" for posit in \ # ["Inferior", "Superior", "Posterior", "Anterior", "Left", "Right"]], textfont = dict( size = orientation_fsize, color = orientation_fcolor, ), # Disable mouse interactions and don't let this show up in the legend hoverinfo = "none", showlegend = False ) ply_dict['orientation_labels'] = pos_info # ========================================================================================== # 3D CAMERA SETUP # ========================================================================================== # Set initial camera position (if `view == None` use default camera position) if view is not None: # import ipdb # ipdb.set_trace() # Initialize `scene` dict if necessary if not orientation_lines: scene = {} # Set up camera position if view == "Axial": scene['camera'] = dict( eye = dict( x = 0, y = -1e-6, z = 2, ) ) elif view == "Sagittal": scene['camera'] = dict( eye = dict( x = -2, y = 0, z = -0.1, ) ) elif view == "Coronal": scene['camera'] = dict( eye = dict( x = 0, y = 2, z = -0.1, ) ) ply_dict['scene'] = scene # Throw back the final dictionary return ply_dict
########################################################################################## def scalarcheck(val,varname,kind=None,bounds=None): """ Local helper function performing sanity checks on scalars """ if not np.isscalar(val) or not plt.is_numlike(val) or not np.isreal(val).all(): raise TypeError("Input `"+varname+"` must be a real scalar!") if not np.isfinite(val): raise TypeError("Input `"+varname+"` must be finite!") if kind == 'int': if (round(val) != val): raise ValueError("Input `"+varname+"` must be an integer!") if bounds is not None: if val < bounds[0] or val > bounds[1]: raise ValueError("Input scalar `"+varname+"` must be between "+str(bounds[0])+" and "+str(bounds[1])+"!") ########################################################################################## def colorcheck(colname,varname): """ Local helper function performing sanity checks on Matplotlib color strings """ if not isinstance(colname,(str,unicode)): raise TypeError(varname+' has to be a string!') if colname.lower() not in cnames.keys() and colname not in cnames.values(): msg = "Unsupported color `"+varname+" = "+colname+"`. Check `matplotlib.colors.cnames` for possible choices. " raise ValueError(msg)