[英]tkinter canvas not displaying line with matplotlib chart
我有一個使用 mplfinance 在 tkinter 中顯示的燭台圖表。 我讓 mplfinance 返回圖形,以便我可以使用 matplotlib 找到用戶可能需要在圖表上畫線的 x 和 y 坐標。
我已成功使用底層 canvas 在圖表上繪制線條。我的想法是將線條保存在數據庫中,以便當用戶返回圖表時,線條仍會顯示。 此外,用戶還應該能夠在返回圖表后編輯或刪除線條。
我已經能夠將這些行保存在數據庫中並也可以檢索它們。 我的問題是當我啟動程序時無法讓它們重新出現在 canvas 上。 該程序正在從數據庫中檢索線條,看起來它正在完成繪制線條的動作。 雖然沒有出現線條。
使用一些打印語句,程序告訴我線條已經畫好了。 我需要做什么才能讓線路出現在 canvas 上? 我的最小示例如下。
我沒有包含用於將行存儲在數據庫中的代碼。 在我的示例中,我要求程序繪制的線條沒有顯示出來。 這是我遇到的唯一問題。 我錯過了什么?
您可以在此處找到我使用的 csv 文件,或者您可以使用任何 csv 文件,該文件具有特定股票的開盤價、最高價、最低價、收盤價、交易量信息。 任何幫助將不勝感激。
from tkinter import *
import pandas as pd
import numpy as np
import datetime as dt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.widgets import MultiCursor
import mplfinance as mpf
from functools import partial
import math
class Example:
def __init__(self, figax, color='#0000FF', width=1):
"""
This class is used to draw on the canvas of a matplotlib chart.
:param: figax The figure axes object created by matplotlib
:param: color The color that should be used currently. The default
color is blue (#0000FF).
:param: width The width of the line stroke. The default is 1.
"""
self.fig, self.ax = figax
self.cur_ax = None
#bbox_height is total height of figure.
self.bbox_height = self.fig.canvas.figure.bbox.height
# bbox_width is total width of figure.
self.bbox_width = self.fig.canvas.figure.bbox.width
ax_len = len(self.ax)
# Create a list to hold the dimensions of the axes.
self.ax_dims = []
# Create a variable to hold the number of axes in the figure.
self.ax_ct = 0
self.ax_bounds = None
# Get the width and height of each axis in pixels.
for i in range(0, ax_len, 2):
self.ax_ct += 1
dims = self.ax[i].get_window_extent().transformed(self.fig.dpi_scale_trans.inverted())
awidth, aheight = dims.width, dims.height
# awidth is in pixels.
awidth *= self.fig.dpi
# aheight is in pixels.
aheight *= self.fig.dpi
d = {'Width': awidth, 'Height': aheight}
self.ax_dims.append(d)
self.ax_bounds = None
self.calc_axes_bounds()
# Set the ID of the object currently being drawn.
self.cur_id = None
self.color = color
self.width = width
self.draw_line()
def setColor(self, color):
self.color = color
def setWidth(self, width):
self.width = width
def calc_axes_bounds(self):
self.ax_bounds = []
# The first axis (ax[0]) will have a top y coordinate of 0.
heightTot = 0
# Calculate the bounding x, y coordinates for each axis.
for i in range(self.ax_ct):
# The x axis is shared by all plots; therefore, all axes
# will start and end at the same x mark.
x0 = 0
x1 = math.ceil(self.ax_dims[i]['Width'])
# Dealing with the top axis.
y0 = heightTot
y1 = self.ax_dims[i]['Height'] + y0
heightTot += y1
d = {'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1}
self.ax_bounds.append(d)
def inaxes(self, x, y):
for i in range(len(self.ax_bounds)):
if (self.ax_bounds[i]['x0'] <= x <= self.ax_bounds[i]['x1']) and (self.ax_bounds[i]['y0'] <= y <= self.ax_bounds[i]['y1']):
self.cur_ax = i
ylim = self.ax[self.cur_ax].get_ylim()
def draw_line(self):
self.cur_ax = 0
self.cur_id = Line(self, 156, 39, 861, 273, self.color, self.width)
print("Done!")
class Line:
def __init__(self, parent, x0, y0, x1, y1, color, width):
self.parent = parent
self.ax = self.parent.ax
self.id = None
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
self.fig = self.parent.fig
self.color = color
self.width = width
#bbox_height is total height of figure.
self.bbox_height = self.fig.canvas.figure.bbox.height
# bbox_width is total width of figure.
self.bbox_width = self.fig.canvas.figure.bbox.width
# The current axis that is being worked with
self.cur_ax = self.parent.cur_ax
#print("Current axis is:", self.cur_ax)
#self.ax_bounds = self.parent.ax_bounds
self.id = None
self.draw()
def draw(self):
print("x0 is:", self.x0)
print("y0 is:", self.y0)
print("x1 is:", self.x1)
print("y1 is:", self.y1)
self.id = self.fig.canvas._tkcanvas.create_line(self.x0, self.y0, self.x1, self.y1, fill=self.color, width=self.width, activewidth=2, smooth=True)
print("ID is:", self.id)
def __str__(self):
return str(self.id)
if __name__ == '__main__':
dashboard = Tk()
dashboard.geometry("1200x700")
dashboard['bg'] = 'grey'
dashboard.title("Example Drawing Tools")
dashboard.state("zoomed") # Makes the window fully enlarged
# Opening data source
df = pd.read_csv("ATOS.csv", index_col=0, parse_dates=True)
dates = df.index.to_pydatetime().tolist()
# Create `marketcolors` to use with the `charles` style:
mc = mpf.make_marketcolors(up='#008000',down='#FF0000', vcdopcod=True, inherit=True)
# Create a new style based on `charles`.
sm_style = mpf.make_mpf_style(base_mpf_style='charles',
marketcolors=mc,
facecolor='#FFFFFF',
edgecolor='#999999',
figcolor='#FFFFFF'
)
figax = mpf.plot(df,
warn_too_much_data=6000,
panel_ratios=(3,1),
type="candle",
volume=True,
figsize=(12, 7),
main_panel=0,
volume_panel=1,
num_panels=2,
tight_layout=True,
scale_padding={'left': 0.02, 'top': 0, 'right': 1.2, 'bottom': 0.5},
ylabel="",
style=sm_style,
returnfig=True
)
fig, ax = figax
vol_ax = ax[2]
vol_ax.set_xlabel("")
vol_ax.set_ylabel("")
canvasbar = FigureCanvasTkAgg(fig, master=dashboard)
cursor = MultiCursor(canvasbar, ax, horizOn=True, vertOn=True, linewidth=0.75, color='#000000')
canvasbar.draw()
examp = Example(figax)
canvasbar.get_tk_widget().grid(row=0, column=0, columnspan=5, padx=0, pady=(0,20))
btn1 = Button(dashboard, text="Exit", command=quit)
btn1.grid(row=0, column=6, padx=5, pady=10, sticky='n')
dashboard.mainloop()
編輯:
這是 function,它允許用戶在屏幕上畫一條線。
def draw_trend_line(self, event):
#print("cur_draw_id is:", str(self.cur_draw_id))
#print("Begin draw_trend_line")
self.event = event
#print("Event (x,y) is:", self.event.x, self.event.y)
if self.cur_draw_id is not None:
self.remove()
xMin = math.ceil(self.ax_bounds[self.cur_ax]['x0'])
xMax = math.ceil(self.ax_bounds[self.cur_ax]['x1'])
yMin = math.ceil(self.ax_bounds[self.cur_ax]['y0'])
yMax = math.ceil(self.ax_bounds[self.cur_ax]['y1'])
#print("yMax is:", yMax)
if self.event.x >= xMax:
x0 = xMax
elif self.event.x <= xMin:
x0 = xMin
else:
x0 = self.event.x
if self.event.y >= yMax:
y0 = yMax
elif self.event.y <= yMin:
y0 = yMin
else:
y0 = self.event.y
# Starting Position
if self.x_start is None:
self.x_start = x0
else:
x0 = self.x_start
if self.y_start is None:
self.y_start = y0
else:
y0 = self.y_start
# Ending Position
if self.event.x >= xMax:
x1 = xMax
elif self.event.x <= xMin:
x1 = xMin
else:
x1 = self.event.x
if self.event.y >= yMax:
y1 = yMax
elif self.event.y <= yMin:
y1 = yMin
else:
y1 = self.event.y
self.cur_draw_id = Line(self, x0, y0, x1, y1, self.color, self.width)
#print("End draw_trend_line")
我希望能夠復制用戶下次打開程序時繪制的線條。 我意識到我必須將該行保存在數據庫中,這對我來說沒有問題。 我可以從數據庫中檢索線的坐標。 該程序只是不顯示它。
打印語句顯示該程序應該正在畫線。 我什至嘗試使用self.fig.canvas.draw()
強制 canvas 重繪。
在 draw_trend_line function 中,我有一個名為self.cur_ax
的變量。 在我的完整程序中,我使用的是面板,因此可能有多個軸。 如果您希望我詳細說明,請隨時提出任何問題。
這本身並不是一個真正的“答案”,但我有一些想法可能會有所幫助,而且將它們寫在這里而不是作為一系列評論更容易。
我希望我能提供更多幫助,但我對 tkinter 不是很熟悉。我希望通過查看工作案例和非工作案例的代碼,然后我可能會發現一些東西。
這是我的想法:我不明白你為什么直接在 canvas ( self.id = self.fig.canvas._tkcanvas.create_line(...)
)上創建線,而 matplotlib(和 mplfinance)在軸上繪制(不在畫布/圖上)。
總的來說,您遇到的問題在我看來是一個 tkinter 相關問題,或者可能是一個 tkinter/matplotlib 問題:如果您可以使用一個非常簡單的 matplotlib 示例(而不是 mplfinance)進行重現,那么它可能更容易隔離。
也就是說,我要指出的是,mplfinance 具有plot 條“任意”線(和趨勢線)的能力。 也許使用alines
mpf.plot()
的 lines kwarg 並在每次用戶請求趨勢線時簡單地重新繪制 plot 會更容易。
最后,這是我為響應另一個 mplfinance 用戶的問題而調整的一些代碼。 該代碼基本上可以滿足您的需求。 此示例的數據來自mplfinance 存儲庫中的examples/data
目錄。 該代碼生成 MACD plot,然后使用 matplotlib 的Figure.ginput()
獲取任意鼠標點擊的位置。 用戶可以點擊 plot 的主要部分,每點擊兩次鼠標就會在兩次鼠標點擊之間在 plot 上畫一條線。 每條線都是通過在調用mpf.plot()
時將兩個點添加到alines
kwarg規范內的線列表來繪制的:
編輯:我通過使用dill
來模擬保存用戶繪制的線條的數據庫,從此處的原始版本稍微調整了下面的代碼。 希望這至少有點幫助,或者給你一些關於如何使用 tkinter 做類似事情的想法。
import pandas as pd
import mplfinance as mpf
import dill
import os
from matplotlib.widgets import MultiCursor
# read the data:
idf = pd.read_csv('../data/SPY_20110701_20120630_Bollinger.csv',
index_col=0,parse_dates=True)
df = idf.loc['2011-07-01':'2011-12-30',:]
# macd related calculations:
exp12 = df['Close'].ewm(span=12, adjust=False).mean()
exp26 = df['Close'].ewm(span=26, adjust=False).mean()
macd = exp12 - exp26
signal = macd.ewm(span=9, adjust=False).mean()
histogram = macd - signal
# initial plot:
apds = [mpf.make_addplot(exp12,color='lime'),
mpf.make_addplot(exp26,color='c'),
mpf.make_addplot(histogram,type='bar',width=0.7,panel=1,
color='dimgray',alpha=1,secondary_y=False),
mpf.make_addplot(macd,panel=1,color='fuchsia',secondary_y=True),
mpf.make_addplot(signal,panel=1,color='b',secondary_y=True),
]
# For some reason, which i have yet to determine, MultiCursor somehow
# causes ymin to be set to zero for the main candlestick Axes, but we
# can correct that problem by passing in specific values:
ymin = min(df['Low']) * 0.98
ymax = max(df['High']) * 1.02
# initial plot with cursor:
if os.path.exists('lines.dill'):
alines = dill.load(open('lines.dill','rb'))
else:
alines = []
fig, axlist = mpf.plot(df,type='candle',addplot=apds,figscale=1.25,
figratio=(8,6),title='\nMACD', ylim=(ymin,ymax),
alines=dict(alines=alines,colors='r'),
style='blueskies',volume=True,volume_panel=2,
panel_ratios=(6,3,2),returnfig=True)
multi = MultiCursor(fig.canvas, axlist[0:2], horizOn=True,
vertOn=True, color='pink', lw=1.2)
fig.canvas.draw_idle()
# ---------------------------------------------------
# set up an event loop where we wait for two
# mouse clicks, and then draw a line in between them,
# and then wait again for another two mouse clicks.
# This is a crude way to do it, but its quick and easy.
# Disadvantage is: user has 8 seconds to provide two clicks
# or the first click will be erased. But the 8 seconds
# repeats as long as the user does not close the Figure,
# so user can draw as many trend lines as they want.
# The advantage of doing it this way is we don't have
# to write all the mouse click handling stuff that's
# already written in `Figure.ginput()`.
not_closed = True
def on_close(event):
global not_closed
global alines
dill.dump(alines, open('lines.dill','wb'))
print('closing, please wait ...')
not_closed = False
fig.canvas.mpl_connect('close_event', on_close)
while not_closed:
vertices = fig.ginput(n=2,timeout=8)
if len(vertices) < 2:
continue
p1 = vertices[0]
p2 = vertices[1]
d1 = df.index[ round(p1[0]) ]
d2 = df.index[ round(p2[0]) ]
alines.append( [ (d1,p1[1]), (d2,p2[1]) ] )
apds = [mpf.make_addplot(exp12,color='lime',ax=axlist[0]),
mpf.make_addplot(exp26,color='c',ax=axlist[0]),
mpf.make_addplot(histogram,type='bar',width=0.7,panel=1,
ax=axlist[2],color='dimgray',alpha=1),
mpf.make_addplot(macd,panel=1,color='fuchsia',ax=axlist[3]),
mpf.make_addplot(signal,panel=1,color='b',ax=axlist[3])
]
mpf.plot(df,ax=axlist[0],type='candle',addplot=apds,ylim=(ymin,ymax),
alines=dict(alines=alines,colors='r'),
style='blueskies',volume=axlist[4],volume_panel=2,
panel_ratios=(6,3,2))
fig.canvas.draw_idle()
以防萬一其他人遇到與我類似的問題,我想發布我在查看 stackoverflow 上的其他一些帖子后提出的解決方案。 其實很簡單。 我在dashboard.mainloop()
的正上方添加了一行。 該行是dashboard.after(20, drawInitialized.draw_line)
。 我還在示例 class 中刪除了對self.draw_line()
的調用。每次啟動應用程序時都會顯示該行。 顯然,這條線的坐標可以存儲在數據庫中,或者如 Daniel Goldfarb 先生所建議的那樣,可以改用“dill”模塊。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.