Source code for papermodels.paper.plot

"""Matplotlib plotting of annotations and elements."""

from __future__ import annotations
from typing import Optional, Any, Union
import pathlib

from matplotlib.figure import Figure
from matplotlib.patches import Polygon
import numpy as np
from ..datatypes.annotation import Annotation
from shapely.ops import polylabel
import textalloc as ta
from ..datatypes.element import Element


[docs] def plot_elements( elements: list[Element], figsize: int | float | tuple[int | float, int | float] = (17, 11), dpi: float = 100, plot_trib_areas: bool = False, plot_extent_polygons: bool = False, plot_subelement_tags: bool = False, plot_elems_by_tag: Optional[list[str]] = None, ) -> Figure: """ Plots the elements in matplotlib. Size and dpi can be adjusted to make the plot bigger/smaller. Size is in inches and dpi stands for "dots per inch". For a biggish plot, values of size=12, dpi=200 gives good results. Colors are based on the kind of element that is being plotted """ if isinstance(figsize, (int, float)): figsize = (figsize, figsize) fig = Figure(figsize=figsize, dpi=dpi, tight_layout=True) ax = fig.gca() has_tags = False min_extent = np.array([float("-inf"), float("-inf")]) max_extent = np.array([float("inf"), float("inf")]) text_annotations = [] initial_positions_x = [] initial_positions_y = [] tags = [] lines_x = [] lines_y = [] linear_polygons = [] point_polygons = [] extent_polygons = [] trib_areas = [] collectors = [] transfers = [] plot_colors = { "poly_edge": "k", "poly_face": (0.5, 0.5, 0.5), "linear_poly": "green", "collector_lines": (0.7, 0.7, 0.7), "transfer_lines": (0.3, 0.3, 0.3), "trib_edge": "red", "trib_face": (0.8, 0.1, 0.0), "extent_edge": "yellow", "extent_face": (0.8, 0.7, 0.0), "prototype_line": "yellow", } highlight_tags = False plot_objs = [] for element in elements: plot_attrs = get_element_plotting_attributes(element, is_subelement=True) plot_objs.append(plot_attrs) if element.subelements: for subelem in element.subelements: plot_attrs = get_element_plotting_attributes( subelem, is_subelement=True ) plot_objs.append(plot_attrs) for po in plot_objs: xy = po["xy"] if sum(max_extent) == float("inf"): min_extent = np.maximum(min_extent, np.max(xy, axis=1)) max_extent = np.minimum(max_extent, np.min(xy, axis=1)) else: min_extent = np.minimum(min_extent, np.min(xy, axis=1)) max_extent = np.maximum(max_extent, np.max(xy, axis=1)) # For tagging if plot_subelement_tags == False and plot_elems_by_tag is None: if "-" not in po["tag"]: tags.append(po["tag"]) initial_positions_x.append(po["anchor_point"][0]) initial_positions_y.append(po["anchor_point"][1]) elif plot_elems_by_tag: if po["tag"] in plot_elems_by_tag: highlight_tags = True tags.append(po["tag"]) initial_positions_x.append(po["anchor_point"][0]) initial_positions_y.append(po["anchor_point"][1]) else: tags.append(po["tag"]) initial_positions_x.append(po["anchor_point"][0]) initial_positions_y.append(po["anchor_point"][1]) if po["is_poly"]: ax.add_patch( Polygon( xy=xy.T, closed=True, # linestyle=annot.line_type, # linewidth=float(annot.line_weight), ec=plot_colors["poly_edge"], fc=plot_colors["poly_face"], alpha=0.6, # zorder=idx, ) ) elif po["is_line"]: lines_x.append(xy[0]) lines_y.append(xy[1]) if po["rank"] == 0: line_color = plot_colors["collector_lines"] z_order = None else: line_color = plot_colors["transfer_lines"] z_order = None if po["extent_polygon_xy"] is not None: line_color = plot_colors["prototype_line"] z_order = 1e6 ax.plot( xy[0], xy[1], # linestyle=annot.line_type, # linewidth=float(annot.line_weight), color=line_color, alpha=1.0, zorder=z_order, ) if po["trib_area_xy"] is not None and plot_trib_areas: ax.add_patch( Polygon( xy=po["trib_area_xy"].T, closed=True, # linestyle=annot.line_type, # linewidth=float(annot.line_weight), ec=plot_colors["trib_edge"], fc=plot_colors["trib_face"], alpha=0.3, zorder=1000, ) ) elif po["extent_polygon_xy"] is not None and plot_extent_polygons: ax.add_patch( Polygon( xy=po["extent_polygon_xy"].T, closed=True, # linestyle=annot.line_type, # linewidth=float(annot.line_weight), ec=plot_colors["extent_edge"], fc=plot_colors["extent_face"], alpha=0.3, zorder=1000, ) ) ax.set_aspect("equal") plot_margin_metric = np.linalg.norm( max_extent - min_extent ) # Distance between bot-left and top-right ax.set_xlim( min_extent[0] - plot_margin_metric * 0.05, max_extent[0] + plot_margin_metric * 0.05, ) ax.set_ylim( min_extent[1] - plot_margin_metric * 0.05, max_extent[1] + plot_margin_metric * 0.05, ) text_color = "k" if highlight_tags: text_color = "r" ta.allocate( ax=ax, x=initial_positions_x, y=initial_positions_y, text_list=tags, # x_lines=lines_x, # y_lines=lines_y, textsize=8, textcolor=text_color, linecolor="k", avoid_label_lines_overlap=True, avoid_crossing_label_lines=True, ) return fig
[docs] def get_element_plotting_attributes( element: Element, is_subelement: bool = False ) -> dict[str, Any]: """ Returns a dict of plotting attributes for the element. e.g. { "is_poly": True, "is_line": False, "is_linear": False, "is_subelement": False, "tag": str, "rank": int, "xy": np.array([[...], [...]]), "anchor_point": np.array([..., ...]), "extent_polygon_xy": np.array([[...], [...]]) | None, "trib_area_xy": np.array([[...], [...]]) | None, } """ is_poly = False is_line = False is_linear = False if element.geometry.geom_type == "Polygon": coords = element.geometry.exterior.coords is_poly = True elif element.geometry.geom_type == "LineString": coords = element.geometry.coords is_line = True trib_xy = None extent_xy = None if element.reaction_type == "linear": is_linear = True if element.trib_area: trib_xy = _exterior_xy(element.trib_area) array_region = getattr(element, "array_region", element.extent_polygon) if array_region: extent_xy = _exterior_xy(array_region) xy = np.array(list(zip(*coords))) tag = element.tag rank = element.rank geom = element.geometry rep_point = np.array(geom.representative_point().coords[0]) centroid_point = np.array(geom.centroid.coords[0]) anchor_point = (rep_point + centroid_point) / 2 plotting_attributes = { "is_poly": is_poly, "is_line": is_line, "is_linear": is_linear, "is_subelement": is_subelement, "tag": tag, "rank": rank, "xy": xy, "anchor_point": anchor_point, "trib_area_xy": trib_xy, "extent_polygon_xy": extent_xy, } return plotting_attributes
[docs] def plot_annotations( annots: list[Annotation] | dict[Annotation, dict], figsize: int | float | tuple[int | float, int | float] = (17, 11), dpi: float = 100, plot_tags: bool = False, plot_annots_by_tag: Optional[list[str]] = None, ) -> Figure: """ Plots that annotations, 'annots' in matplotlib. Size and dpi can be adjusted to make the plot bigger/smaller. Size is in inches and dpi stands for "dots per inch". For a biggish plot, values of size=12, dpi=200 gives good results. """ if isinstance(figsize, (int, float)): figsize = (figsize, figsize) fig = Figure(figsize=figsize, dpi=dpi, tight_layout=True) ax = fig.gca() annotation_dict = isinstance(annots, dict) has_tags = False min_extent = np.array([float("-inf"), float("-inf")]) max_extent = np.array([float("inf"), float("inf")]) text_annotations = [] initial_positions_x = [] initial_positions_y = [] lines_x = [] lines_y = [] for idx, annot in enumerate(annots): if annotation_dict: has_tags = "tag" in annots[annot] xy = xy_vertices(annot.vertices, dpi) if sum(max_extent) == float("inf"): min_extent = np.maximum(min_extent, np.max(xy, axis=1)) max_extent = np.minimum(max_extent, np.min(xy, axis=1)) else: min_extent = np.minimum(min_extent, np.min(xy, axis=1)) max_extent = np.maximum(max_extent, np.max(xy, axis=1)) if annot.object_type.lower() in ( "polygon", "square", "rectangle", "rectangle sketch to scale", ): geom = annots[annot]["geometry"] minx, miny, maxx, maxy = geom.bounds lines_x.append([minx * dpi / 72, maxx * dpi / 72]) lines_y.append([miny * dpi / 72, maxy * dpi / 72]) xy = xy_vertices(annot.vertices, dpi) if sum(max_extent) == float("inf"): min_extent = np.maximum(max_extent, np.max(xy, axis=1)) max_extent = np.minimum(min_extent, np.min(xy, axis=1)) else: min_extent = np.minimum(min_extent, np.min(xy, axis=1)) max_extent = np.maximum(max_extent, np.max(xy, axis=1)) face_color = ( tuple(float(elem) for elem in annot.fill_color) if annot.fill_color is not None else None ) ax.add_patch( Polygon( xy=xy.T, closed=True, linestyle=_dash_linestyle(annot.line_type, annot.line_weight), linewidth=float(annot.line_weight), ec=tuple(float(elem) for elem in annot.line_color), fc=face_color, alpha=0.2 * float(annot.fill_opacity), zorder=idx, ) ) elif annot.object_type.lower() in ("line", "polyline"): xy = xy_vertices(annot.vertices, dpi) lines_x.append(xy[0]) lines_y.append(xy[1]) ax.plot( xy[0], xy[1], linestyle=_dash_linestyle(annot.line_type, annot.line_weight), linewidth=float(annot.line_weight), color=tuple(float(elem) for elem in annot.line_color), alpha=float(annot.line_opacity), zorder=idx, ) if annotation_dict and has_tags and plot_tags: tag = annots[annot]["tag"] geom = annots[annot]["geometry"] rep_point = np.array(geom.representative_point().coords[0]) centroid_point = np.array(geom.centroid.coords[0]) plot_point = (rep_point + centroid_point) / 2 initial_positions_x.append(plot_point[0] * dpi / 72) initial_positions_y.append(plot_point[1] * dpi / 72) text_annotations.append(tag) # ax.annotate( # tag, # plot_point * dpi / 72, # zorder=100 * len(annots), # ) ax.set_aspect("equal") plot_margin_metric = np.linalg.norm( max_extent - min_extent ) # Distance between bot-left and top-right ax.set_xlim( min_extent[0] - plot_margin_metric * 0.05, max_extent[0] + plot_margin_metric * 0.05, ) ax.set_ylim( min_extent[1] - plot_margin_metric * 0.05, max_extent[1] + plot_margin_metric * 0.05, ) ta.allocate( ax=ax, x=initial_positions_x, y=initial_positions_y, text_list=text_annotations, x_lines=lines_x, y_lines=lines_y, textsize=8, textcolor="k", linecolor="k", avoid_label_lines_overlap=True, avoid_crossing_label_lines=True, ) return fig
[docs] def xy_vertices(vertices: str, dpi: float, close=False) -> list[list[float]]: """ Returns a list of lists of floats to emulate a 2d numpy array of x, y values """ x = [] y = [] for idx, ordinate in enumerate(vertices): if idx % 2: y.append(float(ordinate)) else: x.append(float(ordinate)) scaled_vertices = np.asarray([x, y]) * dpi / 72 return scaled_vertices
def _exterior_xy(area) -> np.ndarray: """ Exterior coordinates of an areal geometry as a 2 x n array. For a multi-part area (e.g. a trib band clipped by a non-convex joist container) the largest part is used. """ if hasattr(area, "geoms"): parts = [g for g in area.geoms if g.geom_type == "Polygon"] area = max(parts, key=lambda g: g.area) return np.array(list(zip(*area.exterior.coords))) def _dash_linestyle(line_type: Optional[tuple], line_weight) -> str | tuple: """ Returns a matplotlib linestyle approximating the PDF dash array 'line_type'. PDF dash lengths are in points while matplotlib dash lengths are multiples of the line width, so each length is divided by 'line_weight'. The dashes therefore render at their true size in points, independent of plot zoom. """ if not line_type: return "solid" line_width = float(line_weight) or 1.0 dashes = tuple(line_type) if len(dashes) % 2: # PDF repeats an odd-length array to form on/off pairs dashes = dashes * 2 return (0, tuple(float(length) / line_width for length in dashes))