Can Dash update recent points and extend graph at one callback call?

Dear community! I have a question about Dash operation and prepared minimal working examples showing what works and what doesn’t (I hope someone will help me to fix this). So, there will be two topics connected one with another. Let’s start from simpler one. This is my code generating random candlestick plot which works (it’s funny that the code behavior differs when the Dash server is run from python IDLE and from cmd, I tested this only on Windows 7):

from dash import Dash, dcc, html, Input, Output, State
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
import numpy as np
import time

from threading import Thread, Timer, Lock, Condition
import plotly.graph_objs as go

sendPeriodMs = 200
cbPeriodMS = 75

np.random.seed( 2 )
class Candle():
    def __init__( self, _o = .0, _c = .0, _h = .0, _l = .0, _v = 0, _x = 0 ):
        self.o = _o
        self.c = _c
        self.h = _h
        self.l = _l
        self.v = _v
        self.x = _x

class RandomCandle():
    def __init__( self, _start, _stepLen, _maxTicksInCandle, _avgQtyPerTick ):
        self.price = _start
        self.stepLen = _stepLen
        self.maxTicksInCandle = _maxTicksInCandle
        self.avgQtyPerTick = _avgQtyPerTick
        self.stepsDone = 0
        self.maxTicksInThisCandle = 0
        self.ticksInThisCandle = 0
        self.ticks = []
        self.qty = 0
        self.x = -1
    def generate( self ):

        if self.ticksInThisCandle == 0:
            self.ticks.clear()
            self.maxTicksInThisCandle = np.random.choice(
                                            range( self.maxTicksInCandle // 4,
                                                   self.maxTicksInCandle ) )
            numSteps = 0
            while numSteps == 0:
                numSteps = np.random.choice( range( -7, 8 ) )
            self.price += numSteps * self.stepLen
            self.ticks = [ self.price ]
            _q = 0
            while _q == 0:
                _q = np.random.choice( self.avgQtyPerTick * abs( numSteps ) )
            self.qty = _q
            self.ticksInThisCandle += 1
            self.x += 1
            
        else:    
            numSteps = 0
            while numSteps == 0:
                numSteps = np.random.choice( range( -7, 8 ) )

            _q = 0
            while _q == 0:
                _q = np.random.choice( self.avgQtyPerTick * abs( numSteps ) )
            self.qty += _q

            nextPrice = self.price + numSteps * self.stepLen
            
            self.ticks.append( nextPrice )
            self.price = nextPrice
            self.ticksInThisCandle += 1
        
        _o = self.ticks[0]
        _c = self.ticks[-1]
        _h = max( self.ticks )
        _l = min( self.ticks )
        
        if self.ticksInThisCandle == self.maxTicksInThisCandle: 
            self.ticksInThisCandle = 0
            
        return Candle( _o, _c, _h, _l, _q, self.x )
        
class CandleList():
    def __init__( self ):
        self.data = []
        self.readPosition = 0
        self.mutex = Lock()

    def size( self ):
        with self.mutex:
            return len( self.data )

    def getMutex( self ):
        return self.mutex

    def getList( self ):
        return self.data

    def append( self, _els ):
        with self.mutex:
            if type( _els ) is list:
                for _el in _els:
                    self.data.append( _el )
            else:
                self.data.append( _els )

    def getRecentElements( self, _count = 0 ):
        assert( _count >= 0 )
        res = []
        _size = 0
        with self.mutex:
            _size = len( self.data )
            if _count == 0:
                _count = _size

            _index = 0 if _size <= _count else _size - _count

            while _index < _size:
                res.append( self.data[ _index ] )
                _index += 1

        return res

    def getReadPosition( self ):
        with self.mutex:
            return self.readPosition

    def setReadPosition( self, _pos ):
        with self.mutex:
            self.readPosition = _pos

g_num_bars_curr = 100
qbuff = CandleList()
def get_q(): #is the function really needed when the object is accessed from Dash callbacks
    return qbuff

app = Dash()

def _create_fig( _q ):
    _cdsAll = _q.getList()
    _lock = _q.getMutex()

    xs = []
    os = []
    hs = []
    ls = []
    cs = []

    with _lock:
        _size = len( _cdsAll )
        _startIndex = max( 0, _size - g_num_bars_curr )

        print( '_candList.getRecentElements( 0 ) returned %d candles' % _size )
        for i in range( _startIndex, len( _cdsAll ) ):
            xs.append( _cdsAll[i].x )
            os.append( _cdsAll[i].o )
            hs.append( _cdsAll[i].h )
            ls.append( _cdsAll[i].l )
            cs.append( _cdsAll[i].c )
        if _size > 0:
            _q.readPosition = _size
    __fig = go.Figure( data = [
                go.Candlestick(
                    x = xs,
                    open = os,
                    high = hs,
                    low = ls,
                    close = cs
                )
            ])
    __fig.update_layout( xaxis_rangeslider_visible = False, height = 400 )
    return __fig

def update_layout():
    return html.Div([
                        dcc.Graph( id = 'candles',
                                #animate = True,
                                figure=_create_fig( qbuff ) ),
                        dcc.Interval( id = 'interval', interval = cbPeriodMS )
                    ])

app.layout = update_layout
#fig = go.Figure()

@app.callback(
        Output( "candles", "extendData" ),
        [Input( "interval", "n_intervals" )],
        [State( "candles", 'figure' )]
    )
def update_figure( n_intervals, _fig ):
    #print( 'Adding calnde #%d' % n_intervals )
    _q = get_q()
    _cdsAll = _q.getList()
    _lock = _q.getMutex()

    justAdded = False
    _firstCandle = True

    _xs = []
    _os = []
    _hs = []
    _ls = []
    _cs = []

    with _lock:
        assert( _q.readPosition >= 0 and _q.readPosition <= len( _cdsAll ) )
        print( "(before callback) CandList.readPosition =", _q.readPosition )
        numRead = 0
        for i in range( _q.readPosition, len( _cdsAll ) ):
            justAdded = False
            _cd = _cdsAll[i]
            printCandle( _cd )
            if len(_fig["data"][0]["x"]) == 0:
                justAdded = True
            else:
                #lastBarTime = _fig["data"][0]['x1'][-1]
                #lastBarTimestamp = lastBarTime.timestamp()
                print( '_fig[\"data\"][0][\'x\'][-1] =', _fig["data"][0]['x'][-1], '_cd[i].x =', _cd.x )
                if _fig["data"][0]['x'][-1] != _cd.x:
                    justAdded = True
            print( 'justAdded =', justAdded )

            if justAdded:
                _xs.append( _cd.x )
                _os.append( _cd.o )
                _hs.append( _cd.h )
                _ls.append( _cd.l )
                _cs.append( _cd.c )   
            else:
                assert( _firstCandle )
                _fig['data'][0]['x'][-1] = _cd.x
                _fig['data'][0]['open'][-1] = _cd.o
                _fig['data'][0]['high'][-1] = _cd.h
                _fig['data'][0]['low'][-1] = _cd.l
                _fig['data'][0]['close'][-1] = _cd.c

            numRead += 1
            if _firstCandle:
                _firstCandle = False

            if not justAdded:
                break            #need this to separate updating last bar and adding new bars ( new bars,
                                 #if they are will be added at the next call

        print( "(after callback) num bars read =", numRead )

        if numRead == 0:
            raise PreventUpdate
        else:
            _q.readPosition += numRead

    if justAdded:
        return (
                    {
                       'x': [_xs],
                       'open': [_os],
                        'high': [_hs],
                       'low': [_ls],
                       'close': [_cs]
                    },
                    [0], min( len(_fig["data"][0]["x"]) + len(_xs), g_num_bars_curr )
                )
    else:
        return (
                {
                    "x": [_fig['data'][0]['x']],
                    "open": [_fig['data'][0]['open']],
                    "high": [_fig['data'][0]['high']],
                    "low": [_fig['data'][0]['low']],
                    "close": [_fig['data'][0]['close']]
                },
                [0], len(_fig["data"][0]["x"])
            )    

def printCandle( _cd ):
    print( 'xOCHLV: %d\t%f\t%f\t%f\t%f\t%d' % ( _cd.x, _cd.o, _cd.c, _cd.h, _cd.l, _cd.v ) )

def main():    
    __port = 55555

    rc = RandomCandle( 1000, 1, 20, 5 )
    
    senderObj = Thread( target = sender, args = ( qbuff, rc ) )
    senderObj.start()

    app.run_server( debug = False, port = __port ) #if debug = True, number of run threads are doubled on Windows
    print( 'Dash server is down, joining threads...' )
    try:
        if senderObj.is_alive():
            senderObj.join()
    except RuntimeError as e:
        print( '%s' % e.args[0] )

def sender( _q, _rc ):
    time.sleep( 0.5 ) #maybe this can be deleted without any affection on the code stability

    _cdsAll = _q.getList()
    _lock = _q.getMutex()

    justAdded = None
    numCandles = 1000
    i = 0
    while i < numCandles:
        with _lock:
            cd = _rc.generate()

            justAdded = True
            if len( _cdsAll ) > 0:
                _cdl = _cdsAll[-1]
                if cd.x != _cdl.x:
                    _cdsAll.append( cd )
                else:
                    _cdl.o = cd.o
                    _cdl.h = cd.h
                    _cdl.l = cd.l
                    _cdl.c = cd.c
                    _cdl.v = cd.v

                    if _q.readPosition == len( _cdsAll ):
                        _q.readPosition -= 1
                    justAdded = False
            else:
                _cdsAll.append( cd )
            
            if justAdded:
                print( 'Sent i = %d' % i )
                i += 1

        time.sleep( .001 * sendPeriodMs )

    print( 'Sender thread returns' )


if __name__ == '__main__':
    main()

Please point me out if something in the code could be improved easily. For example, I need to run server with debug = False (Ln 80) because otherwise there two thread instances are launched at line 281 call. I didn’t find how it can be fixed.
Returning the differences between IDLE and cmd, the code above doesn’t work in IDLE but works in cmd. But when cbPeriodMS = 75 at line 12 changed to cbPeriodMS = 200, it works in IDLE (on my machine) too.
And my main question is about update_figure callback. The situation is more obvious when sending period is small, say sendPeriodMs = 10 and callback polling period is much bigger, say cbPeriodMS = 500, the code works fine at this parameters. But I have to break data into two pieces and process first candlestick updating the last one on the plot, do break at the line 239, and then wait for a new callback to process rest candlesticks (and new added ones, if any). So, is it possible to do this in one shot?

OK. Now I can answer on the question in the title myself. If the code needs both to update last bar and add some new ones, one can do the same as the code did when the last bar was updated only: just redraw the whole figure. The changes are below:

#to make parameters when the question really makes sense
sendPeriodMs = 10 
cbPeriodMS = 500

@app.callback(
        Output( "candles", "extendData" ),
        [Input( "interval", "n_intervals" )],
        [State( "candles", 'figure' )]
    )
def update_figure( n_intervals, _fig ):
    #print( 'Adding calnde #%d' % n_intervals )
    _q = get_q()
    _cdsAll = _q.getList()
    _lock = _q.getMutex()

    lastCandleWasUpdated = False
    _firstCandle = True

    _xs = []
    _os = []
    _hs = []
    _ls = []
    _cs = []

    with _lock:
        assert( _q.readPosition >= 0 and _q.readPosition <= len( _cdsAll ) )
        print( "(before callback) CandList.readPosition =", _q.readPosition )
        numRead = 0
        for i in range( _q.readPosition, len( _cdsAll ) ):
            candleWasUpdated = True
            _cd = _cdsAll[i]
            printCandle( _cd )

            print( '_fig[\"data\"][0][\'x\'][-1] =', _fig["data"][0]['x'][-1], '_cd[i].x =', _cd.x )
            if _fig["data"][0]['x'][-1] != _cd.x:
                candleWasUpdated = False
            print( 'candleWasUpdated =', candleWasUpdated )

            if candleWasUpdated:
                assert( _firstCandle )
                _fig['data'][0]['x'][-1] = _cd.x
                _fig['data'][0]['open'][-1] = _cd.o
                _fig['data'][0]['high'][-1] = _cd.h
                _fig['data'][0]['low'][-1] = _cd.l
                _fig['data'][0]['close'][-1] = _cd.c
                lastCandleWasUpdated = True
            else:
                _xs.append( _cd.x )
                _os.append( _cd.o )
                _hs.append( _cd.h )
                _ls.append( _cd.l )
                _cs.append( _cd.c )   


            numRead += 1
            if _firstCandle:
                _firstCandle = False

        print( "(after callback) num bars read =", numRead )

        if numRead == 0:
            raise PreventUpdate
        else:
            _q.readPosition += numRead

    if lastCandleWasUpdated:
        return (
                {
                    "x": [_fig['data'][0]['x']+_xs],
                    "open": [_fig['data'][0]['open']+_os],
                    "high": [_fig['data'][0]['high']+_hs],
                    "low": [_fig['data'][0]['low']+_ls],
                    "close": [_fig['data'][0]['close']+_cs]
                },
                [0], len(_fig["data"][0]["x"])+numRead-1
            )
    else:
        return (
                    {
                       'x': [_xs],
                       'open': [_os],
                        'high': [_hs],
                       'low': [_ls],
                       'close': [_cs]
                    },
                    [0], min( len(_fig["data"][0]["x"]) + len(_xs), g_num_bars_curr )
                )

But for me it’s still unclear, why I should redraw the whole figure when one needs just to adjust the last bar. Is it possible to do this in more optimal way? Or Dash/plotly takes care about all the differences in current and previous data arrays after each callback and doesn’t do much unnecessary job?