Coverage for moptipyapps / binpacking2d / plot_packing.py: 93%

85 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 04:40 +0000

1"""Plot a packing into one figure.""" 

2from collections import Counter 

3from typing import Callable, Final, Iterable 

4 

5import moptipy.utils.plot_defaults as pd 

6import moptipy.utils.plot_utils as pu 

7from matplotlib.artist import Artist # type: ignore 

8from matplotlib.axes import Axes # type: ignore 

9from matplotlib.figure import Figure # type: ignore 

10from matplotlib.patches import Rectangle # type: ignore 

11from matplotlib.text import Text # type: ignore 

12from pycommons.types import type_error 

13 

14from moptipyapps.binpacking2d.packing import ( 

15 IDX_BIN, 

16 IDX_BOTTOM_Y, 

17 IDX_ID, 

18 IDX_LEFT_X, 

19 IDX_RIGHT_X, 

20 IDX_TOP_Y, 

21 Packing, 

22) 

23 

24 

25def default_packing_item_str(item_id: int, item_index: int, 

26 item_in_bin_index: int) -> Iterable[str]: 

27 """ 

28 Get a packing item string(s). 

29 

30 The default idea is to include the item id, the index of the item in the 

31 bin, and the overall index of the item. If the space is insufficient, 

32 we remove the latter or the latter two. Hence, this function returns a 

33 tuple of three strings. 

34 

35 :param item_id: the ID of the packing item 

36 :param item_index: the item index 

37 :param item_in_bin_index: the index of the item in its bin 

38 :return: the string 

39 """ 

40 return (f"{item_id}/{item_in_bin_index}/{item_index}", 

41 f"{item_id}/{item_in_bin_index}", str(item_id)) 

42 

43 

44def plot_packing(packing: Packing | str, 

45 max_rows: int = 3, 

46 max_bins_per_row: int = 3, 

47 default_width_per_bin: float | int | None = 8.6, 

48 max_width: float | int | None = 8.6, 

49 default_height_per_bin: float | int | None = 

50 5.315092303249095, 

51 max_height: float | int | None = 9, 

52 packing_item_str: Callable[ 

53 [int, int, int], str | Iterable[str]] = 

54 default_packing_item_str, 

55 importance_to_font_size_func: Callable[[int], float] = 

56 pd.importance_to_font_size, 

57 dpi: float | int | None = 384.0) -> Figure: 

58 """ 

59 Plot a packing. 

60 

61 Each item is drawn in a different color. Each item rectangle includes, if 

62 there is enough space, the item-ID. If there is more space, also the index 

63 of the item inside the bin (starting at 1) is included. If there is yet 

64 more space, even the overall index of the item is included. 

65 

66 :param packing: the packing or the file to load it from 

67 :param max_rows: the maximum number of rows 

68 :param max_bins_per_row: the maximum number of bins per row 

69 :param default_width_per_bin: the optional default width of a column 

70 :param max_height: the maximum height 

71 :param default_height_per_bin: the optional default height per row 

72 :param max_width: the maximum width 

73 :param packing_item_str: the function converting an item id, 

74 item index, and item-in-bin index to a string or sequence of strings 

75 (of decreasing length) 

76 :param importance_to_font_size_func: the function converting 

77 importance values to font sizes 

78 :param dpi: the dpi value 

79 :returns: the Figure object to allow you to add further plot elements 

80 """ 

81 if isinstance(packing, str): 

82 packing = Packing.from_log(packing) 

83 if not isinstance(packing, Packing): 

84 raise type_error(packing, "packing", (Packing, str)) 

85 if not callable(packing_item_str): 

86 raise type_error(packing_item_str, "packing_item_str", call=True) 

87 

88 # allocate the figure ... this is hacky for now 

89 figure, bin_figures = pu.create_figure_with_subplots( 

90 items=packing.n_bins, max_items_per_plot=1, max_rows=max_rows, 

91 max_cols=max_bins_per_row, min_rows=1, min_cols=1, 

92 default_width_per_col=default_width_per_bin, max_width=max_width, 

93 default_height_per_row=default_height_per_bin, max_height=max_height, 

94 dpi=dpi) 

95 

96 # initialize the different plots 

97 bin_width: Final[int] = packing.instance.bin_width 

98 bin_height: Final[int] = packing.instance.bin_height 

99 axes_list: Final[list[Axes]] = [] 

100 for the_axes, _, _, _, _, _ in bin_figures: 

101 axes = pu.get_axes(the_axes) 

102 axes_list.append(axes) 

103 axes.set_ylim(0, bin_width) # pylint: disable=E1101 

104 axes.set_ybound(0, bin_height) # pylint: disable=E1101 

105 axes.set_xlim(0, bin_width) # pylint: disable=E1101 

106 axes.set_xbound(0, bin_width) # pylint: disable=E1101 

107 axes.set_aspect("equal", None, "C") # pylint: disable=E1101 

108 axes.tick_params( # pylint: disable=E1101 

109 left=False, bottom=False, labelleft=False, labelbottom=False) 

110 

111 # get the color and font styles 

112 colors: Final[tuple] = pd.distinct_colors( 

113 packing.instance.n_different_items) 

114 font_size: Final[float] = importance_to_font_size_func(-1) 

115 

116 # get the transforms needed to obtain text dimensions 

117 renderers: Final[list] = [pu.get_renderer(axes) for axes in axes_list] 

118 inverse: Final[list] = [ 

119 axes.transData.inverted() # type: ignore # pylint: disable=E1101 

120 for axes in axes_list] 

121 

122 z_order: int = 0 # the z-order of all drawing elements 

123 

124 # we now plot the items one-by-one 

125 bin_counters: Counter[int] = Counter() 

126 for item_index in range(packing.instance.n_items): 

127 item_id: int = int(packing[item_index, IDX_ID]) 

128 item_bin: int = int(packing[item_index, IDX_BIN]) 

129 x_left: int = int(packing[item_index, IDX_LEFT_X]) 

130 y_bottom: int = int(packing[item_index, IDX_BOTTOM_Y]) 

131 x_right: int = int(packing[item_index, IDX_RIGHT_X]) 

132 y_top: int = int(packing[item_index, IDX_TOP_Y]) 

133 item_in_bin_index: int = bin_counters[item_bin] + 1 

134 bin_counters[item_bin] = item_in_bin_index 

135 width: int = x_right - x_left 

136 height: int = y_top - y_bottom 

137 

138 background = colors[item_id - 1] 

139 foreground = pd.text_color_for_background(colors[item_id - 1]) 

140 

141 axes = axes_list[item_bin - 1] 

142 rend = renderers[item_bin - 1] 

143 inv = inverse[item_bin - 1] 

144 

145 axes.add_artist(Rectangle( # paint the item's rectangle 

146 xy=(x_left, y_bottom), width=width, height=height, 

147 facecolor=background, linewidth=0.75, zorder=z_order, 

148 edgecolor="black")) 

149 z_order += 1 

150 x_center: float = 0.5 * (x_left + x_right) 

151 y_center: float = 0.5 * (y_bottom + y_top) 

152 

153# get the box label string or string sequence 

154 strs = packing_item_str(item_id, item_index + 1, item_in_bin_index) 

155 if isinstance(strs, str): 

156 strs = [strs] 

157 elif not isinstance(strs, Iterable): 

158 raise type_error( 

159 strs, f"packing_item_str({item_id}, {item_index}, " 

160 f"{item_in_bin_index})", (str, Iterable)) 

161 

162# iterate over the possible box label strings 

163 for i, item_str in enumerate(strs): 

164 if not isinstance(item_str, str): 

165 raise type_error( 

166 str, f"packing_item_str({item_id}, {item_index}, " 

167 f"{item_in_bin_index})[{i}]", str) 

168# Get the size of the text using a temporary text that gets immediately 

169# deleted again. 

170 tmp: Text = axes.text(x=x_center, y=y_center, s=item_str, 

171 fontsize=font_size, 

172 color=foreground, 

173 horizontalalignment="center", 

174 verticalalignment="baseline") 

175 bb_bl = inv.transform_bbox(tmp.get_window_extent( 

176 renderer=rend)) 

177 Artist.set_visible(tmp, False) 

178 Artist.remove(tmp) 

179 del tmp 

180 

181# Check if this text did fit into the rectangle. 

182 if (bb_bl.width < (0.97 * width)) and \ 

183 (bb_bl.height < (0.97 * height)): 

184 # OK, there is enough space. Let's re-compute the y offset 

185 # to do proper alignment using another temporary text. 

186 tmp = axes.text(x=x_center, y=y_center, s=item_str, 

187 fontsize=font_size, 

188 color=foreground, 

189 horizontalalignment="center", 

190 verticalalignment="bottom") 

191 bb_bt = inv.transform_bbox(tmp.get_window_extent( 

192 renderer=rend)) 

193 Artist.set_visible(tmp, False) 

194 Artist.remove(tmp) 

195 del tmp 

196 

197# Now we can really print the actual text with a more or less nice vertical 

198# alignment. 

199 adj = bb_bl.y0 - bb_bt.y0 

200 if adj < 0: 

201 y_center += adj / 3 

202 

203 axes.text(x=x_center, y=y_center, s=item_str, 

204 fontsize=font_size, 

205 color=foreground, 

206 horizontalalignment="center", 

207 verticalalignment="center", 

208 zorder=z_order) 

209 z_order += 1 

210 break # We found a text that fits, so we can quit. 

211 return figure