finplot icon indicating copy to clipboard operation
finplot copied to clipboard

Render lines on zoom

Open IlanKalendarov opened this issue 1 year ago • 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!

IlanKalendarov avatar Apr 25 '23 12:04 IlanKalendarov

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?

highfestiva avatar Apr 26 '23 04:04 highfestiva

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.

IlanKalendarov avatar Apr 26 '23 07:04 IlanKalendarov

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

highfestiva avatar Apr 26 '23 18:04 highfestiva

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.

IlanKalendarov avatar Apr 27 '23 08:04 IlanKalendarov

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!

highfestiva avatar Apr 27 '23 18:04 highfestiva

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

IlanKalendarov avatar Apr 29 '23 09:04 IlanKalendarov

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.

highfestiva avatar Apr 29 '23 20:04 highfestiva

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?

IlanKalendarov avatar Apr 30 '23 08:04 IlanKalendarov

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?

highfestiva avatar Apr 30 '23 21:04 highfestiva

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.

IlanKalendarov avatar May 05 '23 12:05 IlanKalendarov

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?

highfestiva avatar Sep 12 '23 20:09 highfestiva

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

alexgiul avatar Oct 02 '23 15:10 alexgiul

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

highfestiva avatar Dec 01 '23 22:12 highfestiva