highfestiva/finplot

Render lines on zoom

IlanKalendarov opened this issue · 13 comments

Hey, Is there a way to render lines on zoom?
For example you have
lod_candles
lod_labels
It would be great if you would have the same functionality for lines, as I have lots of them and the chart is being kind of laggy.

Thanks!

Yeah, I'm feeling it too during initial render/zoom sometimes. How many lines are we talking, and over how many data points (timestamps)?

Do you only get lag during initial delay and when zooming, or also when moving the crosshair around?

Yeah, I'm feeling it too during initial render/zoom sometimes. How many lines are we talking, and over how many data points (timestamps)?

Do you only get lag during initial delay and when zooming, or also when moving the crosshair around?

I have for example in my chart 3727 lines. Overall I have 8516 data points.

It gets laggy when zooming and also when I move it left or right. The crosshair is not being affected by this.

May I ask what kind of lines? Could you make a tiny example to clarify?

Yeah for example I am coloring the tails of the candles based on the trend, if the trend is up the candle tails will be green and if the trend is down the candle tails will be red.
Here's a code snippet:

def add_trend(self):
      up_trend_df = self.df.loc[self.df['trend'] == 'up', ['date', 'high', 'low']]
      dn_trend_df = self.df.loc[self.df['trend'] == 'down', ['date', 'high', 'low']]
      
      for _, row in up_trend_df.iterrows():
          line = fplt.add_line((row['date'], row['low']), (row['date'], row['high']), color='#00FF00', width=3, ax=self.ax)

      for _, row in dn_trend_df.iterrows():
          line = fplt.add_line((row['date'], row['low']), (row['date'], row['high']), color='#FF0000', width=3, ax=self.ax)

By the way I don't know if there is a better way of doing this without adding lines so this is what I came up with.

Yeah, that's going to be expensive. Hm. Best is to extend the class CandlestickItem (which renders the candles), and overwrite the function candlestick_ochl, but that might be a bit to involved for a newbie? Anyway, the thing you'd like to get to is to make two calls to the p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high)) (in line 1227 or thereabout of __init__.py): one for each color, and then set the color for each group before it.

This is a feature that could be made more accessible for each data point. I think many people have similar problems, so I'll try to see if I can figure something out that can be used for a per-point basis coloring. It's going to take some time to figure out though, so in the mean while the above recommendation is the best I can do. GL!

I tried to so something like this:

Extend the CandlestickItem class:

class CandlestickItemExtend(fplt.CandlestickItem):
    def __init__(self, ax, datasrc, draw_body, draw_shadow, candle_width, colorfunc, resamp=None):
        super().__init__(ax, datasrc, draw_body, draw_shadow, candle_width, colorfunc, resamp=None)

    def generate_picture(self, boundingRect, up_trend_data=None, dn_trend_data=None):
        super().generate_picture(boundingRect)

        p = self.painter

        if up_trend_data is not None:
            p.setPen(pg.mkPen('#00FF00', width=3))
            for _, row in up_trend_data.iterrows():
                x = row['date']
                low, high = row['low'], row['high']
                p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high))

        if dn_trend_data is not None:
            p.setPen(pg.mkPen('#FF0000', width=3))
            for _, row in dn_trend_data.iterrows():
                x = row['date']
                low, high = row['low'], row['high']
                p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high))

Extend candlestick_ochl

def my_candlestick_ochl(self, datasrc, draw_body=True, draw_shadow=True, candle_width=0.6, ax=None, colorfunc=fplt.price_colorfilter, **kwargs):
        up_trend_df = self.df.loc[self.df['trend'] == 'up', ['date', 'high', 'low']]
        dn_trend_df = self.df.loc[self.df['trend'] == 'down', ['date', 'high', 'low']]
        view_bounds = self.ax.vb.viewRect()
        bounding_rect = QtCore.QRectF(view_bounds.x(), view_bounds.y(), view_bounds.width(), view_bounds.height())
        item = fplt.candlestick_ochl(datasrc, draw_body=draw_body, draw_shadow=draw_shadow, candle_width=candle_width, ax=ax, colorfunc=colorfunc)
        data = CandlestickItemExtend(self.ax, datasrc, draw_body=draw_body, draw_shadow=draw_shadow, candle_width=candle_width, colorfunc=colorfunc)
        data.generate_picture(bounding_rect, up_trend_df, dn_trend_df)

this is how I call the function:

self.my_candlestick_ochl(self.df[['date','open','close','high','low', 'trend']], ax=self.ax)

For some reason this does not work I get an error:

  File "/opt/homebrew/lib/python3.11/site-packages/pandas/core/generic.py", line 5902, in __getattr__
    return object.__getattribute__(self, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'DataFrame' object has no attribute 'rows'

Am I any close?

Edit: I was able to overcome this by creating datasrc like so:

data = fplt._create_datasrc(self.ax, datasrc, ncols=5)
item = fplt.candlestick_ochl(datasrc, draw_body=draw_body, draw_shadow=draw_shadow, candle_width=candle_width, ax=ax, colorfunc=colorfunc)
candle = CandlestickItemExtend(self.ax, data, draw_body=draw_body, draw_shadow=draw_shadow, candle_width=candle_width, colorfunc=colorfunc)
candle.generate_picture(bounding_rect, up_trend_df, dn_trend_df)

Now i get :

QPainter::setPen: Painter not active
QPainter::setPen: Painter not active
QPainter::setBrush: Painter not active
QPainter::setPen: Painter not active
QPainter::setPen: Painter not active
QPainter::setBrush: Painter not active
QPainter::drawRects: Painter not active
QPainter::drawRects: Painter not active
QPainter::setPen: Painter not active
QPainter::setPen: Painter not active

The chart opens but I don't see the colors

You see, you need to override the class just like you did, and then use it within the finplot library. Something like this:

fplt.CandlestickItem = CandlestickItemExtend
# and then use like you normally would
fplt.candlestick_ochl(...)
fplt.show()

I was wrong, don't think you need to override fplt.candlestick_ochl(), you could probably just keep that as is. The up_trend_data can't be used as parameters. You need to use closures, global variables or some other way. Try it and let me know.

This works for me!

class CandlestickItemExtend(fplt.CandlestickItem):
    def __init__(self, ax, datasrc, draw_body, draw_shadow, candle_width, colorfunc, resamp=None):
        super().__init__(ax, datasrc, draw_body, draw_shadow, candle_width, colorfunc, resamp=None)
        self.lines = []
        self.up_trend_df = None
        self.dn_trend_df = None
    
    def set_trend_data(self, up_trend_df, dn_trend_df):
        self.up_trend_df = up_trend_df
        self.dn_trend_df = dn_trend_df

    def generate_picture(self, boundingRect):
        if self.up_trend_df is None or self.dn_trend_df is None:
            return
        
        p = self.painter

        if self.up_trend_df is not None:
            p.setPen(pg.mkPen('#00FF00', width=3))
            for x, row in self.up_trend_df.iterrows():
                low, high = row['low'], row['high']
                p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high))

        if self.dn_trend_df is not None:
            p.setPen(pg.mkPen('#FF0000', width=3))
            for x, row in self.dn_trend_df.iterrows():
                low, high = row['low'], row['high']
                p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high))
        super().generate_picture(boundingRect)
fplt.CandlestickItem = CandlestickItemExtend
up_trend_df = self.df.loc[self.df['trend'] == 'up', ['date', 'high', 'low']]
dn_trend_df = self.df.loc[self.df['trend'] == 'down', ['date', 'high', 'low']]
candlestick_item = fplt.candlestick_ochl(self.df[['date','open','close','high','low', 'trend']], ax=self.ax)
candlestick_item.set_trend_data(up_trend_df, dn_trend_df)

Although I get a random red lines on my chart:
image

image

And those are not related to my graph data haha, Any ideas?

Haha, glad to hear it! Sorry, no I have no idea where the big fat red line is coming from. I suspect that you're rendering other stuff than just the candlesticks in your chart?

I wish this was the case but if I am not extending the CandlestickItem then it won't show me those candles. I am using the same df for both cases.

Hi again, sorry for the delay! Is this still a problem? If so: could you post some minimal code that I can use to reproduce the issue?

Hi, I solved adding an "epsilon"

if high > low: p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high)) else: p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, low+1e-10))

@IlanKalendarov Is this still a problem, or did you solve it?