holoviz-topics / EarthSim

Tools for working with and visualizing environmental simulations.
https://earthsim.holoviz.org
BSD 3-Clause "New" or "Revised" License
65 stars 21 forks source link

Integrating the vector plots into complex dashboard #275

Closed kcpevey closed 5 years ago

kcpevey commented 5 years ago

I'm working on adapting my code to include vector visualization in way that is seamless to the user and requires as little code duplication as possible. I tested the vector viz outside of this big code base and didn't have any trouble, but now that its integrated, it has issues. Here are the code snippets:

    def create_animation2(self):
        """ Method to create holoviews dynamic map meshes for vector or scalar datasets"""
        if self.mesh_points is None:
            self.reproject_points()

        if 'BEGSCL' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_scalar(), label='scalar')
            return meshes

        elif 'BEGVEC' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_vector(), label='vector')
            return meshes
Inside of a def that creates a panel: 
        # create the meshes for the dynamic map
        meshes = self.adh_viz.create_animation2()

        # Define function which applies colormap and color_range
        def apply_opts(obj, colormap, color_range):
            return obj.options(cmap=colormap).redim.range(**{obj.vdims[0].name: color_range})

        viz = PolyAndPointAnnotator(path_type=gv.Path, 
                    crs=ccrs.GOOGLE_MERCATOR,
                    point_columns=['depth_elevation'],
                    poly_columns=['name']
                    )

        if meshes.label == 'scalar':
            dynamic = hv.util.Dynamic(rasterize(meshes), operation=apply_opts, streams=[Params(self.cmap_opts), Params(self.display_range)]).options(colorbar=True, height=600, width=600) * self.wmts_widget.view() * viz.polys * viz.points
        elif meshes.label == 'vector':
            dynamic = hv.util.Dynamic(datashade(vectorfield_to_paths(meshes, color='Magnitude', magnitude='Magnitude', scale=0.05), aggregator='mean', cmap=process_cmap('viridis'), precompute=True), operation=apply_opts, streams=[Params(self.cmap_opts), Params(self.display_range)]).options(colorbar=True, height=600, width=600) * self.wmts_widget.view() * viz.polys * viz.points
  1. When I have a vector plot, the apply_opts method is throwing an error with the cmap:
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/util/__init__.py dynamic_operation L656
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/core/spaces.py __getitem__ L1379
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/core/spaces.py _execute_callback L1096
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/core/spaces.py __call__ L735
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/util/__init__.py dynamic_operation L656
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/util/__init__.py _process L640
    /Users/rdchlkcp/ers/gitHub/es-workflows/generic_dashboard/es-workflows/es_workflows/adh/model.py apply_opts L630
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/core/dimension.py options L1459
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/util/__init__.py expand_options L181
    /Users/rdchlkcp/ers/gitHub/holoviews/master/holoviews/holoviews/util/__init__.py _options_error L231
    ValueError: Unexpected option 'cmap' for RGB type across all extensions. No similar options found.
  2. It appears that my time slider (and maybe other widgets) are no longer connected up to my map. Is it not possible/advisable for me to wrap my dynamic calls in an if statment? Could that be affecting things?
jbednar commented 5 years ago

For problem 1, you're using datashade, which does not have a cmap option; cmap is for rasterize. You're already coloring by magnitude, so you presumably don't need any colormapping anyway, so you can just remove that argument.

kcpevey commented 5 years ago

But its in my apply_opts because I need it for the scalar images. So I can't have both image types use the same apply_opts callback?

philippjfr commented 5 years ago

You could simply switch to rasterize for both.

jbednar commented 5 years ago

I don't get how it works at all, given that the magnitude is already mapped to color by vectorfield_to_paths; doesn't that leave no room for colormapping?

kcpevey commented 5 years ago

@jbednar the issue was that I am reusing the apply_opts function for multiple plot types. vectorfield_to_paths doesn't need it, but scalar plotting does. Switching to rasterize for both works.

As for question 2:

This works:

    @param.depends('time','result_label')
    def time_mesh(self):
        # add this time step's data as a vdim under the provided label
        data_points = self.mesh_points.add_dimension(self.result_label, 0,
                                                     self.model[self.result_label].sel(times=self.time).data,
                                                     vdim=True)
        return gv.TriMesh((self.tris[['v0', 'v1', 'v2']], data_points), label=self.result_label, crs=ccrs.GOOGLE_MERCATOR)

    def create_animation(self):
        """ Method to create holoviews dynamic map meshes (1D data only)"""
        if self.mesh_points is None:
            self.reproject_points()
        meshes = hv.DynamicMap(self.time_mesh, label='scalar')
        return meshes

But this doesn't:

    @param.depends('time', 'result_label')
    def time_mesh_scalar(self):
        # add this time step's data as a vdim under the provided label
        data_points = self.mesh_points.add_dimension(self.result_label, 0,
                                                     self.model[self.result_label].sel(times=self.time).data,
                                                     vdim=True)
        return gv.TriMesh((self.tris[['v0', 'v1', 'v2']], data_points), label=self.result_label,
                          crs=ccrs.GOOGLE_MERCATOR)

    @param.depends('time', 'result_label')
    def time_mesh_vector(self):
        vx = self.model[self.result_label].sel(times=self.time).data[:, 0]
        vy = self.model[self.result_label].sel(times=self.time).data[:, 1]
        xs = self.mesh_points.data['x']
        ys = self.mesh_points.data['y']
        with np.errstate(divide='ignore', invalid='ignore'):
            angle = np.arctan2(vy, vx)
        mag = np.sqrt(vx ** 2 + vy ** 2)
        return gv.VectorField((xs, ys, angle, mag), vdims=['Angle', 'Magnitude'],
                         crs=ccrs.GOOGLE_MERCATOR)

    def create_animation2(self):
        """ Method to create holoviews dynamic map meshes for vector or scalar datasets"""
        # check to make sure the mesh points have been set.
        if self.mesh_points is None:
            self.reproject_points()

        if 'BEGSCL' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_scalar, label='scalar')
            return meshes

        elif 'BEGVEC' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_vector, label='vector')
            return meshes

        else:
            log.error('Data type not recognized. Must be BEGSCL or BEGVEC.')

I also added the param.depends decorator to the create_animation function but that didn't work either. What am I doing wrong here?

kcpevey commented 5 years ago

I dont understand how the decorator @param.depends('time','result_label') correctly triggers a redraw when I have just one time_mesh method, but when I have alternating methods, it doesn't trigger a redraw?

aeroaks commented 5 years ago

Hi @kcpevey , I am also interested in using Vector/quiver plots and want it to explore using datashader because of large amount of data. Could we talk on what you have done and what approach we can try? Edit: Thanks @philippjfr , Checking out #273

kcpevey commented 5 years ago

@aeroaks I'm using the latest vectorfield_to_paths code that @philippjfr developed in #273 . My current dataset is 836k data points and that method works well enough for that. Having to call it in a dynamicMap callback is a little laggy with a dataset of that size, but it does work.

kcpevey commented 5 years ago

I needed to move my @param.depends calls around. Working code snippets:

    @param.depends('time')
    def time_mesh_scalar(self):
        # add this time step's data as a vdim under the provided label
        data_points = self.mesh_points.add_dimension(self.result_label, 0,
                                                     self.model[self.result_label].sel(times=self.time).data,
                                                     vdim=True)

        # return a trimesh with this data
        return gv.TriMesh((self.tris[['v0', 'v1', 'v2']], data_points), label=self.result_label,
                          crs=ccrs.GOOGLE_MERCATOR)

    @param.depends('time')
    def time_mesh_vector(self):

        vx = self.model[self.result_label].sel(times=self.time).data[:, 0]
        vy = self.model[self.result_label].sel(times=self.time).data[:, 1]
        xs = self.mesh_points.data['x']
        ys = self.mesh_points.data['y']
        with np.errstate(divide='ignore', invalid='ignore'):
            angle = np.arctan2(vy, vx)
        mag = np.sqrt(vx ** 2 + vy ** 2)
        return gv.VectorField((xs, ys, angle, mag), vdims=['Angle', 'Magnitude'],
                         crs=ccrs.GOOGLE_MERCATOR)

    @param.depends('result_label')
    def create_animation2(self):
        """ Method to create holoviews dynamic map meshes for vector or scalar datasets"""
        # check to make sure the mesh points have been set.
        if self.mesh_points is None:
            self.reproject_points()

        if 'BEGSCL' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_scalar, label='scalar')
            return meshes

        elif 'BEGVEC' in self.model[self.result_label].attrs.keys():
            meshes = hv.DynamicMap(self.time_mesh_vector, label='vector')
            return meshes

        else:
            log.error('Data type not recognized. Must be BEGSCL or BEGVEC.')
    @param.depends('adh_viz.result_label')
    def run(self):

        # create the meshes for the dynamic map
        meshes = self.adh_viz.create_animation2()

        # Define function which applies colormap and color_range
        def apply_opts(obj, colormap, color_range):
            return obj.options(cmap=colormap).redim.range(**{obj.vdims[0].name: color_range}).options(
                clipping_colors={'NaN': 'transparent', 'min': 'transparent'})

        if meshes.label == 'scalar':
            # Apply the colormap and color range dynamically
            dynamic = hv.util.Dynamic(rasterize(meshes), operation=apply_opts, streams=[Params(self.cmap_opts), Params(self.display_range)]
                                     ).options(colorbar=True, height=600, width=600) * self.wmts_widget.view() * self.annotator.polys * self.annotator.points
        elif meshes.label == 'vector':
            # Apply the colormap and color range dynamically
            dynamic = hv.util.Dynamic(rasterize(vectorfield_to_paths(meshes, color='Magnitude', magnitude='Magnitude', scale=0.05), aggregator='mean', cmap=process_cmap('viridis'), precompute=True), operation=apply_opts, streams=[Params(self.cmap_opts), Params(self.display_range)]).options(colorbar=True, height=600, width=600) * self.wmts_widget.view() * self.annotator.polys * self.annotator.points