Хорошо, поэтому я последовал подсказке, указанной @ Johan C в комментариях, что вы могли бы использовать fig.tight_layout()
, чтобы «обмануть» фигуру и установить текст offset_text
художника без необходимости рисовать фигура. Исполнитель offset_text
используется методом ax.ticklabel_format()
для отображения порядка величины (опять же, как указано @ Johan C в комментариях). Этот трюк объясняется в этом посте , который похож на мой и кажется достаточно справедливым решением для большинства случаев. Однако что, если вы не хотите использовать tight_layout
или, что еще хуже, вместо этого вы используете несовместимый constrained_layout
(например, я)?
Резюме:
Так я и сделал много копался в исходном коде matplotlib после трассировки tight_layout
, и, к счастью, мне это удалось. Короче говоря, универсальное решение этой проблемы - вызвать ax.get_tightbbox(renderer)
, где renderer
- средство визуализации фигуры. Это также должно быть дешевле. Следующий MWE показывает, что это работает даже при использовании constrained_layout
:
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.tight_layout import get_renderer
from matplotlib.backends.backend_qt5agg import \
FigureCanvasQTAgg as FigureCanvas
# from matplotlib.transforms import Bbox
# from mpl_toolkits.axes_grid1 import make_axes_locatable
from PyQt5.QtWidgets import QDialog, QApplication, QGridLayout
class MainWindow(QDialog):
def __init__(self):
super().__init__()
fig, ax = plt.subplots(constrained_layout=True)
canvas = FigureCanvas(fig)
lay = QGridLayout(self)
lay.addWidget(canvas)
self.setLayout(lay)
image = np.random.uniform(10000000, 100000000, (100, 100))
image_artist = ax.imshow(image)
colorbar = fig.colorbar(image_artist)
colorbar.ax.ticklabel_format()
renderer = get_renderer(fig)
colorbar.ax.get_tightbbox(renderer)
colorbar.ax.yaxis.offsetText.set_visible(False)
offset_text = colorbar.ax.yaxis.get_offset_text()
exp = offset_text.get_text().split('e')[1].replace('+', '')
colorbar.ax.set_ylabel(rf'Parameter [U${{\times}}10^{{{exp}}}$]')
canvas.draw_idle()
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
Пошаговое объяснение:
Вот что я сделал:
Я посмотрел tight_layout
исходный код . Путем исключения я понял, что важным моментом для работы этого трюка было следующее утверждение:
kwargs = get_tight_layout_figure(
self, self.axes, subplotspec_list, renderer,
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
, что здорово, потому что я также понял, что утверждение, которое «по существу» делает tight_layout
несовместим с constrained_layout
- это вызов subplots_adjust(**kwargs)
.
- Затем я посмотрел на
get_tight_layout_figure
исходный код . Чтобы это повлияло на colorbar
, вам нужно использовать обходной путь, поскольку по умолчанию colorbar
добавляется через экземпляр basi c Axes
, а не через экземпляр AxesSubplot
. Это важное различие, потому что для get_tight_layout_figure
требуется subplotspec_list
, который, в свою очередь, генерируется как get_subplotspec_list
. Последний возвращает None
в случае colorbar.ax
, потому что, хотя экземпляр AxesSubplot
поставляется с locator
, обычный Axes
- нет. locator
- это то, что используется в get_subplotspec_list
для возврата subplotspec
. Обходной путь состоит в том, чтобы использовать подход, описанный внизу здесь , сделав доступными оси цветовой шкалы: from mpl_toolkits.axes_grid1 import make_axes_locatable
arr = np.arange(100).reshape((10, 10))
fig = plt.figure(figsize=(4, 4))
im = plt.imshow(arr, interpolation="none")
divider = make_axes_locatable(plt.gca())
cax = divider.append_axes("right", "5%", pad="3%")
plt.colorbar(im, cax=cax)
plt.tight_layout()
- С этим я смог запустить
get_tight_layout_figure
на моем colorbar.ax
: from matplotlib.tight_layout import get_renderer, get_tight_layout_figure
renderer = get_renderer(fig)
gridspec = colorbar.ax.get_axes_locator().get_subplotspec()
get_tight_layout_figure(fig, [colorbar.ax], [gridspec], renderer)
Опять же, путем исключения я понял, что важным утверждением в get_tight_layout_figure
для работы трюка было следующее выражение:
kwargs = auto_adjust_subplotpars(fig, renderer,
nrows_ncols=(max_nrows, max_ncols),
num1num2_list=num1num2_list,
subplot_list=subplot_list,
ax_bbox_list=ax_bbox_list,
pad=pad, h_pad=h_pad, w_pad=w_pad)
Это снова упростило задачу, потому что для этой функции вам нужны только fig
и renderer
, а также nrows_ncols
, num1num2_list
и subplot_list
. К счастью, последние три аргумента достаточно легко получить / смоделировать, где nrows_ncols
и num1num2_list
- это списки чисел, в этом простом случае (1, 1)
и [(0, 0)]
соответственно, а subplot_list
содержит только colorbar.ax
. Более того, описанный выше обходной путь на самом деле не работает с constrained_laout
, поскольку часть осей шкалы палитры (в частности, метка, о которой идет речь) может быть отрезана:
Итак, как вы уже догадались, я просмотрел исходный код auto_adjust_subplotpars
. И снова, путем исключения, я на этот раз быстро нашел соответствующую строку кода:
tight_bbox_raw = union([ax.get_tightbbox(renderer) for ax in subplots
if ax.get_visible()])
Важная часть здесь, конечно же, ax.get_tightbbox(renderer)
, как вы можете сказать по моему решению . Это насколько я мог его проследить, хотя я считаю, что go должно быть возможно даже немного дальше. На самом деле было не так просто найти соответствующий исходный код для метода get_tightbbox
, потому что, хотя код предполагает, что вызывается Axes.get_tightbbox
, который, по крайней мере, также можно найти в документах (хотя ссылки на исходный код нет), то, что на самом деле используется, это Artist.get_tightbbox
, из которых по какой-то причине нет документации , однако он существует в исходном коде . Я извлек его и сделал свою собственную «отдельную» версию, чтобы посмотреть, могу ли я go еще глубже:
from matplotlib.transforms import Bbox
def get_tightbbox(artist, renderer):
"""
Like `Artist.get_window_extent`, but includes any clipping.
Parameters
----------
renderer : `.RendererBase` instance
renderer that will be used to draw the figures (i.e.
``fig.canvas.get_renderer()``)
Returns
-------
bbox : `.BBox`
The enclosing bounding box (in figure pixel co-ordinates).
"""
bbox = artist.get_window_extent(renderer)
if artist.get_clip_on():
clip_box = artist.get_clip_box()
if clip_box is not None:
bbox = Bbox.intersection(bbox, clip_box)
clip_path = artist.get_clip_path()
if clip_path is not None and bbox is not None:
clip_path = clip_path.get_fully_transformed_path()
bbox = Bbox.intersection(bbox, clip_path.get_extents())
return bbox
Но здесь произошло нечто очень любопытное, которое я не могу объяснить, и в конечном итоге остановило меня от дальнейшего расследования:
бег get_tightbbox(colorbar.ax, renderer)
это не так же, как запущено colorbar.ax.get_tightbbox(renderer)
!
Понятия не имею почему. Запуск get_tightbbox(colorbar.ax, renderer)
, get_tightbbox
выполняется только один раз (как вы могли предположить), но запуск colorbar.ax.get_tightbbox(renderer)
выполняется несколько раз для группы, но не для всех дочерних элементов colorbar.ax
. Я пытался подражать этому, но перебирал детей и запускал get_tightbbox
индивидуально для каждого (в частности, я тестировал это на художнике offset_text
, конечно), но это не дало того же эффекта. Это не работает. Итак, на данный момент colorbar.ax.get_tightbbox(renderer)
- это путь к go.