From 5bf22fc7e3c392c8bd44315ca2d06d7dca7d084e Mon Sep 17 00:00:00 2001 From: sotech117 Date: Thu, 31 Jul 2025 17:27:24 -0400 Subject: add code for analysis of data --- .../site-packages/plotly/matplotlylib/renderer.py | 861 +++++++++++++++++++++ 1 file changed, 861 insertions(+) create mode 100644 venv/lib/python3.8/site-packages/plotly/matplotlylib/renderer.py (limited to 'venv/lib/python3.8/site-packages/plotly/matplotlylib/renderer.py') diff --git a/venv/lib/python3.8/site-packages/plotly/matplotlylib/renderer.py b/venv/lib/python3.8/site-packages/plotly/matplotlylib/renderer.py new file mode 100644 index 0000000..c95de52 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/matplotlylib/renderer.py @@ -0,0 +1,861 @@ +""" +Renderer Module + +This module defines the PlotlyRenderer class and a single function, +fig_to_plotly, which is intended to be the main way that user's will interact +with the matplotlylib package. + +""" + +import warnings + +import plotly.graph_objs as go +from plotly.matplotlylib.mplexporter import Renderer +from plotly.matplotlylib import mpltools + + +# Warning format +def warning_on_one_line(msg, category, filename, lineno, file=None, line=None): + return "%s:%s: %s:\n\n%s\n\n" % (filename, lineno, category.__name__, msg) + + +warnings.formatwarning = warning_on_one_line + + +class PlotlyRenderer(Renderer): + """A renderer class inheriting from base for rendering mpl plots in plotly. + + A renderer class to be used with an exporter for rendering matplotlib + plots in Plotly. This module defines the PlotlyRenderer class which handles + the creation of the JSON structures that get sent to plotly. + + All class attributes available are defined in __init__(). + + Basic Usage: + + # (mpl code) # + fig = gcf() + renderer = PlotlyRenderer(fig) + exporter = Exporter(renderer) + exporter.run(fig) # ... et voila + + """ + + def __init__(self): + """Initialize PlotlyRenderer obj. + + PlotlyRenderer obj is called on by an Exporter object to draw + matplotlib objects like figures, axes, text, etc. + + All class attributes are listed here in the __init__ method. + + """ + self.plotly_fig = go.Figure() + self.mpl_fig = None + self.current_mpl_ax = None + self.bar_containers = None + self.current_bars = [] + self.axis_ct = 0 + self.x_is_mpl_date = False + self.mpl_x_bounds = (0, 1) + self.mpl_y_bounds = (0, 1) + self.msg = "Initialized PlotlyRenderer\n" + + def open_figure(self, fig, props): + """Creates a new figure by beginning to fill out layout dict. + + The 'autosize' key is set to false so that the figure will mirror + sizes set by mpl. The 'hovermode' key controls what shows up when you + mouse around a figure in plotly, it's set to show the 'closest' point. + + Positional agurments: + fig -- a matplotlib.figure.Figure object. + props.keys(): [ + 'figwidth', + 'figheight', + 'dpi' + ] + + """ + self.msg += "Opening figure\n" + self.mpl_fig = fig + self.plotly_fig["layout"] = go.Layout( + width=int(props["figwidth"] * props["dpi"]), + height=int(props["figheight"] * props["dpi"]), + autosize=False, + hovermode="closest", + ) + self.mpl_x_bounds, self.mpl_y_bounds = mpltools.get_axes_bounds(fig) + margin = go.layout.Margin( + l=int(self.mpl_x_bounds[0] * self.plotly_fig["layout"]["width"]), + r=int((1 - self.mpl_x_bounds[1]) * self.plotly_fig["layout"]["width"]), + t=int((1 - self.mpl_y_bounds[1]) * self.plotly_fig["layout"]["height"]), + b=int(self.mpl_y_bounds[0] * self.plotly_fig["layout"]["height"]), + pad=0, + ) + self.plotly_fig["layout"]["margin"] = margin + + def close_figure(self, fig): + """Closes figure by cleaning up data and layout dictionaries. + + The PlotlyRenderer's job is to create an appropriate set of data and + layout dictionaries. When the figure is closed, some cleanup and + repair is necessary. This method removes inappropriate dictionary + entries, freeing up Plotly to use defaults and best judgements to + complete the entries. This method is called by an Exporter object. + + Positional arguments: + fig -- a matplotlib.figure.Figure object. + + """ + self.plotly_fig["layout"]["showlegend"] = False + self.msg += "Closing figure\n" + + def open_axes(self, ax, props): + """Setup a new axes object (subplot in plotly). + + Plotly stores information about subplots in different 'xaxis' and + 'yaxis' objects which are numbered. These are just dictionaries + included in the layout dictionary. This function takes information + from the Exporter, fills in appropriate dictionary entries, + and updates the layout dictionary. PlotlyRenderer keeps track of the + number of plots by incrementing the axis_ct attribute. + + Setting the proper plot domain in plotly is a bit tricky. Refer to + the documentation for mpltools.convert_x_domain and + mpltools.convert_y_domain. + + Positional arguments: + ax -- an mpl axes object. This will become a subplot in plotly. + props.keys() -- [ + 'axesbg', (background color for axes obj) + 'axesbgalpha', (alpha, or opacity for background) + 'bounds', ((x0, y0, width, height) for axes) + 'dynamic', (zoom/pan-able?) + 'axes', (list: [xaxis, yaxis]) + 'xscale', (log, linear, or date) + 'yscale', + 'xlim', (range limits for x) + 'ylim', + 'xdomain' (xdomain=xlim, unless it's a date) + 'ydomain' + ] + + """ + self.msg += " Opening axes\n" + self.current_mpl_ax = ax + self.bar_containers = [ + c + for c in ax.containers # empty is OK + if c.__class__.__name__ == "BarContainer" + ] + self.current_bars = [] + self.axis_ct += 1 + # set defaults in axes + xaxis = go.layout.XAxis( + anchor="y{0}".format(self.axis_ct), zeroline=False, ticks="inside" + ) + yaxis = go.layout.YAxis( + anchor="x{0}".format(self.axis_ct), zeroline=False, ticks="inside" + ) + # update defaults with things set in mpl + mpl_xaxis, mpl_yaxis = mpltools.prep_xy_axis( + ax=ax, props=props, x_bounds=self.mpl_x_bounds, y_bounds=self.mpl_y_bounds + ) + xaxis.update(mpl_xaxis) + yaxis.update(mpl_yaxis) + bottom_spine = mpltools.get_spine_visible(ax, "bottom") + top_spine = mpltools.get_spine_visible(ax, "top") + left_spine = mpltools.get_spine_visible(ax, "left") + right_spine = mpltools.get_spine_visible(ax, "right") + xaxis["mirror"] = mpltools.get_axis_mirror(bottom_spine, top_spine) + yaxis["mirror"] = mpltools.get_axis_mirror(left_spine, right_spine) + xaxis["showline"] = bottom_spine + yaxis["showline"] = top_spine + + # put axes in our figure + self.plotly_fig["layout"]["xaxis{0}".format(self.axis_ct)] = xaxis + self.plotly_fig["layout"]["yaxis{0}".format(self.axis_ct)] = yaxis + + # let all subsequent dates be handled properly if required + + if "type" in dir(xaxis) and xaxis["type"] == "date": + self.x_is_mpl_date = True + + def close_axes(self, ax): + """Close the axes object and clean up. + + Bars from bar charts are given to PlotlyRenderer one-by-one, + thus they need to be taken care of at the close of each axes object. + The self.current_bars variable should be empty unless a bar + chart has been created. + + Positional arguments: + ax -- an mpl axes object, not required at this time. + + """ + self.draw_bars(self.current_bars) + self.msg += " Closing axes\n" + self.x_is_mpl_date = False + + def draw_bars(self, bars): + # sort bars according to bar containers + mpl_traces = [] + for container in self.bar_containers: + mpl_traces.append( + [ + bar_props + for bar_props in self.current_bars + if bar_props["mplobj"] in container + ] + ) + for trace in mpl_traces: + self.draw_bar(trace) + + def draw_bar(self, coll): + """Draw a collection of similar patches as a bar chart. + + After bars are sorted, an appropriate data dictionary must be created + to tell plotly about this data. Just like draw_line or draw_markers, + draw_bar translates patch/path information into something plotly + understands. + + Positional arguments: + patch_coll -- a collection of patches to be drawn as a bar chart. + + """ + tol = 1e-10 + trace = [mpltools.make_bar(**bar_props) for bar_props in coll] + widths = [bar_props["x1"] - bar_props["x0"] for bar_props in trace] + heights = [bar_props["y1"] - bar_props["y0"] for bar_props in trace] + vertical = abs(sum(widths[0] - widths[iii] for iii in range(len(widths)))) < tol + horizontal = ( + abs(sum(heights[0] - heights[iii] for iii in range(len(heights)))) < tol + ) + if vertical and horizontal: + # Check for monotonic x. Can't both be true! + x_zeros = [bar_props["x0"] for bar_props in trace] + if all( + (x_zeros[iii + 1] > x_zeros[iii] for iii in range(len(x_zeros[:-1]))) + ): + orientation = "v" + else: + orientation = "h" + elif vertical: + orientation = "v" + else: + orientation = "h" + if orientation == "v": + self.msg += " Attempting to draw a vertical bar chart\n" + old_heights = [bar_props["y1"] for bar_props in trace] + for bar in trace: + bar["y0"], bar["y1"] = 0, bar["y1"] - bar["y0"] + new_heights = [bar_props["y1"] for bar_props in trace] + # check if we're stacked or not... + for old, new in zip(old_heights, new_heights): + if abs(old - new) > tol: + self.plotly_fig["layout"]["barmode"] = "stack" + self.plotly_fig["layout"]["hovermode"] = "x" + x = [bar["x0"] + (bar["x1"] - bar["x0"]) / 2 for bar in trace] + y = [bar["y1"] for bar in trace] + bar_gap = mpltools.get_bar_gap( + [bar["x0"] for bar in trace], [bar["x1"] for bar in trace] + ) + if self.x_is_mpl_date: + x = [bar["x0"] for bar in trace] + formatter = ( + self.current_mpl_ax.get_xaxis() + .get_major_formatter() + .__class__.__name__ + ) + x = mpltools.mpl_dates_to_datestrings(x, formatter) + else: + self.msg += " Attempting to draw a horizontal bar chart\n" + old_rights = [bar_props["x1"] for bar_props in trace] + for bar in trace: + bar["x0"], bar["x1"] = 0, bar["x1"] - bar["x0"] + new_rights = [bar_props["x1"] for bar_props in trace] + # check if we're stacked or not... + for old, new in zip(old_rights, new_rights): + if abs(old - new) > tol: + self.plotly_fig["layout"]["barmode"] = "stack" + self.plotly_fig["layout"]["hovermode"] = "y" + x = [bar["x1"] for bar in trace] + y = [bar["y0"] + (bar["y1"] - bar["y0"]) / 2 for bar in trace] + bar_gap = mpltools.get_bar_gap( + [bar["y0"] for bar in trace], [bar["y1"] for bar in trace] + ) + bar = go.Bar( + orientation=orientation, + x=x, + y=y, + xaxis="x{0}".format(self.axis_ct), + yaxis="y{0}".format(self.axis_ct), + opacity=trace[0]["alpha"], # TODO: get all alphas if array? + marker=go.bar.Marker( + color=trace[0]["facecolor"], # TODO: get all + line=dict(width=trace[0]["edgewidth"]), + ), + ) # TODO ditto + if len(bar["x"]) > 1: + self.msg += " Heck yeah, I drew that bar chart\n" + (self.plotly_fig.add_trace(bar),) + if bar_gap is not None: + self.plotly_fig["layout"]["bargap"] = bar_gap + else: + self.msg += " Bar chart not drawn\n" + warnings.warn( + "found box chart data with length <= 1, " + "assuming data redundancy, not plotting." + ) + + def draw_legend_shapes(self, mode, shape, **props): + """Create a shape that matches lines or markers in legends. + + Main issue is that path for circles do not render, so we have to use 'circle' + instead of 'path'. + """ + for single_mode in mode.split("+"): + x = props["data"][0][0] + y = props["data"][0][1] + if single_mode == "markers" and props.get("markerstyle"): + size = shape.pop("size", 6) + symbol = shape.pop("symbol") + # aligning to "center" + x0 = 0 + y0 = 0 + x1 = size + y1 = size + markerpath = props["markerstyle"].get("markerpath") + if markerpath is None and symbol != "circle": + self.msg += ( + "not sure how to handle this marker without a valid path\n" + ) + return + # marker path to SVG path conversion + path = " ".join( + [f"{a} {t[0]},{t[1]}" for a, t in zip(markerpath[1], markerpath[0])] + ) + + if symbol == "circle": + # symbols like . and o in matplotlib, use circle + # plotly also maps many other markers to circle, such as 1,8 and p + path = None + shape_type = "circle" + x0 = -size / 2 + y0 = size / 2 + x1 = size / 2 + y1 = size + size / 2 + else: + # triangles, star etc + shape_type = "path" + legend_shape = go.layout.Shape( + type=shape_type, + xref="paper", + yref="paper", + x0=x0, + y0=y0, + x1=x1, + y1=y1, + xsizemode="pixel", + ysizemode="pixel", + xanchor=x, + yanchor=y, + path=path, + **shape, + ) + + elif single_mode == "lines": + mode = "line" + x1 = props["data"][1][0] + y1 = props["data"][1][1] + + legend_shape = go.layout.Shape( + type=mode, + xref="paper", + yref="paper", + x0=x, + y0=y + 0.02, + x1=x1, + y1=y1 + 0.02, + **shape, + ) + else: + self.msg += "not sure how to handle this element\n" + return + self.plotly_fig.add_shape(legend_shape) + self.msg += " Heck yeah, I drew that shape\n" + + def draw_marked_line(self, **props): + """Create a data dict for a line obj. + + This will draw 'lines', 'markers', or 'lines+markers'. For legend elements, + this will use layout.shapes, so they can be positioned with paper refs. + + props.keys() -- [ + 'coordinates', ('data', 'axes', 'figure', or 'display') + 'data', (a list of xy pairs) + 'mplobj', (the matplotlib.lines.Line2D obj being rendered) + 'label', (the name of the Line2D obj being rendered) + 'linestyle', (linestyle dict, can be None, see below) + 'markerstyle', (markerstyle dict, can be None, see below) + ] + + props['linestyle'].keys() -- [ + 'alpha', (opacity of Line2D obj) + 'color', (color of the line if it exists, not the marker) + 'linewidth', + 'dasharray', (code for linestyle, see DASH_MAP in mpltools.py) + 'zorder', (viewing precedence when stacked with other objects) + ] + + props['markerstyle'].keys() -- [ + 'alpha', (opacity of Line2D obj) + 'marker', (the mpl marker symbol, see SYMBOL_MAP in mpltools.py) + 'facecolor', (color of the marker face) + 'edgecolor', (color of the marker edge) + 'edgewidth', (width of marker edge) + 'markerpath', (an SVG path for drawing the specified marker) + 'zorder', (viewing precedence when stacked with other objects) + ] + + """ + self.msg += " Attempting to draw a line " + line, marker, shape = {}, {}, {} + if props["linestyle"] and props["markerstyle"]: + self.msg += "... with both lines+markers\n" + mode = "lines+markers" + elif props["linestyle"]: + self.msg += "... with just lines\n" + mode = "lines" + elif props["markerstyle"]: + self.msg += "... with just markers\n" + mode = "markers" + if props["linestyle"]: + color = mpltools.merge_color_and_opacity( + props["linestyle"]["color"], props["linestyle"]["alpha"] + ) + + if props["coordinates"] == "data": + line = go.scatter.Line( + color=color, + width=props["linestyle"]["linewidth"], + dash=mpltools.convert_dash(props["linestyle"]["dasharray"]), + ) + else: + shape = dict( + line=dict( + color=color, + width=props["linestyle"]["linewidth"], + dash=mpltools.convert_dash(props["linestyle"]["dasharray"]), + ) + ) + if props["markerstyle"]: + if props["coordinates"] == "data": + marker = go.scatter.Marker( + opacity=props["markerstyle"]["alpha"], + color=props["markerstyle"]["facecolor"], + symbol=mpltools.convert_symbol(props["markerstyle"]["marker"]), + size=props["markerstyle"]["markersize"], + line=dict( + color=props["markerstyle"]["edgecolor"], + width=props["markerstyle"]["edgewidth"], + ), + ) + else: + shape = dict( + opacity=props["markerstyle"]["alpha"], + fillcolor=props["markerstyle"]["facecolor"], + symbol=mpltools.convert_symbol(props["markerstyle"]["marker"]), + size=props["markerstyle"]["markersize"], + line=dict( + color=props["markerstyle"]["edgecolor"], + width=props["markerstyle"]["edgewidth"], + ), + ) + if props["coordinates"] == "data": + marked_line = go.Scatter( + mode=mode, + name=( + str(props["label"]) + if isinstance(props["label"], str) + else props["label"] + ), + x=[xy_pair[0] for xy_pair in props["data"]], + y=[xy_pair[1] for xy_pair in props["data"]], + xaxis="x{0}".format(self.axis_ct), + yaxis="y{0}".format(self.axis_ct), + line=line, + marker=marker, + ) + if self.x_is_mpl_date: + formatter = ( + self.current_mpl_ax.get_xaxis() + .get_major_formatter() + .__class__.__name__ + ) + marked_line["x"] = mpltools.mpl_dates_to_datestrings( + marked_line["x"], formatter + ) + (self.plotly_fig.add_trace(marked_line),) + self.msg += " Heck yeah, I drew that line\n" + elif props["coordinates"] == "axes": + # dealing with legend graphical elements + self.draw_legend_shapes(mode=mode, shape=shape, **props) + else: + self.msg += " Line didn't have 'data' coordinates, not drawing\n" + warnings.warn( + "Bummer! Plotly can currently only draw Line2D " + "objects from matplotlib that are in 'data' " + "coordinates!" + ) + + def draw_image(self, **props): + """Draw image. + + Not implemented yet! + + """ + self.msg += " Attempting to draw image\n" + self.msg += " Not drawing image\n" + warnings.warn( + "Aw. Snap! You're gonna have to hold off on " + "the selfies for now. Plotly can't import " + "images from matplotlib yet!" + ) + + def draw_path_collection(self, **props): + """Add a path collection to data list as a scatter plot. + + Current implementation defaults such collections as scatter plots. + Matplotlib supports collections that have many of the same parameters + in common like color, size, path, etc. However, they needn't all be + the same. Plotly does not currently support such functionality and + therefore, the style for the first object is taken and used to define + the remaining paths in the collection. + + props.keys() -- [ + 'paths', (structure: [vertices, path_code]) + 'path_coordinates', ('data', 'axes', 'figure', or 'display') + 'path_transforms', (mpl transform, including Affine2D matrix) + 'offsets', (offset from axes, helpful if in 'data') + 'offset_coordinates', ('data', 'axes', 'figure', or 'display') + 'offset_order', + 'styles', (style dict, see below) + 'mplobj' (the collection obj being drawn) + ] + + props['styles'].keys() -- [ + 'linewidth', (one or more linewidths) + 'facecolor', (one or more facecolors for path) + 'edgecolor', (one or more edgecolors for path) + 'alpha', (one or more opacites for path) + 'zorder', (precedence when stacked) + ] + + """ + self.msg += " Attempting to draw a path collection\n" + if props["offset_coordinates"] == "data": + markerstyle = mpltools.get_markerstyle_from_collection(props) + scatter_props = { + "coordinates": "data", + "data": props["offsets"], + "label": None, + "markerstyle": markerstyle, + "linestyle": None, + } + self.msg += " Drawing path collection as markers\n" + self.draw_marked_line(**scatter_props) + else: + self.msg += " Path collection not linked to 'data', not drawing\n" + warnings.warn( + "Dang! That path collection is out of this " + "world. I totally don't know what to do with " + "it yet! Plotly can only import path " + "collections linked to 'data' coordinates" + ) + + def draw_path(self, **props): + """Draw path, currently only attempts to draw bar charts. + + This function attempts to sort a given path into a collection of + horizontal or vertical bar charts. Most of the actual code takes + place in functions from mpltools.py. + + props.keys() -- [ + 'data', (a list of verticies for the path) + 'coordinates', ('data', 'axes', 'figure', or 'display') + 'pathcodes', (code for the path, structure: ['M', 'L', 'Z', etc.]) + 'style', (style dict, see below) + 'mplobj' (the mpl path object) + ] + + props['style'].keys() -- [ + 'alpha', (opacity of path obj) + 'edgecolor', + 'facecolor', + 'edgewidth', + 'dasharray', (style for path's enclosing line) + 'zorder' (precedence of obj when stacked) + ] + + """ + self.msg += " Attempting to draw a path\n" + is_bar = mpltools.is_bar(self.current_mpl_ax.containers, **props) + if is_bar: + self.current_bars += [props] + else: + self.msg += " This path isn't a bar, not drawing\n" + warnings.warn( + "I found a path object that I don't think is part " + "of a bar chart. Ignoring." + ) + + def draw_text(self, **props): + """Create an annotation dict for a text obj. + + Currently, plotly uses either 'page' or 'data' to reference + annotation locations. These refer to 'display' and 'data', + respectively for the 'coordinates' key used in the Exporter. + Appropriate measures are taken to transform text locations to + reference one of these two options. + + props.keys() -- [ + 'text', (actual content string, not the text obj) + 'position', (an x, y pair, not an mpl Bbox) + 'coordinates', ('data', 'axes', 'figure', 'display') + 'text_type', ('title', 'xlabel', or 'ylabel') + 'style', (style dict, see below) + 'mplobj' (actual mpl text object) + ] + + props['style'].keys() -- [ + 'alpha', (opacity of text) + 'fontsize', (size in points of text) + 'color', (hex color) + 'halign', (horizontal alignment, 'left', 'center', or 'right') + 'valign', (vertical alignment, 'baseline', 'center', or 'top') + 'rotation', + 'zorder', (precedence of text when stacked with other objs) + ] + + """ + self.msg += " Attempting to draw an mpl text object\n" + if not mpltools.check_corners(props["mplobj"], self.mpl_fig): + warnings.warn( + "Looks like the annotation(s) you are trying \n" + "to draw lies/lay outside the given figure size.\n\n" + "Therefore, the resulting Plotly figure may not be \n" + "large enough to view the full text. To adjust \n" + "the size of the figure, use the 'width' and \n" + "'height' keys in the Layout object. Alternatively,\n" + "use the Margin object to adjust the figure's margins." + ) + align = props["mplobj"]._multialignment + if not align: + align = props["style"]["halign"] # mpl default + if "annotations" not in self.plotly_fig["layout"]: + self.plotly_fig["layout"]["annotations"] = [] + if props["text_type"] == "xlabel": + self.msg += " Text object is an xlabel\n" + self.draw_xlabel(**props) + elif props["text_type"] == "ylabel": + self.msg += " Text object is a ylabel\n" + self.draw_ylabel(**props) + elif props["text_type"] == "title": + self.msg += " Text object is a title\n" + self.draw_title(**props) + else: # just a regular text annotation... + self.msg += " Text object is a normal annotation\n" + if props["coordinates"] != "data": + self.msg += " Text object isn't linked to 'data' coordinates\n" + x_px, y_px = ( + props["mplobj"].get_transform().transform(props["position"]) + ) + x, y = mpltools.display_to_paper(x_px, y_px, self.plotly_fig["layout"]) + xref = "paper" + yref = "paper" + xanchor = props["style"]["halign"] # no difference here! + yanchor = mpltools.convert_va(props["style"]["valign"]) + else: + self.msg += " Text object is linked to 'data' coordinates\n" + x, y = props["position"] + axis_ct = self.axis_ct + xaxis = self.plotly_fig["layout"]["xaxis{0}".format(axis_ct)] + yaxis = self.plotly_fig["layout"]["yaxis{0}".format(axis_ct)] + if ( + xaxis["range"][0] < x < xaxis["range"][1] + and yaxis["range"][0] < y < yaxis["range"][1] + ): + xref = "x{0}".format(self.axis_ct) + yref = "y{0}".format(self.axis_ct) + else: + self.msg += ( + " Text object is outside " + "plotting area, making 'paper' reference.\n" + ) + x_px, y_px = ( + props["mplobj"].get_transform().transform(props["position"]) + ) + x, y = mpltools.display_to_paper( + x_px, y_px, self.plotly_fig["layout"] + ) + xref = "paper" + yref = "paper" + xanchor = props["style"]["halign"] # no difference here! + yanchor = mpltools.convert_va(props["style"]["valign"]) + annotation = go.layout.Annotation( + text=( + str(props["text"]) + if isinstance(props["text"], str) + else props["text"] + ), + opacity=props["style"]["alpha"], + x=x, + y=y, + xref=xref, + yref=yref, + align=align, + xanchor=xanchor, + yanchor=yanchor, + showarrow=False, # change this later? + font=go.layout.annotation.Font( + color=props["style"]["color"], size=props["style"]["fontsize"] + ), + ) + self.plotly_fig["layout"]["annotations"] += (annotation,) + self.msg += " Heck, yeah I drew that annotation\n" + + def draw_title(self, **props): + """Add a title to the current subplot in layout dictionary. + + If there exists more than a single plot in the figure, titles revert + to 'page'-referenced annotations. + + props.keys() -- [ + 'text', (actual content string, not the text obj) + 'position', (an x, y pair, not an mpl Bbox) + 'coordinates', ('data', 'axes', 'figure', 'display') + 'text_type', ('title', 'xlabel', or 'ylabel') + 'style', (style dict, see below) + 'mplobj' (actual mpl text object) + ] + + props['style'].keys() -- [ + 'alpha', (opacity of text) + 'fontsize', (size in points of text) + 'color', (hex color) + 'halign', (horizontal alignment, 'left', 'center', or 'right') + 'valign', (vertical alignment, 'baseline', 'center', or 'top') + 'rotation', + 'zorder', (precedence of text when stacked with other objs) + ] + + """ + self.msg += " Attempting to draw a title\n" + if len(self.mpl_fig.axes) > 1: + self.msg += " More than one subplot, adding title as annotation\n" + x_px, y_px = props["mplobj"].get_transform().transform(props["position"]) + x, y = mpltools.display_to_paper(x_px, y_px, self.plotly_fig["layout"]) + annotation = go.layout.Annotation( + text=props["text"], + font=go.layout.annotation.Font( + color=props["style"]["color"], size=props["style"]["fontsize"] + ), + xref="paper", + yref="paper", + x=x, + y=y, + xanchor="center", + yanchor="bottom", + showarrow=False, # no arrow for a title! + ) + self.plotly_fig["layout"]["annotations"] += (annotation,) + else: + self.msg += " Only one subplot found, adding as a plotly title\n" + self.plotly_fig["layout"]["title"] = props["text"] + title_font = dict( + size=props["style"]["fontsize"], color=props["style"]["color"] + ) + self.plotly_fig["layout"]["title_font"] = title_font + + def draw_xlabel(self, **props): + """Add an xaxis label to the current subplot in layout dictionary. + + props.keys() -- [ + 'text', (actual content string, not the text obj) + 'position', (an x, y pair, not an mpl Bbox) + 'coordinates', ('data', 'axes', 'figure', 'display') + 'text_type', ('title', 'xlabel', or 'ylabel') + 'style', (style dict, see below) + 'mplobj' (actual mpl text object) + ] + + props['style'].keys() -- [ + 'alpha', (opacity of text) + 'fontsize', (size in points of text) + 'color', (hex color) + 'halign', (horizontal alignment, 'left', 'center', or 'right') + 'valign', (vertical alignment, 'baseline', 'center', or 'top') + 'rotation', + 'zorder', (precedence of text when stacked with other objs) + ] + + """ + self.msg += " Adding xlabel\n" + axis_key = "xaxis{0}".format(self.axis_ct) + self.plotly_fig["layout"][axis_key]["title"] = str(props["text"]) + title_font = dict( + size=props["style"]["fontsize"], color=props["style"]["color"] + ) + self.plotly_fig["layout"][axis_key]["title_font"] = title_font + + def draw_ylabel(self, **props): + """Add a yaxis label to the current subplot in layout dictionary. + + props.keys() -- [ + 'text', (actual content string, not the text obj) + 'position', (an x, y pair, not an mpl Bbox) + 'coordinates', ('data', 'axes', 'figure', 'display') + 'text_type', ('title', 'xlabel', or 'ylabel') + 'style', (style dict, see below) + 'mplobj' (actual mpl text object) + ] + + props['style'].keys() -- [ + 'alpha', (opacity of text) + 'fontsize', (size in points of text) + 'color', (hex color) + 'halign', (horizontal alignment, 'left', 'center', or 'right') + 'valign', (vertical alignment, 'baseline', 'center', or 'top') + 'rotation', + 'zorder', (precedence of text when stacked with other objs) + ] + + """ + self.msg += " Adding ylabel\n" + axis_key = "yaxis{0}".format(self.axis_ct) + self.plotly_fig["layout"][axis_key]["title"] = props["text"] + title_font = dict( + size=props["style"]["fontsize"], color=props["style"]["color"] + ) + self.plotly_fig["layout"][axis_key]["title_font"] = title_font + + def resize(self): + """Revert figure layout to allow plotly to resize. + + By default, PlotlyRenderer tries its hardest to precisely mimic an + mpl figure. However, plotly is pretty good with aesthetics. By + running PlotlyRenderer.resize(), layout parameters are deleted. This + lets plotly choose them instead of mpl. + + """ + self.msg += "Resizing figure, deleting keys from layout\n" + for key in ["width", "height", "autosize", "margin"]: + try: + del self.plotly_fig["layout"][key] + except (KeyError, AttributeError): + pass + + def strip_style(self): + self.msg += "Stripping mpl style is no longer supported\n" -- cgit v1.2.3-70-g09d2