diff options
author | sotech117 <michael_foiani@brown.edu> | 2025-07-31 17:27:24 -0400 |
---|---|---|
committer | sotech117 <michael_foiani@brown.edu> | 2025-07-31 17:27:24 -0400 |
commit | 5bf22fc7e3c392c8bd44315ca2d06d7dca7d084e (patch) | |
tree | 8dacb0f195df1c0788d36dd0064f6bbaa3143ede /venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py | |
parent | b832d364da8c2efe09e3f75828caf73c50d01ce3 (diff) |
add code for analysis of data
Diffstat (limited to 'venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py')
-rw-r--r-- | venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py | 610 |
1 files changed, 610 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py b/venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py new file mode 100644 index 0000000..4268136 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/matplotlylib/mpltools.py @@ -0,0 +1,610 @@ +""" +Tools + +A module for converting from mpl language to plotly language. + +""" + +import math + +import warnings +import matplotlib.dates + + +def check_bar_match(old_bar, new_bar): + """Check if two bars belong in the same collection (bar chart). + + Positional arguments: + old_bar -- a previously sorted bar dictionary. + new_bar -- a new bar dictionary that needs to be sorted. + + """ + tests = [] + tests += (new_bar["orientation"] == old_bar["orientation"],) + tests += (new_bar["facecolor"] == old_bar["facecolor"],) + if new_bar["orientation"] == "v": + new_width = new_bar["x1"] - new_bar["x0"] + old_width = old_bar["x1"] - old_bar["x0"] + tests += (new_width - old_width < 0.000001,) + tests += (new_bar["y0"] == old_bar["y0"],) + elif new_bar["orientation"] == "h": + new_height = new_bar["y1"] - new_bar["y0"] + old_height = old_bar["y1"] - old_bar["y0"] + tests += (new_height - old_height < 0.000001,) + tests += (new_bar["x0"] == old_bar["x0"],) + if all(tests): + return True + else: + return False + + +def check_corners(inner_obj, outer_obj): + inner_corners = inner_obj.get_window_extent().corners() + outer_corners = outer_obj.get_window_extent().corners() + if inner_corners[0][0] < outer_corners[0][0]: + return False + elif inner_corners[0][1] < outer_corners[0][1]: + return False + elif inner_corners[3][0] > outer_corners[3][0]: + return False + elif inner_corners[3][1] > outer_corners[3][1]: + return False + else: + return True + + +def convert_dash(mpl_dash): + """Convert mpl line symbol to plotly line symbol and return symbol.""" + if mpl_dash in DASH_MAP: + return DASH_MAP[mpl_dash] + else: + dash_array = mpl_dash.split(",") + + if len(dash_array) < 2: + return "solid" + + # Catch the exception where the off length is zero, in case + # matplotlib 'solid' changes from '10,0' to 'N,0' + if math.isclose(float(dash_array[1]), 0.0): + return "solid" + + # If we can't find the dash pattern in the map, convert it + # into custom values in px, e.g. '7,5' -> '7px,5px' + dashpx = ",".join([x + "px" for x in dash_array]) + + # TODO: rewrite the convert_dash code + # only strings 'solid', 'dashed', etc allowed + if dashpx == "7.4px,3.2px": + dashpx = "dashed" + elif dashpx == "12.8px,3.2px,2.0px,3.2px": + dashpx = "dashdot" + elif dashpx == "2.0px,3.3px": + dashpx = "dotted" + return dashpx + + +def convert_path(path): + code = tuple(path[1]) + if code in PATH_MAP: + return PATH_MAP[code] + else: + return None + + +def convert_symbol(mpl_symbol): + """Convert mpl marker symbol to plotly symbol and return symbol.""" + if isinstance(mpl_symbol, list): + symbol = list() + for s in mpl_symbol: + symbol += [convert_symbol(s)] + return symbol + elif mpl_symbol in SYMBOL_MAP: + return SYMBOL_MAP[mpl_symbol] + else: + return "circle" # default + + +def hex_to_rgb(value): + """ + Change a hex color to an rgb tuple + + :param (str|unicode) value: The hex string we want to convert. + :return: (int, int, int) The red, green, blue int-tuple. + + Example: + + '#FFFFFF' --> (255, 255, 255) + + """ + value = value.lstrip("#") + lv = len(value) + return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) + + +def merge_color_and_opacity(color, opacity): + """ + Merge hex color with an alpha (opacity) to get an rgba tuple. + + :param (str|unicode) color: A hex color string. + :param (float|int) opacity: A value [0, 1] for the 'a' in 'rgba'. + :return: (int, int, int, float) The rgba color and alpha tuple. + + """ + if color is None: # None can be used as a placeholder, just bail. + return None + + rgb_tup = hex_to_rgb(color) + if opacity is None: + return "rgb {}".format(rgb_tup) + + rgba_tup = rgb_tup + (opacity,) + return "rgba {}".format(rgba_tup) + + +def convert_va(mpl_va): + """Convert mpl vertical alignment word to equivalent HTML word. + + Text alignment specifiers from mpl differ very slightly from those used + in HTML. See the VA_MAP for more details. + + Positional arguments: + mpl_va -- vertical mpl text alignment spec. + + """ + if mpl_va in VA_MAP: + return VA_MAP[mpl_va] + else: + return None # let plotly figure it out! + + +def convert_x_domain(mpl_plot_bounds, mpl_max_x_bounds): + """Map x dimension of current plot to plotly's domain space. + + The bbox used to locate an axes object in mpl differs from the + method used to locate axes in plotly. The mpl version locates each + axes in the figure so that axes in a single-plot figure might have + the bounds, [0.125, 0.125, 0.775, 0.775] (x0, y0, width, height), + in mpl's figure coordinates. However, the axes all share one space in + plotly such that the domain will always be [0, 0, 1, 1] + (x0, y0, x1, y1). To convert between the two, the mpl figure bounds + need to be mapped to a [0, 1] domain for x and y. The margins set + upon opening a new figure will appropriately match the mpl margins. + + Optionally, setting margins=0 and simply copying the domains from + mpl to plotly would place axes appropriately. However, + this would throw off axis and title labeling. + + Positional arguments: + mpl_plot_bounds -- the (x0, y0, width, height) params for current ax ** + mpl_max_x_bounds -- overall (x0, x1) bounds for all axes ** + + ** these are all specified in mpl figure coordinates + + """ + mpl_x_dom = [mpl_plot_bounds[0], mpl_plot_bounds[0] + mpl_plot_bounds[2]] + plotting_width = mpl_max_x_bounds[1] - mpl_max_x_bounds[0] + x0 = (mpl_x_dom[0] - mpl_max_x_bounds[0]) / plotting_width + x1 = (mpl_x_dom[1] - mpl_max_x_bounds[0]) / plotting_width + return [x0, x1] + + +def convert_y_domain(mpl_plot_bounds, mpl_max_y_bounds): + """Map y dimension of current plot to plotly's domain space. + + The bbox used to locate an axes object in mpl differs from the + method used to locate axes in plotly. The mpl version locates each + axes in the figure so that axes in a single-plot figure might have + the bounds, [0.125, 0.125, 0.775, 0.775] (x0, y0, width, height), + in mpl's figure coordinates. However, the axes all share one space in + plotly such that the domain will always be [0, 0, 1, 1] + (x0, y0, x1, y1). To convert between the two, the mpl figure bounds + need to be mapped to a [0, 1] domain for x and y. The margins set + upon opening a new figure will appropriately match the mpl margins. + + Optionally, setting margins=0 and simply copying the domains from + mpl to plotly would place axes appropriately. However, + this would throw off axis and title labeling. + + Positional arguments: + mpl_plot_bounds -- the (x0, y0, width, height) params for current ax ** + mpl_max_y_bounds -- overall (y0, y1) bounds for all axes ** + + ** these are all specified in mpl figure coordinates + + """ + mpl_y_dom = [mpl_plot_bounds[1], mpl_plot_bounds[1] + mpl_plot_bounds[3]] + plotting_height = mpl_max_y_bounds[1] - mpl_max_y_bounds[0] + y0 = (mpl_y_dom[0] - mpl_max_y_bounds[0]) / plotting_height + y1 = (mpl_y_dom[1] - mpl_max_y_bounds[0]) / plotting_height + return [y0, y1] + + +def display_to_paper(x, y, layout): + """Convert mpl display coordinates to plotly paper coordinates. + + Plotly references object positions with an (x, y) coordinate pair in either + 'data' or 'paper' coordinates which reference actual data in a plot or + the entire plotly axes space where the bottom-left of the bottom-left + plot has the location (x, y) = (0, 0) and the top-right of the top-right + plot has the location (x, y) = (1, 1). Display coordinates in mpl reference + objects with an (x, y) pair in pixel coordinates, where the bottom-left + corner is at the location (x, y) = (0, 0) and the top-right corner is at + the location (x, y) = (figwidth*dpi, figheight*dpi). Here, figwidth and + figheight are in inches and dpi are the dots per inch resolution. + + """ + num_x = x - layout["margin"]["l"] + den_x = layout["width"] - (layout["margin"]["l"] + layout["margin"]["r"]) + num_y = y - layout["margin"]["b"] + den_y = layout["height"] - (layout["margin"]["b"] + layout["margin"]["t"]) + return num_x / den_x, num_y / den_y + + +def get_axes_bounds(fig): + """Return the entire axes space for figure. + + An axes object in mpl is specified by its relation to the figure where + (0,0) corresponds to the bottom-left part of the figure and (1,1) + corresponds to the top-right. Margins exist in matplotlib because axes + objects normally don't go to the edges of the figure. + + In plotly, the axes area (where all subplots go) is always specified with + the domain [0,1] for both x and y. This function finds the smallest box, + specified by two points, that all of the mpl axes objects fit into. This + box is then used to map mpl axes domains to plotly axes domains. + + """ + x_min, x_max, y_min, y_max = [], [], [], [] + for axes_obj in fig.get_axes(): + bounds = axes_obj.get_position().bounds + x_min.append(bounds[0]) + x_max.append(bounds[0] + bounds[2]) + y_min.append(bounds[1]) + y_max.append(bounds[1] + bounds[3]) + x_min, y_min, x_max, y_max = min(x_min), min(y_min), max(x_max), max(y_max) + return (x_min, x_max), (y_min, y_max) + + +def get_axis_mirror(main_spine, mirror_spine): + if main_spine and mirror_spine: + return "ticks" + elif main_spine and not mirror_spine: + return False + elif not main_spine and mirror_spine: + return False # can't handle this case yet! + else: + return False # nuttin'! + + +def get_bar_gap(bar_starts, bar_ends, tol=1e-10): + if len(bar_starts) == len(bar_ends) and len(bar_starts) > 1: + sides1 = bar_starts[1:] + sides2 = bar_ends[:-1] + gaps = [s2 - s1 for s2, s1 in zip(sides1, sides2)] + gap0 = gaps[0] + uniform = all([abs(gap0 - gap) < tol for gap in gaps]) + if uniform: + return gap0 + + +def convert_rgba_array(color_list): + clean_color_list = list() + for c in color_list: + clean_color_list += [ + dict(r=int(c[0] * 255), g=int(c[1] * 255), b=int(c[2] * 255), a=c[3]) + ] + plotly_colors = list() + for rgba in clean_color_list: + plotly_colors += ["rgba({r},{g},{b},{a})".format(**rgba)] + if len(plotly_colors) == 1: + return plotly_colors[0] + else: + return plotly_colors + + +def convert_path_array(path_array): + symbols = list() + for path in path_array: + symbols += [convert_path(path)] + if len(symbols) == 1: + return symbols[0] + else: + return symbols + + +def convert_linewidth_array(width_array): + if len(width_array) == 1: + return width_array[0] + else: + return width_array + + +def convert_size_array(size_array): + size = [math.sqrt(s) for s in size_array] + if len(size) == 1: + return size[0] + else: + return size + + +def get_markerstyle_from_collection(props): + markerstyle = dict( + alpha=None, + facecolor=convert_rgba_array(props["styles"]["facecolor"]), + marker=convert_path_array(props["paths"]), + edgewidth=convert_linewidth_array(props["styles"]["linewidth"]), + # markersize=convert_size_array(props['styles']['size']), # TODO! + markersize=convert_size_array(props["mplobj"].get_sizes()), + edgecolor=convert_rgba_array(props["styles"]["edgecolor"]), + ) + return markerstyle + + +def get_rect_xmin(data): + """Find minimum x value from four (x,y) vertices.""" + return min(data[0][0], data[1][0], data[2][0], data[3][0]) + + +def get_rect_xmax(data): + """Find maximum x value from four (x,y) vertices.""" + return max(data[0][0], data[1][0], data[2][0], data[3][0]) + + +def get_rect_ymin(data): + """Find minimum y value from four (x,y) vertices.""" + return min(data[0][1], data[1][1], data[2][1], data[3][1]) + + +def get_rect_ymax(data): + """Find maximum y value from four (x,y) vertices.""" + return max(data[0][1], data[1][1], data[2][1], data[3][1]) + + +def get_spine_visible(ax, spine_key): + """Return some spine parameters for the spine, `spine_key`.""" + spine = ax.spines[spine_key] + ax_frame_on = ax.get_frame_on() + position = spine._position or ("outward", 0.0) + if isinstance(position, str): + if position == "center": + position = ("axes", 0.5) + elif position == "zero": + position = ("data", 0) + position_type, amount = position + if position_type == "outward" and amount == 0: + spine_frame_like = True + else: + spine_frame_like = False + if not spine.get_visible(): + return False + elif not spine._edgecolor[-1]: # user's may have set edgecolor alpha==0 + return False + elif not ax_frame_on and spine_frame_like: + return False + elif ax_frame_on and spine_frame_like: + return True + elif not ax_frame_on and not spine_frame_like: + return True # we've already checked for that it's visible. + else: + return False # oh man, and i thought we exhausted the options... + + +def is_bar(bar_containers, **props): + """A test to decide whether a path is a bar from a vertical bar chart.""" + + # is this patch in a bar container? + for container in bar_containers: + if props["mplobj"] in container: + return True + return False + + +def make_bar(**props): + """Make an intermediate bar dictionary. + + This creates a bar dictionary which aids in the comparison of new bars to + old bars from other bar chart (patch) collections. This is not the + dictionary that needs to get passed to plotly as a data dictionary. That + happens in PlotlyRenderer in that class's draw_bar method. In other + words, this dictionary describes a SINGLE bar, whereas, plotly will + require a set of bars to be passed in a data dictionary. + + """ + return { + "bar": props["mplobj"], + "x0": get_rect_xmin(props["data"]), + "y0": get_rect_ymin(props["data"]), + "x1": get_rect_xmax(props["data"]), + "y1": get_rect_ymax(props["data"]), + "alpha": props["style"]["alpha"], + "edgecolor": props["style"]["edgecolor"], + "facecolor": props["style"]["facecolor"], + "edgewidth": props["style"]["edgewidth"], + "dasharray": props["style"]["dasharray"], + "zorder": props["style"]["zorder"], + } + + +def prep_ticks(ax, index, ax_type, props): + """Prepare axis obj belonging to axes obj. + + positional arguments: + ax - the mpl axes instance + index - the index of the axis in `props` + ax_type - 'x' or 'y' (for now) + props - an mplexporter poperties dictionary + + """ + axis_dict = dict() + if ax_type == "x": + axis = ax.get_xaxis() + elif ax_type == "y": + axis = ax.get_yaxis() + else: + return dict() # whoops! + + scale = props["axes"][index]["scale"] + if scale == "linear": + # get tick location information + try: + tickvalues = props["axes"][index]["tickvalues"] + tick0 = tickvalues[0] + dticks = [ + round(tickvalues[i] - tickvalues[i - 1], 12) + for i in range(1, len(tickvalues) - 1) + ] + if all([dticks[i] == dticks[i - 1] for i in range(1, len(dticks) - 1)]): + dtick = tickvalues[1] - tickvalues[0] + else: + warnings.warn( + "'linear' {0}-axis tick spacing not even, " + "ignoring mpl tick formatting.".format(ax_type) + ) + raise TypeError + except (IndexError, TypeError): + axis_dict["nticks"] = props["axes"][index]["nticks"] + else: + axis_dict["tick0"] = tick0 + axis_dict["dtick"] = dtick + axis_dict["tickmode"] = None + elif scale == "log": + try: + axis_dict["tick0"] = props["axes"][index]["tickvalues"][0] + axis_dict["dtick"] = ( + props["axes"][index]["tickvalues"][1] + - props["axes"][index]["tickvalues"][0] + ) + axis_dict["tickmode"] = None + except (IndexError, TypeError): + axis_dict = dict(nticks=props["axes"][index]["nticks"]) + base = axis.get_transform().base + if base == 10: + if ax_type == "x": + axis_dict["range"] = [ + math.log10(props["xlim"][0]), + math.log10(props["xlim"][1]), + ] + elif ax_type == "y": + axis_dict["range"] = [ + math.log10(props["ylim"][0]), + math.log10(props["ylim"][1]), + ] + else: + axis_dict = dict(range=None, type="linear") + warnings.warn( + "Converted non-base10 {0}-axis log scale to 'linear'".format(ax_type) + ) + else: + return dict() + # get tick label formatting information + formatter = axis.get_major_formatter().__class__.__name__ + if ax_type == "x" and "DateFormatter" in formatter: + axis_dict["type"] = "date" + try: + axis_dict["tick0"] = mpl_dates_to_datestrings(axis_dict["tick0"], formatter) + except KeyError: + pass + finally: + axis_dict.pop("dtick", None) + axis_dict.pop("tickmode", None) + axis_dict["range"] = mpl_dates_to_datestrings(props["xlim"], formatter) + + if formatter == "LogFormatterMathtext": + axis_dict["exponentformat"] = "e" + return axis_dict + + +def prep_xy_axis(ax, props, x_bounds, y_bounds): + xaxis = dict( + type=props["axes"][0]["scale"], + range=list(props["xlim"]), + showgrid=props["axes"][0]["grid"]["gridOn"], + domain=convert_x_domain(props["bounds"], x_bounds), + side=props["axes"][0]["position"], + tickfont=dict(size=props["axes"][0]["fontsize"]), + ) + xaxis.update(prep_ticks(ax, 0, "x", props)) + yaxis = dict( + type=props["axes"][1]["scale"], + range=list(props["ylim"]), + showgrid=props["axes"][1]["grid"]["gridOn"], + domain=convert_y_domain(props["bounds"], y_bounds), + side=props["axes"][1]["position"], + tickfont=dict(size=props["axes"][1]["fontsize"]), + ) + yaxis.update(prep_ticks(ax, 1, "y", props)) + return xaxis, yaxis + + +def mpl_dates_to_datestrings(dates, mpl_formatter): + """Convert matplotlib dates to iso-formatted-like time strings. + + Plotly's accepted format: "YYYY-MM-DD HH:MM:SS" (e.g., 2001-01-01 00:00:00) + + Info on mpl dates: http://matplotlib.org/api/dates_api.html + + """ + _dates = dates + + # this is a pandas datetime formatter, times show up in floating point days + # since the epoch (1970-01-01T00:00:00+00:00) + if mpl_formatter == "TimeSeries_DateFormatter": + try: + dates = matplotlib.dates.epoch2num([date * 24 * 60 * 60 for date in dates]) + dates = matplotlib.dates.num2date(dates) + except Exception: + return _dates + + # the rest of mpl dates are in floating point days since + # (0001-01-01T00:00:00+00:00) + 1. I.e., (0001-01-01T00:00:00+00:00) == 1.0 + # according to mpl --> try num2date(1) + else: + try: + dates = matplotlib.dates.num2date(dates) + except Exception: + return _dates + + time_stings = [ + " ".join(date.isoformat().split("+")[0].split("T")) for date in dates + ] + return time_stings + + +# dashed is dash in matplotlib +DASH_MAP = { + "10,0": "solid", + "6,6": "dash", + "2,2": "circle", + "4,4,2,4": "dashdot", + "none": "solid", + "7.4,3.2": "dash", +} + +PATH_MAP = { + ("M", "C", "C", "C", "C", "C", "C", "C", "C", "Z"): "o", + ("M", "L", "L", "L", "L", "L", "L", "L", "L", "L", "Z"): "*", + ("M", "L", "L", "L", "L", "L", "L", "L", "Z"): "8", + ("M", "L", "L", "L", "L", "L", "Z"): "h", + ("M", "L", "L", "L", "L", "Z"): "p", + ("M", "L", "M", "L", "M", "L"): "1", + ("M", "L", "L", "L", "Z"): "s", + ("M", "L", "M", "L"): "+", + ("M", "L", "L", "Z"): "^", + ("M", "L"): "|", +} + +SYMBOL_MAP = { + "o": "circle", + "v": "triangle-down", + "^": "triangle-up", + "<": "triangle-left", + ">": "triangle-right", + "s": "square", + "+": "cross", + "x": "x", + "*": "star", + "D": "diamond", + "d": "diamond", +} + +VA_MAP = {"center": "middle", "baseline": "bottom", "top": "top"} |