"""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