import numpy as np
from plotly.graph_objs.layout import Image as _Image
from ...core.util import VersionError
from ...element import Tiles
from .element import ElementPlot
from .selection import PlotlyOverlaySelectionDisplay
[docs]class RGBPlot(ElementPlot):
style_opts = ['opacity']
apply_ranges = True
selection_display = PlotlyOverlaySelectionDisplay()
_supports_geo = True
[docs] def init_graph(self, datum, options, index=0, is_geo=False, **kwargs):
if is_geo:
layer = dict(datum, **options)
dummy_trace = {
'type': 'scattermapbox',
'lat': [None],
'lon': [None],
'mode': 'markers',
'showlegend': False
}
return dict(mapbox=dict(layers=[layer]),
traces=[dummy_trace])
else:
image = dict(datum, **options)
# Create a dummy invisible scatter trace for this image.
# This serves two purposes
# 1. The two points placed on the corners of the image are used by the
# autoscale logic to allow using the autoscale button to properly center
# the image.
# 2. This trace will be given a UID, and this UID will make it possible to
# associate callbacks with the image element. This is needed, in particular
# to support datashader
dummy_trace = {
'type': 'scatter',
'x': [image['x'], image['x'] + image['sizex']],
'y': [image['y'] - image['sizey'], image['y']],
'mode': 'markers',
'marker': {'opacity': 0},
"showlegend": False,
}
return dict(images=[image],
traces=[dummy_trace])
def get_data(self, element, ranges, style, is_geo=False, **kwargs):
try:
import PIL.Image
except ImportError:
raise VersionError("""\
Rendering RGB elements with the plotly backend requires the Pillow package""") from None
img = np.flip(
np.dstack([element.dimension_values(d, flat=False)
for d in element.vdims]),
axis=0
)
if img.dtype.kind == 'f':
img = img * 255
if img.size and (img.min() < 0 or img.max() > 255):
self.param.warning('Clipping input data to the valid '
'range for RGB data ([0..1] for '
'floats or [0..255] for integers).')
img = np.clip(img, 0, 255)
if img.dtype.name != 'uint8':
img = img.astype(np.uint8)
if 0 in img.shape:
img = np.zeros((1, 1, 3), dtype=np.uint8)
if img.ndim != 3 or img.shape[2] not in (3, 4):
raise ValueError(f"Unsupported image array with shape: {img.shape}")
# Ensure axis inversions are handled correctly
l, b, r, t = element.bounds.lbrt()
if self.invert_axes:
img = np.rot90(img.swapaxes(0, 1), 2)
l, b, r, t = b, l, t, r
if self.invert_xaxis:
l, r = r, l
img = np.flip(img, axis=1)
if self.invert_yaxis:
img = np.flip(img, axis=0)
b, t = t, b
if img.shape[2] == 3:
pil_img = PIL.Image.fromarray(img, 'RGB')
else:
pil_img = PIL.Image.fromarray(img, 'RGBA')
source = _Image(source=pil_img).source
if is_geo:
lon_left, lat_top = Tiles.easting_northing_to_lon_lat(l, t)
lon_right, lat_bottom = Tiles.easting_northing_to_lon_lat(r, b)
coordinates = [
[lon_left, lat_top],
[lon_right, lat_top],
[lon_right, lat_bottom],
[lon_left, lat_bottom],
]
layer = {
"sourcetype": "image",
"source": source,
"coordinates": coordinates,
"below": 'traces',
}
return [layer]
else:
return [dict(source=source,
x=l,
y=t,
sizex=r - l,
sizey=t - b,
xref='x',
yref='y',
sizing='stretch',
layer='above')]