-
-
Notifications
You must be signed in to change notification settings - Fork 75
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 #102 from ioam/projecting_stream_callbacks
Add projection aware bokeh stream callbacks
- Loading branch information
Showing
3 changed files
with
271 additions
and
88 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,178 @@ | ||
import numpy as np | ||
|
||
from holoviews.plotting.bokeh.callbacks import ( | ||
RangeXYCallback, BoundsCallback, BoundsXCallback, BoundsYCallback, | ||
PointerXYCallback, PointerXCallback, PointerYCallback, TapCallback, | ||
SingleTapCallback, DoubleTapCallback, MouseEnterCallback, | ||
MouseLeaveCallback, RangeXCallback, RangeYCallback) | ||
from holoviews.streams import (Stream, PointerXY, RangeXY, RangeX, RangeY, | ||
PointerX, PointerY, BoundsX, BoundsY, | ||
Tap, SingleTap, DoubleTap, MouseEnter, | ||
MouseLeave, Bounds, BoundsXY) | ||
|
||
from ...util import project_extents | ||
from .plot import DEFAULT_PROJ | ||
|
||
|
||
def skip(cb, msg, attributes): | ||
""" | ||
Skips applying transforms if data is not geographic. | ||
""" | ||
return (not all(a in msg for a in attributes) or | ||
not getattr(cb.plot, 'geographic', False) or | ||
not hasattr(cb.plot.current_frame, 'crs')) | ||
|
||
|
||
def project_ranges(cb, msg, attributes): | ||
""" | ||
Projects ranges supplied by a callback. | ||
""" | ||
if skip(cb, msg, attributes): | ||
return msg | ||
|
||
x0, x1 = msg.get('x_range', (0, 1000)) | ||
y0, y1 = msg.get('y_range', (0, 1000)) | ||
extents = x0, y0, x1, y1 | ||
x0, y0, x1, y1 = project_extents(extents, DEFAULT_PROJ, | ||
cb.plot.current_frame.crs) | ||
coords = {'x_range': (x0, x1), 'y_range': (y0, y1)} | ||
return {k: v for k, v in coords.items() if k in attributes} | ||
|
||
|
||
def project_point(cb, msg, attributes=('x', 'y')): | ||
""" | ||
Projects a single point supplied by a callback | ||
""" | ||
if skip(cb, msg, attributes): return msg | ||
x, y = msg.get('x', 0), msg.get('y', 0) | ||
crs = cb.plot.current_frame.crs | ||
coordinates = crs.transform_points(DEFAULT_PROJ, np.array([x]), np.array([y])) | ||
msg['x'], msg['y'] = coordinates[0, :2] | ||
return {k: v for k, v in msg.items() if k in attributes} | ||
|
||
|
||
class GeoRangeXYCallback(RangeXYCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoRangeXYCallback, self)._process_msg(msg) | ||
return project_ranges(self, msg, ('x_range', 'y_range')) | ||
|
||
class GeoRangeXCallback(RangeXCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoRangeXCallback, self)._process_msg(msg) | ||
return project_ranges(self, msg, ('x_range',)) | ||
|
||
|
||
class GeoRangeYCallback(RangeYCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoRangeYCallback, self)._process_msg(msg) | ||
return project_ranges(self, msg, ('y_range',)) | ||
|
||
|
||
class GeoBoundsXYCallback(BoundsCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoBoundsXYCallback, self)._process_msg(msg) | ||
if skip(self, msg, ('bounds',)): return msg | ||
msg['bounds'] = project_extents(msg['bounds'], DEFAULT_PROJ, | ||
self.plot.current_frame.crs) | ||
return msg | ||
|
||
|
||
class GeoBoundsXCallback(BoundsXCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoBoundsXCallback, self)._process_msg(msg) | ||
if skip(self, msg, ('boundsx',)): return msg | ||
x0, x1 = msg['boundsx'] | ||
x0, _, x1, _ = project_extents((x0, 0, x1, 0), DEFAULT_PROJ, | ||
self.plot.current_frame.crs) | ||
return {'boundsx': (x0, x1)} | ||
|
||
|
||
class GeoBoundsYCallback(BoundsYCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoBoundsYCallback, self)._process_msg(msg) | ||
if skip(self, msg, ('boundsy',)): return msg | ||
y0, y1 = msg['boundsy'] | ||
_, y0, _, y1 = project_extents((0, y0, 0, y1), DEFAULT_PROJ, | ||
self.plot.current_frame.crs) | ||
return {'boundsy': (y0, y1)} | ||
|
||
|
||
class GeoPointerXYCallback(PointerXYCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoPointerXYCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
class GeoPointerXCallback(PointerXCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoPointerXCallback, self)._process_msg(msg) | ||
return project_point(self, msg, ('x',)) | ||
|
||
|
||
class GeoPointerYCallback(PointerYCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoPointerYCallback, self)._process_msg(msg) | ||
return project_point(self, msg, ('y',)) | ||
|
||
|
||
class GeoTapCallback(TapCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoTapCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
class GeoSingleTapCallback(SingleTapCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoSingleTapCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
class GeoDoubleTapCallback(DoubleTapCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoDoubleTapCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
class GeoMouseEnterCallback(MouseEnterCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoMouseEnterCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
class GeoMouseLeaveCallback(MouseLeaveCallback): | ||
|
||
def _process_msg(self, msg): | ||
msg = super(GeoMouseLeaveCallback, self)._process_msg(msg) | ||
return project_point(self, msg) | ||
|
||
|
||
callbacks = Stream._callbacks['bokeh'] | ||
|
||
callbacks[RangeXY] = GeoRangeXYCallback | ||
callbacks[RangeX] = GeoRangeXCallback | ||
callbacks[RangeY] = GeoRangeYCallback | ||
callbacks[Bounds] = GeoBoundsXYCallback | ||
callbacks[BoundsXY] = GeoBoundsXYCallback | ||
callbacks[BoundsX] = GeoBoundsXCallback | ||
callbacks[BoundsY] = GeoBoundsYCallback | ||
callbacks[PointerXY] = GeoPointerXYCallback | ||
callbacks[PointerX] = GeoPointerXCallback | ||
callbacks[PointerY] = GeoPointerYCallback | ||
callbacks[Tap] = GeoTapCallback | ||
callbacks[SingleTap] = GeoSingleTapCallback | ||
callbacks[DoubleTap] = GeoDoubleTapCallback | ||
callbacks[MouseEnter] = GeoMouseEnterCallback | ||
callbacks[MouseLeave] = GeoMouseLeaveCallback |
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,84 @@ | ||
""" | ||
Module for geographic bokeh plot baseclasses. | ||
""" | ||
|
||
import param | ||
import numpy as np | ||
|
||
from cartopy.crs import GOOGLE_MERCATOR | ||
from bokeh.models.tools import BoxZoomTool | ||
from bokeh.models import MercatorTickFormatter, MercatorTicker | ||
from holoviews.plotting.bokeh.element import ElementPlot, OverlayPlot as HvOverlayPlot | ||
|
||
from ...element import is_geographic, _Element | ||
from ...util import project_extents | ||
|
||
DEFAULT_PROJ = GOOGLE_MERCATOR | ||
|
||
class GeoPlot(ElementPlot): | ||
""" | ||
Plotting baseclass for geographic plots with a cartopy projection. | ||
""" | ||
|
||
default_tools = param.List(default=['save', 'pan', 'wheel_zoom', | ||
BoxZoomTool(match_aspect=True), 'reset'], | ||
doc="A list of plugin tools to use on the plot.") | ||
|
||
show_grid = param.Boolean(default=False, doc=""" | ||
Whether to show gridlines on the plot.""") | ||
|
||
# Project operation to apply to the element | ||
_project_operation = None | ||
|
||
def __init__(self, element, **params): | ||
super(GeoPlot, self).__init__(element, **params) | ||
self.geographic = is_geographic(self.hmap.last) | ||
|
||
|
||
def _axis_properties(self, axis, key, plot, dimension=None, | ||
ax_mapping={'x': 0, 'y': 1}): | ||
axis_props = super(GeoPlot, self)._axis_properties(axis, key, plot, | ||
dimension, ax_mapping) | ||
if self.geographic: | ||
dimension = 'lon' if axis == 'x' else 'lat' | ||
axis_props['ticker'] = MercatorTicker(dimension=dimension) | ||
axis_props['formatter'] = MercatorTickFormatter(dimension=dimension) | ||
return axis_props | ||
|
||
|
||
def get_extents(self, element, ranges): | ||
""" | ||
Subclasses the get_extents method using the GeoAxes | ||
set_extent method to project the extents to the | ||
Elements coordinate reference system. | ||
""" | ||
extents = super(GeoPlot, self).get_extents(element, ranges) | ||
if not getattr(element, 'crs', None) or not self.geographic: | ||
return extents | ||
elif any(e is None or not np.isfinite(e) for e in extents): | ||
extents = None | ||
else: | ||
try: | ||
extents = project_extents(extents, element.crs, DEFAULT_PROJ) | ||
except: | ||
extents = None | ||
return (np.NaN,)*4 if not extents else extents | ||
|
||
|
||
def get_data(self, element, ranges, style): | ||
if self._project_operation and self.geographic and element.crs != DEFAULT_PROJ: | ||
element = self._project_operation(element) | ||
return super(GeoPlot, self).get_data(element, ranges, style) | ||
|
||
|
||
class OverlayPlot(GeoPlot, HvOverlayPlot): | ||
""" | ||
Subclasses the HoloViews OverlayPlot to add custom behavior | ||
for geographic plots. | ||
""" | ||
|
||
def __init__(self, element, **params): | ||
super(OverlayPlot, self).__init__(element, **params) | ||
self.geographic = any(element.traverse(is_geographic, [_Element])) | ||
if self.geographic: | ||
self.show_grid = False |