Source code for moptipyapps.binpacking2d.plot_packing

"""Plot a packing into one figure."""
from collections import Counter
from typing import Callable, Final, Iterable

import moptipy.utils.plot_defaults as pd
import moptipy.utils.plot_utils as pu
from matplotlib.artist import Artist  # type: ignore
from matplotlib.axes import Axes  # type: ignore
from matplotlib.figure import Figure  # type: ignore
from matplotlib.patches import Rectangle  # type: ignore
from matplotlib.text import Text  # type: ignore
from pycommons.types import type_error

from moptipyapps.binpacking2d.packing import (
    IDX_BIN,
    IDX_BOTTOM_Y,
    IDX_ID,
    IDX_LEFT_X,
    IDX_RIGHT_X,
    IDX_TOP_Y,
    Packing,
)


[docs] def default_packing_item_str(item_id: int, item_index: int, item_in_bin_index: int) -> Iterable[str]: """ Get a packing item string(s). The default idea is to include the item id, the index of the item in the bin, and the overall index of the item. If the space is insufficient, we remove the latter or the latter two. Hence, this function returns a tuple of three strings. :param item_id: the ID of the packing item :param item_index: the item index :param item_in_bin_index: the index of the item in its bin :return: the string """ return (f"{item_id}/{item_in_bin_index}/{item_index}", f"{item_id}/{item_in_bin_index}", str(item_id))
[docs] def plot_packing(packing: Packing | str, max_rows: int = 3, max_bins_per_row: int = 3, default_width_per_bin: float | int | None = 8.6, max_width: float | int | None = 8.6, default_height_per_bin: float | int | None = 5.315092303249095, max_height: float | int | None = 9, packing_item_str: Callable[ [int, int, int], str | Iterable[str]] = default_packing_item_str, importance_to_font_size_func: Callable[[int], float] = pd.importance_to_font_size, dpi: float | int | None = 384.0) -> Figure: """ Plot a packing. Each item is drawn in a different color. Each item rectangle includes, if there is enough space, the item-ID. If there is more space, also the index of the item inside the bin (starting at 1) is included. If there is yet more space, even the overall index of the item is included. :param packing: the packing or the file to load it from :param max_rows: the maximum number of rows :param max_bins_per_row: the maximum number of bins per row :param default_width_per_bin: the optional default width of a column :param max_height: the maximum height :param default_height_per_bin: the optional default height per row :param max_width: the maximum width :param packing_item_str: the function converting an item id, item index, and item-in-bin index to a string or sequence of strings (of decreasing length) :param importance_to_font_size_func: the function converting importance values to font sizes :param dpi: the dpi value :returns: the Figure object to allow you to add further plot elements """ if isinstance(packing, str): packing = Packing.from_log(packing) if not isinstance(packing, Packing): raise type_error(packing, "packing", (Packing, str)) if not callable(packing_item_str): raise type_error(packing_item_str, "packing_item_str", call=True) # allocate the figure ... this is hacky for now figure, bin_figures = pu.create_figure_with_subplots( items=packing.n_bins, max_items_per_plot=1, max_rows=max_rows, max_cols=max_bins_per_row, min_rows=1, min_cols=1, default_width_per_col=default_width_per_bin, max_width=max_width, default_height_per_row=default_height_per_bin, max_height=max_height, dpi=dpi) # initialize the different plots bin_width: Final[int] = packing.instance.bin_width bin_height: Final[int] = packing.instance.bin_height axes_list: Final[list[Axes]] = [] for the_axes, _, _, _, _, _ in bin_figures: axes = pu.get_axes(the_axes) axes_list.append(axes) axes.set_ylim(0, bin_width) # pylint: disable=E1101 axes.set_ybound(0, bin_height) # pylint: disable=E1101 axes.set_xlim(0, bin_width) # pylint: disable=E1101 axes.set_xbound(0, bin_width) # pylint: disable=E1101 axes.set_aspect("equal", None, "C") # pylint: disable=E1101 axes.tick_params( # pylint: disable=E1101 left=False, bottom=False, labelleft=False, labelbottom=False) # get the color and font styles colors: Final[tuple] = pd.distinct_colors( packing.instance.n_different_items) font_size: Final[float] = importance_to_font_size_func(-1) # get the transforms needed to obtain text dimensions renderers: Final[list] = [pu.get_renderer(axes) for axes in axes_list] inverse: Final[list] = [ axes.transData.inverted() # type: ignore # pylint: disable=E1101 for axes in axes_list] z_order: int = 0 # the z-order of all drawing elements # we now plot the items one-by-one bin_counters: Counter[int] = Counter() for item_index in range(packing.instance.n_items): item_id: int = int(packing[item_index, IDX_ID]) item_bin: int = int(packing[item_index, IDX_BIN]) x_left: int = int(packing[item_index, IDX_LEFT_X]) y_bottom: int = int(packing[item_index, IDX_BOTTOM_Y]) x_right: int = int(packing[item_index, IDX_RIGHT_X]) y_top: int = int(packing[item_index, IDX_TOP_Y]) item_in_bin_index: int = bin_counters[item_bin] + 1 bin_counters[item_bin] = item_in_bin_index width: int = x_right - x_left height: int = y_top - y_bottom background = colors[item_id - 1] foreground = pd.text_color_for_background(colors[item_id - 1]) axes = axes_list[item_bin - 1] rend = renderers[item_bin - 1] inv = inverse[item_bin - 1] axes.add_artist(Rectangle( # paint the item's rectangle xy=(x_left, y_bottom), width=width, height=height, facecolor=background, linewidth=0.75, zorder=z_order, edgecolor="black")) z_order += 1 x_center: float = 0.5 * (x_left + x_right) y_center: float = 0.5 * (y_bottom + y_top) # get the box label string or string sequence strs = packing_item_str(item_id, item_index + 1, item_in_bin_index) if isinstance(strs, str): strs = [strs] elif not isinstance(strs, Iterable): raise type_error( strs, f"packing_item_str({item_id}, {item_index}, " f"{item_in_bin_index})", (str, Iterable)) # iterate over the possible box label strings for i, item_str in enumerate(strs): if not isinstance(item_str, str): raise type_error( str, f"packing_item_str({item_id}, {item_index}, " f"{item_in_bin_index})[{i}]", str) # Get the size of the text using a temporary text that gets immediately # deleted again. tmp: Text = axes.text(x=x_center, y=y_center, s=item_str, fontsize=font_size, color=foreground, horizontalalignment="center", verticalalignment="baseline") bb_bl = inv.transform_bbox(tmp.get_window_extent( renderer=rend)) Artist.set_visible(tmp, False) Artist.remove(tmp) del tmp # Check if this text did fit into the rectangle. if (bb_bl.width < (0.97 * width)) and \ (bb_bl.height < (0.97 * height)): # OK, there is enough space. Let's re-compute the y offset # to do proper alignment using another temporary text. tmp = axes.text(x=x_center, y=y_center, s=item_str, fontsize=font_size, color=foreground, horizontalalignment="center", verticalalignment="bottom") bb_bt = inv.transform_bbox(tmp.get_window_extent( renderer=rend)) Artist.set_visible(tmp, False) Artist.remove(tmp) del tmp # Now we can really print the actual text with a more or less nice vertical # alignment. adj = bb_bl.y0 - bb_bt.y0 if adj < 0: y_center += adj / 3 axes.text(x=x_center, y=y_center, s=item_str, fontsize=font_size, color=foreground, horizontalalignment="center", verticalalignment="center", zorder=z_order) z_order += 1 break # We found a text that fits, so we can quit. return figure