python - graficas - Cómo obtener una figura de Matplotlib para desplazarse+cambiar el tamaño correctamente en una GUI de Tkinter
matplotlib tkinter (2)
Tengo una GUI de Tkinter que muestra un gráfico de Matplotlib (Python 2.7.3 con Matplotlib 1.2.0rc2) y permite al usuario configurar ciertos aspectos de la gráfica. Las tramas tienden a agrandarse, por lo que la figura está envuelta en un lienzo de desplazamiento. Un aspecto de la configuración de la trama es cambiar su tamaño.
Ahora bien, mientras la trama se desplaza correctamente, por una parte, y el redimensionamiento también funciona en la otra, las dos operaciones no funcionan en combinación. A continuación hay una secuencia de comandos para demostrar el efecto. (Perdón por la longitud, no pude obtenerla más corta). Puede desplazarse por la gráfica (usando las barras de desplazamiento), y puede hacerse cada vez más pequeña (usando los botones). Sin embargo, cada vez que se desplaza, la figura se restablece a su tamaño original . Evidentemente, me gustaría que el tamaño de la figura no cambie con el uso de las barras de desplazamiento.
import math
from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
import Tkconstants
from matplotlib import pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
def addScrollingFigure(figure, frame):
# set up a canvas with scrollbars
canvas = Canvas(frame)
canvas.grid(row=0, column=0, sticky=Tkconstants.NSEW)
xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
yScrollbar = Scrollbar(frame)
xScrollbar.grid(row=1, column=0, sticky=Tkconstants.EW)
yScrollbar.grid(row=0, column=1, sticky=Tkconstants.NS)
canvas.config(xscrollcommand=xScrollbar.set)
xScrollbar.config(command=canvas.xview)
canvas.config(yscrollcommand=yScrollbar.set)
yScrollbar.config(command=canvas.yview)
# plug in the figure
figAgg = FigureCanvasTkAgg(figure, canvas)
mplCanvas = figAgg.get_tk_widget()
mplCanvas.grid(sticky=Tkconstants.NSEW)
# and connect figure with scrolling region
canvas.create_window(0, 0, window=mplCanvas)
canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL))
def changeSize(figure, factor):
oldSize = figure.get_size_inches()
print "old size is", oldSize
figure.set_size_inches([factor * s for s in oldSize])
print "new size is", figure.get_size_inches()
print
figure.canvas.draw()
if __name__ == "__main__":
root = Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
frame = Frame(root)
frame.grid(column=0, row=0, sticky=Tkconstants.NSEW)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
figure = plt.figure(dpi=150, figsize=(4, 4))
plt.plot(xrange(10), [math.sin(x) for x in xrange(10)])
addScrollingFigure(figure, frame)
buttonFrame = Frame(root)
buttonFrame.grid(row=0, column=1, sticky=Tkconstants.NS)
biggerButton = Button(buttonFrame, text="larger",
command=lambda : changeSize(figure, 1.5))
biggerButton.grid(column=0, row=0)
smallerButton = Button(buttonFrame, text="smaller",
command=lambda : changeSize(figure, .5))
smallerButton.grid(column=0, row=1)
root.mainloop()
Creo que me falta algo acerca de cómo la trama y el lienzo de desplazamiento están unidos; He intentado canvas.create_window(...)
configurar el lienzo de desplazamiento (con canvas.create_window(...)
y canvas.config(...)
) después de cada llamada changeSize
, pero eso no ayudó. Una de las alternativas que obtuve para trabajar fue regenerar toda la configuración (figura, lienzo, barras de desplazamiento) después de cada cambio de tamaño. (Sin embargo, aparte de parecer un poco brutal, tenía el problema de que no podía deshacerse de las figuras antiguas de forma adecuada, lo que hacía que el programa acumulara bastante memoria con el tiempo).
Entonces, ¿alguien tiene alguna idea sobre cómo lograr que esas barras de desplazamiento se comporten correctamente después de las operaciones de cambio de tamaño?
Acabo de tropezar con el mismo problema, y por lo que puedo ver (por experimentación), más allá de figure.set_size_inches()
, también debes establecer el nuevo tamaño de mplCanvas
y de la ventana creada con canvas para él, antes de hacer figure.canvas.draw()
(que a su vez también obliga a uno a usar variables globales o definiciones de clase). Además, no es necesario "reticular" mplCanvas
parecer, ya que ya es un elemento secundario del canvas
, que ya está "cuadriculado". Y probablemente desee anclar NW, por lo que en cada cambio de tamaño, la trama se vuelve a dibujar en 0,0 en la esquina superior izquierda.
Esto es lo que funcionó para mí (también probé con el marco "interno" como en la barra de desplazamiento de Python Tkinter para el marco , pero eso no funcionó, algo de eso queda al final del fragmento):
import math
import sys
if sys.version_info[0] < 3:
from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
import Tkconstants
else:
from tkinter import Tk, Button, Frame, Canvas, Scrollbar
import tkinter.constants as Tkconstants
from matplotlib import pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import pprint
frame = None
canvas = None
def printBboxes(label=""):
global canvas, mplCanvas, interior, interior_id, cwid
print(" "+label,
"canvas.bbox:", canvas.bbox(Tkconstants.ALL),
"mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL))
def addScrollingFigure(figure, frame):
global canvas, mplCanvas, interior, interior_id, cwid
# set up a canvas with scrollbars
canvas = Canvas(frame)
canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW)
xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
yScrollbar = Scrollbar(frame)
xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW)
yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS)
canvas.config(xscrollcommand=xScrollbar.set)
xScrollbar.config(command=canvas.xview)
canvas.config(yscrollcommand=yScrollbar.set)
yScrollbar.config(command=canvas.yview)
# plug in the figure
figAgg = FigureCanvasTkAgg(figure, canvas)
mplCanvas = figAgg.get_tk_widget()
#mplCanvas.grid(sticky=Tkconstants.NSEW)
# and connect figure with scrolling region
cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW)
printBboxes("Init")
canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
def changeSize(figure, factor):
global canvas, mplCanvas, interior, interior_id, frame, cwid
oldSize = figure.get_size_inches()
print("old size is", oldSize)
figure.set_size_inches([factor * s for s in oldSize])
wi,hi = [i*figure.dpi for i in figure.get_size_inches()]
print("new size is", figure.get_size_inches())
print("new size pixels: ", wi,hi)
mplCanvas.config(width=wi, height=hi) ; printBboxes("A")
#mplCanvas.grid(sticky=Tkconstants.NSEW)
canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B")
canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
figure.canvas.draw() ; printBboxes("C")
print()
if __name__ == "__main__":
root = Tk()
root.rowconfigure(1, weight=1)
root.columnconfigure(1, weight=1)
frame = Frame(root)
frame.grid(column=1, row=1, sticky=Tkconstants.NSEW)
frame.rowconfigure(1, weight=1)
frame.columnconfigure(1, weight=1)
figure = plt.figure(dpi=150, figsize=(4, 4))
plt.plot(range(10), [math.sin(x) for x in range(10)])
addScrollingFigure(figure, frame)
buttonFrame = Frame(root)
buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS)
biggerButton = Button(buttonFrame, text="larger",
command=lambda : changeSize(figure, 1.5))
biggerButton.grid(column=1, row=1)
smallerButton = Button(buttonFrame, text="smaller",
command=lambda : changeSize(figure, .5))
smallerButton.grid(column=1, row=2)
root.mainloop()
"""
interior = Frame(canvas) #Frame(mplCanvas) #cannot
interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW)
canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200)
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW)
canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200)
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
"""
Una nota interesante es que mplCanvas
obedecerá el tamaño si aumenta de tamaño (como al hacer clic en "más grande"), pero conserva el tamaño anterior si se reduce:
$ python2.7 test.py
('' Init'', ''canvas.bbox:'', (0, 0, 610, 610), ''mplCanvas.bbox:'', (0, 0, 600, 600))
## here click "larger":
(''old size is'', array([ 4.06666667, 4.06666667]))
(''new size is'', array([ 6.1, 6.1]))
(''new size pixels: '', 915.0, 915.0)
('' A'', ''canvas.bbox:'', (0, 0, 925, 925), ''mplCanvas.bbox:'', (0, 0, 926, 926))
('' B'', ''canvas.bbox:'', (0, 0, 915, 915), ''mplCanvas.bbox:'', (0, 0, 926, 926))
('' C'', ''canvas.bbox:'', (0, 0, 915, 915), ''mplCanvas.bbox:'', (0, 0, 926, 926))
()
## here click "larger":
(''old size is'', array([ 6.1, 6.1]))
(''new size is'', array([ 9.15, 9.15]))
(''new size pixels: '', 1372.4999999999998, 1372.4999999999998)
('' A'', ''canvas.bbox:'', (0, 0, 915, 915), ''mplCanvas.bbox:'', (0, 0, 926, 926))
('' B'', ''canvas.bbox:'', (0, 0, 1372, 1372), ''mplCanvas.bbox:'', (0, 0, 926, 926))
('' C'', ''canvas.bbox:'', (0, 0, 1372, 1372), ''mplCanvas.bbox:'', (0, 0, 1372, 1372))
()
## here click "smaller":
(''old size is'', array([ 9.14666667, 9.14666667]))
(''new size is'', array([ 4.57333333, 4.57333333]))
(''new size pixels: '', 686.0, 686.0)
('' A'', ''canvas.bbox:'', (0, 0, 1372, 1372), ''mplCanvas.bbox:'', (0, 0, 1372, 1372))
('' B'', ''canvas.bbox:'', (0, 0, 686, 686), ''mplCanvas.bbox:'', (0, 0, 1372, 1372))
('' C'', ''canvas.bbox:'', (0, 0, 686, 686), ''mplCanvas.bbox:'', (0, 0, 1372, 1372))
()
El mismo comportamiento de mplCanvas
se puede ver en Python3.2, también ... no estoy seguro si esto es una especie de error, o yo tampoco estoy entendiendo algo bien :)
Tenga en cuenta también que este escalado de esta forma no maneja el cambio de tamaño de fuentes de ejes / tics, etc. (las fuentes intentarán mantener el mismo tamaño); esto es lo que eventualmente puedo obtener con el código anterior (tics truncados):
... y es aún peor si agrega etiquetas de ejes, etc.
De todos modos, espero que esto ayude,
¡Aclamaciones!
Derecha; después de la discusión de la barra de desplazamiento en esta respuesta , terminé pasando por esto:
- ¿Cómo configuro el título de la figura y el tamaño de la fuente de las etiquetas de los ejes en Matplotlib?
- Cómo cambiar el tamaño de fuente en un diagrama matplotlib
- Las subtramas de Python dejan espacio para las etiquetas de ejes comunes
- Tamaño exacto de la figura en matplotlib con título, etiquetas de ejes
- Subplots Matplotlib_adjust hspace para títulos y xlabels no se superponen?
.. y creo que logré obtener una especie de código de escala que también escala (algo) las etiquetas y el relleno, por lo que (aproximadamente) toda la trama encaja dentro (nota, la segunda imagen usa la escala "media" de imgur):
Para tamaños muy pequeños, las etiquetas comienzan a desaparecer de nuevo, pero aún así es válida para una variedad de tamaños.
Tenga en cuenta que para matplotlib
más matplotlib
(> = 1.1.1), hay una función figure.tight_layout()
que hace los márgenes (pero no el tamaño de fuente) para casos como este (es una subparcela única), pero si está usando un matplotlib
más matplotlib
, puedes hacer figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86)
que es lo que hace este ejemplo; y ha sido probado en:
$ python2.7 -c ''import matplotlib; print(matplotlib.__version__)''
0.99.3
$ python3.2 -c ''import matplotlib; print(matplotlib.__version__)''
1.2.0
( tight_layout
ver si puedo copiar tight_layout
para tight_layout
antiguo; desafortunadamente, requiere un conjunto bastante complejo de funciones incluidas de tight_layout.py , que a su vez requiere que Figure y Axes también tengan especificaciones específicas, no presentes en v.0.99 )
Como subplots_adjust
toma parámetros relativos (de 0.0 a 1.0), en principio podemos simplemente configurarlos una vez, y esperamos que sean válidos para nuestro rango de escala deseado. Para el resto (escalado de fuentes y etiqueta), consulte el siguiente código:
import math
import sys
if sys.version_info[0] < 3:
from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
import Tkconstants
else:
from tkinter import Tk, Button, Frame, Canvas, Scrollbar
import tkinter.constants as Tkconstants
import matplotlib
from matplotlib import pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import pprint, inspect
frame = None
canvas = None
ax = None
def printBboxes(label=""):
global canvas, mplCanvas, interior, interior_id, cwid, figure
print(" "+label,
"canvas.bbox:", canvas.bbox(Tkconstants.ALL),
"mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL),
"subplotpars:", figure.subplotpars.__dict__ )
def addScrollingFigure(figure, frame):
global canvas, mplCanvas, interior, interior_id, cwid
# set up a canvas with scrollbars
canvas = Canvas(frame)
canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW)
xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
yScrollbar = Scrollbar(frame)
xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW)
yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS)
canvas.config(xscrollcommand=xScrollbar.set)
xScrollbar.config(command=canvas.xview)
canvas.config(yscrollcommand=yScrollbar.set)
yScrollbar.config(command=canvas.yview)
# plug in the figure
figAgg = FigureCanvasTkAgg(figure, canvas)
mplCanvas = figAgg.get_tk_widget()
# and connect figure with scrolling region
cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW)
printBboxes("Init")
changeSize(figure, 1)
def changeSize(figure, factor):
global canvas, mplCanvas, interior, interior_id, frame, cwid
oldSize = figure.get_size_inches()
print("old size is", oldSize)
figure.set_size_inches([factor * s for s in oldSize])
wi,hi = [i*figure.dpi for i in figure.get_size_inches()]
print("new size is", figure.get_size_inches())
print("new size pixels: ", wi,hi)
mplCanvas.config(width=wi, height=hi) ; printBboxes("A")
canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B")
canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
tz.set_fontsize(tz.get_fontsize()*factor)
for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] +
ax.get_xticklabels() + ax.get_yticklabels()):
item.set_fontsize(item.get_fontsize()*factor)
ax.xaxis.labelpad = ax.xaxis.labelpad*factor
ax.yaxis.labelpad = ax.yaxis.labelpad*factor
#figure.tight_layout() # matplotlib > 1.1.1
figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86)
figure.canvas.draw() ; printBboxes("C")
print()
if __name__ == "__main__":
global root, figure
root = Tk()
root.rowconfigure(1, weight=1)
root.columnconfigure(1, weight=1)
frame = Frame(root)
frame.grid(column=1, row=1, sticky=Tkconstants.NSEW)
frame.rowconfigure(1, weight=1)
frame.columnconfigure(1, weight=1)
figure = plt.figure(dpi=150, figsize=(4, 4))
ax = figure.add_subplot(111)
ax.plot(range(10), [math.sin(x) for x in range(10)])
#tz = figure.text(0.5,0.975,''The master title'',horizontalalignment=''center'', verticalalignment=''top'')
tz = figure.suptitle(''The master title'')
ax.set_title(''Tk embedding'')
ax.set_xlabel(''X axis label'')
ax.set_ylabel(''Y label'')
print(tz.get_fontsize()) # 12.0
print(ax.title.get_fontsize(), ax.xaxis.label.get_fontsize(), ax.yaxis.label.get_fontsize()) # 14.4 12.0 12.0
addScrollingFigure(figure, frame)
buttonFrame = Frame(root)
buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS)
biggerButton = Button(buttonFrame, text="larger",
command=lambda : changeSize(figure, 1.2))
biggerButton.grid(column=1, row=1)
smallerButton = Button(buttonFrame, text="smaller",
command=lambda : changeSize(figure, 0.833))
smallerButton.grid(column=1, row=2)
qButton = Button(buttonFrame, text="quit",
command=lambda : sys.exit(0))
qButton.grid(column=1, row=3)
root.mainloop()