简体   繁体   English

tkinter canvas 不显示与 matplotlib 图表的线

[英]tkinter canvas not displaying line with matplotlib chart

I have a candlestick chart that I am displaying in tkinter using mplfinance.我有一个使用 mplfinance 在 tkinter 中显示的烛台图表。 I have mplfinance return the figure so that I can use matplotlib to find the x and y coordinates that the user may need to draw lines on the chart.我让 mplfinance 返回图形,以便我可以使用 matplotlib 找到用户可能需要在图表上画线的 x 和 y 坐标。

I have been successful with drawing lines on the chart using the underlying canvas. My idea is to save the lines in a database so that when the user returns to the chart, the lines are still displayed.我已成功使用底层 canvas 在图表上绘制线条。我的想法是将线条保存在数据库中,以便当用户返回图表时,线条仍会显示。 In addition, the user should be able to edit or delete the lines as well after returning to the chart.此外,用户还应该能够在返回图表后编辑或删除线条。

I have been able to save the lines in the database and retrieve them as well.我已经能够将这些行保存在数据库中并也可以检索它们。 My problem is that I cannot get them to reappear on the canvas when I start the program.我的问题是当我启动程序时无法让它们重新出现在 canvas 上。 The program is retrieving the lines from the database, and it appears that it is going through the motions of drawing the lines.该程序正在从数据库中检索线条,看起来它正在完成绘制线条的动作。 The lines are not appearing though.虽然没有出现线条。

Using a few print statements, the program is telling me that the lines have been drawn.使用一些打印语句,程序告诉我线条已经画好了。 What do I need to do in order to get the lines to appear on the canvas?我需要做什么才能让线路出现在 canvas 上? My minimal example is below.我的最小示例如下。

I have not included the code for storing the lines in the database.我没有包含用于将行存储在数据库中的代码。 In my example, the line I am asking the program to draw is not showing up.在我的示例中,我要求程序绘制的线条没有显示出来。 That is the only problem I am having.这是我遇到的唯一问题。 What am I missing?我错过了什么?

You can find the csv file that I use here , or you can use any csv file that has open, high, low, close, volume information for a particular equity.您可以在此处找到我使用的 csv 文件,或者您可以使用任何 csv 文件,该文件具有特定股票的开盘价、最高价、最低价、收盘价、交易量信息。 Any help would be greatly appreciated.任何帮助将不胜感激。

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()

Edit:编辑:

This is the function that allows the user to draw a line on the screen.这是 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")

I want to be able to replicate the lines the user draws when they open the program the next time.我希望能够复制用户下次打开程序时绘制的线条。 I realize that I have to save the line in a database, which I have no problems with.我意识到我必须将该行保存在数据库中,这对我来说没有问题。 I can retrieve the coordinates for the line from the database.我可以从数据库中检索线的坐标。 The program just doesn't display it.该程序只是不显示它。

The print statements show that the program is supposedly drawing the line.打印语句显示该程序应该正在画线。 I have even tried forcing the canvas to redraw using self.fig.canvas.draw() .我什至尝试使用self.fig.canvas.draw()强制 canvas 重绘。

In the draw_trend_line function, I have a variable called self.cur_ax .在 draw_trend_line function 中,我有一个名为self.cur_ax的变量。 In my full program, I am using panels, so there could be multiple axes.在我的完整程序中,我使用的是面板,因此可能有多个轴。 Please feel free to ask any questions about anything that you want me to elaborate on.如果您希望我详细说明,请随时提出任何问题。

This isn't really an "answer" per se, but I have a number of ideas that may help and it's just easier to write them here instead of as a series of comments.这本身并不是一个真正的“答案”,但我有一些想法可能会有所帮助,而且将它们写在这里而不是作为一系列评论更容易。

I wish I could help more, but I am not very familiar with tkinter. I was hoping by seeing the code for both the working and non-working case then I might spot something.我希望我能提供更多帮助,但我对 tkinter 不是很熟悉。我希望通过查看工作案例和非工作案例的代码,然后我可能会发现一些东西。

Here are my thoughts: I don't understand why you are creating the line directly on the canvas ( self.id = self.fig.canvas._tkcanvas.create_line(...) ) whereas matplotlib (and mplfinance) draw on the Axes (not on the canvas/Figure).这是我的想法:我不明白你为什么直接在 canvasself.id = self.fig.canvas._tkcanvas.create_line(...) )上创建线,而 matplotlib(和 mplfinance)轴上绘制(不在画布/图上)。

Overall the issue you are having seems to me a tkinter related issue, or perhaps a tkinter/matplotlib problem: If you could reproduce with a very simple matplotlib example (instead of mplfinance) then it may be easier to isolate.总的来说,您遇到的问题在我看来是一个 tkinter 相关问题,或者可能是一个 tkinter/matplotlib 问题:如果您可以使用一个非常简单的 matplotlib 示例(而不是 mplfinance)进行重现,那么它可能更容易隔离。

That said, I would point out that mplfinance has the ability to plot "arbitrary" lines (and trend lines).也就是说,我要指出的是,mplfinance 具有plot 条“任意”线(和趋势线)的能力。 Perhaps it would be easier for you to use the alines kwarg of mpf.plot() and simply re-paint the plot each time the user requests a trend line.也许使用alines mpf.plot()的 lines kwarg 并在每次用户请求趋势线时简单地重新绘制 plot 会更容易。


Finally, here is some code that I tweaked in response to another mplfinance user's question.最后,这是我为响应另一个 mplfinance 用户的问题而调整的一些代码。 The code does basically what you want.该代码基本上可以满足您的需求。 The data for this example comes from the examples/data directory in the mplfinance repository .此示例的数据来自mplfinance 存储库中的examples/data目录。 The code makes a MACD plot, and then uses matplotlib's Figure.ginput() to get the location of any mouse clicks.该代码生成 MACD plot,然后使用 matplotlib 的Figure.ginput()获取任意鼠标点击的位置。 The user can click on the main portion of the plot, and every two mouse clicks will result in drawing a line on the plot between the two mouse clicks.用户可以点击 plot 的主要部分,每点击两次鼠标就会在两次鼠标点击之间在 plot 上画一条线。 Each line is drawn by adding the two points to the list of lines within the alines kwarg specification in the call to mpf.plot() :每条线都是通过在调用mpf.plot()时将两个点添加到alines kwarg规范内的线列表来绘制的:

EDIT: I've tweaked the code below somewhat from the original version here by using dill to simulate your database that holds the lines that the user has drawn.编辑:我通过使用dill来模拟保存用户绘制的线条的数据库,从此处的原始版本稍微调整了下面的代码。 Hopefully this is at least somewhat helpful, or gives you some ideas as to how you may do something similar with tkinter.希望这至少有点帮助,或者给你一些关于如何使用 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()

Just in case someone else runs into a similar problem that I had, I want to post the solution that I came up with after looking at some of the other posts on stackoverflow.以防万一其他人遇到与我类似的问题,我想发布我在查看 stackoverflow 上的其他一些帖子后提出的解决方案。 It actually is very simple.其实很简单。 I added one line right above dashboard.mainloop() .我在dashboard.mainloop()的正上方添加了一行。 That line is dashboard.after(20, drawInitialized.draw_line) .该行是dashboard.after(20, drawInitialized.draw_line) I also removed the call to self.draw_line() in the Example class. It displays the line when the application is started every time.我还在示例 class 中删除了对self.draw_line()的调用。每次启动应用程序时都会显示该行。 Obviously, the coordinates for this line could be stored in a database, or as Mr. Daniel Goldfarb suggested, the 'dill' module could be used instead.显然,这条线的坐标可以存储在数据库中,或者如 Daniel Goldfarb 先生所建议的那样,可以改用“dill”模块。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM