簡體   English   中英

tkinter canvas 不顯示與 matplotlib 圖表的線

[英]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 不是很熟悉。我希望通過查看工作案例和非工作案例的代碼,然后我可能會發現一些東西。

這是我的想法:我不明白你為什么直接在 canvasself.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM