Source code for geoml.viz.plotly

# geoML - machine learning models for geospatial data
# Copyright (C) 2020  Ítalo Gomes Gonçalves
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR a PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import numpy as _np
import pandas as _pd
import collections.abc as _collections_abc


def _deep_update(d, u):
    """
    https://stackoverflow.com/questions/3232943/
    update-value-of-a-nested-dictionary-of-varying-depth
    """
    for k, v in u.items():
        if isinstance(v, _collections_abc.Mapping):
            d[k] = _deep_update(d.get(k, {}), v)
        else:
            d[k] = v
    return d


[docs] def aspect_ratio_2d(vertical_exaggeration: float = 1) -> dict: return {"xaxis": {"constrain": "domain", "showexponent": "none", "exponentformat": "none"}, "yaxis": {"scaleanchor": "x", "showexponent": "none", "exponentformat": "none", "scaleratio": vertical_exaggeration}}
[docs] def aspect_ratio_3d(bounding_box, vertical_exaggeration: float = 1) -> dict: # aspect ratio aspect = bounding_box.max[0] - bounding_box.min[0] aspect = aspect / aspect[0] aspect[-1] = aspect[-1] * vertical_exaggeration # expanded bounding box exp_box = _np.stack([bounding_box.min[0] - 0.1 * aspect, bounding_box.max[0] + 0.1 * aspect], axis=0) return {"scene": {"aspectratio": {"x": aspect[0], "y": aspect[1], "z": aspect[2]}, "xaxis": {"showexponent": "none", "exponentformat": "none", "range": exp_box[:, 0]}, "yaxis": {"showexponent": "none", "exponentformat": "none", "range": exp_box[:, 1]}, "zaxis": {"showexponent": "none", "exponentformat": "none", "range": exp_box[:, 2]}}}
[docs] def bounding_box_3d(bounding_box, **kwargs): idx = _np.array([0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1]) idy = _np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1]) idz = _np.array([0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1]) obj = {"type": "scatter3d", "mode": "lines", "x": bounding_box[idx, 0], "y": bounding_box[idy, 1], "z": bounding_box[idz, 2], "hoverinfo": "x+y+z", "name": "bounding box"} obj = _deep_update(obj, kwargs) return [obj]
[docs] def planes_3d(coordinates, dip, azimuth, size=1, **kwargs): # conversions dip = -dip * _np.pi / 180 azimuth = (90 - azimuth) * _np.pi / 180 strike = azimuth - _np.pi / 2 # dip and strike vectors dipvec = _np.concatenate([ _np.array(_np.cos(dip) * _np.cos(azimuth), ndmin=2).transpose(), _np.array(_np.cos(dip) * _np.sin(azimuth), ndmin=2).transpose(), _np.array(_np.sin(dip), ndmin=2).transpose()], axis=1)*size strvec = _np.concatenate([ _np.array(_np.cos(strike), ndmin=2).transpose(), _np.array(_np.sin(strike), ndmin=2).transpose(), _np.zeros([dipvec.shape[0], 1])], axis=1)*size # surfaces surf = [] for i, point in enumerate(coordinates): surf_points = _np.stack([ point + dipvec[i], point - 0.5*strvec[i] - 0.5*dipvec[i], point + 0.5*strvec[i] - 0.5*dipvec[i] ], axis=0) obj = {"type": "mesh3d", "x": surf_points[:, 0], "y": surf_points[:, 1], "z": surf_points[:, 2], "hoverinfo": "x+y+z+text"} obj = _deep_update(obj, kwargs) surf.append(obj) return surf
[docs] def arrows_3d(coordinates, directions, size=1, **kwargs): head = coordinates + size / 2 * directions tail = coordinates - size / 2 * directions x = [] y = [] z = [] for i in range(head.shape[0]): x.extend([tail[i, 0], head[i, 0], None]) y.extend([tail[i, 1], head[i, 1], None]) z.extend([tail[i, 2], head[i, 2], None]) obj = {"type": "scatter3d", "mode": "lines", "x": x, "y": y, "z": z, "hoverinfo": "none"} obj = _deep_update(obj, kwargs) return [obj]
[docs] def numeric_points_3d(coordinates, values, **kwargs): # x, y, z, and text as lists text = [str(num) for num in values] x = coordinates[:, 0].tolist() y = coordinates[:, 1].tolist() z = coordinates[:, 2].tolist() obj = {"mode": "markers", "text": text, "marker": {"color": values}} obj.update({"type": "scatter3d", "x": x, "y": y, "z": z, "hoverinfo": "x+y+z+text"}) obj = _deep_update(obj, kwargs) return [obj]
[docs] def categorical_points_3d(coordinates, values, colors, **kwargs): """ Visualization of categorical data. Parameters ---------- coordinates : ndarray The 3D coordinates. values : ndarray, Series The categories to plot. colors : dict Dictionary mapping the data labels to plotly color specifications. kwargs Additional arguments passed on to plotly's scatter3d function. Returns ------- out : list A singleton list containing the data to plot in plotly's format. """ # x, y, z, and colors as lists color_val = _np.arange(len(colors)) colors_dict = dict(zip(colors.keys(), color_val)) colors2 = _pd.Series(values).map(colors_dict).tolist() colors3 = [colors.get(item) for item in colors] cmax = len(color_val) - 1 text = values.tolist() x = coordinates[:, 0].tolist() y = coordinates[:, 1].tolist() z = coordinates[:, 2].tolist() obj = {"mode": "markers", "text": text, "marker": {"color": colors2, "colorscale": [[i / cmax, colors3[i]] for i in color_val]}} obj.update({"type": "scatter3d", "x": x, "y": y, "z": z, "hoverinfo": "x+y+z+text"}) obj = _deep_update(obj, kwargs) return [obj]
[docs] def isosurface(verts, faces, values=None, **kwargs): obj = {"type": "mesh3d", "x": verts[:, 0], "y": verts[:, 1], "z": verts[:, 2], "i": faces[:, 0], "j": faces[:, 1], "k": faces[:, 2]} if values is not None: obj["intensity"] = values obj = _deep_update(obj, kwargs) return [obj]
[docs] def segments_3d(coordinates, labels, colors, **kwargs): # x, y, z, and colors as lists labels = _pd.Series(labels) color_val = _np.arange(len(colors)) colors_dict = dict(zip(colors.keys(), color_val)) colors2 = labels.map(colors_dict).tolist() colors3 = [colors.get(item) for item in colors] cmax = len(color_val) - 1 n_data = coordinates.shape[0] text = labels.tolist() x = coordinates[:, 0] y = coordinates[:, 1] z = coordinates[:, 2] x = x.tolist() y = y.tolist() z = z.tolist() # inserting None elements to break undesired lines # points object will always have an even number of rows # None elements must be inserted every 3rd position indexes = _np.flip(_np.arange(start=0, stop=n_data, step=2)) for idx in indexes: colors2.insert(idx, "rgb(0,0,0)") # dummy color x.insert(idx, None) y.insert(idx, None) z.insert(idx, None) text.insert(idx, None) obj = {"type": "scatter3d", "x": x, "y": y, "z": z, "hoverinfo": "x+y+z+text", "mode": "lines", "text": text, "line": {"color": colors2, "colorscale": [[i / cmax, colors3[i]] for i in color_val]}} obj = _deep_update(obj, kwargs) return [obj]
[docs] def numeric_section_3d(gridded_x, gridded_y, gridded_z, values, **kwargs): obj = {"type": "surface", "x": gridded_x, "y": gridded_y, "z": gridded_z, "surfacecolor": values} obj = _deep_update(obj, kwargs) return [obj]
[docs] def mpl_to_plotly(cmap, level): rgba = _np.array(cmap(level)) rgb = _np.round(rgba[:-1] * 255) rgb = tuple(int(num) for num in rgb) color_str = "rgb(%d,%d,%d)" % rgb return color_str