diff --git a/pozo/__init__.py b/pozo/__init__.py index 45f32438..398540cd 100644 --- a/pozo/__init__.py +++ b/pozo/__init__.py @@ -8,7 +8,7 @@ from .axes import Axis # noqa from .tracks import Track # noqa from .graphs import Graph # noqa -from .annotations import Note # noqa +from .annotations import DepthNote, PolygonNote, LineNote # noqa import pozo.themes as themes # noqa import pozo.renderers as renderers # noqa diff --git a/pozo/annotations.py b/pozo/annotations.py index e4922aeb..2ccc8bee 100644 --- a/pozo/annotations.py +++ b/pozo/annotations.py @@ -1,22 +1,227 @@ import pozo +import plotly.graph_objects as go +from abc import ABC -#TODO this doesn't handle units -#TODO xp doesn't handle units or check indices -#TODO what else doesn't handle units -class Note(): - def __init__(self, depth, *, line={}, text="", width=1, fillcolor = 'lightskyblue', opacity=.5, show_text=True): - if not ( ( pozo.is_array(depth) and len(depth) == 2 ) or pozo.is_scalar_number(depth) ): - raise TypeError("depth must be two numbers in a tuple or list or just one number") + +# TODO this doesn't handle units +# TODO xp doesn't handle units or check indices +# TODO what else doesn't handle units +class Note(ABC): + def __init__( + self, + *, + line={}, + width=1, + fillcolor="lightskyblue", + opacity=0.5, + text="", + yshift=-5, + showarrow=False, + show_text=False + ): if not isinstance(line, dict): raise TypeError("line must be a dictionary") if width < -1 or width > 1: raise ValueError("width must be between -1 and 1") # TODO add further constraints on changes - self.depth = depth - self.line = line - self.fillcolor = fillcolor - self.opacity = None - self.show_text = True - self.text = text - self.width = width + self.line = line + self.fillcolor = fillcolor + self.opacity = opacity + self.width = width + self.text = text + self.yshift = yshift + self.showarrow = showarrow + self.show_text = show_text + + +class DepthNote(Note): # EN DESARROLLO + def __init__( + self, + depth, + *, + line={}, + text="", + width=1, + fillcolor="lightskyblue", + opacity=0.5, + show_text=True, + yshift=-5, + showarrow=False, + ): + super().__init__( + line=line, + width=width, + fillcolor=fillcolor, + opacity=opacity, + text=text, + yshift=yshift, + showarrow=showarrow, + ) + if not ( + (pozo.is_array(depth) and len(depth) == 2) or pozo.is_scalar_number(depth) + ): + raise TypeError( + "depth must be two numbers in a tuple or list or just one number" + ) + if not isinstance(line, dict): + raise TypeError("line must be a dictionary") + if width < -1 or width > 1: + raise ValueError("width must be between -1 and 1") + self.depth = depth + self.show_text = show_text + self.text = text + + +class PolygonNote(Note): # EN DESARROLLO + def __init__( + self, + x=[], + y=[], + xaxis="xaxis1", + yaxis="yaxis1", + fill="toself", + line={}, + text="", + yshift=-5, + showarrow=False, + show_text=False, + **kwargs, + ): + mode = kwargs.pop("mode", "lines") + width = kwargs.pop("width", 1) + fillcolor = kwargs.pop("fillcolor", "lightskyblue") + fillgradient = kwargs.pop("fillgradient", None) + fillpattern = kwargs.pop("fillpattern", None) + opacity = kwargs.pop("opacity", 0.5) + bgcolor = kwargs.pop("bgcolor", None) + bgcolorsrc = kwargs.pop("bgcolorsrc", None) + bordercolor = kwargs.pop("bordercolor", None) + bordercolorsrc = kwargs.pop("bordercolorsrc", None) + font = kwargs.pop("font", None) + groupnorm = kwargs.pop("groupnorm", None) + hoverinfo = kwargs.pop("hoverinfo", None) + hoverinfosrc = kwargs.pop("hoverinfosrc", None) + hoverlabel = kwargs.pop("hoverlabel", None) + hoveron = kwargs.pop("hoveron", None) + hovertemplate = kwargs.pop("hovertemplate", None) + hovertemplatesrc = kwargs.pop("hovertemplatesrc", None) + hovertext = kwargs.pop("hovertext", None) + hovertextsrc = kwargs.pop("hovertextsrc", None) + legend = kwargs.pop("legend", None) + legendgroup = kwargs.pop("legendgroup", None) + legendgrouptitle = kwargs.pop("legendgrouptitle", None) + legendrank = kwargs.pop("legendrank", None) + legendwidth = kwargs.pop("legendwidth", None) + ids = kwargs.pop("ids", None) + + super().__init__( + line=line, + width=width, + fillcolor=fillcolor, + opacity=opacity, + text=text, + yshift=yshift, + showarrow=showarrow, + ) + if not isinstance(line, dict): + raise TypeError("line must be a dictionary") + if width < -1 or width > 1: + raise ValueError("width must be between -1 and 1") + self.x = x + self.y = y + self.yaxis = xaxis + self.xaxis = yaxis + self.fill = fill + self.mode = mode + self.show_text = show_text + self.fillgradient = fillgradient + self.fillpattern = fillpattern + self.bgcolor = bgcolor + self.bgcolorsrc = bgcolorsrc + self.bordercolor = bordercolor + self.bordercolorsrc = bordercolorsrc + self.font = font + self.groupnorm = groupnorm + self.hoverinfo = hoverinfo + self.hoverinfosrc = hoverinfosrc + self.hoverlabel = hoverlabel + self.hoveron = hoveron + self.hovertemplate = hovertemplate + self.hovertemplatesrc = hovertemplatesrc + self.hovertext = hovertext + self.hovertextsrc = hovertextsrc + self.legend = legend + self.legendgroup = legendgroup + self.legendgrouptitle = legendgrouptitle + self.legendrank = legendrank + self.legendwidth = legendwidth + self.ids = ids + + +class LineNote(Note, go.Scatter): # EN DESARROLLO + def __init__( + self, + x0=0, + y0=0, + x1=None, + y1=None, + xref="xaxis1", + yref="yaxis1", + line={}, + text="", + yshift=-5, + showarrow=False, + show_text=False, + **kwargs, + ): + width = kwargs.pop("width", 1) + fillcolor = kwargs.pop("fillcolor", "lightskyblue") + opacity = kwargs.pop("opacity", 0.5) + hoverinfo = kwargs.pop("hoverinfo", None) + hoverinfosrc = kwargs.pop("hoverinfosrc", None) + hoverlabel = kwargs.pop("hoverlabel", None) + hovertemplate = kwargs.pop("hovertemplate", None) + hovertemplatesrc = kwargs.pop("hovertemplatesrc", None) + hovertext = kwargs.pop("hovertext", None) + hovertextsrc = kwargs.pop("hovertextsrc", None) + ids = kwargs.pop("ids", None) + + if not isinstance(line, dict): + raise TypeError("line must be a dictionary") + if width < -1 or width > 1: + raise ValueError("width must be between -1 and 1") + if y1 is None or x1 is None: + raise ValueError("You must use values for x1 and y1") + Note.__init__( + self, + line=line, + width=width, + fillcolor=fillcolor, + opacity=opacity, + text=text, + yshift=yshift, + showarrow=showarrow, + show_text=show_text, + ) + go.Scatter.__init__( + self, + x0=x0, + y0=y0, + x1=x1, + y1=y1, + xref=xref, + yref=yref, + line=line, + width=width, + fillcolor=fillcolor, + opacity=opacity, + hoverinfo=hoverinfo, + hoverinfosrc=hoverinfosrc, + hoverlabel=hoverlabel, + hovertemplate=hovertemplate, + hovertemplatesrc=hovertemplatesrc, + hovertext=hovertext, + hovertextsrc=hovertextsrc, + ids=ids, + ) diff --git a/pozo/graphs.py b/pozo/graphs.py index c9086241..f50bf568 100644 --- a/pozo/graphs.py +++ b/pozo/graphs.py @@ -80,8 +80,7 @@ def __init__(self, *args, **kwargs): self.process_data(*args, **my_kwargs) if len(args) == 1 and include and len(include) != 0: self.reorder_all_tracks(include) - self.note_dict = {} - self.note_list = [] + self.notes = {} def summarize_curves(self, *selectors, **kwargs): return self.renderer.summarize_curves(self, selectors=selectors, **kwargs) diff --git a/pozo/renderers/plotly.py b/pozo/renderers/plotly.py index 4c857cf4..197ac140 100644 --- a/pozo/renderers/plotly.py +++ b/pozo/renderers/plotly.py @@ -7,7 +7,6 @@ import re import weakref import multiprocessing -import itertools from ipywidgets import IntProgress import numpy as np @@ -20,6 +19,7 @@ import pozo.renderers as pzr import pozo.themes as pzt import pozo.units as pzu +from pozo.annotations import DepthNote, PolygonNote, LineNote re_space = re.compile(' ') re_power = re.compile(r'\*\*') @@ -29,6 +29,63 @@ def toTarget(axis): return axis[0] + axis[-1] +def process_note(note, xref, yref, left_margin=0): + def make_shape(note, xref, yref): + x_lower_bound = 0 + x_upper_bound = 1 + if note.width > 0: + x_upper_bound = note.width + elif note.width < 0: + x_lower_bound = 1 + note.width + + shape = dict( + xref = xref, + x0 = x_lower_bound+left_margin, + x1 = x_upper_bound, + yref = yref + ) + default_line = dict( + color = 'black', + width = 1, + dash = 'dot', + ) + if pozo.is_array(note.depth) and len(note.depth) == 2: + shape['type'] = 'rect' + shape['y0'] = note.depth[0] + shape['y1'] = note.depth[1] + default_line['width'] = 0 + default_line.update(note.line) + shape['line'] = default_line + shape['fillcolor'] = note.fillcolor + shape['layer'] = "above" + shape['opacity'] = .5 + elif pozo.is_scalar_number(note.depth): + shape['type'] = 'line' + shape['y0'] = shape['y1'] = note.depth + default_line.update(note.line) + shape['line'] = default_line + else: + raise TypeError("Range must be a number or two numbers in a tuple or list") + return shape + def make_note(note, xref, yref, is_line): # if graph, + annotation = dict( + text=note.text, + axref = xref if xref != 'paper' else None, + ayref = yref if yref != 'paper' else None, + xref=xref, # could be domain or paper + x=1, + yref=yref, + y=note.depth if is_line else note.depth[0], + yshift=-5, + showarrow=False, + ) + return annotation + shape = make_shape(note, xref, yref) + annotation = None + if note.show_text: + annotation = make_note(note, xref, yref, shape['type'] == 'line') + return shape, annotation + def javascript(): add_scroll = '''var css = '.plot-container { overflow: auto; }', head = document.getElementsByTagName('head')[0], @@ -88,6 +145,10 @@ def javascript(): showline=False, position=0, gridcolor="#f0f0f0", + tickmode = 'linear', + tick0 = 0, + dtick = 100, + minor=dict(tickcolor="black", tickmode='auto', nticks=5, showgrid=True) # domain=[?,?], # generated # maxallowed=, # generated # minallowed=, # generated @@ -155,63 +216,6 @@ def _hidden(self, themes, override=False): if hidden or override: themes.pop() return hidden or override - def _process_note(self, note, xref, yref, left_margin=0): - def _make_shape(note, xref, yref): - x_lower_bound = 0 - x_upper_bound = 1 - if note.width > 0: - x_upper_bound = note.width - elif note.width < 0: - x_lower_bound = 1 + note.width - - shape = dict( - xref = xref, - x0 = x_lower_bound+left_margin, - x1 = x_upper_bound, - yref = yref - ) - default_line = dict( - color = 'black', - width = 1, - dash = 'dot', - ) - if pozo.is_array(note.depth) and len(note.depth) == 2: - shape['type'] = 'rect' - shape['y0'] = note.depth[0] - shape['y1'] = note.depth[1] - default_line['width'] = 0 - default_line.update(note.line) - shape['line'] = default_line - shape['fillcolor'] = note.fillcolor - shape['layer'] = "above" - shape['opacity'] = .5 - elif pozo.is_scalar_number(note.depth): - shape['type'] = 'line' - shape['y0'] = shape['y1'] = note.depth - default_line.update(note.line) - shape['line'] = default_line - else: - raise TypeError("Range must be a number or two numbers in a tuple or list") - return shape - def _make_note(note, xref, yref, is_line): # if graph, - annotation = dict( - text=note.text, - axref = xref if xref != 'paper' else None, - ayref = yref if yref != 'paper' else None, - xref=xref, # could be domain or paper - x=1, - yref=yref, - y=note.depth if is_line else note.depth[0], - yshift=-5, - showarrow=False, - ) - return annotation - shape = _make_shape(note, xref, yref) - annotation = None - if note.show_text: - annotation = _make_note(note, xref, yref, shape['type'] == 'line') - return shape, annotation - def get_layout(self, graph, xp=None, **kwargs): if not isinstance(graph, pozo.Graph): raise TypeError("Layout must be supplied a graph object.") @@ -333,7 +337,7 @@ def get_layout(self, graph, xp=None, **kwargs): if posmap['depth_track_number'] >= len(posmap['tracks_axis_numbers']): posmap['depth_auto_right'] = True max_text = 0 - for note in itertools.chain(list(graph.note_dict.values()) + graph.note_list): + for note in list(graph.notes.values()): max_text = max(max_text, len(note.text)) posmap['x_annotation_pixel_width'] = max_text*10 posmap['pixel_width'] += max_text*10 @@ -546,11 +550,11 @@ def get_layout(self, graph, xp=None, **kwargs): depth_margin = self.template['depth_axis_width']/posmap['pixel_width'] layout['shapes'] = [] layout['annotations'] = [] - for note in itertools.chain(list(graph.note_dict.values()) + graph.note_list): - s, a = self._process_note(note, - xref="paper", - yref=toTarget(posmap['track_y']), - left_margin = posmap['xp_end']+depth_margin) + for note in list(graph.notes.values()): + s, a = process_note(note, + xref="paper", + yref=toTarget(posmap['track_y']), + left_margin = posmap['xp_end']+depth_margin) if s: layout['shapes'].append(s) if a: layout['annotations'].append(a) @@ -564,10 +568,10 @@ def get_layout(self, graph, xp=None, **kwargs): track_index += 1 if not bool(posmap['with_xp']) and posmap['tracks_axis_numbers'][track_index] == "depth": track_index +=1 - for note in itertools.chain(list(track.note_dict.values()) + track.note_list): - s, a = self._process_note(note, - xref='x'+str(posmap['tracks_axis_numbers'][track_index][0]) + ' domain', - yref=toTarget(posmap['track_y'])) + for note in list(track.notes.values()): + s, a = process_note(note, + xref='x'+str(posmap['tracks_axis_numbers'][track_index][0]) + ' domain', + yref=toTarget(posmap['track_y'])) if s: layout['shapes'].append(s) if a: layout['annotations'].append(a) @@ -1004,6 +1008,7 @@ def __init__(self, x=None, y=None, colors=[None], **kwargs): self.colors = colors self.y = y self.x = x + self.notes = {} self._colors_by_id = {} self._figures_by_id = weakref.WeakValueDictionary() # Do figures contain their colors? so we can clear up _colors_by_id @@ -1012,11 +1017,12 @@ def add_figure(self, fig): def render(self, **kwargs): static = kwargs.pop("static", False) - layout = self.create_layout(**kwargs) # container_width, size + layout= self.create_layout(**kwargs) # container_width, size traces = self.create_traces(**kwargs) # container_width, depth_range, size, static + display(traces) if static: - self.last_fig = go.Figure(data=traces, layout=layout) + self.last_fig = go.Figure(traces, layout=layout) return self.last_fig fig = xpFigureWidget(data=traces, layout=layout, renderer=self) self.add_figure(fig) @@ -1024,27 +1030,110 @@ def render(self, **kwargs): return fig - def create_layout(self, container_width=None, size=None, xaxis="xaxis1", yaxis="yaxis1"): - if not size: size = self.size + def create_layout( + self, container_width=None, size=None, xaxis="xaxis1", yaxis="yaxis1" + ): + + if not size: + size = self.size margin = (120) / size if container_width is not None else 0 - return { - "width" : size, - "height" : size, - xaxis : dict( - title = self.x.get_name(), - range = self.xrange, - linecolor = "#888", - linewidth = 1, - ), - yaxis : dict( - title = self.y.get_name(), - range = self.yrange, - domain = (margin, 1), - linecolor = "#888", - linewidth = 1, - ), - "showlegend" : True - } + + layout = {} + layout["shapes"] = [] + layout["annotations"] = [] + for note in list(self.notes.values()): + if isinstance(note, DepthNote): + shape, annotation = process_note( + note, xref="paper", yref=toTarget(yaxis) + ) + if isinstance(note, PolygonNote): # EN DESARROLLO + shape = dict( + type="line", + x=note.x, + y=note.y, + xaxis=xaxis, + yaxis=yaxis, + fill=note.fill, + mode=note.mode, + line=dict( + color=note.line["color"] + if "color" in note.line + else "RoyalBlue" + ), + ) + annotation = None + if note.show_text: + annotation = dict( + text=note.text, + axref="paper", + ayref="y1", + xref="paper", + x=note.x[0], + yref="y1", + y=note.y[0], + yshift=note.yshift, + showarrow=note.showarrow, + ) + elif isinstance(note, LineNote): # EN DESARROLLO + shape = dict( + type="line", + x0=note.x0, + y0=note.y0, + x1=note.x1, + y1=note.y1, + xref=toTarget(xaxis), + yref=toTarget(yaxis), + line=dict( + color=note.line["color"] + if "color" in note.line + else "RoyalBlue", + width=note.line["width"] if "width" in note.line else 3, + dash=note.line["dash"] if "dash" in note.line else "solid", + ), + ) + annotation = None + if note.show_text: + annotation = dict( + text=note.text, + axref=note.xaxis if note.xaxis != "paper" else None, + ayref=note.yaxis if note.yaxis != "paper" else None, + xref=note.xaxis, + x=note.x0, + yref=note.yaxis, + y=note.y0, + yshift=note.yshift, + showarrow=note.showarrow, + ) + elif isinstance(note, dict): + shape = note.shape + annotation = note.annotation + + if shape: + layout["shapes"].append(shape) + if annotation: + layout["annotations"].append(annotation) + + return { + "width": size, + "height": size, + xaxis: dict( + title=self.x.get_name(), + range=self.xrange, + linecolor="#888", + linewidth=1, + ), + yaxis: dict( + title=self.y.get_name(), + range=self.yrange, + domain=(margin, 1), + linecolor="#888", + linewidth=1, + ), + "shapes": list(layout["shapes"]), + "annotations": list(layout["annotations"]), + "showlegend": True, + } + def create_traces(self, depth_range=None, container_width=None, size=None, static=False, xaxis="xaxis1", yaxis="yaxis1", color_lock={}, by_index=False): if not size: size = self.size @@ -1086,7 +1175,8 @@ def create_traces(self, depth_range=None, container_width=None, size=None, stati for trace in trace_definitions: plotly_traces.append(go.Scattergl(trace)) if trace['name'] in color_lock: - # originally we did this post-process modification because we didn't have plotly.colors.get_colorscale(str) + # originally we did this post-process modification + # because we didn't have plotly.colors.get_colorscale(str) # we could now do it in create trace color_range = color_lock[trace['name']] cs_sel = plotly_traces[-1]['marker']['colorscale'] # this is the selected @@ -1098,6 +1188,54 @@ def create_traces(self, depth_range=None, container_width=None, size=None, stati plotly_traces[-1].meta['colorscale_calculated'] = tuple(cs_calc) plotly_traces[-1].meta['colorscale_selected'] = tuple(cs_sel) # this should lock it, otherwise, it goes back to auto + + notes = self.notes + for note in notes: + note_obj = notes[note] + if isinstance(note_obj, DepthNote): + shape, _ = process_note(note_obj, xref="paper", yref=toTarget(yaxis)) + shape["name"]=note + plotly_traces.append(go.Scattergl(shape)) + elif isinstance(note_obj, PolygonNote): + shape = dict( + type="line", + x=note_obj.x, + y=note_obj.y, + xaxis=xaxis, + yaxis=yaxis, + fill=note_obj.fill, + mode=note_obj.mode, + line=dict( + color=note_obj.line["color"] + if "color" in note_obj.line + else "RoyalBlue" + ), + name=note, + ) + plotly_traces.append(go.Scatter(shape)) + elif isinstance(note_obj, LineNote): + shape = dict( + type="line", + x0=note_obj.x0, + y0=note_obj.y0, + x1=note_obj.x1, + y1=note_obj.y1, + xref=toTarget(xaxis), + yref=toTarget(yaxis), + line=dict( + color=note_obj.line["color"] + if "color" in note.line + else "RoyalBlue", + width=note_obj.line["width"] if "width" in note_obj.line else 3, + dash=note_obj.line["dash"] if "dash" in note_obj.line else "solid", + ), + name=note, + ) + plotly_traces.append(go.Scattergl(shape)) + elif isinstance(note, dict): + shape = note.shape + plotly_traces.append(go.Scattergl(shape)) + return plotly_traces @@ -1248,7 +1386,7 @@ def make_xp_depth_video(folder_name, graph, start, window, end, xp=True, output= fade = tail.tolist() + [1]*(window_index-tail_size) for i, cursor in enumerate(frame_count): render_counter.value += 1 - graph.note_dict['Depth Highlight-xxx'] = pozo.Note( + graph.notes['Depth Highlight-xxx'] = pozo.DepthNote( (depth[cursor], depth[cursor+window_index]), show_text=False, ) @@ -1267,7 +1405,7 @@ def make_xp_depth_video(folder_name, graph, start, window, end, xp=True, output= path=folder_name+"/"+str(i)+".png", ) ) - del graph.note_dict['Depth Highlight-xxx'] + del graph.notes['Depth Highlight-xxx'] if not cpus: cpus = multiprocessing.cpu_count() if cpus == 1: diff --git a/pozo/tracks.py b/pozo/tracks.py index 61cc2ec1..2c5e1f4c 100644 --- a/pozo/tracks.py +++ b/pozo/tracks.py @@ -14,8 +14,7 @@ def get_name(self): _child_type="axis" def __init__(self, *args, **kwargs): super().__init__(**kwargs) - self.note_dict = {} - self.note_list = [] + self.notes = {} for ar in args: self.add_axes(ar)