Coverage for /usr/lib/python3/dist-packages/matplotlib/layout_engine.py: 68%

69 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2Classes to layout elements in a `.Figure`. 

3 

4Figures have a ``layout_engine`` property that holds a subclass of 

5`~.LayoutEngine` defined here (or *None* for no layout). At draw time 

6``figure.get_layout_engine().execute()`` is called, the goal of which is 

7usually to rearrange Axes on the figure to produce a pleasing layout. This is 

8like a ``draw`` callback but with two differences. First, when printing we 

9disable the layout engine for the final draw. Second, it is useful to know the 

10layout engine while the figure is being created. In particular, colorbars are 

11made differently with different layout engines (for historical reasons). 

12 

13Matplotlib supplies two layout engines, `.TightLayoutEngine` and 

14`.ConstrainedLayoutEngine`. Third parties can create their own layout engine 

15by subclassing `.LayoutEngine`. 

16""" 

17 

18from contextlib import nullcontext 

19 

20import matplotlib as mpl 

21 

22from matplotlib._constrained_layout import do_constrained_layout 

23from matplotlib._tight_layout import (get_subplotspec_list, 

24 get_tight_layout_figure) 

25 

26 

27class LayoutEngine: 

28 """ 

29 Base class for Matplotlib layout engines. 

30 

31 A layout engine can be passed to a figure at instantiation or at any time 

32 with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the 

33 layout engine ``execute`` function is called at draw time by 

34 `~.figure.Figure.draw`, providing a special draw-time hook. 

35 

36 .. note:: 

37 

38 However, note that layout engines affect the creation of colorbars, so 

39 `~.figure.Figure.set_layout_engine` should be called before any 

40 colorbars are created. 

41 

42 Currently, there are two properties of `LayoutEngine` classes that are 

43 consulted while manipulating the figure: 

44 

45 - ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the 

46 axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or 

47 not (see `.colorbar.make_axes`); 

48 - ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being 

49 run if it is not compatible with the layout engine. 

50 

51 To implement a custom `LayoutEngine`: 

52 

53 1. override ``_adjust_compatible`` and ``_colorbar_gridspec`` 

54 2. override `LayoutEngine.set` to update *self._params* 

55 3. override `LayoutEngine.execute` with your implementation 

56 

57 """ 

58 # override these in subclass 

59 _adjust_compatible = None 

60 _colorbar_gridspec = None 

61 

62 def __init__(self, **kwargs): 

63 super().__init__(**kwargs) 

64 self._params = {} 

65 

66 def set(self, **kwargs): 

67 raise NotImplementedError 

68 

69 @property 

70 def colorbar_gridspec(self): 

71 """ 

72 Return a boolean if the layout engine creates colorbars using a 

73 gridspec. 

74 """ 

75 if self._colorbar_gridspec is None: 

76 raise NotImplementedError 

77 return self._colorbar_gridspec 

78 

79 @property 

80 def adjust_compatible(self): 

81 """ 

82 Return a boolean if the layout engine is compatible with 

83 `~.Figure.subplots_adjust`. 

84 """ 

85 if self._adjust_compatible is None: 

86 raise NotImplementedError 

87 return self._adjust_compatible 

88 

89 def get(self): 

90 """ 

91 Return copy of the parameters for the layout engine. 

92 """ 

93 return dict(self._params) 

94 

95 def execute(self, fig): 

96 """ 

97 Execute the layout on the figure given by *fig*. 

98 """ 

99 # subclasses must implement this. 

100 raise NotImplementedError 

101 

102 

103class PlaceHolderLayoutEngine(LayoutEngine): 

104 """ 

105 This layout engine does not adjust the figure layout at all. 

106 

107 The purpose of this `.LayoutEngine` is to act as a placeholder when the 

108 user removes a layout engine to ensure an incompatible `.LayoutEngine` can 

109 not be set later. 

110 

111 Parameters 

112 ---------- 

113 adjust_compatible, colorbar_gridspec : bool 

114 Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever 

115 layout engine it is replacing. 

116 

117 """ 

118 def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs): 

119 self._adjust_compatible = adjust_compatible 

120 self._colorbar_gridspec = colorbar_gridspec 

121 super().__init__(**kwargs) 

122 

123 def execute(self, fig): 

124 return 

125 

126 

127class TightLayoutEngine(LayoutEngine): 

128 """ 

129 Implements the ``tight_layout`` geometry management. See 

130 :doc:`/tutorials/intermediate/tight_layout_guide` for details. 

131 """ 

132 _adjust_compatible = True 

133 _colorbar_gridspec = True 

134 

135 def __init__(self, *, pad=1.08, h_pad=None, w_pad=None, 

136 rect=(0, 0, 1, 1), **kwargs): 

137 """ 

138 Initialize tight_layout engine. 

139 

140 Parameters 

141 ---------- 

142 pad : float, 1.08 

143 Padding between the figure edge and the edges of subplots, as a 

144 fraction of the font size. 

145 h_pad, w_pad : float 

146 Padding (height/width) between edges of adjacent subplots. 

147 Defaults to *pad*. 

148 rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1). 

149 rectangle in normalized figure coordinates that the subplots 

150 (including labels) will fit into. 

151 """ 

152 super().__init__(**kwargs) 

153 for td in ['pad', 'h_pad', 'w_pad', 'rect']: 

154 # initialize these in case None is passed in above: 

155 self._params[td] = None 

156 self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) 

157 

158 def execute(self, fig): 

159 """ 

160 Execute tight_layout. 

161 

162 This decides the subplot parameters given the padding that 

163 will allow the axes labels to not be covered by other labels 

164 and axes. 

165 

166 Parameters 

167 ---------- 

168 fig : `.Figure` to perform layout on. 

169 

170 See also: `.figure.Figure.tight_layout` and `.pyplot.tight_layout`. 

171 """ 

172 info = self._params 

173 renderer = fig._get_renderer() 

174 with getattr(renderer, "_draw_disabled", nullcontext)(): 

175 kwargs = get_tight_layout_figure( 

176 fig, fig.axes, get_subplotspec_list(fig.axes), renderer, 

177 pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'], 

178 rect=info['rect']) 

179 if kwargs: 

180 fig.subplots_adjust(**kwargs) 

181 

182 def set(self, *, pad=None, w_pad=None, h_pad=None, rect=None): 

183 for td in self.set.__kwdefaults__: 

184 if locals()[td] is not None: 

185 self._params[td] = locals()[td] 

186 

187 

188class ConstrainedLayoutEngine(LayoutEngine): 

189 """ 

190 Implements the ``constrained_layout`` geometry management. See 

191 :doc:`/tutorials/intermediate/constrainedlayout_guide` for details. 

192 """ 

193 

194 _adjust_compatible = False 

195 _colorbar_gridspec = False 

196 

197 def __init__(self, *, h_pad=None, w_pad=None, 

198 hspace=None, wspace=None, rect=(0, 0, 1, 1), 

199 compress=False, **kwargs): 

200 """ 

201 Initialize ``constrained_layout`` settings. 

202 

203 Parameters 

204 ---------- 

205 h_pad, w_pad : float 

206 Padding around the axes elements in figure-normalized units. 

207 Default to :rc:`figure.constrained_layout.h_pad` and 

208 :rc:`figure.constrained_layout.w_pad`. 

209 hspace, wspace : float 

210 Fraction of the figure to dedicate to space between the 

211 axes. These are evenly spread between the gaps between the axes. 

212 A value of 0.2 for a three-column layout would have a space 

213 of 0.1 of the figure width between each column. 

214 If h/wspace < h/w_pad, then the pads are used instead. 

215 Default to :rc:`figure.constrained_layout.hspace` and 

216 :rc:`figure.constrained_layout.wspace`. 

217 rect : tuple of 4 floats 

218 Rectangle in figure coordinates to perform constrained layout in 

219 (left, bottom, width, height), each from 0-1. 

220 compress : bool 

221 Whether to shift Axes so that white space in between them is 

222 removed. This is useful for simple grids of fixed-aspect Axes (e.g. 

223 a grid of images). See :ref:`compressed_layout`. 

224 """ 

225 super().__init__(**kwargs) 

226 # set the defaults: 

227 self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'], 

228 h_pad=mpl.rcParams['figure.constrained_layout.h_pad'], 

229 wspace=mpl.rcParams['figure.constrained_layout.wspace'], 

230 hspace=mpl.rcParams['figure.constrained_layout.hspace'], 

231 rect=(0, 0, 1, 1)) 

232 # set anything that was passed in (None will be ignored): 

233 self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace, 

234 rect=rect) 

235 self._compress = compress 

236 

237 def execute(self, fig): 

238 """ 

239 Perform constrained_layout and move and resize axes accordingly. 

240 

241 Parameters 

242 ---------- 

243 fig : `.Figure` to perform layout on. 

244 """ 

245 width, height = fig.get_size_inches() 

246 # pads are relative to the current state of the figure... 

247 w_pad = self._params['w_pad'] / width 

248 h_pad = self._params['h_pad'] / height 

249 

250 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad, 

251 wspace=self._params['wspace'], 

252 hspace=self._params['hspace'], 

253 rect=self._params['rect'], 

254 compress=self._compress) 

255 

256 def set(self, *, h_pad=None, w_pad=None, 

257 hspace=None, wspace=None, rect=None): 

258 """ 

259 Set the pads for constrained_layout. 

260 

261 Parameters 

262 ---------- 

263 h_pad, w_pad : float 

264 Padding around the axes elements in figure-normalized units. 

265 Default to :rc:`figure.constrained_layout.h_pad` and 

266 :rc:`figure.constrained_layout.w_pad`. 

267 hspace, wspace : float 

268 Fraction of the figure to dedicate to space between the 

269 axes. These are evenly spread between the gaps between the axes. 

270 A value of 0.2 for a three-column layout would have a space 

271 of 0.1 of the figure width between each column. 

272 If h/wspace < h/w_pad, then the pads are used instead. 

273 Default to :rc:`figure.constrained_layout.hspace` and 

274 :rc:`figure.constrained_layout.wspace`. 

275 rect : tuple of 4 floats 

276 Rectangle in figure coordinates to perform constrained layout in 

277 (left, bottom, width, height), each from 0-1. 

278 """ 

279 for td in self.set.__kwdefaults__: 

280 if locals()[td] is not None: 

281 self._params[td] = locals()[td]