holoviz / geoviews

Simple, concise geographical visualization in Python
http://geoviews.org
BSD 3-Clause "New" or "Revised" License
584 stars 75 forks source link

Path does not support string type vdims for color_index #272

Closed ktrapeznikov closed 5 years ago

ktrapeznikov commented 5 years ago

color_index for Path objects only works for number-like fields not categorical fields like strings

I want to color paths by a categorical value stored as a string in a GeoPandas data frame. Here is an example data frame of road segments stored as LineStrings:

 index      highway                                           geometry  \
0     19    secondary  LINESTRING (-71.1314593 42.4147212, -71.131563...   
1     20    secondary  LINESTRING (-71.13245980000001 42.4145678, -71...   
2     21  residential  LINESTRING (-71.13634260000001 42.411618, -71....   
3     22  residential  LINESTRING (-71.1692982 42.4129473, -71.168724...   
4     23  residential  LINESTRING (-71.1735769 42.4195657, -71.174599...   

   color_field  
0     0.945582  
1     0.796972  
2     0.603900  
3     0.436615  
4     0.636555 

I want to color each path according to the highway type.

%%opts WMTS [width=500 height=500] 
%%opts Path [color_index="highway" colorbar=True] (cmap="blues" line_width=3)
map_tiles = gv.tile_sources.CartoDark
map_tiles*gv.Path(geodf, vdims = ["color_field","highway"], label = "roads")

If I do, I get the following error:

ValueError                                Traceback (most recent call last)
~/miniconda3/envs/pyviz/lib/python3.6/site-packages/IPython/core/formatters.py in __call__(self, obj, include, exclude)
    968 
    969             if method is not None:
--> 970                 return method(include=include, exclude=exclude)
    971             return None
    972         else:

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/core/dimension.py in _repr_mimebundle_(self, include, exclude)
   1391         combined and returned.
   1392         """
-> 1393         return Store.render(self)
   1394 
   1395 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/core/options.py in render(cls, obj)
   1451         data, metadata = {}, {}
   1452         for hook in hooks:
-> 1453             ret = hook(obj)
   1454             if ret is None:
   1455                 continue

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/ipython/display_hooks.py in pprint_display(obj)
    270     if not ip.display_formatter.formatters['text/plain'].pprint:
    271         return None
--> 272     return display(obj, raw_output=True)
    273 
    274 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/ipython/display_hooks.py in display(obj, raw_output, **kwargs)
    240     elif isinstance(obj, (CompositeOverlay, ViewableElement)):
    241         with option_state(obj):
--> 242             output = element_display(obj)
    243     elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    244         with option_state(obj):

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/ipython/display_hooks.py in wrapped(element)
    140         try:
    141             max_frames = OutputSettings.options['max_frames']
--> 142             mimebundle = fn(element, max_frames=max_frames)
    143             if mimebundle is None:
    144                 return {}, {}

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/ipython/display_hooks.py in element_display(element, max_frames)
    186         return None
    187 
--> 188     return render(element)
    189 
    190 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/ipython/display_hooks.py in render(obj, **kwargs)
     63         renderer = renderer.instance(fig='png')
     64 
---> 65     return renderer.components(obj, **kwargs)
     66 
     67 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/renderer.py in components(self, obj, fmt, comm, **kwargs)
    247         # Bokeh has to handle comms directly in <0.12.15
    248         comm = False if bokeh_version < '0.12.15' else comm
--> 249         return super(BokehRenderer, self).components(obj,fmt, comm, **kwargs)
    250 
    251 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/renderer.py in components(self, obj, fmt, comm, **kwargs)
    319             plot = obj
    320         else:
--> 321             plot, fmt = self._validate(obj, fmt)
    322 
    323         widget_id = None

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/renderer.py in _validate(self, obj, fmt, **kwargs)
    218         if isinstance(obj, tuple(self.widgets.values())):
    219             return obj, 'html'
--> 220         plot = self.get_plot(obj, renderer=self, **kwargs)
    221 
    222         fig_formats = self.mode_formats['fig'][self.mode]

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/renderer.py in get_plot(self_or_cls, obj, doc, renderer, **kwargs)
    132             curdoc().theme = self_or_cls.theme
    133         doc.theme = self_or_cls.theme
--> 134         plot = super(BokehRenderer, self_or_cls).get_plot(obj, renderer, **kwargs)
    135         plot.document = doc
    136         return plot

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/renderer.py in get_plot(self_or_cls, obj, renderer, **kwargs)
    205             init_key = tuple(v if d is None else d for v, d in
    206                              zip(plot.keys[0], defaults))
--> 207             plot.update(init_key)
    208         else:
    209             plot = obj

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/plot.py in update(self, key)
    593     def update(self, key):
    594         if len(self) == 1 and ((key == 0) or (key == self.keys[0])) and not self.drawn:
--> 595             return self.initialize_plot()
    596         item = self.__getitem__(key)
    597         self.traverse(lambda x: setattr(x, '_updated', True))

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/geoviews/plotting/bokeh/plot.py in initialize_plot(self, ranges, plot, plots, source)
     86     def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
     87         opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
---> 88         fig = super(GeoPlot, self).initialize_plot(ranges, plot, plots, **opts)
     89         if self.geographic and self.show_bounds and not self.overlaid:
     90             from . import GeoShapePlot

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/element.py in initialize_plot(self, ranges, plot, plots)
   1829             if self.tabs:
   1830                 subplot.overlaid = False
-> 1831             child = subplot.initialize_plot(ranges, plot, plots)
   1832             if isinstance(element, CompositeOverlay):
   1833                 # Ensure that all subplots are in the same state

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/geoviews/plotting/bokeh/plot.py in initialize_plot(self, ranges, plot, plots, source)
     86     def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
     87         opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
---> 88         fig = super(GeoPlot, self).initialize_plot(ranges, plot, plots, **opts)
     89         if self.geographic and self.show_bounds and not self.overlaid:
     90             from . import GeoShapePlot

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/element.py in initialize_plot(self, ranges, plot, plots, source)
   1047         self.handles['plot'] = plot
   1048 
-> 1049         self._init_glyphs(plot, element, ranges, source)
   1050         if not self.overlaid:
   1051             self._update_plot(key, plot, style_element)

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/element.py in _init_glyphs(self, plot, element, ranges, source)
    994         else:
    995             style = self.style[self.cyclic_index]
--> 996             data, mapping, style = self.get_data(element, ranges, style)
    997             current_id = element._plot_id
    998 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/geoviews/plotting/bokeh/plot.py in get_data(self, element, ranges, style)
    132         if self._project_operation and self.geographic:
    133             element = self._project_operation(element, projection=proj)
--> 134         return super(GeoPlot, self).get_data(element, ranges, style)
    135 
    136 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/path.py in get_data(self, element, ranges, style)
     82         if cdim:
     83             dim_name = util.dimension_sanitizer(cdim.name)
---> 84             cmapper = self._get_colormapper(cdim, element, ranges, style)
     85             mapping['line_color'] = {'field': dim_name, 'transform': cmapper}
     86             vals[dim_name] = []

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/holoviews/plotting/bokeh/element.py in _get_colormapper(self, eldim, element, ranges, style, factors, colors, group, name)
   1455                 cmapper.update(**opts)
   1456         else:
-> 1457             cmapper = colormapper(palette=palette, **opts)
   1458             self.handles[name] = cmapper
   1459             self.handles['color_dim'] = eldim

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/models/mappers.py in __init__(self, palette, **kwargs)
     41         if palette is not None:
     42             kwargs['palette'] = palette
---> 43         super(ColorMapper, self).__init__(**kwargs)
     44 
     45 @abstract

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/model.py in __init__(self, **kwargs)
    259         self._document = None
    260         self._temp_document = None
--> 261         super(Model, self).__init__(**kwargs)
    262         default_theme.apply_to_model(self)
    263 

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/has_props.py in __init__(self, **properties)
    252 
    253         for name, value in properties.items():
--> 254             setattr(self, name, value)
    255 
    256     def __setattr__(self, name, value):

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/has_props.py in __setattr__(self, name, value)
    279 
    280         if name in props or (descriptor is not None and descriptor.fset is not None):
--> 281             super(HasProps, self).__setattr__(name, value)
    282         else:
    283             matches, text = difflib.get_close_matches(name.lower(), props), "similar"

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/property/descriptors.py in __set__(self, obj, value, setter)
    544             raise RuntimeError("%s.%s is a readonly property" % (obj.__class__.__name__, self.name))
    545 
--> 546         self._internal_set(obj, value, setter=setter)
    547 
    548     def __delete__(self, obj):

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/property/descriptors.py in _internal_set(self, obj, value, hint, setter)
    765 
    766         '''
--> 767         value = self.property.prepare_value(obj, self.name, value)
    768 
    769         old = self.__get__(obj, obj.__class__)

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/property/bases.py in prepare_value(self, obj_or_cls, name, value)
    326                     break
    327             else:
--> 328                 raise e
    329         else:
    330             value = self.transform(value)

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/property/bases.py in prepare_value(self, obj_or_cls, name, value)
    319         try:
    320             if validation_on():
--> 321                 self.validate(value)
    322         except ValueError as e:
    323             for tp, converter in self.alternatives:

~/miniconda3/envs/pyviz/lib/python3.6/site-packages/bokeh/core/property/bases.py in validate(self, value, detail)
    451                 nice_join([ cls.__name__ for cls in self._underlying_type ]), value, type(value).__name__
    452             )
--> 453             raise ValueError(msg)
    454 
    455     def from_json(self, json, models=None):

ValueError: expected a value of type Real, got footway of type str

(Sorry for the long error message).

If change the color_index to color_field a float then everything works fine.

%%opts WMTS [width=500 height=500] 
%%opts Path [color_index="color_field" colorbar=True] (cmap="blues" line_width=3)
map_tiles = gv.tile_sources.CartoDark
map_tiles*gv.Path(geodf, vdims = ["color_field","highway"], label = "roads")
image

versions:

geoviews                  1.6.0                      py_0    pyviz/label/dev
geoviews-core             1.6.0                      py_0    pyviz/label/dev
holoviews                 1.11.0             pyh39e3cac_0    pyviz/label/dev
bokeh                     1.0.0                      py_0    pyviz/label/dev
philippjfr commented 5 years ago

Does the same problem occur if you change it to this?

%%opts Path [colorbar=True] (cmap="blues" line_width=3 color='highway')

Since holoviews 1.11 was released the *_index options are now considered deprecated and data dimensions can now be mapped directly to style options.

ktrapeznikov commented 5 years ago

Yep. That fixed it! Thanks.

However, colorbar still does not show for categorical values.

philippjfr commented 5 years ago

Bokeh does not support colorbars for categorical values, it should however support a legend. Does %%opts Path [show_legend=True] work?

ktrapeznikov commented 5 years ago

Yep. I had to remove label="roads" from the Path declaration. Otherwise the only legend was "roads".

philippjfr commented 5 years ago

Yep. I had to remove label="roads" from the Path declaration. Otherwise the only legend was "roads".

The more specific legend should probably take precedence so that isn't necessary, but that's an issue to raise in holoviews.