python/smartyal/21datalab/bokeh_web/widgets.py

widgets.py


import sys
import os
import time
import random
import numpy
import datetime

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

import requests
import json
import logging
from logging.handlers import RotatingFileHandler
import copy
import random
import time
import threading
import sse
import pytz
import traceback



from bokeh.models import DatetimeTickFormatter, ColumnDataSource, BoxSelectTool, BoxAnnotation, Label, LegendItem, Legend, HoverTool, BoxEditTool, TapTool, Circle
from bokeh.models import Range1d,DataRange1d, Span,LinearAxis, Band, LogAxis
from bokeh import events
from bokeh.models.widgets import RadioButtonGroup, Paragraph, Toggle, MultiSelect, Button, Select, CheckboxButtonGroup,Dropdown
from bokeh.plotting import figure, curdoc
from bokeh.layouts import layout,widgetbox, column, row, Spacer
from bokeh.models import Range1d, PanTool, WheelZoomTool, ResetTool, ToolbarBox, Toolbar, Selection, BoxZoomTool
from bokeh.models import FuncTickFormatter, CustomJSHover, SingleIntervalTicker, DatetimeTicker, CustomJS
from bokeh.themes import Theme
from pytz import timezone
from bokeh.models.glyphs import Rect
from bokeh.models.glyphs import Quad
from bokeh.models.glyphs import VArea,VBar

from bokeh.models.renderers import GlyphRenderer



#RenderLevel = Enumeration(image, underlay, glyph, guide, annotation, overlay)

haveLogger = False
globalAlpha = 1.0#0.3

globalAnnotationLevel  = "image"
globalBackgroundsLevel = "underlay"
globalThresholdsLevel = "underlay"
globalBandsLevel = "overlay"
globalAnnotationsAlpha = 0.90
globalThresholdsAlpha = 0.5
globalBackgroundsAlpha = 0.2
globalBackgroundsHighlightAlpha = 0.6
globalY2width = 2

globalRESTTimeout = 90
globalInfinity = 1000*1000
BOX_ANNO = False # set this false to use the Rect for annos, true=use Boxannotations


def setup_logging(loglevel=logging.DEBUG,tag = ""):
    global haveLogger
    print("setup_logging",haveLogger)
    if not haveLogger:
        # fileName = 'C:/Users/al/devel/ARBEIT/testmyapp.log'
        #logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', level=loglevel)

        #remove all initial handlers, e.g. console
        allHandlers = logging.getLogger('').handlers
        for h in allHandlers:
            logging.getLogger('').removeHandler(h)


        formatter = logging.Formatter('%(asctime)s %(name)-12s thid%(thread)d %(levelname)-8s %(message)s')
        console = logging.StreamHandler()
        console.setLevel(loglevel)
        console.setFormatter(formatter)
        logging.getLogger('').addHandler(console)

        #logfile = logging.FileHandler('./log/widget_'+'%08x' % random.randrange(16 ** 8)+".log")
        #logfile = logging.FileHandler('./widget_' + '%08x' % random.randrange(16 ** 8) + ".log")
        if tag == "":
            tag = '%08x' % random.randrange(16 ** 8)
        logfile = RotatingFileHandler('./log/widget_' + tag+ ".log", maxBytes=1000 * 1000 * 100, backupCount=10)  # 10x100MB = 1GB max
        logfile.setLevel(loglevel)
        logfile.setFormatter(formatter)
        logging.getLogger('').addHandler(logfile)
        haveLogger = True




#import model
from dates import date2secs,secs2date, secs2dateString
from dates import epochToIsoString
import themes #for nice colorsing



#give a part tree of nodes, return a dict with const=value
def get_const_nodes_as_dict(tree):
    consts ={}
    for node in tree:
        if node["type"] in ["const","variable"]:
            consts[node["name"]]=node["value"]
        if node["type"] == "referencer":
            if "forwardPaths" in node:
                consts[node["name"]] = node["forwardPaths"]

    return consts




class TimeSeriesWidgetDataServer():
    """
        a helper class for the time series widget dealing with the connection to the backend rest model server
        it also caches settings which don't change over time
    """
    def __init__(self,modelUrl,avatarPath,userId=None):
        self.url = modelUrl # get the data from here
        self.path = avatarPath # here is the struct for the timeserieswidget
        #self.timeOffset = 0 # the timeoffset of display in seconds (from ".displayTimeZone)
        self.annotations = {}
        self.pendingScoreVariablesUpdate = False# is set to true if there has been a silent update of the score variables list without telling the ts widget
        self.scoreVariables = []
        self.events = None
        self.sseCb = None # the callbackfunction on event
        self.objectsInfo = {} # a dict holding node ids and more info of the current objects in the display
        self.userId = userId

        self.__init_logger(logging.DEBUG)
        self.__init_proxy()
        self.__init_cookies()
        self.__get_settings()
        self.__init_sse()

    def __del__(self):
        print("del times series widget ") #try to understand what bokeh is doing :)

    def __init_sse(self):
        self.sse = sse.SSEReceiver(f'{self.url}event/stream',self.sse_cb,logger=None,cookies = {"21api":self.cookie}) # self,url,callback = None, logger = None, cookies = None):
        self.sse.start()

    def sse_cb(self,data):
        #self.logger.debug(f'sse {data}, {self.settings["observerIds"]}, my id {id(self)}')
        #now we filter out the events which are for me
        if data["data"]!="":
            try:
                dataString = data["data"]
                dataString = dataString.replace("'",'"') # json needs double quote for key/values entries
                parseData = json.loads(dataString)
                #self.logger.debug(f"parsed event {parseData}")
                if data["event"].startswith("global") or ("nodeId" in parseData and parseData["nodeId"] in self.settings["observerIds"]): #only my own observers are currently taken
                        #self.logger.info("sse match")
                        data["data"]=parseData # replace the string with the parsed info
                        if self.sseCb:
                            self.sseCb(data)
            except Exception as ex:
                self.logger.error(f"sse_Cb error {ex}, {sys.exc_info()[0]} {str(traceback.format_exc())}")

    def sse_stop(self):
        self.sse.stop()
    def sse_register_cb(self,cb):
        self.sseCb = cb

    def __init_proxy(self):
        """
            try to open the proxies file, set the local proxy if possible
        """
        self.proxySetting = {}
        try:
            with open('proxies.json','r') as f:
                self.proxySetting = json.loads(f.read())
                self.logger.info("using a proxy!")
        except:
            self.logger.info("no proxy used")
            pass

    def __init_cookies(self):
        #experimental for the cloud setup
        try:
            myPath = os.path.dirname(os.path.realpath(__file__))
            f=open(myPath+"/../../../users.json") #for now just a file, later we use a db
            users = json.loads(f.read())
            f.close()
            #find the user by id
            for name,data in users.items():
                if data["id"]==self.userId:
                    self.cookie = data["cookie"]          
                    self.logger.info(f"set new cookie for {self.userId}:{self.cookie}")  
        except Exception as ex:
            self.cookie=None #  no cookie availabe
            self.logger.error(f"cant load cookie from {self.userId}:{ex}")
        self.logger.debug(f"__init_cookies return {self.userId}:{self.cookie}") 

    
    def __init_logger(self, level=logging.DEBUG):
        setup_logging(loglevel = level, tag=self.path)
        self.logger = logging.getLogger("TSServer")
        self.logger.setLevel(level)
        #handler = logging.StreamHandler()
        #formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
        #handler.setFormatter(formatter)
        #self.logger.addHandler(handler)
        #self.logger.setLevel(level)



    def __web_call(self,method,path,reqData):
        """
            this functions makes a call to the backend model serer to get data
            Args:
                method(string) one of ["GET","POST"]
                path: the nodepath to the time series widtet
                reqData: a dictionary with request data for te query like the list of variables, time limits etc.
            Returns (dict):
                the data from the backend as dict
        """
        self.logger.info("__web_call %s @%s data:%s",method,path,str(reqData))

        response = None
        now = datetime.datetime.now()
        if method.upper() == "GET":
            try:
                response = requests.get(self.url + path, timeout=globalRESTTimeout,proxies=self.proxySetting,cookies={"21api":self.cookie})
            except Exception as ex:
                self.logger.error("requests.get msg:"+str(ex))

        elif method.upper() == "POST":
            now = datetime.datetime.now()
            try:
                response = requests.post(self.url + path, data=json.dumps(reqData), timeout=globalRESTTimeout,
                                         proxies=self.proxySetting,cookies={"21api":self.cookie})
            except Exception as ex:
                self.logger.error("requets.post " + str(ex))

        after = datetime.datetime.now()
        diff = (after-now).total_seconds()
        self.logger.info("response "+str(response)+" took "+ str(diff))
        if not response:
            self.logger.error("Error calling web " + path )
            return None
        else:
            rData = json.loads(response.content.decode("utf-8"))
            return rData

    def get_selected_variables_sync(self, fetch = True):

        selectedVars = []
        if fetch:
            selectedVars1 = self.__web_call("post", "_getbranchpretty", {"node":self.path+".selectedVariables","depth": 100})
            self.mirror["selectedVariables"]= selectedVars1
            if "hasY2Axis" in self.mirror and self.mirror["hasY2Axis"][".properties"]["value"] == True:
                selectedVars2 = self.__web_call("post", "_getbranchpretty", {"node":self.path+".selectedVariablesY2","depth": 100})
                self.mirror["selectedVariablesY2"] = selectedVars2

        if 1:
            #from mirror
            leavesproperties = self.mirror["selectedVariables"][".properties"]["leavesProperties"]
            if leavesproperties:
                nodes = [v for k,v in leavesproperties.items()]
            else:
                nodes=[]

            if "hasY2Axis" in self.mirror and self.mirror["hasY2Axis"][".properties"]["value"]:
                leavesproperties2 = self.mirror["selectedVariablesY2"][".properties"]["leavesProperties"]
                if leavesproperties2:
                    nodes.extend([v for k, v in leavesproperties2.items()])


        #build the info as id:info plus browsepath:info, so we have a faster lookup; both browsepath and id are unique
        selectedVars = [node["browsePath"] for node in nodes]
        self.objectsInfo = {}
        for node in nodes:
            info = copy.deepcopy(node)
            self.objectsInfo[node["id"]]=info
            self.objectsInfo[node["browsePath"]]=info

        self.selectedVariables=copy.deepcopy(selectedVars)
        self.logger.debug(f"get_selected_variables_sync => {self.selectedVariables}")
        return selectedVars

    def is_y2_variable(self,browsePath):
        try:
            if not self.has_y2():
                return False
            if not browsePath:
                return False

            if "selectedVariablesY2" in self.mirror:
                leavesproperties2 = self.mirror["selectedVariablesY2"][".properties"]["leavesProperties"]
                if leavesproperties2:
                    for k,node in leavesproperties2.items():
                        if node["browsePath"] == browsePath:
                            return True
                        if node["id"] == browsePath:
                            return True

            #now check the score vars
            if any([text in browsePath.lower() for text in ["score","limit","expected"]]):
                varName = self.original_name(browsePath)
                leavesproperties2 = self.mirror["selectedVariablesY2"][".properties"]["leavesProperties"]
                if leavesproperties2 and varName:
                    for k, node in leavesproperties2.items():
                        if varName in node["browsePath"]:
                            return True
        except Exception as ex:
            self.logger.error("is y2 error {ex}")

        return False

    def get_path(self):
        return self.path

    def original_name(self, extName):
        return '_'.join(extName.split('.')[-1].split('_')[:-1])


    def update_background_info_from_mirror(self):
        if not hasattr(self,"settings"):
            #at the startup the settings are not yet deployed
            return

        try:
            background = {}
            background["hasBackground"] = self.mirror["hasBackground"][".properties"]["value"]
            background["background"] = self.mirror["background"][".properties"]["forwardRefs"][0]
            background["backgroundMap"] = self.mirror["backgroundMap"][".properties"]["value"]
            if all(key in background for key in ["hasBackground","background","backgroundMap"]):
                self.settings["background"]=copy.deepcopy(background)
            else:
                self.settings["background"]={"hasBackground":False}
                    #we dont have a valid background definition

        except Exception as ex:
            self.logger.error(f"problem loading background {ex}, {str(sys.exc_info()[1])}, disabling background")
            self.settings["background"] = {"hasBackground": False}


    def __get_settings(self):
        """
            get all the settings of the widget and store them also in the self.settings cache
            Returns: none

        """

        self.fetch_mirror()



        #request = [self.path]
        #info = self.__web_call("post","_get",request)

        widgetChildren = [ node[".properties"] for k,node in self.mirror.items() if k!=".properties"] #replace the webcall with data from the mirror
        #also provide it as info for later down

        info=[{"children":widgetChildren}]
        self.settings = get_const_nodes_as_dict(info[0]["children"])

        #compatibility for old style with the settings, we now derive them from the mirror


        #self.settings = get_const_nodes_as_dict(widgetChildren)


        #also grab the selected
        self.get_selected_variables_sync(fetch=False)
        #request = self.path+".selectedVariables"
        #self.selectedVariables=[]
        #nodes = self.__web_call("post","_getleaves",request)
        #for node in nodes:
        #    self.selectedVariables.append(node["browsePath"])
        #get the selectable
        #nodes = self.__web_call('POST',"_getleaves",self.path+'.selectableVariables')
        #self.selectableVariables = []
        #for node in nodes:
        #    self.selectableVariables.append(node["browsePath"])
        #also remeber the timefield as path
        #request = self.path+".table"
        #nodes = self.__web_call("post","_getleaves",request)
        #this should return only one node
        #timerefpath = nodes[0]["browsePath"]+".timeField"
        #another call to get it right
        #nodes = self.__web_call("post", "_getleaves", timerefpath)
        #self.timeNode = nodes[0]["browsePath"]
  

        #now for the annotations
        #if (self.settings["hasAnnotation"] == True) or (self.settings["hasThreshold"] == True):
        #    response = self.__web_call("post","_get",[self.path+"."+"hasAnnotation"])
        #    annotationsInfo = get_const_nodes_as_dict(response[0]["children"])
        #    self.settings.update(annotationsInfo)
        self.annotations=self.fetch_annotations() # get all the annotations

        #for the events
        if "hasEvents" in self.settings and self.settings["hasEvents"] == True:
            self.events = self.fetch_events()

        #grab the info for the buttons
        myButtons=[]
        #for node in info[0]["children"]:
        #    if node["name"]=="buttons":
        #        myButtons = node["children"]

        #now get more info on the buttons
        if myButtons != []:
            buttonInfo = self.__web_call("post","_get",myButtons)
            self.settings["buttons"]=[]
            for button in buttonInfo:
                #find the caption and target
                caption = ""
                target = ""
                for child in button["children"]:
                    if child["name"] == "caption":
                        caption=child["value"]
                    if child["name"] == "onClick":
                        targets = child["forwardRefs"]
                if targets != "":
                    #create that button
                    self.settings["buttons"].append({"name":caption,"targets":targets.copy()})


        # now compile info for the observer #new observers
        # we remeber all ids of observers in our widget
        self.settings["observerIds"]=[]
        for node in info[0]["children"]:
            if node["type"]=="observer":
                self.settings["observerIds"].append(node["id"])

        # now grab the info for the backgrounds
        try:
            background={}
            for node in info[0]["children"]:
                if node["name"] == "hasBackground":
                    background["hasBackground"] = node["value"]
                if node["name"] == "background" and background["hasBackground"]==True:
                    # we take only the first entry (there should be only one) of the referencer:
                    # this is the nodeId of the background values
                    background["background"]=node["forwardRefs"][0]
                if node["name"] == "backgroundMap":
                    background["backgroundMap"] = copy.deepcopy(node["value"])      #the json map for background values and color mapping
            if all(key in background for key in ["hasBackground","background","backgroundMap"]):
                self.settings["background"]=copy.deepcopy(background)
            else:
                self.settings["background"]={"hasBackground":False}
                    #we dont have a valid background definition
        except Exception as ex:
            self.logger.error(f"problem loading background {ex}, {str(sys.exc_info()[1])}, disabling background")
            self.settings["background"] = {"hasBackground": False}


        #self.logger.debug("SERVER.SETTINGS-------------------------")
        #self.logger.debug("%s",json.dumps(self.settings,indent=4))


    def fetch_events(self):
        # return a dict with {id:eventdict}
        nodes = self.__web_call("post", "_getleaves", self.path + ".hasEvents.events")
        self.logger.debug(f"fetch_events: {len(nodes)} events")
        query = {"nodes":[node["id"] for node in nodes if node["type"]=="eventseries"]}
        events = self.__web_call("post","_getEvents",query)
        self.logger.debug(f"events result {len(events)}")
        self.events = events
        return events   # dict "nodeid":{"events":{"one":....,"two":... }"eventMap"....},"id2":{}

    def get_events(self):
        return copy.deepcopy(self.events)


    def fetch_annotations_old(self):
        # return a dict with {id:annotationdict}
        #get a fresh copy of the annotations
        nodes = self.__web_call("post", "_getleaves", self.path + ".hasAnnotation.annotations")
        self.logger.debug(f"_fetch_annotations(): {len(nodes)} annotations")
        # now parse the stuff and build up our information
        annotations = {}
        for node in nodes:
            if node["type"] == "annotation":
                try:
                    annotation = get_const_nodes_as_dict(node["children"])
                    annotation["browsePath"]=node["browsePath"]
                    annotation["id"]=node["id"]
                    annotation["name"] = node["name"]
                    #convert some stuff
                    if "startTime" in annotation:
                        annotation["startTime"] = date2secs(
                            annotation["startTime"]) * 1000
                    if "endTime" in annotation:
                        annotation["endTime"] = date2secs(
                            annotation["endTime"]) * 1000
                    if annotation["type"] in ["threshold","motif"]:
                        # we also pick the target, only the first
                        annotation["variable"] =  annotation["variable"][0]
                    annotations[node["id"]]=annotation
                except Exception as ex:
                    self.logger.error(f"problem loading annotations {ex}, {str(sys.exc_info()[1])}")
                    continue
        #self.logger.debug("server annotations" + json.dumps(self.annotations, indent=4))
        self.annotations = copy.deepcopy(annotations)
        return annotations


    def fetch_annotations(self):
        # return a dict with {id:annotationdict}
        #get a fresh copy of the annotations
        nodes = self.__web_call("post", "_getAnnotations", self.path + ".hasAnnotation.annotations")
        if not nodes:
            nodes = {}
        for id,node in nodes.items():
            if "startTime" in node:
                node["startTime"] = date2secs(node["startTime"])*1000
            if "endTime" in node:
                node["endTime"] = date2secs(node["endTime"]) * 1000
        self.logger.debug(f"_fetch_annotations(): {len(nodes)} annotations")
        self.annotations = copy.deepcopy(nodes)
        return nodes

    def fetch_annotations_differential(self,info):
        #args is a dict :{"new":[id1,id2], "delete":[id3,id4] "modify":[id5...] lists that contain ids where there have been changes

        #the deletes
        for id in info["delete"]:
            if id in self.annotations:
                del self.annotations[id]


        #merge the new and modify and put them in our local info
        nodes = {k:v for k,v in info["new"].items() if k not in self.annotations}
        nodes.update(info["modify"])

        for id,annotation in nodes.items():
            try:
                # convert some stuff
                if "startTime" in annotation and type(annotation["startTime"]) is str:
                        annotation["startTime"] = date2secs(annotation["startTime"]) * 1000
                if "endTime" in annotation and type(annotation["endTime"]) is str:
                    annotation["endTime"] = date2secs(annotation["endTime"]) * 1000
                if annotation["type"] in ["threshold", "motif"]:
                    # we also pick the target, only the first
                    annotation["variable"] = annotation["variable"][0] if type(annotation["variable"]) is list else annotation["variable"]
                self.annotations[id] = annotation #update or new entry
            except Exception as ex:
                self.logger.error(f"problem loading annotations {ex}, {str(sys.exc_info()[1])}")
                continue
        return self.annotations






    ##############################
    ## INTERFACE FOR THE WIDGET
    ##############################

    def execute_function(self,descriptor):
        """ trigger the execution of a registered function in the backend """
        return self.__web_call("POST","_execute",descriptor)


    def get_values(self,varList):
        """  get a list of values of variables this is for type varialb, const """
        return self.__web_call("POST","_getvalue",varList)


    def get_data(self,variables,start=None,end=None,bins=300,aggregation=None):

        """
            retrieve a data table from the backend
            Args:
                variables(list): the nodes from which the data is retrieved
                start (float): the startime in epoch ms
                end (float): the endtime in epoch ms
                bins (int): the number of samples to be retrieved between the start and end time
            Returns (dict):
                the body of the response of the data request of the backend
        """
        self.logger.debug("server.get_data()")
        if not aggregation:
            aggregation=self.get_aggregation()

        varList = self.selectedVariables.copy()
        #include background values if it has background enabled
        if self.settings["background"]["hasBackground"]==True:
            #check about background
            if "background" in self.mirror["visibleElements"][".properties"]["value"] and self.mirror["visibleElements"][".properties"]["value"]["background"] == True:
                #include the node it holding the backgrounds
                varList.append(self.settings["background"]["background"])
             
        # now get data from server
        if "xAxisType" in self.mirror and self.mirror["xAxisType"][".properties"]["value"]=="number":
            pass
        else:
            #default is datetime and we must do the *1000
            if start:
                start=start/1000
            if end:
                end=end/1000

        #include the nans for _limitMin, _limitMax _expected
        includeAllNan = [var for var in varList if (var.endswith("_limitMin") or var.endswith("_limitMax") or var.endswith("_expected")) ]
        if includeAllNan == []:
            includeAllNan = False
        body = {
            "nodes": varList,
             "startTime" : start,
             "endTime" :   end,
            "bins":bins,
            "resampleMethod":aggregation,
            "includeTimeStamps": "02:00",
            "includeIntervalLimits" : True,
            "includeAllNan":includeAllNan
        }
        #("includeallnan",includeAllNan)
        r=self.__web_call("POST","_getdata",body)
        if not r:
            return None
        #convert the time to ms since epoch
        for entry in r:
            if entry.endswith("__time"):
                times = numpy.asarray(r[entry])
                debug = copy.deepcopy(times.tolist())
                if "xAxisType" in self.mirror and self.mirror["xAxisType"][".properties"]["value"]=="number":
                    r[entry]=times.tolist()
                else:
                    #need to convert
                    r[entry]=(times*1000).tolist()
                #print(f"times {debug}")
                #print(f"times {entry} {[epochToIsoString(t) for t in debug]}")
        #make them all lists and make all inf/nan etc to nan
        for k,v in r.items():
            r[k]=[value if numpy.isfinite(value) else numpy.nan for value in v]

        #self.logger.debug(str(r))
        return r


    def get_time_node(self):
        return self.timeNode

    def get_mirror(self):
        return self.mirror

    def fetch_mirror(self,small = False):
        if small:
            query = {"node":self.path,"depth":1,"ignore":["observer"]}
        else:
            query = {"node":self.path,"depth":100,"ignore":["targets","hasAnnotation.anno","hasAnnotation.new","selectableVariables","contextMenu","showHide"]}
        self.mirror = self.__web_call("post", "_getbranchpretty", query)
        self.update_score_variables_from_mirror()
        self.update_background_info_from_mirror()
        return self.mirror

    def fetch_score_variables(self):

        old = copy.deepcopy(self.scoreVariables)

        nodes = self.__web_call("post", "_getleaves", self.path + ".scoreVariables")
        scoreVariables = [node["browsePath"] for node in nodes]
        self.scoreVariables = copy.deepcopy(scoreVariables)
        #self.logger.debug(f"fetch_score_variables : old {old}, new:{self.scoreVariables}")

        if old != self.scoreVariables or self.pendingScoreVariablesUpdate:
            self.pendingScoreVariablesUpdate = False
            return True # have something new
        else:
            return False

    def update_score_variables_from_mirror(self):
        scoreVars = []
        for id,node in self.mirror["scoreVariables"][".properties"]["leavesProperties"].items():
            scoreVars.append(node["browsePath"])
        self.pendingScoreVariablesUpdate = True # if the TSwidget asks later if there was a diff in the meantime, we better say yes to not skip updates
        self.scoreVariables = scoreVars



    def get_current_colors(self):
        return self.mirror["currentColors"][".properties"]["value"]

    def update_current_colors(self,currentColors):
        if currentColors == self.mirror["currentColors"][".properties"]["value"]:
            return # nothing to do
        nodesToModify = [{"browsePath": self.path + ".currentColors", "value": currentColors}]
        self.mirror["currentColors"][".properties"]["value"] = currentColors
        self.__web_call('POST', 'setProperties', nodesToModify)

    #def get_variables_selectable(self):
    #    """ returns the selectable variables from the cache"""
    #    return copy.deepcopy(self.selectableVariables)

    def get_variables_selected(self):
        """ return list of selected variables from the cache"""
        return copy.deepcopy(self.selectedVariables)

    def has_y2(self):
        if "hasY2Axis" in self.mirror and self.mirror["hasY2Axis"][".properties"]["value"]:
            return True
        return False

    def get_annotations(self):
        #return self.annotations
        return copy.deepcopy(self.annotations) #this must be a deepcopy, as we make compares of old/new

    def bokeh_time_to_string(self,epoch):
        localtz =  timezone(self.settings["timeZone"])
        dt = datetime.datetime.fromtimestamp(epoch/1000, localtz)
        return dt.isoformat()

    def get_score_variables(self):
        return copy.deepcopy(self.scoreVariables)

    def is_score_variable(self,variableBrowsePath):
        if variableBrowsePath in self.scoreVariables:
            return True
        if variableBrowsePath.upper().endswith("SCORE"):
            return True
        return False

    def get_score_marker(self,variableBrowsePath):
        #look for the uiInfo in the mirror
        #find this variable and potential info about the display
        for id,info in self.mirror["scoreVariables"][".properties"]["leavesProperties"].items():
            if info["browsePath"] == variableBrowsePath:
                if "uiInfo" in info:
                    return info["uiInfo"]["marker"]
        return "cross"

    def get_static_line_color(self,variableBrowsePath):
        for id,info in self.mirror["selectedVariables"][".properties"]["leavesProperties"].items():
            if "browsePath" in info and info["browsePath"]==variableBrowsePath:
                if "uiInfo" in info and "lineColor" in info["uiInfo"]:
                    return info["uiInfo"]["lineColor"]
        return None


    #start and end are ms(!) sice epoch, tag is a string
    def add_annotation(self,start=0,end=0,tag="unknown",type="time",min=0,max=0, var = None):
        """
            add a new user annotation to the model and also add it to the local cache
            Args:
                start(float): the start time in epcoh ms
                end(float): the end time in epoch ms
                tag (string) the tag to be set for this annotation
            Returns:
                the node browsePath of this new annotation
        """

        #place a new annotation into path
        nodeName = '%08x' % random.randrange(16 ** 8)
        annoPath = self.path + "." + "hasAnnotation.newAnnotations."+nodeName
        if type == "time":
            nodesToCreate = [
                {"browsePath": annoPath,"type":"annotation"},
                {"browsePath": annoPath + '.type',"type":"const","value":"time"},
                {"browsePath": annoPath + '.startTime',"type":"const","value":self.bokeh_time_to_string(start)},
                {"browsePath": annoPath + '.endTime', "type": "const", "value":self.bokeh_time_to_string(end)},
                {"browsePath": annoPath + '.tags', "type": "const", "value": [tag]}
                ]
        elif type =="threshold":
            nodesToCreate = [
                {"browsePath": annoPath, "type": "annotation"},
                {"browsePath": annoPath + '.type', "type": "const", "value": "threshold"},
                {"browsePath": annoPath + '.min', "type": "const", "value": min},
                {"browsePath": annoPath + '.max', "type": "const", "value": max},
                {"browsePath": annoPath + '.tags', "type": "const", "value": [tag]},
                {"browsePath": annoPath + '.variable', "type": "referencer", "targets": [var]}

            ]
        elif type == "motif":
            nodesToCreate = [
                {"browsePath": annoPath, "type": "annotation"},
                {"browsePath": annoPath + '.type', "type": "const", "value": "motif"},
                {"browsePath": annoPath + '.startTime', "type": "const", "value": self.bokeh_time_to_string(start)},
                {"browsePath": annoPath + '.endTime', "type": "const", "value": self.bokeh_time_to_string(end)},
                {"browsePath": annoPath + '.tags', "type": "const", "value": [tag]},
                {"browsePath": annoPath + '.variable', "type": "referencer", "targets": [var]}

            ]
        else:
            self.logger.error(f"can't create anno type {type}")
            return None

        self.logger.debug("creating anno %s",str(nodesToCreate))
        #res = self.__web_call('POST','_create',nodesToCreate)
        res = [self.__web_call('POST','_createAnnotation',nodesToCreate)] #returns a single value

        if res:
            #the first is our node id
            #now also update our internal list
            anno  = {"startTime":start,"endTime":end,"tags":[tag],"min":min,"max":max,"type":type,"variable":var,"id":res[0],"name":nodeName,"browsePath":annoPath}
            self.annotations[anno["id"]] = copy.deepcopy(anno)
            return anno
        else:
            return None


    def adjust_annotation(self,anno):
        """
            change an exising annotation and write it back to the model via REST
            Args:
                anno [dict]: contains entries to be overwritten in the original annotation dict
        """
        if anno["id"] not in self.annotations:
            return False
        self.logger.debug(f"ser .adjust_annotation {anno}")
        self.annotations[anno["id"]].update(anno)
        path = anno["id"]# we build a "fancy" browsepath as nodeid.name.name
        if anno['type'] in ["time","motif"]:
            #for time annotation we write the startTime and endTime
            nodesToModify =[
                {"browsePath": path + ".startTime", "value":self.bokeh_time_to_string(anno["startTime"])},
                {"browsePath": path + ".endTime", "value": self.bokeh_time_to_string(anno["endTime"])}
            ]
        elif anno['type'] == "threshold":
            nodesToModify = [
                {"browsePath": path + ".min", "value": anno["min"]},
                {"browsePath": path + ".max", "value": anno["max"]}

            ]
        else:
            self.logger.error("adjust_annotations : unsopported type")
            return

        res = self.__web_call('POST', 'setProperties', nodesToModify)





    def delete_annotations(self,deleteList):
        """ delete existing annotation per browsePath from model and cache"""
        for nodePath in deleteList:
            del self.annotations[nodePath]
        self.__web_call("POST","_delete",deleteList)
        pass

    def set_variables_selected(self, varList, updateLocalNow=True):
        """ update the currently selected variables to cache and backend
            if we set updateLocalNow to False, we do not update the local list, that means we will
            only detect the changes in the next sse event
        """
        if self.has_y2():
            #this function is mainly used for deleting lines on legend click
            # so we try to find out if a line is a y2 axis line and handle them accordingly
            oldLines = self.get_variables_selected()
            oldLines2 = [line for line in oldLines if self.is_y2_variable(line)]
            oldLines1 = list(set(oldLines)-set(oldLines2))

            newLines2 = [line for line in varList if self.is_y2_variable(line)]
            if set(newLines2)!= set(oldLines2):
                #must write the second y axis
                query = {"deleteExisting": True, "parent": self.path + ".selectedVariablesY2", "add": list(newLines2), "allowDuplicates":False}
                self.__web_call("POST", "_references", query)
            if set(newLines2):
                newLines1 = list(set(varList)-set(newLines2))
            else:
                newLines1 = varList
            query = {"deleteExisting": True, "parent": self.path + ".selectedVariables", "add": list(newLines1), "allowDuplicates":False}
            self.__web_call("POST", "_references", query)
            if updateLocalNow:
                self.selectedVariables=varList.copy()



        else:
            query={"deleteExisting":True,"parent":self.path+".selectedVariables","add":varList}
            self.__web_call("POST","_references",query)
            if updateLocalNow:
                self.selectedVariables=varList.copy()
        return


    def add_variables_selected(self,addList,addListY2=[],updateLocalNow=True):


        selectedList = copy.deepcopy(self.get_variables_selected())
        selectedList.extend(addList)
        #selectedList.extend(addListY2)

        if addList:
            query={"deleteExisting":True,"parent":self.path+".selectedVariables","add":selectedList,"allowDuplicates":False}
            self.__web_call("POST","_references",query)

        if self.has_y2() and addListY2:
            #with y2 we need to distiguish the scores for y2 or not
            query = {"deleteExisting": False, "parent": self.path + ".selectedVariablesY2", "add": addListY2,"allowDuplicates":False}
            self.__web_call("POST", "_references", query)

        if updateLocalNow:
            selectedList.extend(addListY2)
            self.selectedVariables=selectedList

        return

    def add_variables_selected_y2(self,addList,updateLocalNow=True):
        #selectedList = copy.deepcopy(self.get_variables_selected())
        #selectedList.extend(addList)
        query={"deleteExisting":False,"parent":self.path+".selectedVariablesY2","add":addList}
        self.__web_call("POST","_references",query)
        if updateLocalNow:
            self.selectedVariables.extend(addList)
        return


    def get_settings(self):
        return copy.deepcopy(self.settings)

    def get_aggregation(self):
        if "aggregation" in self.mirror:
            return self.mirror["aggregation"][".properties"]["value"]
        else:
            return None

    def refresh_settings(self):
        self.__get_settings()

    def set_background_highlight(self,x,y,backStart,backEnd,remove=False):
        if remove:
            query = {"browsePath":self.path+".backgroundHighlight","type":"variable","value":{}}
        else:
            query = {"browsePath":self.path+".backgroundHighlight","type":"variable","value":{
                "x":x/1000,
                "y":y,
                "left":backStart/1000,
                "right":backEnd/1000,
                "start":self.bokeh_time_to_string(backStart),
                "end":self.bokeh_time_to_string(backEnd)
            }}
        self.__web_call("POST","_create",[query])


    def select_annotation(self,annoList):
        #anno list is a list of browsepaths
        query = {"deleteExisting": True, "parent": self.path + ".hasAnnotation.selectedAnnotations", "add": annoList}
        self.__web_call("POST", "_references", query)
        return

    def set_xy_range(self,start,end,yMin=None,yMax=None):
        startTimeString = self.bokeh_time_to_string(start)
        endTimeString = self.bokeh_time_to_string(end)

        self.mirror["startTime"][".properties"]["value"]=startTimeString
        self.mirror["endTime"][".properties"]["value"] = endTimeString


        query= [
            {"browsePath": self.path+".startTime","value":startTimeString},
            {"browsePath": self.path + ".endTime", "value": endTimeString}]
        if type(yMin) is None and type(yMax) is None:
            pass
        else:
            if "yLimits" in self.mirror:
                self.mirror["yLimits"][".properties"]["value"]=[yMin,yMax] #directly local write to avoid circle activities via event
                query.append({"browsePath": self.path +"yLimits","value":[yMin,yMax]})

        self.__web_call("POST","setProperties",query)
        return

    def set_y_range(self,yMin=None,yMax=None):

        if "yLimits" in self.mirror:
            self.mirror["yLimits"][".properties"]["value"] = [yMin, yMax]
            query = [{"browsePath": self.path + ".yLimits", "value": [yMin, yMax]}]
            self.__web_call("POST", "setProperties", query)
        return True


class TimeSeriesWidget():
    def __init__(self, dataserver,curdoc=None):
        self.curdoc = curdoc
        self.id = "id#"+str('%8x'%random.randrange(16**8))
        self.__init_logger()
        self.logger.debug("__init TimeSeriesWidget()")
        self.server = dataserver
        self.height = 600
        self.width = 900
        self.lines = {} #keeping the line objects
        self.legendItems ={} # keeping the legend items
        self.legend ={}
        self.hasLegend = False
        #self.data = None
        self.columnData = {}
        self.inPeriodicCb = False
        self.dispatchList = [] # a list of function to be executed in the bokeh app context
                                # this is needed e.g. to assign values to renderes etc
        self.dispatchLock = threading.Lock() # need a lock for the dispatch list
        #self.dispatcherRunning = False # set to true if a function is still running
        self.annotationTags = []
        self.hoverTool = None
        self.showThresholds = True # initial value to show or not the thresholds (if they are enabled)
        self.showMotifs = False
        self.streamingMode = False # is set to true if streaming mode is on
        self.annotations = {} #   holding the bokeh objects of the annotations
        self.userZoomRunning = False # set to true during user pan/zoom to avoid stream updates at that time
        self.inStreamUpdate = False # set true inside the execution of the stream update
        self.backgrounds = [] #list of current boxannotations dict entries: these are not the renderers
        self.threadsRunning = True # the threads are running: legend watch
        self.annotationsVisible = False # we are currently not showing annotations
        self.boxModifierVisible = False # we are currently no showing the modifiert lines
        self.backgroundHighlightVisible = False # we currently show a background hightlighted
        self.renderers = {}  # each element is ["id":["renderer":object,"info":annoDict] these are the created renderers to be later used e.g. annotations

        self.streamingUpdateData = None
        self.showBackgrounds = False
        self.showAnnotationTags = [] # a list with tags to display currently
        self.showAnnotations = False # curently displaying annotations
        self.currentAnnotationVariable = None
        self.currentAnnotationTag = None

        self.renderersLock = threading.Lock()
        self.renderersGarbage = [] # a list of renderers to be deleted when time allowes

        self.autoAdjustY = True # autoscaling of the y axis
        self.inPan = False      #we are not currently in pan mode
        self.annoHovers=[]      #holding the objects for hovering annotatios (extra glyph, eg. a circle)

        self.eventLines = {}    #holding event line renderes and the columndatasources
        self.eventsVisible = False  #set true if events are currently turned on
        self.scrollLabel = None
        self.annotationsInfo = {}       #holding annotations
        self.hasY2 = False

        self.__init_figure() #create the graphical output

        self.init_additional_elements() # we need the observer already here eg for the scores s we might modifiy the backend and rely on the callback

        self.__init_new_observer()  #

        self.debug = None
    class ButtonCb():
        """
            a wrapper class for the user button callbacks. we need this as we are keeping parameters with the callback
            and the bokehr callback system does not extend there
        """
        def __init__(self,parent,parameter):
            self.parameter = parameter
            self.parent = parent
        def cb(self):
            self.parent.logger.info("user button callback to trigger %s",str(self.parameter))
            self.parent.server.execute_function(self.parameter[0]) # we just trigger the first reference
            pass


    def __init_logger(self, level=logging.DEBUG):
        """initialize the logging object"""
        setup_logging()
        self.logger = logging.getLogger("TSWidget")
        self.logger.setLevel(logging.DEBUG)
        #handler = logging.StreamHandler()

        #formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
        #handler.setFormatter(formatter)
        #self.logger.addHandler(handler)

        #self.logger.setLevel(level)


    def log_error(self):
        self.logger.error(f"{sys.exc_info()[1]}, {traceback.format_exc()}")

    def observer_cb(self,data):
        """
         called by the ts server on reception of an event from the model server
         events are
         - update for lines (selection of lines, add, remove lines)
         - update in the backgrounds
         - streaming update
           we can do calls to the restservice here but can't work with the bokeh data, therefore we
           dispatch functions to be executed in the callback from the bokeh loop

        """
        self.logger.debug(f"observer_cb {data['event']}")
        if data["event"] == "timeSeriesWidget.variables" or data["event"] == "global.timeSeries.value":
            #refresh the lines
            if data["event"] == "timeSeriesWidget.variables":
                self.server.get_selected_variables_sync() # get the new set of lines
                self.__dispatch_function(self.update_scores)
            self.logger.debug("dispatch the refresh lines")
            self.__dispatch_function(self.refresh_plot) #this includes the backgrounds
        elif data["event"] == "timeSeriesWidget.background":
            self.logger.debug("dispatch the refresh background")
            self.__dispatch_function(self.refresh_backgrounds)

        elif data["event"] == "global.series.stream":
            #this is a stream update, we only do something if the stream update in inside the visible area
            if self.stream_update_is_relevant(data):
                self.__dispatch_function(self.stream_update_new, arg=copy.deepcopy(data))
            else:
                self.logger.debug("stream update not relevant, ignore!")

        elif data["event"] == "timeSeriesWidget.stream":
            self.logger.debug(f"self.streamingMode {self.streamingMode}")
            if self.streamingMode and not self.streamingUpdateData:
                self.logger.debug("get stream data")
                #we update the streaming every second
                #get fresh data, store it into a variable and make the update on dispatch in the context of bokeh
                variables = self.server.get_variables_selected()
                variablesRequest = variables.copy()
                #variablesRequest.append("__time")  # make sure we get the time included
                #self.logger.debug(f"request stream data{self.streamingInterval}")
                self.streamingUpdateDataInterval = self.streamingInterval #store this to check later if it has changed
                self.streamingUpdateData = self.server.get_data(variablesRequest, -self.streamingInterval, None,
                                                                self.server.get_settings()["bins"])  # for debug
                self.__dispatch_function(self.stream_update)
            elif not self.streamingMode:
                #do the same as for the "timeseriesWidget.variables" event
                # refresh the lines
                self.server.get_selected_variables_sync()  # get the new set of lines
                self.logger.debug("dispatch the refresh lines")
                self.__dispatch_function(self.update_scores)
                self.__dispatch_function(self.refresh_plot)

        elif data["event"] == "timeSeriesWidget.annotations":
            self.logger.info(f"must reload annotations")
            #self.reInitAnnotationsVisible = self.annotationsVisible #store the state
            # sync from the server
            #self.__dispatch_function(self.reinit_annotations)
            self.__dispatch_function(self.update_annotations_and_thresholds,arg=copy.deepcopy(data))

        elif data["event"] == "global.annotations":
            self.__dispatch_function(self.update_annotations_and_thresholds,arg=copy.deepcopy(data))

        elif data["event"] == "timeSeriesWidget.eventSeries":
            self.logger.debug(f"must reload events")
            self.__dispatch_function(self.update_events,data)

        elif data["event"] == "global.eventSeries.value":
            self.logger.debug(f"must reload events")
            self.__dispatch_function(self.update_events, data)

        elif data["event"] == "timeSeriesWidget.visibleElements":
            self.logger.debug("update the visible Elements")
            eventData  = data["data"]

            #check for start/EndTime
            if "sourcePath" in eventData:
                serverPath = self.server.get_path()
                for var in ["startTime","endTime","yLimits"]:
                    if eventData["sourcePath"] == serverPath+"."+var:
                        if self.server.get_mirror()[var][".properties"]["value"] == eventData["value"]:
                            self.logger.info("sync x asis not needed")
                            return # ignore this event
                            #must sync the x axis




            oldMirror = copy.deepcopy(self.server.get_mirror())
            visibleElementsOld = oldMirror["visibleElements"][".properties"]["value"]
            visibleTagsOld =oldMirror["hasAnnotation"]["visibleTags"][".properties"]["value"]
            if "hasEvents" in oldMirror:
                visibleEventsOld = oldMirror["hasEvents"]["visibleEvents"][".properties"]["value"]
            else:
                visibleEventsOld = None

            newMirror = copy.deepcopy(self.server.fetch_mirror())
            visibleElementsNew = newMirror["visibleElements"][".properties"]["value"]
            visibleTagsNew = newMirror["hasAnnotation"]["visibleTags"][".properties"]["value"]
            if "hasEvents" in newMirror:
                visibleEventsNew = newMirror["hasEvents"]["visibleEvents"][".properties"]["value"]
            else:
                visibleEventsNew = None

            self.logger.debug(f"mirror old {oldMirror} => new {newMirror}")

            for entry in ["thresholds","annotations","scores","background","motifs","events"]:
                #check for turn on:
                if entry in visibleElementsNew and visibleElementsNew[entry] == True:
                    if not entry in visibleElementsOld or visibleElementsOld[entry] == False:
                        # element was turned on
                        if entry == "annotations":
                            self.__dispatch_function(self.show_annotations)
                        elif entry == "thresholds":
                            self.__dispatch_function(self.show_thresholds)
                        elif entry == "scores":
                            self.__dispatch_function(self.show_scores)
                        elif entry == "background":
                            self.__dispatch_function(self.show_backgrounds)
                        elif entry == "motifs":
                            self.__dispatch_function(self.show_motifs)
                        elif entry == "events":
                            self.__dispatch_function(self.show_all_events)

                if entry in visibleElementsOld and visibleElementsOld[entry] == True:
                    if not entry in visibleElementsNew or visibleElementsNew[entry]== False:
                        # element was turned off
                        if entry == "annotations":
                            self.__dispatch_function(self.hide_annotations)
                        elif entry == "thresholds":
                            self.__dispatch_function(self.hide_thresholds)
                        elif entry == "scores":
                            self.__dispatch_function(self.hide_scores)
                        elif entry == "background":
                            self.__dispatch_function(self.hide_backgrounds)
                        elif entry == "motifs":
                            self.__dispatch_function(self.hide_motifs)
                        elif entry == "events":
                            self.eventsVisible=False # set this false right away (especially important for "views" as they might trigger the hide and show both in a row
                            self.__dispatch_function(self.hide_all_events)


            #visible tag selections for annotations has changed
            if (visibleTagsOld != visibleTagsNew) and self.showAnnotations:
                self.__dispatch_function(self.update_annotations)

            if (visibleEventsOld != visibleEventsNew) and self.eventsVisible:
                self.__dispatch_function(self.show_all_events)
            #startime/endtime has changed

            if (oldMirror["startTime"][".properties"]["value"] !=
                newMirror["startTime"][".properties"]["value"]) or (
                 oldMirror["endTime"][".properties"]["value"] !=
                 newMirror["endTime"][".properties"]["value"]):
                start = date2secs(newMirror["startTime"][".properties"]["value"])*1000
                end = date2secs(newMirror["endTime"][".properties"]["value"])*1000
                #self.rangeStart = date2secs(newMirror["startTime"][".properties"]["value"])*1000
                #self.rangeEnd = date2secs(newMirror["endTime"][".properties"]["value"])*1000
                self.logger.debug("start/end changed")
                times = {"start":start,"end":end}
                self.__dispatch_function(self.sync_x_axis,times)

            if "yLimits" in oldMirror and "yLimits" in newMirror and oldMirror["yLimits"][".properties"]["value"] != newMirror["yLimits"][".properties"]["value"]:
                self.__dispatch_function(self.set_y_axis, newMirror["yLimits"][".properties"]["value"])


            #check if streaming mode has changed
            if oldMirror["streamingMode"][".properties"]["value"] != newMirror["streamingMode"][".properties"]["value"]:
                if newMirror["streamingMode"][".properties"]["value"]:
                    self.start_streaming()
                else:
                    self.stop_streaming()

            if oldMirror["panOnlyX"][".properties"]["value"] != newMirror["panOnlyX"][".properties"]["value"]:
                self.set_pan_tool(newMirror["panOnlyX"][".properties"]["value"])

            if "showMarker" in newMirror:
                if oldMirror["showMarker"][".properties"]["value"] != newMirror["showMarker"][".properties"]["value"]:
                    if newMirror["showMarker"][".properties"]["value"]:
                        self.__dispatch_function(self.show_marker)
                    else:
                        self.__dispatch_function(self.hide_marker)

            if "showLegend" in newMirror:
                if oldMirror["showLegend"][".properties"]["value"] != newMirror["showLegend"][".properties"]["value"]:
                    if newMirror["showLegend"][".properties"]["value"]:
                        self.__dispatch_function(self.show_legend)
                    else:
                        self.__dispatch_function(self.hide_legend)

            if "autoScaleY" in newMirror:
                self.autoAdjustY = newMirror["autoScaleY"][".properties"]["value"]
                if newMirror["autoScaleY"][".properties"]["value"]==True and oldMirror["autoScaleY"][".properties"]["value"]==False:
                    #turn on autoscale
                    self.__dispatch_function(self.adjust_y_axis_limits)

        elif data["event"] == "timeSeriesWidget.values":
            #the data has changed, typically the score values?
            pass

        elif data["event"] == "timeSeriesWidget.newAnnotation":
            #self.logger.debug(f"draw anno!")
            self.__dispatch_function(self.draw_new_annotation)

    def update_scores(self):
        if self.server.fetch_score_variables():
           if self.showScores:
                self.show_scores()

    def show_legend(self):
        self.plot.legend.visible = True

    def hide_legend(self):
        self.plot.legend.visible = False

    def hide_marker(self):
        self.remove_renderers([lin+"_marker" for lin in self.lines])
    def show_marker(self):
        self.logger.debug("show marker")

        for variableName in self.lines:

            markerName = variableName + "_marker"
            try:
                color = self.lines[variableName].glyph.line_color
            except:
                self.logger.warning(f"show_marker() {variableName} has no glyph")
                continue
            if self.server.is_y2_variable(variableName):
                marker = self.plot.circle(x="x", y="y", line_color=color, fill_color=color,
                                          source=self.columnData[variableName], name=markerName,y_range_name="y2",
                                          size=7)  # x:"time", y:variableName #the legend must havee different name than the source bug

            else:
                marker = self.plot.circle(x="x",y="y", line_color=color, fill_color=color,
                                      source=self.columnData[variableName], name=markerName,
                                      size=3)  # x:"time", y:variableName #the legend must havee different name than the source bug


        pass

    def update_column_datas(self,newData):

        if self.columnData =={}:
            self.logger.info("init the colum data")
            #for var in self.server.get_variables_selectable():
            #    self.columnData[var]=ColumnDataSource({"x":[],"y":[]})

        if "__time" in newData:
            del newData["time"]


        for var in newData:
            if not var.endswith("__time"):
                if var.endswith("_limitMax"):
                    #special dictionary
                    minName = var[:-len("_limitMax")]+"_limitMin"
                    if minName in newData:
                        dic = {"x":newData[var+"__time"],
                               "upper":newData[var],
                               "lower":newData[minName],
                               "y":newData[var]} # is needed for the auto adjust y limits
                        dic = self.insert_band_breaks(dic)
                    else:
                        #min is missing, can't process
                        continue
                else:
                    dic = {"y":newData[var],
                           "x":newData[var+"__time"]}
                if var in self.columnData:
                    self.columnData[var].data = dic #update
                else:
                    self.columnData[var] = ColumnDataSource(dic)

    def insert_band_breaks(self,band):
        # we insert a break where any of the values of the bandDict values is numpy.nan
        # the band dict has keys x,y,upper,lower, where y is the upper
        fill = 0
        # check start and end
        for entry in ["x", "y", "upper", "lower"]:
            band[entry] = numpy.asarray(band[entry], dtype=numpy.float64)

        if len(band["lower"])==0 or len(band["upper"])==0:
            return band


        if ~numpy.isfinite(band["lower"][-1]) or ~numpy.isfinite(band["upper"][-1]):
            band["upper"][-1] = fill
            band["lower"][-1] = fill
            band["y"][-1] = numpy.nan
            band["x"][-1] = band["x"][-2]

        if ~numpy.isfinite(band["lower"][0]) or ~numpy.isfinite(band["upper"][0]):
            band["upper"][0] = fill
            band["lower"][0] = fill
            band["y"][0] = numpy.nan
            band["x"][0] = band["x"][1]

        inf1 = numpy.isfinite(band["lower"])
        indices1 = numpy.where(~inf1)[0]
        inf2 = numpy.isfinite(band["upper"])
        indices2 = numpy.where(~inf2)[0]
        indices = numpy.append(indices1, indices2)
        indices = set(indices) - set([0, len(band["lower"]) - 1])
        indices = numpy.asarray(list(indices))
        indices = list(numpy.sort(indices))
        # now we have a sorted list of indices where eiher lower or upper is nan/inf meaning this is a break
        # for the break we do the following: we need the scheme as an example
        # (t,v): (1,1),(2,2),(2,0),(3,0),(3,4),(4,5) etc 
        # we have the data
        # t= 1 2 3 4 5 6 7
        # v= 1 2 n 4 5 6 7
        # so we create
        # t= 1 2 2 4 4 5 6 7
        # v  1 2 0 0 4 5 6 7
        print("indices", indices)

        insertX = []
        for indx in indices:
            # add a start and convert the inf to a end of the break
            band['x'][indx] = band['x'][indx + 1]
            band['y'][indx] = numpy.nan
            band["lower"][indx] = fill
            band["upper"][indx] = fill

            insertX.append(band['x'][indx - 1])

            # now the inserts
        band['x'] = numpy.insert(band['x'], indices, insertX)
        band['y'] = numpy.insert(band['y'], indices, [numpy.nan] * len(insertX))
        band['lower'] = numpy.insert(band['lower'], indices, [fill] * len(insertX))
        band['upper'] = numpy.insert(band['upper'], indices, [fill] * len(insertX))

        return band


    def sync_x_axis(self,times=None):
        self.logger.debug(f"sync_x_axis x ")

        variables = self.server.get_variables_selected()
        start = times["start"]
        end = times["end"]
        #self.set_x_axis(start,end)
        variablesRequest = variables.copy()
        variablesRequest.append("__time")  # make sure we get the time included
        newData = self.server.get_data(variablesRequest, start, end,
                                                        self.server.get_settings()["bins"])  # for debug
        self.update_column_datas(newData)

        self.set_x_axis(start, end)
        #self.plot.x_range.start = start
        #self.plot.x_range.end = end
        self.autoAdjustY = self.server.get_mirror()["autoScaleY"][".properties"]["value"]
        self.adjust_y_axis_limits()

    def set_y_axis(self,limits):
        self.plot.y_range.start = limits[0]
        self.plot.y_range.end = limits[1]


    def draw_new_annotation(self):
        data = self.server.fetch_mirror()
        entry =  data["nextNewAnnotation"][".properties"]["value"]
        if entry["type"] == "time":
            self.boxSelectTool.dimensions = "width"
            self.set_active_drag_tool(self.boxSelectTool)
            self.currentAnnotationTag = entry["tag"]
        elif entry["type"] == "threshold":
            self.boxSelectTool.dimensions = "height"
            self.set_active_drag_tool(self.boxSelectTool)
            self.currentAnnotationTag = "threshold"
            self.currentAnnotationVariable = entry["variable"]
        elif entry["type"] == "motif":
            self.boxSelectTool.dimensions = "width"
            self.set_active_drag_tool(self.boxSelectTool)
            self.currentAnnotationTag = "motif"
            self.currentAnnotationVariable = entry["variable"]


    def _compare_anno(self,anno1,anno2):

        keysInBoth = set(anno1.keys()).intersection(set(anno2.keys()))

        for k in keysInBoth:
            if k == "browsePath":
                continue
            elif k in ["startTime", "endTime"]:
                diff = abs(anno1[k]-anno2[k])
                if diff  <  0.1:
                    continue
                else:
                    self.logger.debug(f'compare failded time diff {diff}')
                    return False
            else:
                if anno1[k] != anno2[k]:
                    print(f"compare failed {k}, {anno1[k]}  {anno2[k]}")
                    return False
        return True



    def update_annotations_and_thresholds_old(self,arg=None):
        self.logger.debug(f"update_annotations {arg}")
        # this is called when the backend has changed annotation leaves or values, it adjusts annotations
        # and thresholds

        #avoid reload if an envelope embedded in a annotation is changed
        if "data" in arg and "sourcePath" in arg["data"]:
            splitted = arg["data"]["sourcePath"].split('.')
            if len(splitted)>2 and splitted[-2]=="envelope":
                self.logger.info("skip anno update due to envelope")
                return
            # modifies give the value
            if "value" in arg["data"]:
                #check if the annotation is in our known list
                annotationBrowsePath = '.'.join(arg["data"]["sourcePath"].split('.')[:-1])

                lookup = {v["browsePath"]:k for k,v in self.server.get_annotations().items()}
                if annotationBrowsePath in lookup:
                    #build the _eventInfo to avoid the fetch
                    id = lookup[annotationBrowsePath]
                    updatedAnno = copy.deepcopy(self.server.get_annotations()[id])
                    changeKey = arg["data"]["sourcePath"].split('.')[-1]
                    updatedAnno[changeKey]=arg["data"]["value"]
                    if changeKey != "variable" and "variable" in updatedAnno:
                        updatedAnno["variable"] = updatedAnno["variable"] # events from the outside deliver the variable as list (the forward refs from the referencer, internally, we only keep a string
                    eventInfo = {"new":{},"delete":{},"modify":{id:updatedAnno}}
                    arg["data"]["_eventInfo"] = eventInfo




        lastAnnotations = self.server.get_annotations()
        if "data" in arg and "_eventInfo" in arg["data"]:
            newAnnotations = self.server.fetch_annotations_differential(arg["data"]["_eventInfo"])
            differential = True
        else:
            newAnnotations = self.server.fetch_annotations()
            differential = False


        #check for deletes
        # the delete check is fast enough so no need to improve with differential
        deleteList = [] # a list of ids
        for annoId,anno in lastAnnotations.items():
            if annoId not in newAnnotations:
                self.logger.debug(f"update_annotations() -- annotations was deleted on server: {annoId}, {lastAnnotations[annoId]['name']}")
                deleteList.append(annoId)
                if annoId in self.renderers:
                    with self.renderersLock:
                        self.renderersGarbage.append(self.renderers[annoId]["renderer"])
                    del self.renderers[annoId]
                else:
                    self.delete_annotations([annoId])
        self.logger.debug(f"update_annotations() -- must delete {deleteList}")

        if self.boxModifierVisible:
            if self.boxModifierAnnotationName in deleteList:
                self.box_modifier_hide()



        #now the new ones
        createdTimeAnnos = []

        if not differential:
            annosToIterate = newAnnotations
        else:
            #take on the the nodes from the incoming
            annosToIterate = arg["data"]["_eventInfo"]["new"]
            annosToIterate.update(arg["data"]["_eventInfo"]["modify"])

        self.logger.debug(f"annosToIterate {annosToIterate}")

        for annoId,anno in annosToIterate.items():
            if anno["type"] == "time":
                if not self.find_renderer(annoId):# not in self.renderers:# and self.showAnnotations:
                    self.logger.debug(f"new annotations {annoId}")
                    self.draw_annotation(anno,visible=False) #will be activated later with show_annotations
                    createdTimeAnnos.append(annoId)
                else:
                    #check if is has changed
                    #if anno != self.renderers[annoId]["info"]:
                    if not self._compare_anno(anno,self.renderers[annoId]["info"] ):
                        self.logger.debug(f"update_annotations() -- annotation has changed {annoId} {self.renderers[annoId]['info']} => {anno}")

                        if BOX_ANNO:
                            isVisible = self.renderers[annoId]["renderer"] in self.plot.renderers # remember if the annotation was currently visible
                            with self.renderersLock:
                                self.renderersGarbage.append(self.renderers[annoId]["renderer"])
                            del self.renderers[annoId]# kick out the entry,
                        # if the currently selected is being changed, we hide the box modifier
                        if self.boxModifierVisible:
                            if self.boxModifierAnnotationName == annoId:
                                self.box_modifier_hide()

                        # now recreate: if the annotation was visible before (was in the plot.renderers
                        # then we show it again, if not, we decide later in the show_annotations if it will be shown or not
                        # depending on selected tags etc. this covers especially the exception case where a user
                        # draws a new annotation, which is a currently NOT activated tag, then modifies that new annotation:
                        # it should stay visible!
                        if BOX_ANNO:
                            if isVisible:
                                self.draw_annotation(anno, visible=True)        #show right away because it was visible before
                            else:
                                self.draw_annotation(anno, visible=False)       # show later if allowed depending on tags etc.
                                createdTimeAnnos.append(annoId)                 #show later if allowed
                        else:
                            self.update_annotation_data(anno,annoId)
            if anno["type"] in ["threshold","motif"]:
                # for thresholds/motifs we do not support delete/create per backend, only modify
                # so check for modifications here
                # it might not be part of the renderers: maybe thresholds are currently off
                if annoId in self.renderers and not self._compare_anno(anno,self.renderers[annoId]["info"]):
                    self.logger.debug(f"update_annotations() -- thresholds/motif has changed {annoId} {self.renderers[annoId]['info']} => {anno}")
                    with self.renderersLock:
                        self.renderersGarbage.append(self.renderers[annoId]["renderer"])
                    del self.renderers[annoId]  # kick out the entry, the remaining invisible renderer will stay in bokeh as garbage
                    #if the currently selected is being changed, we hide the box modifier
                    if self.boxModifierVisible:
                        if self.boxModifierAnnotationName == annoId:
                            self.box_modifier_hide()
                    # now recreate
                    if anno["type"] =="threshold":
                        self.draw_threshold(anno)
                    else:
                        self.draw_motif(anno)

        #now execute the changes
        if 0:
            for entry in deleteList:
                # we only switch it invisible for now, we don't delete the
                # renderer, as this takes too long
                r = self.find_renderer(entry)
                if r:
                    r.visible = False

        if self.showAnnotations and createdTimeAnnos != []:
            self.show_annotations(createdTimeAnnos,fetch=False) # this will put them to the plot renderes

        #self.show_annotations()

        self.remove_renderers() # execute at least the deletes


    def update_annotations_and_thresholds_old_part(self,arg,lastAnnotations,newAnnotations,differential):
        self.logger.debug(f"update_annotations_and_thresholds_old_part")


        #check for deletes
        # the delete check is fast enough so no need to improve with differential
        deleteList = [] # a list of ids
        for annoId,anno in lastAnnotations.items():
            if anno["type"]=="time":
                continue
            if annoId not in newAnnotations:
                self.logger.debug(f"update_annotations() -- annotations was deleted on server: {annoId}, {lastAnnotations[annoId]['name']}")
                deleteList.append(annoId)
                if annoId in self.renderers:
                    with self.renderersLock:
                        self.renderersGarbage.append(self.renderers[annoId]["renderer"])
                    del self.renderers[annoId]
                else:
                    self.delete_annotations([annoId])
        self.logger.debug(f"update_annotations() -- must delete {deleteList}")

        if self.boxModifierVisible:
            if self.boxModifierAnnotationName in deleteList:
                self.box_modifier_hide()



        #now the new ones
        createdTimeAnnos = []

        if not differential:
            annosToIterate = newAnnotations
        else:
            #take on the the nodes from the incoming
            annosToIterate = arg["data"]["_eventInfo"]["new"]
            annosToIterate.update(arg["data"]["_eventInfo"]["modify"])

        self.logger.debug(f"annosToIterate {annosToIterate}")

        for annoId,anno in annosToIterate.items():
            if anno["type"] in ["threshold","motif"]:
                # for thresholds/motifs we do not support delete/create per backend, only modify
                # so check for modifications here
                # it might not be part of the renderers: maybe thresholds are currently off
                if annoId in self.renderers and not self._compare_anno(anno,self.renderers[annoId]["info"]):
                    self.logger.debug(f"update_annotations() -- thresholds/motif has changed {annoId} {self.renderers[annoId]['info']} => {anno}")
                    with self.renderersLock:
                        self.renderersGarbage.append(self.renderers[annoId]["renderer"])
                    del self.renderers[annoId]  # kick out the entry, the remaining invisible renderer will stay in bokeh as garbage
                    #if the currently selected is being changed, we hide the box modifier
                    if self.boxModifierVisible:
                        if self.boxModifierAnnotationName == annoId:
                            self.box_modifier_hide()
                    # now recreate
                    if anno["type"] =="threshold":
                        self.draw_threshold(anno)
                    else:
                        self.draw_motif(anno)

        #now execute the changes
        if 0:
            for entry in deleteList:
                # we only switch it invisible for now, we don't delete the
                # renderer, as this takes too long
                r = self.find_renderer(entry)
                if r:
                    r.visible = False

        #if self.showAnnotations and createdTimeAnnos != []:
        #    self.show_annotations(createdTimeAnnos,fetch=False) # this will put them to the plot renderes

        #self.show_annotations()

        self.remove_renderers() # execute at least the deletes




    def update_annotations_and_thresholds(self,arg=None):
        self.logger.debug(f"update_annotations {arg}")
        # this is called when the backend has changed annotation leaves or values, it adjusts annotations
        # and thresholds

        #avoid reload if an envelope embedded in a annotation is changed
        if "data" in arg and "sourcePath" in arg["data"]:
            splitted = arg["data"]["sourcePath"].split('.')
            if len(splitted)>2 and splitted[-2]=="envelope":
                self.logger.info("skip anno update due to envelope")
                return
            # modifies give the value
            if "value" in arg["data"]:
                #check if the annotation is in our known list
                annotationBrowsePath = '.'.join(arg["data"]["sourcePath"].split('.')[:-1])

                lookup = {v["browsePath"]:k for k,v in self.server.get_annotations().items()}
                if annotationBrowsePath in lookup:
                    #build the _eventInfo to avoid the fetch
                    id = lookup[annotationBrowsePath]
                    updatedAnno = copy.deepcopy(self.server.get_annotations()[id])
                    changeKey = arg["data"]["sourcePath"].split('.')[-1]
                    updatedAnno[changeKey]=arg["data"]["value"]
                    if changeKey != "variable" and "variable" in updatedAnno:
                        updatedAnno["variable"] = [updatedAnno["variable"]] # events from the outside deliver the variable as list (the forward refs from the referencer, internally, we only keep a string
                    eventInfo = {"new":{},"delete":{},"modify":{id:updatedAnno}}
                    arg["data"]["_eventInfo"] = eventInfo

        #once more, make sure that the variables are not a list
        if "data" in arg and "_eventInfo" in arg["data"]:
            for entry in ["new","delete","modify"]:
                for id,info in arg["data"]["_eventInfo"][entry].items():
                    for k,v in info.items():
                        if k=="variable" and type(v) is list:
                            arg["data"]["_eventInfo"][entry][id][k]=v[0] #take the first of the variables from the list


        lastAnnotations = self.server.get_annotations()
        hasModifies = False
        modified = None
        deleted = None
        if "data" in arg and "_eventInfo" in arg["data"]:
            if arg["data"]["_eventInfo"]["modify"]:
                hasModifies = True
                modified = arg["data"]["_eventInfo"]["modify"]
            newAnnotations = self.server.fetch_annotations_differential(arg["data"]["_eventInfo"]) #this will write the new anno to our internal mirror, also executing the modify or delete
            differential = True
            if arg["data"]["_eventInfo"]["delete"]:
                deleted = arg["data"]["_eventInfo"]["delete"]
        else:
            newAnnotations = self.server.fetch_annotations()
            differential = False

        # now we have written the update to the server
        # we now rewrite the annotations
        # new and missing will be identified by the show function
        if hasModifies:
            self.show_annotations(fetch=False,checkModifies=hasModifies, modified = modified )
        else:
            self.show_annotations(fetch=True, checkModifies=hasModifies, modified = modified)

        self.update_annotations_and_thresholds_old_part(arg,lastAnnotations,newAnnotations,differential)



    def update_annotation_data(self,anno,annoId):

        start = anno["startTime"]
        end = anno["endTime"]

        infinity = globalInfinity
        # we must use varea, as this is the only one glyph that supports hatches and does not create a blue box when zooming out
        # self.logger.debug(f"have pattern with hatch {pattern}, tag {tag}, color{color} ")

        self.logger.debug(f'from  {self.renderers[anno["id"]]["source"].data["w"]} => {end-start}')
        #self.renderers[anno["id"]]["source"].data["w"][0]= self.renderers[anno["id"]]["source"].data["w"][0]*0.5

        #source = ColumnDataSource({"l": [start], "w": [end - start], "y": [-infinity], "height": [3 * infinity]})
        self.renderers[anno["id"]]["source"].data = {"l": [start+(end-start)/2],"w": [end-start],"y": [-infinity],"height": [3 * infinity]}
        #self.renderers[anno["id"]]["source"].data = dict(self.renderers[anno["id"]]["source"].data)




    def __legend_check(self):
        try:
            # now we also check if we have a legend click which means that we must delete a variable from the selection
            # self.logger.debug("RENDERERS CHECK --------------------------")
            deleteList = []
            for r in self.plot.renderers:
                if r.name and r.name in self.server.get_variables_selected() and r.visible == False:
                    # there was a click on the legend to hide the variables
                    self.logger.debug("=>>>>>>>>>>>>>>>>>DELETE FROM plot:" + r.name)
                    self.logger.debug("=>>>>>>>>>>>>>>>>>DELETE FROM plot:" + r.name)
                    deleteList.append(r.name)


            if deleteList != []:
                # now make a second run and check the _score variables of the deletlist
                deleteScoreNames = [deletePath.split('.')[-1]+"_score" for deletePath in deleteList]
                deleteExpectedNames = [deletePath.split('.')[-1]+"_expected" for deletePath in deleteList]
                for r in self.plot.renderers:
                    if r.name and (r.name.split('.')[-1] in deleteScoreNames or r.name.split('.')[-1] in deleteExpectedNames):
                        deleteList.append(r.name) #take the according score as well


                # now prepare the new list:
                newVariablesSelected = [var for var in self.server.get_variables_selected() if var not in deleteList]
                self.logger.debug("new var list" + str(newVariablesSelected))
                self.server.set_variables_selected(newVariablesSelected)
                # self.__dispatch_function(self.refresh_plot)

                #now delete potential markers and expected
                self.remove_renderers([lin+"_marker" for lin in deleteList])

        except Exception as ex:
            self.logger.error("problem during __legend_check" + str(ex))

        return (deleteList != [])


    def __init_new_observer(self):
        self.server.sse_register_cb(self.observer_cb)


    def __init_figure(self):

        """
            initialize the time series widget, plot the lines, create controls like buttons and menues
            also hook the callbacks
        """

        self.hoverCounter = 0
        self.newHover = None
        self.hoverTool = None # forget the old hovers
        self.showBackgrounds = False
        self.showThresholds = False
        self.showMotifs = False
        self.showScores = False
        self.buttonWidth = 70

        #layoutControls = []# this will later be applied to layout() function

        settings = self.server.get_settings()
        mirror = self.server.get_mirror()

        if "width" in settings:
            self.width = settings["width"]
        if "height" in settings:
            self.height = settings["height"]

        """ 
        #set the theme
        if settings["theme"] == "dark":
            self.curdoc().theme = Theme(json=themes.darkTheme)
            self.lineColors = themes.darkLineColors
            self.plot.xaxis.major_label_text_color = themes.darkTickColor
        else:
            self.curdoc().theme = Theme(json=themes.whiteTheme)
            self.lineColors = themes.whiteLineColors
            self.plot.xaxis.major_label_text_color = themes.whiteTickColor
        """
        #self.cssClasses = {"button":"button_21","groupButton":"group_button_21","multiSelect":"multi_select_21"}
        #self.cssClasses = {"button": "button_21_sm", "groupButton": "group_button_21_sm", "multiSelect": "multi_select_21_sm"}
        #self.layoutSettings = {"controlPosition":"bottom"} #support right and bottom, the location of the buttons and tools


        #initial values
        try:
            self.rangeStart = date2secs(settings["startTime"])*1000
            self.rangeEnd = date2secs(settings["endTime"])*1000
        except:
            self.rangeStart = None
            self.rangeEnd = None
            self.logger.error("range start, end error, use default full")

        #create figure
        """
           the creation of the figure was reworked as this is a work around for a well known bug (in 1.04), see here
           https://github.com/bokeh/bokeh/issues/7497

           it's a bokeh problem with internal sync problems of frontend and backend, so what we do now is:
           1) use toolbar_location = None to avoid auto-creation of toolbar
           2) create tools by hand
           3) assign them to the figure with add_tools()
           4) create a toolbar and add it to the layout by hand
        """

        if self.server.get_mirror()["panOnlyX"][".properties"]["value"]==True:
            self.wheelZoomTool = WheelZoomTool(dimensions="width")
            self.panTool = PanTool(dimensions="width")
        else:
            self.wheelZoomTool = WheelZoomTool()#dimensions="width")
            self.panTool = PanTool()#dimensions="width")

        tools = [self.wheelZoomTool, self.panTool]
        """
        self.wheelZoomTool = WheelZoomTool()
        self.wheelZoomToolX = WheelZoomTool(dimensions = "width")
        self.panTool = PanTool()
        tools = [self.wheelZoomTool,self.wheelZoomToolX,self.panTool]
        """

        if settings["hasAnnotation"] == True:
            self.boxSelectTool = BoxSelectTool(dimensions="width")
            tools.append(self.boxSelectTool)
        elif settings["hasThreshold"] == True:
            self.boxSelectTool = BoxSelectTool(dimensions="height")
            tools.append(self.boxSelectTool)
        tools.append(ResetTool())
        self.freeZoomTool = BoxZoomTool()
        tools.append(self.freeZoomTool)








        if "yAxisType" in mirror and mirror ["yAxisType"][".properties"]["value"]=="log":
            yAxisType = "log"
        else:
            yAxisType = "linear"
        
        if "xAxisType" in mirror and mirror ["xAxisType"][".properties"]["value"]=="number":
            if yAxisType == "log":
                fig = figure(toolbar_location=None, plot_height=self.height,
                     plot_width=self.width,
                     sizing_mode="scale_width",
                     x_range=(0,1),y_axis_type=yAxisType) 
            else:
                fig = figure(toolbar_location=None, plot_height=self.height,
                     plot_width=self.width,
                     sizing_mode="scale_width",
                     x_range=(0,1),y_axis_type=yAxisType,y_range=Range1d())

        else:
            if yAxisType == "log":
                fig = figure(toolbar_location=None, plot_height=self.height,
                     plot_width=self.width,
                     sizing_mode="scale_width",
                     x_axis_type='datetime',x_range=(0,1),y_axis_type=yAxisType) 
            else:
                fig = figure(toolbar_location=None, plot_height=self.height,
                     plot_width=self.width,
                     sizing_mode="scale_width",
                     x_axis_type='datetime',x_range=(0,1),y_axis_type=yAxisType,y_range=Range1d())

        
        
        self.plot = fig

        # set the theme
        if settings["theme"] == "dark":
            self.curdoc().theme = Theme(json=themes.darkTheme)
            self.lineColors = themes.darkLineColors
            self.plot.xaxis.major_label_text_color = themes.darkTickColor
            self.plot.yaxis.major_label_text_color = themes.darkTickColor
        else:
            self.curdoc().theme = Theme(json=themes.whiteTheme)
            self.lineColors = themes.whiteLineColors
            self.plot.xaxis.major_label_text_color = themes.whiteTickColor
            self.plot.yaxis.major_label_text_color = themes.whiteTickColor


        #b1 = date2secs(datetime.datetime(2015,2,13,3,tzinfo=pytz.UTC))*1000
        #b2 = date2secs(datetime.datetime(2015,2,13,4,tzinfo=pytz.UTC))*1000
        #wid = 20*60*1000 # 20 min
        #self.boxData = ColumnDataSource({'x': [b1,b2], 'y':[0,0],'width': [5, 5],'height':[300,300],"alpha":[1,1,0.2]})

        #self.boxRect = self.plot.rect(x="x", y="y", width="width", height="height",source=self.boxData)
        #self.boxRect = self.plot.rect('x', 'y', 'width', 'height', source=self.boxData,width_units="screen")#, height_units="screen")#, height_units="screen")
        self.boxModifierTool=BoxEditTool( renderers=[],num_objects=0,empty_value=0.1)#,dimensions="width")
        self.box_modifier_init()
        #self.box_modifier_show()

        # possible attribures to boxedittool:
        # custom_icon, custom_tooltip, dimensions, empty_value, js_event_callbacks, js_property_callbacks, name, num_objects, renderers, subscribed_events
        #self.plot.add_layout(self.boxRect)
        #self.boxModifierRect.data_source.on_change("selected",self.box_cb)
        #self.boxRect.data_source.on_change("active", self.box_cb_2)

        tools.append(self.boxModifierTool)





        for tool in tools:
            fig.add_tools(tool) # must assign them to the layout to have the actual use hooked
        toolBarBox = ToolbarBox()  #we need the strange creation of the tools to avoid the toolbar to disappear after
                                   # reload of widget, then drawing an annotations (bokeh bug?)
        toolBarBox.toolbar = Toolbar(tools=tools,active_inspect=None,active_scroll=self.wheelZoomTool,active_drag = None)
        #active_inspect = [crosshair],
        # active_drag =                         # here you can assign the defaults
        # active_scroll =                       # wheel_zoom sometimes is not working if it is set here
        # active_tap
        toolBarBox.toolbar_location = "right"
        toolBarBox.toolbar.logo = None # no bokeh logo

        self.tools = toolBarBox
        self.toolBarBox = toolBarBox

        if "xAxisType" in mirror and mirror["xAxisType"][".properties"]["value"]=="number":
            if "xAxisUnit" in mirror:
               self.plot.xaxis.formatter = FuncTickFormatter(code = """
                let x=tick.toString();
                return x + " %s";
                """%mirror['xAxisUnit']['.properties']['value'])
            else:
                #no formatter
                pass
        else:
            self.plot.xaxis.formatter = FuncTickFormatter(code = """
                let local = moment(tick).tz('%s');
                let datestring =  local.format();
                return datestring.slice(0,-6);
                """%settings["timeZone"])

            self.plot.xaxis.ticker = DatetimeTicker(desired_num_ticks=5)# give more room for the date time string (default was 6)

            self.plot.xgrid.ticker = self.plot.xaxis.ticker

        self.build_second_y_axis()

        self.show_hide_scroll_label() #it must be created at startup and then visible=True/False, the later add_layout did not work

        self.refresh_plot()

        #hook in the callback of the figure
        self.plot.x_range.on_change('start', self.range_cb)
        self.plot.x_range.on_change('end', self.range_cb)
        self.plot.on_event(events.Pan, self.event_cb)
        self.plot.on_event(events.PanStart, self.event_cb)
        self.plot.on_event(events.PanEnd, self.event_cb)
        self.plot.on_event(events.LODEnd, self.event_cb)
        self.plot.on_event(events.Reset, self.event_cb)
        self.plot.on_event(events.SelectionGeometry, self.event_cb)
        self.plot.on_event(events.Tap,self.event_cb)
        #self.plot.on_event(events.MouseWheel, self.mouse_cb)


        #make the controls
        layoutControls =[]

        #Annotation drop down
        if 0: #no drop down for now
            labels=[]
            if settings["hasAnnotation"] == True:
                labels = settings["tags"]
                labels.append("-erase-")
            if settings["hasThreshold"] == True:
                labels.extend(["threshold","-erase threshold-"])
            if labels:
                menu = [(label,label) for label in labels]
                self.annotationDropDown = Dropdown(label="Annotate: "+str(labels[0]), menu=menu,width=self.buttonWidth,css_classes = ['dropdown_21'])
                self.currentAnnotationTag = labels[0]
                self.annotationDropDown.on_change('value', self.annotation_drop_down_on_change_cb)
                #self.annotation_drop_down_on_change_cb() #call it to set the box select tool right and the label
                layoutControls.append(self.annotationDropDown)

        """ 
        currently disabled
        
        # show Buttons
        # initially everything is disabled
        # check background, threshold, annotation, streaming
        self.showGroupLabels = []
        self.showGroupLabelsDisplay=[]
        if self.server.get_settings()["hasAnnotation"] == True:
            self.showGroupLabels.append("Annotation")
            self.showGroupLabelsDisplay.append("Anno")
        if self.server.get_settings()["background"]["hasBackground"]:
            self.showGroupLabels.append("Background")
            self.showGroupLabelsDisplay.append("Back")
            self.showBackgrounds = False # initially off
        if self.server.get_settings()["hasThreshold"] == True:
            self.showGroupLabels.append("Threshold")
            self.showGroupLabelsDisplay.append("Thre")
            self.showThresholds = False # initially off
        if self.server.get_settings()["hasStreaming"] == True:
            self.showGroupLabels.append("Streaming")
            self.showGroupLabelsDisplay.append("Stream")
            self.streamingMode = False # initially off
        self.showGroup = CheckboxButtonGroup(labels=self.showGroupLabelsDisplay)
        self.showGroup.on_change("active",self.show_group_on_click_cb)
        layoutControls.append(row(self.showGroup))
        """

        #make the custom buttons
        buttonControls = []
        self.customButtonsInstances = []
        if "buttons" in settings:
            self.logger.debug("create user buttons")
            #create the buttons
            for entry in settings["buttons"]:
                button = Button(label=entry["name"],width=self.buttonWidth)#,css_classes=['button_21'])
                instance = self.ButtonCb(self,entry["targets"])
                button.on_click(instance.cb)
                buttonControls.append(button)
                self.customButtonsInstances.append(instance)

        #make the debug button
        if "hasReloadButton" in self.server.get_settings():
            if self.server.get_settings()["hasReloadButton"] == True:
                #we must create a reload button
                button = Button(label="reload",width=self.buttonWidth)#, css_classes=['button_21'])
                button.on_click(self.reset_all)
                buttonControls.append(button)


        if 0: # turn this helper button on to put some debug code
            self.debugButton= Button(label="debug")
            self.debugButton.on_click(self.debug_button_cb)
            self.debugButton2 = Button(label="debug2")
            self.debugButton2.on_click(self.debug_button_2_cb)
            buttonControls.append(self.debugButton)
            buttonControls.append(self.debugButton2)


        layoutControls.extend(buttonControls)

        #build the layout


        self.layout = layout([row(children=[self.plot, self.tools], sizing_mode="fixed")], row(layoutControls, width=int(self.width*0.6),sizing_mode="scale_width"))
        #self.layout = layout([row(children=[self.plot, self.tools], sizing_mode="fixed")])

        if self.server.get_settings()["hasAnnotation"] == True:
            self.init_annotations() # we create all annotations that we have into self.annotations

        if "hasEvents" in self.server.get_settings() and self.server.get_settings()["hasEvents"] == True:
            self.init_events()


    def init_additional_elements(self):
        #now also display further elements
        visibleElements = self.server.get_mirror()["visibleElements"][".properties"]["value"]
        if "annotations" in visibleElements and visibleElements["annotations"] == True:
            self.show_annotations(fetch=False)

        if "thresholds" in visibleElements and visibleElements["thresholds"] == True:
            self.show_thresholds()

        if "background" in visibleElements and visibleElements["background"] == True:
            #self.showBackgrounds=True
            self.show_backgrounds()

        if "scores" in visibleElements and visibleElements["scores"] == True:
            self.show_scores()

        if "motifs" in visibleElements and visibleElements["motifs"] == True:
            self.show_motifs()

        if self.server.get_mirror()["streamingMode"][".properties"]["value"] == True:
            self.start_streaming()

    def set_active_drag_tool(self,tool):
        #we need to change the default selection of active drag and then write the list of tools to the toolsbar
        # the list must be different, otherwise the write will not cause the "rebuild" of the tools
        # so we take the last from the list and hide it shortly
        self.logger.debug(f"set active drag tool, {tool}")

        if hasattr(self,"toolBarBox"): #check this: at the startup we are not yet fully supplied, so nothing to do here
            if self.toolBarBox.toolbar.active_drag == tool:
                self.logger.debug("active drag already active")
                return
            store = self.toolBarBox.toolbar.tools
            self.toolBarBox.toolbar.tools = store[:-1] # write something else so we have a change to force the rebuild
            #now set the active drag
            self.toolBarBox.toolbar.active_drag = tool
            self.toolBarBox.toolbar.tools = store

    def set_pan_tool(self,panOnlyX=True):
        self.logger.debug(f"set x only pan: {panOnlyX}")
        if hasattr(self,"toolBarBox"): #check this: at the startup we are not yet fully supplied, so nothing to do here

            store = self.toolBarBox.toolbar.tools

            if panOnlyX==True:
                self.wheelZoomTool = WheelZoomTool(dimensions="width")
                self.panTool = PanTool(dimensions="width")
            else:
                self.wheelZoomTool = WheelZoomTool()#dimensions="width")
                self.panTool = PanTool()#dimensions="width")


            store =[self.wheelZoomTool,self.panTool]+store[2:]
            self.toolBarBox.toolbar.tools = store

    def build_second_y_axis(self):
        mi = self.server.get_mirror()
        if "hasY2Axis" in mi and mi["hasY2Axis"][".properties"]["value"]:
            self.hasY2 = True
            self.plot.extra_y_ranges = {"y2": Range1d(start=0, end=1)}
            self.y2Axis = LinearAxis(y_range_name="y2",
                                     axis_line_width=globalY2width*2,
                                     major_tick_line_width=globalY2width*2,
                                     minor_tick_line_width=globalY2width*2,
                                     major_tick_line_color = themes.darkTickColor,
                                     minor_tick_line_color = themes.darkTickColor,
                                     axis_line_color = themes.darkTickColor,
                                     #major_label_standoff = 10,
                                     major_label_text_align = "left",
                                     major_label_text_color = themes.darkTickColor,
                                     major_label_text_font_style = "bold",
                                     #major_label_text_font_size = "100%"
                                      )
            self.y2Axis.visible = True
            self.plot.add_layout(self.y2Axis, 'right')
            self.plot.min_border_right=50

            self.plot.yaxis.major_label_text_color = themes.darkTickColor
    def debug_button_2_cb(self):
        data = copy.deepcopy(self.mysource.data)
        #data["l"][0]=data["l"][0]-self.debugWidth
        #self.mysource.data=data

        #print(f"len {len(data['l'])}")
        #delete a random


        #l=len(data)
        #for k in data.keys():
        #    data[k]=data[k][100:105]

        #data["l"].append(data["l"][0]-self.debugWidth*4)

        #data["alp"][0]=0
        #for i in range(len(data["alp"])):
        #    if data["c"][i]=="blue":
        #        data["alp"][i]=0
        left = self.plot.x_range.start
        right = self.plot.x_range.end
        number = 2000
        width = (right - left) / number
        self.debugWidth = width
        infinity = globalInfinity
        colors=["red" if i % 2 else "blue" for i in range(number)]
        data={"l":[left + i*width for i in range(number)],
                              "c":colors,
                              "alp":[0.5]*number,
                              "halp": ['x'] * number}

        self.mysource.data = dict(data)    #magically apply


    def debug_button_cb(self):



        left = self.plot.x_range.start
        right = self.plot.x_range.end
        number = 2000
        width = (right-left)/number
        self.debugWidth = width
        infinity = globalInfinity

        '''
        colors=["red" if i % 2 else "blue" for i in range(number)]
        c = ColumnDataSource({"l":[left + i*width for i in range(number)],
                              "c":colors,
                              "alp":[0.5]*number,
                              "halp": ['x'] * number})
        '''
        c = ColumnDataSource({"l":[],"c":[],"alp":[],"halp": []})

        self.mysource = c
        recta = Rect(x="l", y=-infinity, width=width/2, height=3*infinity, fill_color="c",fill_alpha="alp",line_alpha=0,hatch_pattern=None,hatch_alpha=0.5)
        g = GlyphRenderer(data_source=self.mysource, glyph=recta,level = globalBackgroundsLevel)


        '''
        c2 = ColumnDataSource({"l":[left + i*width+width/2 for i in range(number)],
                                          "w":[width/2]*number,
                                         "y":[-infinity]*number,
                                         "height":[3*infinity]*number
        })
        self.mysource = c2
        recta2 = Rect(x="l", y="y", width="w", height="height", fill_color="blue",fill_alpha=0.5,line_alpha=0)
        g2 = GlyphRenderer(data_source=c2, glyph=recta2,level = globalBackgroundsLevel)
        '''



        self.add_renderers([g])





    def setup_toolbar(self):
        self.logger.debug("set back")
        self.toolBarBox.toolbar.active_drag = self.debug["next"]
        self.toolBarBox.toolbar.tools = self.debug["value"]
        self.debug = None


    def box_cb(self,attr,old,new):
        self.debug("BOXCB")

    def box_update(self,x1,x2):
        self.boxData.data["xs"]=[x1,x2]

    def show_group_on_click_cb(self,attr,old,new):
        # in old, new we get a list of indices which are active
        self.logger.debug("show_group_on_click_cb "+str(attr)+str(old)+str(new))
        turnOn = [self.showGroupLabels[index] for index in (set(new)-set(old))]
        turnOff = [self.showGroupLabels[index] for index in (set(old)-set(new))]
        if "Background" in turnOn:
            self.showBackgrounds = True
            self.refresh_backgrounds()
        if "Background" in turnOff:
            self.showBackgrounds = False
            self.refresh_backgrounds()
        if "Annotation" in turnOn:
            self.show_annotations()
        if "Annotation" in turnOff:
            self.hide_annotations()
        if "Threshold" in turnOn:
            self.showThresholds = True
            self.show_thresholds()
        if "Threshold" in turnOff:
            self.showThresholds = False
            self.hide_thresholds()
        if "Streaming" in turnOn:
            self.start_streaming()
        if "Streaming" in turnOff:
            self.stop_streaming()

    def start_streaming(self):
        self.logger.debug(f"start_streaming {self.rangeEnd-self.rangeStart}")
        #get data every second and push it to the graph
        self.streamingInterval = self.rangeEnd-self.rangeStart # this is the currently selected "zoom"
        self.streamingUpdateData = None
        self.streamingMode = True
        self.__dispatch_function(self.show_hide_scroll_label)





    def stop_streaming(self):
        self.logger.debug("stop streaming")
        self.streamingMode = False
        self.__dispatch_function(self.show_hide_scroll_label)

    def show_hide_scroll_label(self):
        self.logger.debug(f"show scroll label {self.width-165}, {self.height-50} {self.scrollLabel}, {self.streamingMode}")
        #creation
        if not self.scrollLabel:
            self.scrollLabel = Label(x=self.width-165, y=self.height-50, x_units='screen', y_units='screen',
                  text=' auto scroll mode on ', text_font_size="12px", text_color=themes.textcolor,
                  border_line_color=themes.textcolor, border_line_alpha=1.0,
                  background_fill_color='black', background_fill_alpha=1.0)
            if not self.streamingMode:
                self.scrollLabel.visible = False
            self.plot.add_layout(self.scrollLabel)
        if self.streamingMode:
            self.scrollLabel.visible = True
        else:
            self.scrollLabel.visible = False


    def annotation_drop_down_on_change_cb(self,attr,old,new):
        mytag = self.annotationDropDown.value
        self.logger.debug("annotation_drop_down_on_change_cb " + str(mytag))
        self.annotationDropDown.label = "Annotate: "+mytag
        self.currentAnnotationTag = mytag
        if "threshold" in mytag:
            #we do a a threshold annotation, adjust the tool
            self.boxSelectTool.dimensions = "height"
        else:
            self.boxSelectTool.dimensions = "width"


    """
    def annotations_radio_group_cb(self,args):
        #called when a selection is done on the radio button for the annoations
        option = self.annotationButtons.active  # gives a 0,1 list, get the label now
        # tags = self.server.get_settings()["tags"]
        mytag = self.annotationTags[option]
        self.logger.debug("annotations_radio_group_cb "+str(mytag))
        if "threshold" in mytag:
            #we do a a threshold annotation, adjust the tool
            self.boxSelectTool.dimensions = "height"
        else:
            self.boxSelectTool.dimensions = "width"
    """

    def testCb(self, attr, old, new):
        self.logger.debug("testCB "+"attr"+str(attr)+"\n old"+str(old)+"\n new"+str(new))
        self.logger.debug("ACTIVE: "+str(self.plot.toolbar.active_drag))

    def remove_hover(self):

        self.hoverTool.renderers=[]


        self.logger.debug(f"remove hover")

        #self.remove_hover_2()
        #self.__dispatch_function(self.remove_hover_2)
        #self.__dispatch_function(self.__make_tooltips)
        #self.__make_tooltips()

    def remove_hover_2(self):
        self.logger.debug(f"remove hover2")

        self.hoverTool.renderers = []
        store = self.toolBarBox.toolbar.tools
        newTools = []
        for entry in self.toolBarBox.toolbar.tools:
            if type(entry) != HoverTool:
                newTools.append(entry)
        self.toolBarBox.toolbar.tools = newTools
        self.hoverTool = None

        #self.__make_tooltips()


    def __make_tooltips(self, force=False):
        #return
        #make the hover tool
        """
            if we create a hover tool, it only appears if we plot a line, we need to hook the hover tool to the figure and the toolbar separately:
            to the figure to get the hover functionality, there we also need to add all renderers to the hover by hand if we create line plots later on
            still haven't found a way to make the hover tool itself visible when we add it to the toolbar; it does appear when we draw a new line,
            if we change edit/del and add lines, (including their renderers, we need to del/add those renderes to the hover tools as well

        """
        renderers = []

        #check if lines have changed:
        if self.hoverTool: ###kna
            newLines = set([v for k,v in self.lines.items() if not self.server.is_score_variable(k) ]) # only the non-score lines
            newEventLines = set([ v["renderer"] for k,v in self.eventLines.items()])
            newLines.update(newEventLines)
            newLines.update(set(self.annoHovers))
            hoverLines = set(self.hoverTool.renderers)
            if newLines != hoverLines or force:

                self.logger.debug(f"reset hover tool MUSt UPDATE newLines {newLines}, hoverLines{hoverLines}")
                self.hoverTool.renderers = []
                store = self.toolBarBox.toolbar.tools
                newTools = []
                for entry in self.toolBarBox.toolbar.tools:
                    if type(entry) != HoverTool:
                        newTools.append(entry)
                self.toolBarBox.toolbar.tools = newTools
                self.hoverTool = None

        if not self.hoverTool or  newLines != hoverLines or force:
            renderers = []

            for k, v in self.eventLines.items():
                self.logger.debug(f"add {k} to hover")
                renderers.append(v["renderer"])

            for k, v in self.lines.items():
                if not self.server.is_score_variable(k):
                    self.logger.debug(f"add line {k} t hover {type(v)} {type(v) is GlyphRenderer}")
                    if type(v) is GlyphRenderer:
                        renderers.append(v)

            self.logger.info(f"number of new hovers {len(renderers)}")
            #for h in self.annoHovers:
            #    #print(f"add hover {h}")
            #    renderers.append(h)


            for h in self.annoHovers:
                renderers.append(h)

        #reverse the renderers to give the lines the prio
        if renderers:
            renderers.reverse()

        if not self.hoverTool:
            #we do this only once

            self.logger.info("MAKE TOOLTIPS"+str(self.hoverCounter))
            hover = HoverTool(renderers=renderers) #must apply them here to be able to dynamically change them
            #hover.tooltips = [("name","$name"),("time", "@__time{%Y-%m-%d %H:%M:%S.%3N}"),("value","@$name{0.000}")] #show one digit after dot
            #hover.tooltips = [("name", "$name"), ("time", "@{__time}{%f}"),
            #                 ("value", "@$name{0.000}")]  # show one digit after dot


            #hover.tooltips = [("name", "$name"), ("time", "@{x}{%f}"),
            #                  ("value", "@y{0.000}")]  # show one digit after dot
            hover.tooltips = [("name", "$name"), ("time", "$x{%f}"),
                              ("value", "@y{0.000}")]  # show one digi
            if 0:
                mytooltip = """
                     < script>
                        //.bk-tooltip>div:not(:first-child) {display:none;}
                        console.log("hier hallo");
                     < /script>
    
                     < b>X:  < /b> @x  < br>
                     < b>Y:  < /b> @y
                """
            #hover.tooltips = mytooltip

            #hover.formatters={'__time': 'datetime'}
            #custom = """var local = moment(value).tz('%s'); return local.format();"""%self.server.get_settings()["timeZone"]

            mirror = self.server.get_mirror()
            if "xAxisType" in mirror and mirror["xAxisType"][".properties"]["value"]=="number":
                if "xAxisUnit" in mirror:
                    custom = """ return value +" %s";"""%mirror["xAxisUnit"][".properties"]["value"]
                else:
                    custom = """ return value;""" #just return the plain number
            else:
                custom = """var local = moment(value).tz('%s'); return local.format();"""%self.server.get_settings()["timeZone"]
            #custom2 = """var neu;neu = source.data['test'][0]; return String(value);"""
            #self.testSource = ColumnDataSource({"test":[67]*1000})
            hover.formatters = {'__time': CustomJSHover(code=custom)}
            custom3 = """ console.log(cb_data);"""
            hover.formatters = {'$x': CustomJSHover(code=custom)}#, 'z':CustomJSHover(args=dict(source=self.testSource),code=custom2)} #xxxkna
            #hover.callback=CustomJS(code=custom3)

            if self.server.get_settings()["hasHover"] in ['vline','hline','mouse']:
                hover.mode = self.server.get_settings()["hasHover"]
            hover.mode = "mouse"
            hover.line_policy = 'interp'#need this instead of nearest for the event lines: they end in +- infinity, with the "nearest", they would show their tooltip hover at the end of their line, outside the visible area
            hover.point_policy = "follow_mouse"
            self.plot.add_tools(hover)

            self.hoverTool = hover
            self.toolBarBox.toolbar.tools.append(hover)  # apply he hover tool to the toolbar




        # we do this every time
        # reapply the renderers to the hover tool
        if 0:
            renderers = []
            self.hoverTool.renderers = []
            renderers = []
            for k, v in self.lines.items():

                if not self.server.is_score_variable(k):
                    self.logger.debug(f"add line {k} t hover")
                    renderers.append(v)
            self.hoverTool.renderers = renderers



    def stream_update_backgrounds(self):
        """ we update the background by following this algo:
            - take the last existing entry in the backgrounds
            - do we have a new one which starts inside the last existing?
              NO: find the
        """
        #make current backgrounds from the latest data and check against the existing backgrounds, put those which we need to append
        newBackgrounds = self.make_background_entries(self.streamingUpdateData)
        addBackgrounds = [] # the backgrounds to be created new
        self.logger.debug("stream_update_backgrounds")
        if self.backgrounds == []:
            #we don't have backgrounds yet, make them
            self.hide_backgrounds()
        else:
            # we have backgrounds
            # now see if we have to adjust the last background
            for entry in newBackgrounds:
                if entry["start"]  < = self.backgrounds[-1]["end"] and entry["end"] > self.backgrounds[-1]["end"]:
                    # this is the first to show, an overlapping or extending one, we cant' extend the existing easily, so
                    # we put the new just right of the old
                    addEntry = {"start": self.backgrounds[-1]["end"], "end": entry["end"], "value":entry["value"], "color": entry["color"]}
                    addBackgrounds.append(addEntry)
                if entry["start"] > self.backgrounds[-1]["end"] and entry["end"]> self.backgrounds[-1]["end"]:
                    #these are on the right side of the old ones, just add them
                    addBackgrounds.append(entry)

        boxes =[]

        for back in addBackgrounds:
            name = "__background"+str('%8x'%random.randrange(16**8))
            newBack = BoxAnnotation(left=back["start"], right=back["end"],
                                    fill_color=back["color"],
                                    fill_alpha=globalBackgroundsAlpha,
                                    level = globalBackgroundsLevel,
                                    name=name)  # +"_annotaion
            boxes.append(newBack)
            back["rendererName"]=name
            self.backgrounds.append(back) # put it in the list of backgrounds for later use

        self.plot.renderers.extend(boxes)

        #remove renderes out of sight
        deleteList = []
        for r in self.plot.renderers:
            if r.name and "__background" in r.name:
                #self.logger.debug(f"check {r.name}, is is {r.right} vs starttime {self.plot.x_range.start}")
                #this is a background, so let's see if it is out of sight
                if r.right  <  self.plot.x_range.start:
                    #this one can go, we can't see it anymore
                    deleteList.append(r.name)
        self.logger.debug(f"remove background renderes out of sight{deleteList}")
        if deleteList:
            self.remove_renderers(deleteList=deleteList)


        #newBackgrounds = self.make_background_entries(self.streamingUpdateData)
        #self.hide_backgrounds()
        #self.show_backgrounds()


        return

    def stream_update_new(self,data):
        """
            this is triggered from the "global.series.stream" event, we first need to check if there is any id in this
            event which we want
        :return:
        """

        #check for variable/score update
        if data["data"]["_eventInfo"]["startTime"]  <  self.plot.x_range.end / 1000 or self.streamingMode:
            for browsePath in data["data"]["_eventInfo"]["browsePaths"]:
                if browsePath in self.lines and self.lines[browsePath].visible == True:
                    if (self.streamingMode and data["data"]["_eventInfo"]["startTime"] > self.plot.x_range.end/1000):
                        appendingDataArrived = True
                    else:
                        appendingDataArrived = False
                    self.refresh_plot(appendingDataArrived)
                    break

        allEventIds = [v["nodeId"] for k, v in self.eventLines.items()]
        for id in data["data"]["_eventInfo"]["nodeIds"]:
            if id in allEventIds:
                #must update the events
                self.update_events()
                break

    def stream_update_is_relevant(self,data):
        """
            this is triggered from the "global.series.stream" event,
            we first need to check if it is relevant for us
            event which we want
        :return:
        """
        try:
            #first check if we have an event update info, then we pick it
            allEventIds = [v["nodeId"] for k,v in self.eventLines.items()]
            for id in data["data"]["_eventInfo"]["nodeIds"]:
                if id in allEventIds:
                    return True # a currently visible event type is updated

            if not self.streamingMode and data["data"]["_eventInfo"]["startTime"]>self.plot.x_range.end/1000:
                #the update is outside (to the right) of the visible area, so ignore
                return False
            # now check if any of the ids are relevant
            # they can be an line, a score, a background or an event
            for browsePath in data["data"]["_eventInfo"]["browsePaths"]:
                if browsePath in self.lines and self.lines[browsePath].visible==True:
                    return True # a variable or score is updated

        except:
            self.log_error()
        return False



    def stream_update(self):
        try:
            self.inStreamUpdate = True # to tell the range_cb that the range adjustment was not from the user
            self.logger.debug("stream update")#+str(self.streamingUpdateData))
            if self.streamingUpdateData:
                if not self.userZoomRunning:
                    if not self.streamingUpdateDataInterval == self.streamingInterval:
                        #the interval has changed in the meantime due to user pan/zoom, we skip this data, get fresh one
                        self.streamingUpdateData = None
                        self.inStreamUpdate = False
                        self.logger.warning("streaming interval has changed")
                        return

                    self.logger.debug(f"apply data {self.streamingUpdateData.keys()},")
                    self.update_column_datas(self.streamingUpdateData)
                    mini,maxi = self.get_min_max_times(self.streamingUpdateData)
                    self.logger.debug(f"streaming x_range: start {mini} end {maxi}, interv {self.streamingInterval}, {maxi-self.streamingInterval} ")
                    self.set_x_axis(maxi-self.streamingInterval,maxi)
                    self.adjust_y_axis_limits()
                    if self.showBackgrounds:
                        self.stream_update_backgrounds()

                    self.streamingUpdateData = None #the thread can get new data
                else:
                    self.logger.info("user zoom running, try later")
                    #user is panning, zooming, we should wait and try again later
                    self.__dispatch_function(self.stream_update)
        except Exception as ex:
            self.logger.error(f"stream_update error {ex}")
        self.inStreamUpdate = False
        self.streamingUpdateData = None


    def reset_all(self):
        """
            this is an experimental function that reloads the widget in the frontend
            it should be executed as dispatched
        """
        self.logger.debug("self.reset_all()")
        self.server.refresh_settings()

        #clear out the figure
        self.hasLegend = False # to make sure the __init_figure makes a new legend
        self.plot.renderers = [] # no more renderers
        #self.data = None #no more data
        self.columnData={}
        self.lines = {} #no more lines



        self.__init_figure()
        #self.__init_observer()
        self.__init_new_observer()

        self.curdoc().clear()
        self.curdoc().add_root(self.get_layout())

    def __dispatch_function(self,function,arg=None):
        """
            queue a function to be executed in the periodic callback from the bokeh app main loop
            this is needed for functions which are triggered from a separate thread but need to be
            executed in the context of the bokeh app loop

        Args:
            function: functionpointer to be executed
        """
        with self.dispatchLock:
            self.logger.debug(f"__dispatch_function {function.__name__}")#, arg: {arg}")
            self.dispatchList.append({"function":function,"arg":arg})


    #def is_second_axis(self,name):
    #    return ".score" in name

    def adjust_y_axis_limits(self,force=False):
        """
            this function automatically adjusts the limts of the y-axis that the data fits perfectly in the plot window
        """
        self.logger.debug(f"adjust_y_axis_limits self.autoAdjustY:{self.autoAdjustY}")

        if not self.autoAdjustY:
            ## only rescale the box_modifier, this is needed in streaming to keep the left
            # and right limits
            self.box_modifier_rescale() # only rescale the box_modifier, this is needed in streaming to keep the left
            if not force:
                return

        lineData = []
        selected = self.server.get_variables_selected()
        for item in self.columnData:
            if item in selected and not "score" in item and not self.server.is_y2_variable(item):
                yData = self.columnData[item].data["y"]
                if len(yData) >= 2:
                    # the outer left and right are ignored in the scaling to avoid influence of
                    # points that are included in the data query and which are far away due to a missin data area
                    # if you have small variation of values and then a gap and than a totally different value
                    # and that value is part of the query but only one point, the small variations can't be seen
                    # now it's possible, this problem was introduced via the "include borders" style of the data
                    # query to get the connecting lines to the next point OUT of the visible area
                    yData=yData[1:-1]
                lineData.extend(yData)

        if len(lineData) > 0:
            all_data = numpy.asarray(lineData, dtype=numpy.float)
            try:
                dataMin = numpy.nanmin(lineData)
                dataMax = numpy.nanmax(lineData)
            except:
                self.logger.warning("all values are nan")
                dataMin = dataMax = 0
            if not numpy.isfinite(dataMin) or not numpy.isfinite(dataMax):
                dataMin=dataMax=0
                self.logger.warning("all values are nan")
            if dataMin==dataMax:
                dataMin -= 1
                dataMax += 1
            # Adjust the Y min and max with 2% border
            yMin = dataMin - (dataMax - dataMin) * 0.02
            yMax = dataMax + (dataMax - dataMin) * 0.02
            self.logger.debug("current y axis limits" + str(yMin)+" "+str(yMax))

            self.plot.y_range.start = yMin
            self.plot.y_range.end = yMax
            
            self.box_modifier_rescale()

        else:
            self.logger.warning("not y axix to arrange")


        #y2 axis
        if self.server.has_y2():
            lineData = []
            selected = self.server.get_variables_selected()
            for item in self.columnData:
                if item in selected and self.server.is_y2_variable(item):
                    yData = self.columnData[item].data["y"]
                    if len(yData) >= 2:
                        # the outer left and right are ignored in the scaling to avoid influence of
                        # points that are included in the data query and which are far away due to a missin data area
                        # if you have small variation of values and then a gap and than a totally different value
                        # and that value is part of the query but only one point, the small variations can't be seen
                        # now it's possible, this problem was introduced via the "include borders" style of the data
                        # query to get the connecting lines to the next point OUT of the visible area
                        yData = yData[1:-1]
                    lineData.extend(yData)

            if len(lineData) > 0:
                all_data = numpy.asarray(lineData, dtype=numpy.float)
                try:
                    dataMin = numpy.nanmin(lineData)
                    dataMax = numpy.nanmax(lineData)
                except:
                    self.logger.warning("all y2 values are nan")
                    dataMin = dataMax = 0
                if not numpy.isfinite(dataMin) or not numpy.isfinite(dataMax):
                    self.logger.warning("all y2 values are nan")
                    dataMin=dataMax=0
                if dataMin==dataMax:
                    dataMin -= 1
                    dataMax += 1
                # Adjust the Y min and max with 2% border
                yMin = dataMin - (dataMax - dataMin) * 0.02
                yMax = dataMax + (dataMax - dataMin) * 0.02
                self.logger.debug("current y axis limits" + str(yMin) + " " + str(yMax))

                self.plot.extra_y_ranges["y2"].start = yMin
                self.plot.extra_y_ranges["y2"].end = yMax

                self.box_modifier_rescale()

            else:
                self.logger.warning("not y2 axis to arrange")



    def box_modifier_init(self):
        self.logger.debug("box_modifier_init")
        self.boxModifierWidth = 8

        b1 = date2secs(datetime.datetime(2015, 2, 13, 3, tzinfo=pytz.UTC)) * 1000
        b2 = date2secs(datetime.datetime(2015, 2, 13, 4, tzinfo=pytz.UTC)) * 1000
        wid = 20 * 60 * 1000  # 20 min
        self.boxModifierData = ColumnDataSource( {'x': [b1, b2], 'y': [0, 0], 'width': [self.boxModifierWidth, self.boxModifierWidth], 'height': [300, 300] })

        self.boxModifierRectHorizontal = self.plot.rect('x', 'y', 'width', 'height', source=self.boxModifierData, width_units="screen",line_width=1,line_dash="dotted",line_color="white",fill_color="white" )  # , height_units="screen")#, height_units="screen")
        self.boxModifierRectVertical = self.plot.rect('x', 'y', 'width', 'height', source=self.boxModifierData, height_units="screen",line_width=1,line_dash="dotted",line_color="white",fill_color="white")  # , height_units="screen")#, height_units="screen")


        self.box_modifier_hide()# remove the renderers

    def box_modifier_tap(self, x=None, y=None):

        self.logger.debug(f"box_modifier_tap x:{x} y:{y}")
        candidates = []
        #check if we are inside a visible annotation
        for annoId, anno in self.server.get_annotations().items():
            #self.logger.debug("check anno "+annoName+" "+anno["type"])
            candidate = False
            if anno["type"] in ["time","motif"]:
                if anno["startTime"] < x and anno["endTime"]>x:
                    #we are inside this annotation:
                    candidate=True
            elif anno["type"] == "threshold":
                if "variable" in anno and self.server.is_y2_variable(anno["variable"]):
                    y2=self.convert_y1_to_y2(y)
                    if anno["min"]  <  y2 and anno["max"] > y2:
                        candidate = True
                else:
                    if anno["min"]  <  y and anno["max"] > y:
                        candidate = True
            if candidate:
                if self.find_renderer(anno["id"]):
                    #we are inside this anno and it is visible,
                    candidates.append(annoId)
                    if self.boxModifierVisible:
                        pass # we rotate activation later
                    else:
                        self.box_modifier_show(annoId, anno)
                        return

        if candidates:
            if self.boxModifierAnnotationName in candidates:

                candidates.append(candidates[0])  # if wrap around
                next = candidates.index(self.boxModifierAnnotationName)+1
                annoNext = candidates[next]
                #self.logger.debug(f"candidates, next {annoNext}")
                self.box_modifier_show(annoNext, self.server.get_annotations()[annoNext])
                return
            else:
                annoId = candidates[0]
                #self.logger.debug(f"only {annoId}")
                self.box_modifier_show(annoId,self.server.get_annotations()[annoId])
                return

        else:

            backgroundSelected = self.background_highlight_show(x,y)
            if backgroundSelected:
                return



        #we are not inside an annotation, we hide the box modifier
        self.box_modifier_hide(auto=True)

    def background_highlight_show(self,x,y):
        if self.backgroundHighlightVisible:
            #havent found one
            self.background_highlight_hide()

        for r in self.plot.renderers:
            if r.name:
                if r.name.startswith("__background"):
                    backStart = r.left
                    backEnd = r.right
                    if x >= backStart and x  < = backEnd:
                        #alphaNow = r.fill_alpha
                        r.fill_alpha = globalBackgroundsHighlightAlpha#alphaNow + 0.5 * (1 - alphaNow)
                        self.logger.debug("inside Background!")
                        self.server.set_background_highlight(x,y,backStart,backEnd)
                        self.backgroundHighlightVisible = True
                        return True



        return False

    def background_highlight_hide(self):
        if self.backgroundHighlightVisible:
            self.backgroundHighlightVisible=False
            for r in self.plot.renderers:
                if r.name:
                    if r.name.startswith("__background"):
                        alphaNow = r.fill_alpha
                        if alphaNow != globalBackgroundsAlpha:
                            r.fill_alpha = globalBackgroundsAlpha
                            self.server.set_background_highlight(0,0,0,0,remove=True)
                            return


    def box_modifier_show(self,annoName,anno):
        """
            Args:
                annoName: the key in the annotationlist (=id in the model)
        """

        self.logger.debug(f"box_modifier_show {annoName}")

        if self.boxModifierVisible:
            if self.boxModifierAnnotationName == annoName:
                #this one is already visible, we are done
                return False
            else:
                # if another is already visible, we hide it first
                # but we keep the tool active, so don't call box_modifier_hide() here
                self.boxModifierRectVertical.visible = False  # hide the renderer
                self.boxModifierRectHorizontal.visible = False  # hide the renderer


        self.boxModifierAnnotationName = annoName
        boxYCenter = float(self.plot.y_range.start + self.plot.y_range.end) / 2
        boxXCenter = float(self.plot.x_range.start + self.plot.x_range.end) / 2
        boxYHeight = (self.plot.y_range.end - self.plot.y_range.start) * 4
        boxXWidth = (self.plot.x_range.end - self.plot.x_range.start) *4

        if anno["type"] in ["time","motif"]:
            start = anno["startTime"]
            end = anno["endTime"]
            self.boxModifierData.data = {'x': [start, end], 'y': [boxYCenter, boxYCenter], 'width': [self.boxModifierWidth, self.boxModifierWidth], 'height': [boxYHeight, boxYHeight]}
            self.boxModifierRectHorizontal.visible=True
            self.boxModifierOldData = dict(copy.deepcopy(self.boxModifierData.data))
            self.boxModifierVisible = True
            #self.plot.renderers.append(self.boxModifierRectHorizontal)
            self.boxModifierTool.renderers = [self.boxModifierRectHorizontal]  # ,self.boxModifierRectVertical]

        if anno["type"] == "threshold":
            if "variable" in anno and self.server.is_y2_variable(anno["variable"]):
                ys= [self.convert_y2_to_y1(anno['min']),self.convert_y2_to_y1(anno['max'])]
            else:
                ys = [anno['min'], anno['max']]
            self.boxModifierData.data = {'x': [boxXCenter, boxXCenter], 'y': ys, 'width': [boxXWidth,boxXWidth], 'height': [self.boxModifierWidth, self.boxModifierWidth]}

            self.boxModifierRectVertical.visible=True
            self.boxModifierOldData = dict(copy.deepcopy(self.boxModifierData.data))
            self.boxModifierVisible = True
            #self.plot.renderers.append(self.boxModifierRectVertical)
            self.boxModifierTool.renderers = [self.boxModifierRectVertical]

        self.set_active_drag_tool(self.boxModifierTool)
        self.server.select_annotation(annoName)
        return True

    def box_modifier_hide(self,auto = False):
        """
            if auto is set, we check if visible before
        """
        if auto and not self.boxModifierVisible:
            self.set_active_drag_tool(self.panTool)
            return

        self.boxModifierVisible = False
        self.boxModifierRectVertical.visible = False #hide the renderer
        self.boxModifierRectHorizontal.visible = False #hide the renderer

        self.set_active_drag_tool(self.panTool) # this is actually pretty slow ~ 500ms

        self.server.select_annotation([]) # unselect all
        #also remove the renderer from the renderers
        #self.remove_renderers(renderers=[self.boxModifierRectHorizontal,self.boxModifierRectVertical])



    # this is called when we resize the plot via variable selection, mouse wheel etc
    def box_modifier_rescale(self):
        self.logger.info(f"box_modifier_rescale self.boxModifierVisible{self.boxModifierVisible}, self.inPan{self.inPan}")
        #self.logger.debug(f"box_modifier_rescale self.boxModifierVisible={self.boxModifierVisible}")
        if self.boxModifierVisible == False:
            return

        # also, if we currently move the boxmodifier around per drag and drop,
        # we don't want to touch it here until the user releases
        if self.inPan:
            self.logger.info("box_modifier_rescale skipped in pan")
            return

        anno = self.server.get_annotations()[self.boxModifierAnnotationName]
        if anno["type"] in ["time","motif"]:
            #adjust the limits to span the rectangles on full view area
            boxYCenter = float((self.plot.y_range.start + self.plot.y_range.end)/2)
            boxYHeight = (self.plot.y_range.end - self.plot.y_range.start)*4
            data = dict(copy.deepcopy(self.boxModifierData.data))
            data['y'] = [boxYCenter, boxYCenter]
            data['height'] = [boxYHeight, boxYHeight]
            self.boxModifierData.data = data
        if anno["type"] == "threshold":
            boxXCenter = float((self.plot.x_range.start + self.plot.x_range.end) / 2)
            data = dict(copy.deepcopy(self.boxModifierData.data))
            data['x'] = [boxXCenter, boxXCenter]
            self.boxModifierData.data = data

    def adjust_annotation(self,anno):
        if anno["type"]=="time":
            if self.find_renderer(anno["id"]):
                if anno["rendererType"] == "VArea":
                    source = self.renderers[anno["id"]]["source"]
                    source.patch({'x':[ (0,anno["startTime"]),(1,anno["endTime"]) ]})
                elif anno["rendererType"] == "VBar":
                    source = self.renderers[anno["id"]]["source"]
                    start = anno["startTime"]
                    end = anno["endTime"]
                    source.patch({'x':[(0,start + (end - start) / 2)],'w':[(0,end-start)]})
                else:
                    #boxannotation must be recreated
                    self.remove_renderers(anno["id"])
                    self.draw_annotation(anno,visible=True)

                #self.renderers[anno["id"]]["source"]["x"]=[anno["startTime"],anno["endTime"]]

    def box_modifier_modify(self):
        self.logger.debug(f"box_modifier_modify {self.boxModifierVisible}, now => {self.boxModifierData.data}")
        if self.boxModifierVisible == False:
            return False

        anno = self.server.get_annotations()[self.boxModifierAnnotationName]
        self.logger.debug(f" box_modifier_modify {anno}")


        if anno["type"] in ["time","motif"]:
            if self.boxModifierData.data['x'][1]  < = self.boxModifierData.data['x'][0]:
                #end before start not possible
                self.logger.warning("box_modifier_modify end before start error")
                return False

            # re-center the y axis height to avoid vertical out-shifting
            boxYCenter = float(self.plot.y_range.start + self.plot.y_range.end) / 2
            boxYHeight = (self.plot.y_range.end - self.plot.y_range.start) * 4
            self.boxModifierData.data['y'] = [boxYCenter, boxYCenter]
            self.boxModifierData.data['height'] = [boxYHeight, boxYHeight]

            #now modify it:
            # adjust the local value in the timeseries server,
            # correct the visible glyph of the annotation
            # push it back to the model

            #may this is just a zoom, so check if start or endtime has changed
            if anno["startTime"] != self.boxModifierData.data['x'][0] or anno["endTime"] != self.boxModifierData.data['x'][1]:
                # sanity check: end not before start
                anno["startTime"] = self.boxModifierData.data['x'][0]
                anno["endTime"] = self.boxModifierData.data['x'][1]
                self.server.adjust_annotation(anno)#wait for observer to notify the real change


        elif anno["type"] == "threshold":
            if self.boxModifierData.data['y'][1]  < = self.boxModifierData.data['y'][0]:
                #end before start not possible
                self.logger.warning("box_modifier_modify min gt max error")
                return False
                # sanity check: end not before start
            #now move the box back in
            boxXCenter = float(self.plot.x_range.start + self.plot.x_range.end) / 2
            self.boxModifierData.data['x'] = [boxXCenter, boxXCenter]

            #maybe just a zoom
            if self.server.is_y2_variable(anno["variable"]):
                modifierMin = self.convert_y1_to_y2(self.boxModifierData.data['y'][0])
                modifierMax = self.convert_y1_to_y2(self.boxModifierData.data['y'][1])
            else:
                modifierMin = self.boxModifierData.data['y'][0]
                modifierMax = self.boxModifierData.data['y'][1]



            if anno["min"] != modifierMin or anno["max"] != modifierMax:
                anno["min"] = modifierMin
                anno["max"] = modifierMax
                self.server.adjust_annotation(anno)#s(self.boxModifierAnnotationName, anno)
                self.remove_renderers(deleteMatch=anno["id"],deleteFromLocal=True)
                self.draw_threshold(anno)#self.boxModifierAnnotationName,anno['variable'])



        else:
            self.logger.error(f"we don't support annos of type {anno['type']}")
            return False



        return True


    def check_boxes(self):
        if self.inPan:
            self.logger.debug("check_boxes skip in pan")
            return

        if self.boxModifierVisible:
            try:
                #self.logger.debug(self.boxData.data)
                #self.logger.debug(self.toolBarBox.toolbar.active_drag)

                if len(self.boxModifierData.data["x"]) != 2:
                    self.logger.warning("box modifier >2:  restore")
                    self.boxModifierData.data = copy.deepcopy(self.boxModifierOldData)


                new = json.dumps(self.boxModifierData.data)
                old = json.dumps(self.boxModifierOldData)
                if old!= new:
                    if not self.box_modifier_modify():
                        self.logger.warning("box modifier invalid,  restore")
                        self.boxModifierData.data = dict(copy.deepcopy(self.boxModifierOldData))


                    self.boxModifierOldData = dict(copy.deepcopy(self.boxModifierData.data))

            except Exception as ex:
                self.logger.error(f"check_boxes {ex}")


    def check_y2_spacing(self):
        """
            this was a trial to dynamically adjust the right border spacing, as bokeh messes them up sometimes
        :return:
        """
        if self.server.has_y2():
            diff = (self.plot.extra_y_ranges["y2"].end - self.plot.extra_y_ranges["y2"].start)
            print("diff",diff)
            MSD = len(str(int(self.plot.extra_y_ranges["y2"].start)))
            if diff < 1:
                LSD = round(-numpy.log10(diff))
            else:
                LSD = 0
            ticklen = MSD+LSD
            newValue = 50+   20 * ticklen
            print(f"{MSD} {LSD} {ticklen} => {newValue}")

            #ticklen = max(len(str(self.plot.extra_y_ranges["y2"].start)),len(str(self.plot.extra_y_ranges["y2"].end)))
            #newValue = 50+10*ticklen
            if newValue != self.plot.min_border_right:
                print("set")
                if newValue>200:
                    newValue = 500
                self.plot.min_border_right = newValue#newValue # settings this during runtime has no effect :(


    def periodic_cb(self):
        """
            called periodiaclly by the bokeh system
            here, we execute function that modifiy bokeh variables etc via the dispatching list
            this is needed, as modifications to data or parameters in the bokeh objects
            are only possible withing the bokeh thread, not from any other.

            attention: make sure this functin does normally not last longer than the periodic call back period, otherwise
            bokeh with not do anything else than this function here

        """
        #self.logger.debug("periodic_cb")
        if self.inPeriodicCb:
            self.logger.error("in periodic cb")
            return
        self.inPeriodicCb = True

        try:
            start = time.time()
            self.check_boxes()
            #self.check_y2_spacing()
            legendChange =  self.__legend_check() # check if a user has deselected a variable
            #try: # we need this, otherwise the inPeriodicCb will not be reset

            #self.logger.debug("enter periodic_cb")

            executelist=[]
            with self.dispatchLock:
                if self.dispatchList:
                    executelist = self.dispatchList.copy()
                    self.dispatchList = []

            for entry in executelist: # avoid double execution
                fkt = entry["function"]
                arg = entry["arg"]
                self.logger.info(f"now executing dispatched fkt {fkt.__name__}")# arg {arg}")
                if arg:
                    fkt(arg) # execute the functions which wait for execution and must be executed from this context
                else:
                    fkt()

        except Exception as ex:            self.logger.error(f"Error in periodic callback {ex} , {str(traceback.format_exc())}")

        if legendChange or executelist != []:
            self.logger.debug(f"periodic_cb was {time.time()-start}")

        self.inPeriodicCb = False

    def __get_free_color(self,varName = None):
        """
            get a currently unused color from the given palette, we need to make this a function, not just a mapping list
            as lines come and go and therefore colors become free again

            Returns:
                a free color code

        """

        #try to find the color first:
        if varName:
            currentLinesColors = self.server.get_current_colors()
            if varName in currentLinesColors:
                return currentLinesColors[varName]["lineColor"]

            #not found, get the static color if given
            col = self.server.get_static_line_color(varName)
            if col:
                return col


        #not found, get a new one

        usedColors =  [self.lines[lin].glyph.line_color for lin in self.lines if hasattr(self.lines[lin],"glyph")]
        for color in self.lineColors:
            if color not in usedColors:
                return color
        return "green" # as default




    def __plot_lines(self,newVars = None,appendingDataArrived=False,forceYRescale=False,allowXAxisReset = True):
        """ plot the currently selected variables as lines, update the legend
            if newVars are given, we only plot them and leave the old
        """
        self.logger.debug("@__plot_lines")

        if newVars == None:
            #take them all fresh
            newVars = self.server.get_variables_selected()

        #first, get fresh data
        settings= self.server.get_settings()
        variables = self.server.get_variables_selected()
        mirr = self.server.get_mirror()
        if "autoScaleY" in mirr:
            self.autoAdjustY = mirr["autoScaleY"][".properties"]["value"]

        if "resetXAxisAfterBlankCanvas" in mirr:
            canResetXAxis = mirr["resetXAxisAfterBlankCanvas"][".properties"]["value"]
        else:
            canResetXAxis = False #as default we dont reset

        showMarker = False
        if "showMarker" in mirr:
            showMarker = mirr["showMarker"][".properties"]["value"]
        #self.logger.debug("@__plot_lines:from server var selected %s",str(newVars))
        variablesRequest = variables.copy()
        variablesRequest.append("__time")   #make sure we get the time included
        #self.logger.debug("@__plot_lines:self.variables, bins "+str(variablesRequest)+str( settings["bins"]))
        if self.streamingMode and appendingDataArrived:
            getData = self.server.get_data(variablesRequest, -self.streamingInterval, None,self.server.get_settings()["bins"])
        else:
            if not self.lines and canResetXAxis and allowXAxisReset:
                # if we don't have lines yet, we get the full time range of the "new" variable
                start,end = None,None
            else:
                start,end = self.rangeStart,self.rangeEnd
            getData = self.server.get_data(variablesRequest,start,end,settings["bins"]) # for debug

        #self.logger.debug("GETDATA:"+str(getData))
        if not getData:
            self.logger.error(f"no data received")
            return
        if self.rangeStart == None or self.streamingMode:
            mini,maxi = self.get_min_max_times(getData)
            if maxi==mini:
                maxi = mini+1
                mini = mini-1
            minY,maxY = self.get_min_max_y(getData)
            if minY == maxY:
                maxY = minY+1
                minY = minY-1
            #write it back
            self.rangeStart = mini#getData["__time"][0]
            self.rangeEnd   = maxi#getData["__time"][-1]
            self.server.set_xy_range(self.rangeStart, self.rangeEnd,minY,maxY)

        #del getData["__time"]
        #getData["__time"]=[0]*settings["bins"] # dummy for the hover


        """
        if newVars == []:
            #self.data.data = getData  # also apply the data to magically update
            for k,v in getData.items():
                if not k.endswith("__time"):
                    self.columnData[k].data ={"y":v,"x":getData[k+"__time"]}
        else:
            self.logger.debug("new column data source")
            if self.data is None:
                #first time
                self.data = ColumnDataSource(getData)  # this will magically update the plot, we replace all data
                #also new store
                self.columnData  = {}
                for k,v in getData.items():
                    if not k.endswith("__time"):
                        self.columnData[k]=ColumnDataSource({"y":v,"x":getData[k+"__time"]})

            else:
                #add more data
                for variable in getData:
                    if variable not in self.columnData:#data.data:
                        self.columnData[variable]=ColumnDataSource({"y":v,"x":getData[k+"__time"]})
                        #self.data.add(getData[variable],name=variable)
        """
        self.update_column_datas(getData)

        #self.logger.debug(f"self.columnData {self.columnData}")
        self.adjust_y_axis_limits(force=forceYRescale)
        #timeNode = "__time"
        #now plot var

        if newVars != []:
            #sort the limits to the end so that the lines are created first, then the band can take the same color
            newList = []
            newMaxes = []
            for elem in newVars:
                if elem.endswith("_limitMax") or elem.endswith("_limitMin") or elem.endswith("_expected"):
                    newMaxes.append(elem)
                else:
                    newList.append(elem)
            newVars = newList+newMaxes

        if newVars and not self.lines and canResetXAxis and allowXAxisReset:
            #this is the first lines after a blank canvas
            #set this again for the reset at the end
            resetXAxis=True
        else:
            resetXAxis=False

        for variableName in newVars:
            if variableName.endswith('__time'):
                continue
            color = self.__get_free_color(variableName)
            self.logger.debug("new color ist"+color)

            self.logger.debug(f"plotting line {variableName}, is score: {self.server.is_score_variable(variableName)}")
            if self.server.is_score_variable(variableName):
                scoreMarker = self.server.get_score_marker(variableName)
                #this is a red circle score varialbe
                if not self.server.is_y2_variable(variableName):
                    if scoreMarker == "x":
                        self.lines[variableName] = self.plot.asterisk(x="x", y="y", line_color="red", fill_color=None,
                                                                source=self.columnData[variableName], name=variableName,size=10)  # x:"time", y:variableName #the legend must havee different name than the source bug
                    elif scoreMarker=="+":
                        self.lines[variableName] = self.plot.cross(x="x", y="y", line_color="red", fill_color=None,
                                                                source=self.columnData[variableName], name=variableName,size=10)
                    else:
                        #default is circle
                        self.lines[variableName] = self.plot.circle(x="x", y="y", line_color="red", fill_color=None,
                                                                source=self.columnData[variableName], name=variableName,
                                                                size=7)  # x:"time", y:variableName #the legend must havee different name than the source bug
                else:
                    #is a y2 score
                    if self.server.has_y2():
                        if scoreMarker == "x":
                            self.lines[variableName] = self.plot.asterisk(x="x", y="y", line_color="red", fill_color=None,
                                                                        source=self.columnData[variableName],
                                                                        name=variableName,
                                                                        size=10, y_range_name="y2")  # x:"time", y:variableName #the legend must havee different name than the source bug
                        elif scoreMarker == "+":
                            self.lines[variableName] = self.plot.cross(x="x", y="y", line_color="red", fill_color=None,
                                                                    source=self.columnData[variableName],
                                                                    name=variableName, size=10, y_range_name="y2")
                        else:
                            # default is circle
                            self.lines[variableName] = self.plot.circle(x="x", y="y", line_color="red", fill_color=None,
                                                                        source=self.columnData[variableName],
                                                                        name=variableName,
                                                                        size=7, y_range_name="y2")  # x:"time", y:variableName #the legend must havee different name than the source bug


            elif variableName.endswith("_limitMax"):
                if variableName in self.columnData:# if it is in the column data we can process
                    #we found both min and max
                    thisLineColor = None
                    originalVarName = variableName.split('.')[-1][:-len("_expected")]
                    for lineName in self.lines:
                        if lineName.split('.')[-1] == originalVarName:
                            thisLineColor = self.lines[lineName].glyph.line_color
                            break
                    if not thisLineColor:
                        thisLineColor = "gray"
                    band = None 
                    if self.server.is_y2_variable(variableName):
                        if self.server.has_y2():#only create this if we have a y2, otherwise bokeh gets problems with log axis
                            band = Band(base='x', lower='lower', upper='upper', level=globalBandsLevel,fill_color = thisLineColor,
                                        fill_alpha=0.4, line_width=0,source = self.columnData[variableName],name=variableName,y_range_name="y2")
                    else:
                        band = Band(base='x', lower='lower', upper='upper', level=globalBandsLevel,
                                    fill_color=thisLineColor,
                                    fill_alpha=0.4, line_width=0, source=self.columnData[variableName],
                                    name=variableName)
                    if band:
                        self.lines[variableName] = band
                        self.plot.add_layout(band)
                continue
            elif variableName.endswith("_limitMin"):
                continue


            else:
                #these are the lines

                """ 
                if ".score" in variableName:
                    # this is a score 0..1 line
                    self.lines[variableName] = self.plot.line(x="x", y="y", color="gray", line_dash="dotted",
                                                              source=self.columnData[variableName], name=variableName, line_width=2,
                                                              y_range_name="y2")  # x:
                """
                if 1:

                    if variableName.endswith("_expected"):
                        # this is a special case of a line which we display dotted in the same color as the original one
                        # try to find the corresponding variable
                        thisLineColor = None
                        originalVarName = variableName.split('.')[-1][:-len("_expected")]
                        for lineName in self.lines:
                            if lineName.split('.')[-1] == originalVarName:
                                thisLineColor = self.lines[lineName].glyph.line_color
                                break
                        if not thisLineColor:
                            thisLineColor = color
                        if self.server.is_y2_variable(variableName):
                            if self.server.has_y2():
                                self.lines[variableName] = self.plot.line(x="x", y="y", color=thisLineColor,
                                                                  source=self.columnData[variableName], name=variableName,
                                                                  line_width=4, line_dash="dashed",y_range_name="y2")
                        else:
                            self.lines[variableName] = self.plot.line(x="x", y="y", color=thisLineColor,
                                                                      source=self.columnData[variableName],
                                                                      name=variableName,
                                                                      line_width=4, line_dash="dashed")
                    else:

                        #this is a real line
                        #self.debugStore =copy.deepcopy(getData)
                        #self.lines[variableName] = self.plot.line(x=variableName+"__time", y=variableName, color=color,
                        #                              source=self.data, name=variableName,line_width=2)  # x:"time", y:variableName #the legend must havee different name than the source bug
                        if self.server.is_y2_variable(variableName):
                            if self.server.has_y2():
                                self.lines[variableName] = self.plot.line(x="x", y="y", color=color,
                                                                      source=self.columnData[variableName],
                                                                      name=variableName, line_width=4,
                                                                      y_range_name="y2")
                        else:
                            self.lines[variableName] = self.plot.line(x="x", y="y", color=color,
                                                                      source=self.columnData[variableName],
                                                                      name=variableName,line_width=2)

                        if showMarker:
                            markerName = variableName+"_marker"
                            if self.server.is_y2_variable(variableName):
                                if self.server.has_y2():
                                    marker = self.plot.circle(x="x", y="y", line_color=color, fill_color=color,
                                                          source=self.columnData[variableName], name=markerName,
                                                          size=7,y_range_name="y2")  # x:"time", y:variableName #the legend must havee different name than the source bug

                            else:
                                marker = self.plot.circle(x="x",y="y", line_color=color, fill_color=color,
                                                      source=self.columnData[variableName], name=markerName,size=3)  # x:"time", y:variableName #the legend must havee different name than the source bug
                #legend only for lines
                self.legendItems[variableName] = LegendItem(label='.'.join(variableName.split('.')[-2:]),
                                                            renderers=[self.lines[variableName]])

            #we set the lines and glypsh to no change their behaviour when selections are done, unfortunately, this doesn't work, instead we now explicitly unselect in the columndatasource
            self.lines[variableName].nonselection_glyph = None  # autofading of not selected lines/glyphs is suppressed
            self.lines[variableName].selection_glyph = None     # self.data.selected = Selection(indices = [])

            #self.legendItems[variableName] = LegendItem(label=variableName,renderers=[self.lines[variableName]])

            if self.showThresholds:
                self.show_thresholds_of_line(variableName)
            if self.showMotifs:
                self.show_motifs_of_line(variableName)

        #compile the new colors
        nowColors = {}
        for variableName,glyph in self.lines.items():
            if variableName.endswith("_limitMax"):
                nowColors[variableName] = {"lineColor": glyph.line_color}
            else:
                nowColors[variableName] = {"lineColor":glyph.glyph.line_color}
        self.server.update_current_colors(nowColors)




        #now make a legend
        #legendItems=[LegendItem(label=var,renderers=[self.lines[var]]) for var in self.lines]
        legendItems = [v for k,v in self.legendItems.items()]
        if not self.hasLegend:
            #at the first time, we create the "Legend" object
            self.plot.add_layout(Legend(items=legendItems))
            self.plot.legend.location = "top_left"
            self.plot.legend.click_policy = "hide"
            self.hasLegend = True
            #check if we need to hide it on start
            mirr = self.server.get_mirror()
            if "showLegend" in mirr:
                if mirr["showLegend"][".properties"]["value"] == False:
                    self.plot.legend.visible=False
        else:
            self.plot.legend.items = legendItems #replace them

        if resetXAxis:
            #        #we must explicitly disable this functionality, else we default reset the axis
            #        #as it is not wanted in all cases, e.g. when we remove a var and add a var, we want to keep the zoom, or bunch plots etc
          self.reset_x_axis() 
        
        self.set_x_axis()
        self.server.set_y_range(self.plot.y_range.start,self.plot.y_range.end)
        #self.adjust_y_axis_limits()

        return getData # so that later executed function don't need to get the data again

    def range_cb(self, attribute,old, new):
        """
            callback by bokeh system when the scaling have changed (roll the mouse wheel), see bokeh documentation
        """
        #we only store the range, and wait for an LOD or PANEnd event to refresh
        #self.logger.debug(f"range_cb {attribute}")
        if attribute == "start":
            self.rangeStart = new
        if attribute == "end":
            self.rangeEnd = new
        if self.streamingMode == True and not self.inStreamUpdate:
            self.userZoomRunning = True
        #print("range cb"+str(attribute),self.rangeStart,self.rangeEnd)
        #self.logger.debug(f"leaving range_cb with userzoom running {self.userZoomRunning}")

    def get_min_max_times(self,newData):
        mini = 1000*1000*1000*1000*1000
        maxi = 0
        for k,v in newData.items():
            if k.endswith("__time"):
                check=[mini]
                if len(v)>=2:
                    check.extend(v[1:-1])
                else:
                    check.extend(v)
                mini =  min(check)
                check=[maxi]
                if len(v)>=2:
                    check.extend(v[1:-1])
                else:
                    check.extend(v)
                maxi=max(check)
        return mini,maxi

    def get_min_max_y(self,newData):
        mini = 1000*1000*1000*1000*1000
        maxi = 0
        for k,v in newData.items():
            if not k.endswith("__time"):
                check=[mini]
                check.extend(v[1:-1])
                mini =  min(check)
                check=[maxi]
                check.extend(v[1:-1])
                maxi=max(check)
        return mini,maxi


    def reset_x_axis(self):
        mini = 1000*1000*1000*1000*1000
        maxi = 0
        for item in self.columnData:
            mini=min(min(self.columnData[item].data["x"]),mini)
            maxi = max(max(self.columnData[item].data["x"]),maxi)
        if mini==maxi:
            mini=mini-1
            maxi=mini+2
        self.set_x_axis(start=mini,end=maxi)

    def set_x_axis(self,start=None,end=None):
        if start:
            self.rangeStart = start
        if end:
            self.rangeEnd = end
        self.plot.x_range.start = self.rangeStart
        self.plot.x_range.end   = self.rangeEnd

    def refresh_plot(self,appendingDataArrived = False):
        """
            # get data from the server and plot the lines
            # if the current zoom is out of range, we will resize it:
            # zoom back to max zoom level shift
            # or shift left /right to the max positions possible
            # if there are new variables, we will rebuild the whole plot
            # appendingDataArrived: if set true and we are in streaming mode, we get the right-most data instead of the current zoom level
        """
        self.logger.debug("refresh_plot()")
        #have the variables changed?

        #make the differential analysis: what do we currently show and what are we supposed to show?
        currentLines = [lin for lin in self.lines] #these are the names of the current vars
        backendLines = self.server.get_variables_selected()
        deleteLines = list(set(currentLines)-set(backendLines))
        newLines = list(set(backendLines)-set(currentLines))
        #special case:
        # the _limitMin lines are not listed in the self.lines, as we keep only the limitMax to store the band, so the limitmin
        # is a variable in the backend but not in the lines, keep them out of the discussio here
        newLines = [line for line in newLines if not line.endswith("_limitMin")]

        #now restore original order
        newLinesOrdered=[line for line in backendLines if line in newLines]
        newLines = newLinesOrdered


        scoreVars = self.server.get_score_variables()  # we assume ending in _score

        self.logger.debug("diffanalysis new"+str(newLines)+"  del "+str(deleteLines))


        # now delete the ones to delete

        # automatically add the score to the deletion if a variable has one

        """
        additionalDeletes = []
        for key in deleteLines:
            if not key in scoreVars:
                #this line is not a score itself
                scoreNodeName = key.split('.')[-1]+'_score' # we assume root.myvariable.var1, then we build var1_score
                #now check
                for lineName in currentLines:
                    if scoreNodeName in lineName:
                        additionalDeletes.append(lineName)
        deleteLines.extend(additionalDeletes)
        deleteLines=list(set(deleteLines)) # avoid duplicates
        """

        if deleteLines:
            removeLegendKeys = []
            removeSelfLines = []
            self.plot.legend.items=[] # avoid errors later, we might remove the glyph but the legend needs it
            for key in deleteLines:
                self.lines[key].visible = False
                removeSelfLines.append(key)
                removeLegendKeys.append(key)

            #remove the lines
            self.remove_renderers(deleteLines)
            #remove the according thresholds if any
            for lin in deleteLines:
                self.remove_renderers(self.find_thresholds_of_line(lin),deleteFromLocal=True)
                self.remove_renderers(self.find_motifs_of_line(lin),deleteFromLocal=True)
                #marker = self.find_renderer(lin+"_marker")
                #if marker:
                #    self.remove_renderers(renderers=[marker])

            extraDeleteRenderers = self.find_extra_renderers_of_lines(deleteLines)
            for r in extraDeleteRenderers:
                if r.name in self.legendItems:
                    #del self.legendItems[r.name]
                    removeLegendKeys.append(r.name)

            self.remove_renderers(renderers=extraDeleteRenderers,deleteFromLocal=True)# remove scores, expected, markers

            #also remove extra lines which are not found in the renderes don'T know why
            """
            for delkey in self.find_extra_names_of_lines(deleteLines):
                for key in self.lines:
                    if key.endswith(delkey):
                        self.lines[key].visible = False
                        removeSelfLines.append(key)
                        removeLegendKeys.append(key)
            """ 

            #rebuild the legend is done at the end of the plot_lines
            for key in removeLegendKeys:
                if key in self.legendItems:
                    del self.legendItems[key]
            for key in removeSelfLines: # remove them after the legend remove
                if key in self.lines:
                    del self.lines[key]

            #also delete the links from the model in the backend
            #put together all deletes
            serverDeletes = deleteLines.copy()
            serverDeletes.extend([r.name for r in extraDeleteRenderers])
            newServerSelection=[lin for lin in backendLines if lin not in serverDeletes]
            #self.logger.debug(f"SET SELECTED {newServerSelection} {backendLines}")
            if set(newServerSelection) != set(backendLines):
                self.server.set_variables_selected(newServerSelection)




        #create the new ones

        #automatically add scores if needed: if the user adds a variable to the tree, we might need to add also the score
        if self.showScores:
            additionalLines=[]
            additionalLinesY2=[]
            currentLineEndings = [name.split('.')[-1] for name in currentLines]
            for key in newLines:
                scoreName = key.split('.')[-1]+"_score"
                for scoreVar in scoreVars:
                    if scoreName in scoreVar:
                        #we add this one only if it is not there already
                        if scoreName not in currentLineEndings:
                            if self.server.has_y2():
                                #if according line is a y2 line
                                if self.server.is_y2_variable(key):
                                    additionalLinesY2.append(scoreVar)
                                else:
                                    additionalLines.append(scoreVar)
                            else:
                                additionalLines.append(scoreVar)
            if additionalLines or additionalLinesY2:
                additionalLines=list(set(additionalLines))# remove duplicates
                additionalLinesY2 = list(set(additionalLinesY2))
                self.logger.debug(f"MUST add scores: {additionalLines}.. in the next event")
                self.server.add_variables_selected(additionalLines,addListY2=additionalLinesY2)
                #return # wait for next event


        if deleteLines or newLines:
            forceYRescale = True
        else:
            forceYRescale = False
        if deleteLines:
            allowXAxisReset = False
        else:
            allowXAxisReset = True
        data = self.__plot_lines(newVars = newLines,appendingDataArrived=appendingDataArrived,forceYRescale=forceYRescale,allowXAxisReset=allowXAxisReset) # the data contain all visible time series including the background
        #todo: make this differential as well
        if self.server.get_settings()["background"]["hasBackground"]:
            self.refresh_backgrounds(data)

        if self.server.get_settings()["hasHover"] not in [False,None]:
            self.__make_tooltips() #must be the last in the drawings

        if deleteLines or newLines:
            self.server.get_selected_variables_sync()#make sure we update the changes completely

    def refresh_backgrounds_old(self):
        """ check if backgrounds must be drawn if not, we just hide them"""
        self.hide_backgrounds()
        if self.showBackgrounds:
            self.show_backgrounds()

    def refresh_backgrounds(self,data = None):
        if self.backgroundHighlightVisible:
            return #don't touch a running selection
        self.background_highlight_hide()
        # we show the new backgrounds first and then delete the old to avoid the short empty time, looks a bit better
        deleteList = []
        for r in self.plot.renderers:
            if r.name:
                if "__background" in r.name:
                    deleteList.append(r.name)
        if self.showBackgrounds:
            self.show_backgrounds(data = data)
        if deleteList:
            self.remove_renderers(deleteList=deleteList)


    def var_select_button_cb(self):
        """
            UI callback, called when the variable selection button was clicked
        """
        #apply the selected vars to the plot and the backend
        currentSelection = self.variablesMultiSelect.value
        #write the changes to the backend
        self.server.set_variables_selected(currentSelection)
        self.refresh_plot()


    def mouse_cb(self,event):
        """
            example implementation for separate y axis zoom
        """
        print(f"mouse, {event.sx}")
        if event.delta  <  0:
            factor = -1
        else:
            factor = 1
        if event.sx < 500:
            #left axix

            size = self.plot.y_range.end - self.plot.y_range.start
            self.plot.y_range.start = self.plot.y_range.start + factor*size/5
            self.plot.y_range.end = self.plot.y_range.end - factor*size / 5
        else:
            size = self.plot.extra_y_ranges["y2"].end - self.plot.extra_y_ranges["y2"].start
            self.plot.extra_y_ranges["y2"].end = self.plot.extra_y_ranges["y2"].end -factor*size/5
            self.plot.extra_y_ranges["y2"].start = self.plot.extra_y_ranges["y2"].start + factor*size / 5


    def event_cb(self,event):
        """
            the event callback from the UI for any user interaction: zoom, select, annotate etc
            Args:
                event (bokeh event): the event that happened
        """

        eventType = str(event.__class__.__name__)
        msg = " "
        for k in event.__dict__:
            msg += str(k) + " " + str(event.__dict__[k]) + " "
        self.logger.debug("event " + eventType + msg)
        #print("event " + eventType + msg)

        if eventType in ["PanStart","Pan"]:
            if self.streamingMode:
                self.userZoomRunning = True # the user is starting with pannin, we old the ui updates during user pan
            self.inPan = True

        """
        if eventType == "PanEnd":
            #self.refresh_plot()
            if self.streamingMode:
                self.userZoomRunning = False # the user is finished with zooming, we can now push data to the UI again
            #self.logger.debug(f"{self.toolBarBox.toolbar.active_pan}")
            self.autoAdjustY = False
            self.refresh_plot()
        """

        #if eventType == "LODEnd":
        if eventType in ["LODEnd","PanEnd"]:
            self.inPan = False
            if self.streamingMode:
                self.userZoomRunning = False # the user is finished with zooming, we can now push data to the UI again
                # also update the zoom level during streaming
                self.streamingInterval = self.plot.x_range.end - self.plot.x_range.start #.rangeEnd - self.rangeStart
                self.logger.debug(f"new streaming interval: {self.streamingInterval}")
            #if self.server.get_settings()["autoScaleY"][".properties"]["value"] == True
            if eventType=="LODEnd":# self.boxModifierVisible:
                self.autoAdjustY = self.server.get_mirror()["autoScaleY"][".properties"]["value"]
                self.server.set_xy_range(self.rangeStart,self.rangeEnd)
                self.refresh_plot()

        if eventType == "Reset":
            self.reset_plot_cb()

        if eventType == "SelectionGeometry":
            #option = self.annotationButtons.active # gives a 0,1 list, get the label now
            #tags = self.server.get_settings()["tags"]
            #mytag = self.annotationTags[option]
            for k,v in self.columnData.items():
                #v.selected = Selection(indices=[]) #not allowed in bokeh 2.01 f
                v.selected.indices = []
                v.data = dict(v.data)
                pass

            mytag =self.currentAnnotationTag
            #self.logger.info("TAGS"+str(self.annotationTags)+"   "+str(option))

            #self.data.selected = Selection(indices=[])  # suppress real selection
            if mytag != None:
                self.edit_annotation_cb(event.__dict__["geometry"]["x0"],event.__dict__["geometry"]["x1"],mytag,event.__dict__["geometry"]["y0"],event.__dict__["geometry"]["y1"])
        if eventType == "Tap":
            #self.logger.debug(f"TAP {self.annotationsVisible}, {event.__dict__['sx']}")
            #plot all attributes
            #self.logger.debug(f"legend {self.plot.legend.width}")
            self.box_modifier_tap(event.__dict__["x"],event.__dict__["y"]  )
            self.logger.debug(f"TAP done")


        self.logger.debug(f"leave event with user zomm running{self.userZoomRunning}")
    def reset_plot_cb(self):
        self.logger.debug("reset plot")
        self.rangeStart = None
        self.rangeEnd = None
        self.box_modifier_hide() # reset the selection
        self.refresh_plot()



    def delete_annotations(self,annoIds,apply=True):

        for tag,v in self.annotationsInfo.items():
            hasChanged = False
            for id in annoIds:
                if id in v["data"]["id"]:
                    #get the index and rework the table: we take out the index of this match
                    fIdx =v["data"]["id"].index(id)
                    for k,original in v["data"].items():
                        self.annotationsInfo[tag]["data"][k]=[item for idx, item in enumerate(original) if idx != fIdx]
                    hasChanged = True
            if hasChanged and apply:
                # apply the update
                self.annotationsInfo[tag]["ColumnDataSource"].data = dict(self.annotationsInfo[tag]["data"])
            hasChanged = False

    def find_renderer(self,rendererName):
        for r in self.plot.renderers:
            if r.name:
                if r.name == rendererName:
                    return r
        #also look through the annotations
        for k,v in self.annotationsInfo.items():
            if rendererName in v["data"]["name"]:
                return v["renderer"]

        return None



    def add_renderers(self,addList):
        self.plot.renderers.extend(addList)

    def remove_renderers(self,deleteList=[],deleteMatch="",renderers=[],deleteFromLocal = False):
        """
         this functions removes renderers (plotted elements from the widget), we find the ones to delete based on their name attribute
         Args:
            deletelist: a list or set of renderer names to be deleted
            deleteMatch(string) a part of the name to be deleted, all renderer that have this string in their names will be removed
            renderers : a list of bokeh renderers to be deleted
        """

        deletedRenderers = []
        #sanity check:
        with self.renderersLock:
            if self.renderersGarbage:
                self.logger.info(f"renderers garbage collector {self.renderersGarbage}")
                renderers.extend(self.renderersGarbage)
                self.renderersGarbage = []

        if deleteList == [] and deleteMatch == "" and renderers == []:
            return
        #self.logger.debug(f"remove_renderers(), {deleteList}, {deleteMatch}, {renderers}")

        deleteList = deleteList.copy() # we will modify it
        newRenderers = []
        for r in self.plot.renderers:
            if r in renderers:
                deletedRenderers.append(r)
                continue # we ignore this one and do NOT add it to the renderers, this will hide the object
            if r.name:
                if r.name in deleteList:
                    self.logger.debug(f"remove_renderers {r.name}")
                    deleteList.remove(r.name) # reduce the list to speed up looking later
                    deletedRenderers.append(r)
                    continue  # we ignore this one and do NOT add it to the renderers, this will hide the object
                elif deleteMatch != "" and deleteMatch in r.name:
                    deletedRenderers.append(r)
                    continue  # we ignore this one and do NOT add it to the renderers, this will hide the object
                else:
                    newRenderers.append(r)  # we keep this one, as it doesnt mathc the deletersl
            else:
                newRenderers.append(r)  # if we have no name, we can't filter, keep this

        self.plot.renderers = newRenderers

        if deleteFromLocal:
            #delete this also from the local renderers list:
            delList = []
            for k,v in self.renderers.items():
                if v["renderer"] in deletedRenderers:
                    delList.append(k)
            for k in delList:
                del self.renderers[k]


    def annotation_toggle_click_cb(self,toggleState):
        """
            callback from ui for turning on/off the annotations
            Args:
                toggleState (bool): true for set, false for unset
        """
        if toggleState:
            self.showAnnotationToggle.label = "hide Annotations"
            self.show_annotations()
        else:
            #remove all annotations from plot
            self.showAnnotationToggle.label = "show Annotations"
            self.hide_annotations()

    def threshold_toggle_click_cb(self,toggleState):
        """
            callback from ui for turning on/off the threshold annotations
            Args:
                toggleState (bool): true for set, false for unset
        """
        if toggleState:
            self.showThresholdToggle.label = "hide Thresholds"
            self.showThresholds = True
            self.show_thresholds()
        else:
            #remove all annotations from plot
            self.showThresholdToggle.label = "show Thresholds"
            self.showThresholds = False
            self.hide_thresholds()

    def show_thresholds(self):
        """
            check which lines are currently shown and show their thresholds as well
        """
        #if not self.showThresholds:
        #    return

        self.showThresholds = True

        for annoName,anno in self.server.get_annotations().items():
            #self.logger.debug("@show_thresholds "+annoName+" "+anno["type"])
            if anno["type"]=="threshold":
                # we only show the annotations where the lines are also there
                self.logger.debug("@show_thresholds "+annoName+" "+anno["type"]+"and the lines are currently"+str(list(self.lines.keys())))
                if anno["variable"] in self.lines:
                    self.draw_threshold(anno)#,anno["variable"])


    def show_motifs(self):
        self.showMotifs = True
        for annoName,anno in self.server.get_annotations().items():
            #self.logger.debug("@show_thresholds "+annoName+" "+anno["type"])
            if anno["type"]=="motif":
                # we only show the annotations where the lines are also there
                self.logger.debug("@show_motifs "+annoName+" "+anno["type"]+"and the lines are currently"+str(list(self.lines.keys())))
                if anno["variable"] in self.lines:
                    self.draw_motif(anno)#,anno["variable"])

    def hide_motifs(self):
        self.showMotifs = False
        self.box_modifier_hide()
        annotations = self.server.get_annotations()
        timeAnnos = [anno for anno in annotations.keys() if annotations[anno]["type"] == "motif"]
        self.remove_renderers(deleteList=timeAnnos, deleteFromLocal=True)


    def hide_thresholds(self):
        """ hide the current annotatios in the widget of type time"""
        self.showThresholds=False

        self.box_modifier_hide()
        annotations = self.server.get_annotations()
        timeAnnos = [anno for anno in annotations.keys() if annotations[anno]["type"]=="threshold" ]
        self.remove_renderers(deleteList=timeAnnos,deleteFromLocal=True)




    def backgroundbutton_cb(self,toggleState):
        """
            event callback function triggered by the UI
            toggleStat(bool): True/False on toggle is set or not
        """
        if toggleState:
            self.backgroundbutton.label = "hide Backgrounds"
            self.showBackgrounds = True
            self.show_backgrounds(None)
        else:
            self.backgroundbutton.label = "show Backgrounds"
            self.hide_backgrounds()
            self.showBackgrounds = False


    def init_annotations(self):
        # we assume that annotations are part of the model,
        ## get the annotations from the server and build the renderers, plot them if wanted
        ## but only the time annotations, the others are created and destroyed on demand
        #self.visibleAnnotations = set() # a set

        return
        self.logger.debug(f"init_annotations() {len(self.server.get_annotations())} annotations..")

        #now we build all renderers for the time annos and don't show them now
        for annoname, anno in self.server.get_annotations().items():
            if anno["type"] == "time":
                self.draw_annotation(anno,False)

        self.logger.debug("init annotations done")


    def init_events(self):
        self.logger.debug(f"init_events")
        #create all renderers but don't show them
        visible = self.server.get_mirror()["visibleElements"][".properties"]["value"]
        if "events" in visible and visible["events"]==True:
            self.eventsVisible = True #currently turned on
            self.logger.debug(f"init_events visible")
            self.show_all_events()
        else:
            self.logger.debug("init events invisible")


    def show_all_events(self):
        self.logger.debug("show_all_events")
        data = self.server.get_events()
        self.eventsVisible = True
        if data:
            self.show_events(data)

    def init_annotations_old(self):
        """
            chreate the actual bokeh objects based on existing annotations, this speeds up the process a lot when show
            ing the annotations later, we will keep the created objecs in the self.annotations list and apply it to
            the renderes later, this will only be used for "time" annotations, the others are called thresholds
        """
        self.annotations={}
        self.logger.debug(f"init {len(self.server.get_annotations())} annotations..")
        for annoname, anno in self.server.get_annotations().items():
            if "type" in anno and anno["type"] != "time":
                continue # ignore any other type
            self.draw_annotation(annoname,add_layout=False)
        #now we have all bokeh objects in the self.annotations
        self.logger.debug("init_annotations.. done")


    def update_annotations(self):
        """
            this is called when the tags have changed and we might have to update the currently visible annos
        """
        if self.showAnnotations:
            self.show_annotations()

    def show_annotations_old(self, annoIdFilter=[],fetch=True):
        """
            show annotations and hide annotations according to their tags (compare with visibleTags
        """

        self.logger.debug("show_annotations()")
        self.showAnnotations = True
        if fetch:
            mirror = self.server.fetch_mirror()
        else:
            mirror = self.server.get_mirror()
        allowedTags = mirror["hasAnnotation"]["visibleTags"][".properties"]["value"]
        self.showAnnotationTags = [tag for tag in allowedTags if allowedTags[tag]]
        self.logger.debug(f"show annotation tags {self.showAnnotationTags}")

        addList = []
        removeList = []

        for k, v in self.renderers.items():
            if annoIdFilter:
                if k not in annoIdFilter:
                    continue
            if v["info"]["type"] != "time":
                continue # only the time annotations
            if not v["renderer"] in self.plot.renderers:
                #this renderer is not yet in the renderers, check if we are allowed to show it
                if any ([True for tag in v["info"]["tags"] if tag in self.showAnnotationTags ]):
                    addList.append(v["renderer"])
            if v["renderer"] in self.plot.renderers:
                #this renderer is already there, check if we might need to hide it
                if not any([True for tag in v["info"]["tags"] if tag in self.showAnnotationTags]):
                    removeList.append(v["renderer"])
                    # is the box modifier on this currently active?
                    # if the currently selected is being hidden, we hide the box modifier
                    if self.boxModifierVisible:
                        if self.boxModifierAnnotationName == k:
                            self.box_modifier_hide()

        self.logger.debug(f"add {len(addList)} annotations to plot remove {len(removeList)} from plot")
        self.plot.renderers.extend(addList)
        self.remove_renderers(renderers=removeList)


    def hide_annotations_by_tag(self,tag,modified=None, deleted = None,force=None):
        # force: hide all annotations
        # if modified given: then delete only the modified
        # if deleted given: then delete only the deleted
        #empty all lists
        hasChanged = False

        changedOrModifiedIds = []
        if modified:
            changedOrModifiedIds.extend(list(modified.keys()))

        if self.boxModifierVisible:
            if force or (modified and self.boxModifierAnnotationName in changedOrModifiedIds) or (deleted and self.boxModifierAnnotationName in deleted):#self.annotationsInfo[tag]["data"]["id"]:
                self.box_modifier_hide()

        if any(self.annotationsInfo[tag]["data"]["drawn"]):
            #delete only the non-drawn
            serverAnnos = self.server.get_annotations()
            hasChanged = True
            new = {key:[] for key in self.annotationsInfo[tag]["data"]}
            for index in range(len(self.annotationsInfo[tag]["data"]["drawn"])):
                if self.annotationsInfo[tag]["data"]["drawn"][index]:
                    annoId = self.annotationsInfo[tag]["data"]["id"][index]
                    # we rescue this annotation only if it was not deleted on the server!
                    #print(f"this anno id was draws {annoId} and is on server {annoId in serverAnnos}")
                    if annoId in serverAnnos:
                        self.add_anno_to_list(serverAnnos[annoId],tag,manual=True,listPointer=new)
                    #for key in new:
                    #    new[key].append(self.annotationsInfo[tag]["data"][key][index])



            self.annotationsInfo[tag]["data"] = new
        else:
            #no anno was drawn manually, so hide them all
            for key in self.annotationsInfo[tag]["data"]:
                if self.annotationsInfo[tag]["data"][key]!=[]:
                    hasChanged = True
                    self.annotationsInfo[tag]["data"][key]=[]

        return hasChanged

    def show_annotations(self, fetch=True, checkModifies=False, newAnno=None, modified = None):
        """
            show annotations and hide annotations according to their tags (compare with visibleTags
            if anno is given, we only display the anno (additionally)
            newAnnotation: just add this annotation now, nothing else to do
            modified: the modified annotations in a dict form ["id":["name", "startimeae"....},"id2":{}...}
        """

        #
        refreshTooltips = False
        fetchNow = False
        newAnnoCreated = False

        self.logger.debug(f"show_annotations() {fetch}, {checkModifies}, {newAnno}, {modified}")
        self.showAnnotations = True
        if fetch:
            mirror = self.server.fetch_mirror()
            self.server.fetch_annotations()
        else:
            mirror = self.server.get_mirror()

        allowedTags = mirror["hasAnnotation"]["visibleTags"][".properties"]["value"]
        self.showAnnotationTags = [tag for tag in allowedTags if allowedTags[tag]]
        self.logger.debug(f"show annotation tags {self.showAnnotationTags}")

        generalVisible = mirror["visibleElements"][".properties"]["value"]["annotations"]

        #there is a special case where a tag does not exist anymore because the user renamed it
        # check this here
        currentTagsinModel = set(mirror["hasAnnotation"]["visibleTags"][".properties"]["value"].keys())
        currentTagsinUI = set(self.annotationsInfo.keys())
        disappearedTags = currentTagsinUI - currentTagsinModel
        for tag in disappearedTags:
            #del the hover entry
            myrenderer = self.annotationsInfo[tag]["renderer"]
            
            newAnnoHovers = []
            for hov in self.annoHovers:
                if hov == myrenderer:
                    continue #skip
                else:
                    newAnnoHovers.append(hov)
            self.annoHovers = newAnnoHovers
            #del the annotations
            self.hide_annotations_by_tag(tag,force=True)
            self.remove_renderers(renderers=[myrenderer])
            mustApply = True
            refreshTooltips=True


        
        
        for tag,visible in mirror["hasAnnotation"]["visibleTags"][".properties"]["value"].items():
            # first we check if for all tags we have general entries in the annotationsInfo
            mustApply = False
            if not tag in self.annotationsInfo:

                self.logger.debug(f"new tag {tag}")
                self.annotationsInfo[tag]={"data":{"center":[],"width":[],"id":[],"name":[],"anno":[],"drawn":[],"x":[],"y":[]},
                                           "ColumnDataSource":None,
                                           "renderer":None,
                                           "glyph":None,}
                self.annotationsInfo[tag]["ColumnDataSource"]=ColumnDataSource(self.annotationsInfo[tag]["data"])
                self.create_annotations_glyph(tag)
                refreshTooltips=True
                #also draw them
            if not visible or not generalVisible:
                if self.hide_annotations_by_tag(tag,modified = modified, force=True):
                    mustApply = True
                if newAnno:
                    self.add_anno_to_list(newAnno, tag, True)
                    #ewAnnoCreated = newAnnoCreated or created
                    #self.logger.debug(f"newAnnoCreated,n created {newAnnoCreated}, {created}")
                    mustApply = True

                #this tag is visible, let's see if we have any difference, we make this check to avoid unnecessary updates of the columndatas
            else:
                existingIds = set(self.annotationsInfo[tag]["data"]["id"])
                newIds,invalidIds = self.get_new_valid_annotation_ids(tag)#set([anno["id"] for annoname, anno in self.server.get_annotations().items() if anno["type"]=="time" and tag in anno["tags"]])

                existingIds = existingIds-invalidIds #also ignore the invalid on the existing (yes, they are there, they should not, fix this)
                
                if checkModifies:
                    modifiedTags = []
                    for k,v in modified.items():
                        if "tags" in v: modifiedTags.extend(v["tags"])


                if newIds-existingIds or existingIds-newIds or (checkModifies and tag in modifiedTags):
                    #we modify this tags's annotations only if they have changed
                    mustApply = True
                    if (newIds-existingIds or existingIds-newIds):
                        self.logger.debug(f"ids have changed {newIds-existingIds} {existingIds-newIds}")
                        refreshTooltips = True
                        fetchNow = True
                    #we have more or less, let's rebuild
                    self.hide_annotations_by_tag(tag,modified = modified,deleted = existingIds-newIds )
                    for annoname, anno in self.server.get_annotations().items():
                        self.add_anno_to_list(anno,tag,False)
                        #newAnnoCreated = newAnnoCreated or created
                        #self.logger.debug(f"newAnnoCreated,m created {newAnnoCreated}, {created}")
                # if this tag is visible, we convert all "drawn" annotation to standard annotations
                # doing so, they will be hidden on the next invisible switch
                if any(self.annotationsInfo[tag]["data"]["drawn"]):
                    self.annotationsInfo[tag]["data"]["drawn"]=[False]*len(self.annotationsInfo[tag]["data"]["drawn"])
                    mustApply=True
            #if there is a manual add, we do it here
            #apply the update
            if mustApply:
                self.annotationsInfo[tag]["ColumnDataSource"].data = dict(self.annotationsInfo[tag]["data"])

        self.logger.debug(f"show_annotations() done {fetchNow}")
        
        if fetchNow:
            self.server.fetch_annotations()#we had different ids in the annos, fetch now
            
        if refreshTooltips:
            self.__make_tooltips(force=True)
        
            
    def get_new_valid_annotation_ids(self,tag):
        #return only valid annotations
        returnIds = set()
        invalidIds = set()
        for annoname, anno in self.server.get_annotations().items(): 
            if anno["type"]=="time" and tag in anno["tags"]:
                if anno["startTime"]  <  anno["endTime"]:
                    returnIds.add(anno["id"])
                else:
                    self.logger.warning(f"skip annotation: start>end {anno}")
                    invalidIds.add(anno["id"])
        return returnIds,invalidIds
        

    def add_anno_to_list(self,anno,tag,manual=False,listPointer=None):
        if type(listPointer) is type(None):
            listPointer = self.annotationsInfo[tag]["data"]

        if anno["type"]=="time" and tag in anno["tags"]:
            start = anno["startTime"]
            end = anno["endTime"]

            #sanity checks
            if start>end:
                self.logger.error(f"skip annotation: start>end {anno}")
                return False

            listPointer["center"].append((end + start) / 2)
            listPointer["width"].append(end - start)
            listPointer["name"].append(anno["id"])
            listPointer["id"].append(anno["id"])
            listPointer["anno"].append(anno)
            listPointer["drawn"].append(manual)
            listPointer["x"].append((end + start) / 2)
            listPointer["y"].append(0)
        return True


    def hide_annotations_old(self):
        self.showAnnotations = False
        """ hide the current annotatios in the widget of type time"""
        annotations = self.server.get_annotations()
        timeAnnos = [anno  for anno in annotations.keys() if annotations[anno]["type"]=="time" ]
        self.logger.debug("hide_annotations "+str(timeAnnos))
        self.remove_renderers(deleteList=timeAnnos)
        #self.annotationsVisible = False
        self.box_modifier_hide()

    def hide_annotations(self):
        self.box_modifier_hide()
        self.show_annotations()



    def get_layout(self):
        """ return the inner layout, used by the main"""
        return self.layout
    def set_curdoc(self,curdoc):
        self.curdoc = curdoc
        #curdoc().theme = Theme(json=themes.defaultTheme) # this is to switch the theme

    def remove_annotations(self,deleteList):
        """
            remove annotation from plot, object list and from the server
            modelPath(list of string): the model path of the annotation, the modelPath-node must contain children startTime, endTime, colors, tags
        """
        self.remove_renderers(deleteList=deleteList)
        self.server.delete_annotations(deleteList)
        for anno in deleteList:
            if anno in self.annotations:
                del self.annotations[anno]


    def draw_motif(self,anno):
        """ draw the boxannotation for a motif
            Args:
                 modelPath(string): the path to the annotation, the modelPath-node must contain children startTime, endTime, colors, tags
        """
        self.logger.debug(f"draw motif {anno}")
        try:
            #if the box is there already, then we skip
            if anno["id"] in self.renderers:
                self.logger.warning(f"have this already {anno['id']}")
                return
            color = self.lines[anno["variable"]].glyph.line_color

            start = anno["startTime"]
            end = anno["endTime"]

            infinity = globalInfinity
            # we must use varea, as this is the only one glyph that supports hatches and does not create a blue box when zooming out
            # self.logger.debug(f"have pattern with hatch {pattern}, tag {tag}, color{color} ")

            """
            source = ColumnDataSource(dict(x=[start, end], y1=[-infinity, -infinity], y2=[infinity, infinity]))
            area = VArea(x="x", y1="y1", y2="y2",
                         fill_color="black",
                         name=anno["id"],
                         fill_alpha=0.2,
                         hatch_color=color,
                         hatch_pattern="v",
                         hatch_alpha=0.5)
            
            """
            # this overcomes the bokeh bug that on super zoom, the hatch pattern of varea disappears:
            # Vbar behaves correctly
            source = ColumnDataSource(dict(x=[start+(end-start)/2], t=[infinity], b=[-infinity],w=[end-start]))
            area = VBar(x="x", top="t", bottom="b", width="w", fill_color="black",
                         name=anno["id"],
                         fill_alpha=0.2,
                         hatch_color=color,
                         hatch_pattern="v",
                         hatch_alpha=0.5)


            #    bokeh hack to avoid adding the renderers directly: we create a renderer from the glyph and store it for later bulk assing to the plot
            # which is a lot faster than one by one
            myrenderer = GlyphRenderer(data_source=source, glyph=area, name=anno['id'])
            self.add_renderers([myrenderer])

            self.renderers[anno["id"]] = {"renderer": myrenderer, "info": copy.deepcopy(anno),
                                      "source": source}  # we keep this renderer to speed up later

        except Exception as ex:
            self.logger.error("error draw motif"+str(ex))
            return None





    def draw_annotation__old(self, anno, visible=False):
        """
            draw one time annotation on the plot
            Args:
             anno: the annotation
             visible: true/false
        """
        try:
            #self.logger.debug(f"draw_annotation  {anno['name']} visible {visible}")

            tag = anno["tags"][0]
            mirror = self.server.get_mirror()
            myColors = mirror["hasAnnotation"]["colors"][".properties"]["value"]
            myTags = mirror["hasAnnotation"]["tags"][".properties"]["value"]

            try: # to set color and pattern
                if type(myColors) is list:
                    tagIndex = myTags.index(tag)
                    pattern = None
                    color = myColors[tagIndex]
                elif type(myColors) is dict:
                    color = myColors[tag]["color"]
                    pattern = myColors[tag]["pattern"]
                    if not pattern is None:
                        if pattern not in [" ",".","o","-","|","+",":","@","/","\\","x",",","`","v",">","*"]:
                            pattern = 'x'
            except:
                color = None
                pattern = None
            if not color:
                self.logger.error("did not find color for boxannotation")
                color = "red"

            start = anno["startTime"]
            end = anno["endTime"]

            infinity=globalInfinity
            # we must use varea, as this is the only one glyph that supports hatches and does not create a blue box when zooming out
            #self.logger.debug(f"have pattern with hatch {pattern}, tag {tag}, color{color} ")
            

            if not pattern is None:
                """
                source = ColumnDataSource(dict(x=[start, end], y1=[-infinity, -infinity], y2=[infinity, infinity]))
                area = VArea(x="x",y1="y1",y2="y2",
                                    fill_color=color,
                                    name=anno["id"],
                                    fill_alpha=globalAnnotationsAlpha,
                                    hatch_color="black",
                                    hatch_pattern=pattern,
                                    hatch_alpha=1.0)
                rendererType = "VArea"
                """
                source = ColumnDataSource(dict(x=[start + (end - start) / 2], t=[infinity], b=[-infinity], w=[end - start]))
                area = VBar(x="x", top="t", bottom="b", width="w",
                            fill_color=color,
                            name=anno["id"],
                            fill_alpha=globalAnnotationsAlpha,
                            hatch_color="black",
                            hatch_pattern=pattern,
                            hatch_alpha=1.0)
                
                
                myrenderer = GlyphRenderer(data_source=source, glyph=area, name=anno['id'])
                myrenderer.level = globalThresholdsLevel
                rendererType = "VBar"
            else:
                #we use a Boxannotation as this is a lot more efficient in bokeh
                """ 
                area = VArea(x="x", y1="y1", y2="y2",
                                    fill_color=color,
                                    name=anno["id"],
                                    fill_alpha=globalAlpha)
                """
                if any([True for tag in anno["tags"] if "anomaly" in tag]):
                    # if we have an anomaly to draw, we put it on top
                    level = globalThresholdsLevel
                else:
                    level = globalAnnotationLevel

                if BOX_ANNO:
                    source = None
                    myrenderer = BoxAnnotation(left=start,right=end,fill_color=color,fill_alpha=globalAnnotationsAlpha,name=anno['id'],level=level)
                    rendererType = "BoxAnnotation"
                else:
                    #use rect
                    source = ColumnDataSource({"l": [start+(end-start)/2],"w": [end-start],"y": [-infinity],"height": [3 * infinity]})
                    recta = Rect(x="l", y="y", width="w", height="height", fill_color=color, fill_alpha=globalAnnotationsAlpha)
                    myrenderer = GlyphRenderer(data_source=source, glyph=recta, name=anno['id'],level=level)
                    rendererType = "Rect"

                    if myrenderer not in self.annoHovers:
                        self.annoHovers.append(myrenderer)


            # bokeh hack to avoid adding the renderers directly: we create a renderer from the glyph and store it for later bulk assing to the plot
            # which is a lot faster than one by one

            if 0: #this was a trial for an extra object to hover the annotations
                dic = {"y": [0],
                       "x": [start+(end-start)/20],
                       "w":[end-start],
                       "h":[infinity],
                       "l":[start],
                       "r":[end-start],
                       "t":[infinity],
                       "b":[-infinity],
                       "f":[0.9]}
                col = ColumnDataSource(dic)
                # the only glyph that worked for hovering was the circle, rect, quad did not work
                #annoHover = self.plot.circle(x="x", y="f",size=15, fill_color="white",fill_alpha=0.5,source=col,name="annohover",y_range_name="y2",line_color="white",line_width=2) #works
                 #self.annoHovers.append(annoHover)

            if visible:
                self.add_renderers([myrenderer])

            self.renderers[anno["id"]] = {"renderer": myrenderer, "info": copy.deepcopy(anno),"source": source,"rendererType":rendererType}  # we keep this renderer to speed up later


            self.__make_tooltips(force=True) #xxxkna

        except Exception as ex:
            self.logger.error(f"error draw annotation {anno}"+str(ex))
            return None

    def create_annotations_glyph(self, tag):
        """
            draw one time annotation on the plot
            Args:
             anno: the annotation
             visible: true/false
        """
        self.logger.debug(f"create_annotations_glyph {tag}")
        try:
            # self.logger.debug(f"draw_annotation  {anno['name']} visible {visible}")
            mirror = self.server.get_mirror()
            myColors = mirror["hasAnnotation"]["colors"][".properties"]["value"]
            myTags = mirror["hasAnnotation"]["tags"][".properties"]["value"]

            try:  # to set color and pattern
                if type(myColors) is list:
                    tagIndex = myTags.index(tag)
                    pattern = None
                    color = myColors[tagIndex]
                elif type(myColors) is dict:
                    color = myColors[tag]["color"]
                    pattern = myColors[tag]["pattern"]
                    if not pattern is None:
                        if pattern not in [" ", ".", "o", "-", "|", "+", ":", "@", "/", "\\", "x", ",", "`", "v", ">",
                                           "*"]:
                            pattern = 'x'
            except:
                color = None
                pattern = None
            if not color:
                self.logger.error("did not find color for boxannotation")
                color = "red"

            #now we have color and pattern
             
            self.annotationsInfo[tag]["glyph"]=Rect(x="center",
                                                    y=1,#-infinity/4,
                                                    width="width",
                                                    height=globalInfinity,
                                                    height_units='screen', 
                                                    fill_color=color,
                                                    fill_alpha=globalAnnotationsAlpha,
                                                    hatch_color="black",
                                                    hatch_pattern=pattern,
                                                    hatch_alpha=0.5,
                                                    line_alpha=0
                                                    )

            self.annotationsInfo[tag]["renderer"]= GlyphRenderer(data_source=self.annotationsInfo[tag]["ColumnDataSource"], glyph=self.annotationsInfo[tag]["glyph"], name="Annotation:"+tag, level=globalAnnotationLevel)
            self.add_renderers([self.annotationsInfo[tag]["renderer"]])

            if self.annotationsInfo[tag]["renderer"] not in self.annoHovers:
                self.annoHovers.append(self.annotationsInfo[tag]["renderer"])

        except Exception as ex:
            self.logger.error(f"error create_annotations_glyphn {tag}" + str(ex))
            return None

    def hide_all_events(self):
        self.eventsVisible = False
        self.hide_events()

    def hide_events(self,keep=[],selectId=None):
        """
            hide all events excpect the keep list
            keep: a list of event tags
            selectId: give a nodeid, we only work on the renderes of that node
        """
        #hide the ones which are not here

        deleteList=[]
        for nodeId,entry in self.eventLines.items():
            if selectId and selectId != nodeId:
                continue# we have a id filter and it did not match
            if entry["eventString"] not in keep:
                deleteList.append(nodeId)
        for id in deleteList:
            self.remove_renderers(renderers=[self.eventLines[id]["renderer"]])
            del self.eventLines[id]

    def __make_colum_data_for_events(self,times):
        times = numpy.asarray(times)
        times = times * 1000  # in ms for bokeh
        infi = globalInfinity
        mirror = self.server.get_mirror()
        if "yAxisType" in mirror and mirror ["yAxisType"][".properties"]["value"]=="log":
            lower = 1e-50
        else:
            lower = -infi 
        
        x = []
        y = []
        for t in times:
            x.extend([t, t, numpy.nan]) #need the nan to avoid connecting diagonals between the vertical lines
            y.extend([lower, infi, numpy.nan])
        return {"x": x, "y": y}

    def show_events(self,eventsData,redraw=False):
        """
            show all currently visible event tags

            Args:
                nodeId: the node id
                redraw: if set to true, we delete the lines of a node and redraw them, if not, we keep them
                eventes: a dict containing  {"nodeid":{"events":{"one":[t1,t2,t3],"two":[t1,t,2,t3]...}},"nodeid2":{}
        """
        self.eventsVisible = True
        myColors = self.server.get_mirror()["hasEvents"]["colors"][".properties"]["value"]
        visibleEvents = self.server.get_mirror()["hasEvents"]["visibleEvents"][".properties"]["value"]
        visible = [k for k,v in visibleEvents.items() if v == True]

        #hide the ones which are not here
        self.hide_events(keep=visible)

        deliveredKeys = []  # build a list of all eventLines that we updated with the data
        #now show the ones to show
        for nodeId, eventInfo in eventsData.items():
            if redraw:
                #make sure we delete all lines of this node
                self.hide_events(selectId=nodeId)  # delete all lines of this node

            for eventString,times in eventInfo["events"].items():
                if eventString not in visible:
                    continue
                key = nodeId+"."+eventString
                deliveredKeys.append(key) # remember that we got this in the data
                dic = self.__make_colum_data_for_events(times)

                if key not in self.eventLines:
                    #only draw if it is not there yet
                    if eventString in myColors:
                        color = myColors[eventString]["color"]
                    else:
                        color = "yellow"
                    source = ColumnDataSource(dic)
                    li = self.plot.line(x="x",y="y", source=source,color=color,line_width=2,name=key)
                    self.eventLines[key] = {"renderer":li,"data":source,"eventString":eventString,"nodeId":nodeId}
                else:
                    #line is there already, update per bokeh data replacement
                    self.eventLines[key]['data'].data = dic

        #now check the eventLines which have not been in the data delivered, those must be deleted, as the backend has no data for them anymore
        toBeDeleted = set(self.eventLines.keys())-set(deliveredKeys)
        for id in toBeDeleted:
            self.remove_renderers(renderers=[self.eventLines[id]["renderer"]])
            del self.eventLines[id]

        #now also update the hovers
        self.__make_tooltips()


    def update_events_old(self,observerEvent):
        """
            observerEvent: the event data coming from the observer
            eventData contains [" < nodeid>":"events:[]....} for one node
        """
        self.logger.debug(f"update_evetns {observerEvent}")
        #simply kill and redraw all events
        self.hide_events()
        self.server.fetch_events()
        self.show_all_events()

    def update_events(self,observerEvent=None):
        """
            observerEvent: the event data coming from the observer
            eventData contains [" < nodeid>":"events:[]....} for one node
        """
        self.logger.debug(f"update_evetns {observerEvent}")
        eventsData = self.server.fetch_events()


        self.show_events(eventsData)
        """
        for nodeId, eventInfo in eventsData.items():
            if nodeId!=observerEvent["data"]["sourceId"]:
                continue #only the new event series are touched
            #xxx also check if this event is new or update
            for eventString, times in eventInfo["events"].items():
                self.logger.debug(f"eventlines {self.eventLines.keys()}")
                #prepare the update data
                key = nodeId + "." + eventString
                dic = self.__make_colum_data_for_events(times)
                #update the data
                self.eventLines[key]['data'].data = dic

        self.show_all_events()
        """



    def find_thresholds_of_line(self,path):
        """
            find the hreshold annotations that belong to a line given as model path
            Args:
                path: the path to the variable
            Returns:
                (list of strings of the threshold sannotations that belong to this variable
        """
        result = []
        for k,v in self.server.get_annotations().items():
            if v["type"] == "threshold":
                if v["variable"] == path:
                    result.append(k)
        self.logger.debug("@find_thresholds of line returns "+path+" => "+str(result))
        return result





    def find_motifs_of_line(self,path):
        result = []
        for k,v in self.server.get_annotations().items():
            if v["type"] == "motif":
                if v["variable"] == path:
                    result.append(k)
        self.logger.debug("@find_motifs_of_line of line returns "+path+" => "+str(result))
        return result


    def find_extra_names_of_lines(self,lines,markers=True,scores=True,expected=True,bands=True):
        deleteNames = []
        for line in lines:
            name = line.split('.')[-1]
            if scores:
                deleteNames.append(name + "_score")
            if expected:
                deleteNames.append(name + "_expected")
            if markers:
                deleteNames.append(name + "_marker")
            if bands:
                deleteNames.append(name + "_limitMax")
                deleteNames.append(name + "_limitMin")
                deleteNames.append(name + "_anomalyScore")
        return deleteNames

    def find_extra_renderers_of_lines(self,lines,markers=True,scores=True,expected=True,bands=True):
        if type(lines) is not list:
            lines = [lines]

        extraRenderes = []
        deleteNames = self.find_extra_names_of_lines(lines,markers,scores,expected,bands)

        for r in self.plot.renderers:
            if r.name and (r.name.split('.')[-1] in deleteNames):
                extraRenderes.append(r)  # take the according score as well

        for r in extraRenderes:
            self.logger.debug(f"remove extra {r.name}")
        return extraRenderes

    def show_motifs_of_line(self,path):
        self.logger.debug("@show_motifs_of_line " + path)
        motifs = self.find_motifs_of_line(path)
        annotations = self.server.get_annotations()
        for motif in motifs:
            self.draw_motif(annotations[motif])  # ,path)

    def show_thresholds_of_line(self,path):
        self.logger.debug("@show_threasholds_of_line "+path)
        thresholds = self.find_thresholds_of_line(path)
        annotations = self.server.get_annotations()
        for threshold in thresholds:
            self.draw_threshold(annotations[threshold])#,path)

    """
        def hide_thresholds_of_line(self,path):
        thresholds = self.find_thresholds_of_line(path)
        self.remove_renderers(deleteList=thresholds,deleteFromLocal=True)
    """

    def draw_threshold(self, annoDict):#, linePath=None):
        """ draw the boxannotation for a threshold
            Args:
                 modelPath(string): the path to the annotation, the modelPath-node must contain children startTime, endTime, colors, tags
        """
        self.logger.debug(f"draw thresholds {annoDict}")
        try:
            #if the box is there already, then we skip
            if annoDict["id"] in self.renderers:
                self.logger.warning(f"have this already {annoDict['id']}")
                return
            #foundRenderer = self.find_renderer(annoDict["id"])
            #if foundRenderer:
            #    #nothing to do
            #    return



            #annotations = self.server.get_annotations()
            # now get the first tag, we only use the first
            #tag = annoDict["tags"][0]



            color = self.lines[annoDict["variable"]].glyph.line_color

            min = annoDict["min"]
            max = annoDict["max"]
            if min>max:
                max,min = min,max # swap them

            # print("draw new anno",color,start,end,modelPath)

            if self.server.is_y2_variable(annoDict["variable"]):
                newAnno = BoxAnnotation(top=max, bottom=min,
                                        fill_color=color,
                                        fill_alpha=globalThresholdsAlpha,
                                        level=globalThresholdsLevel,
                                        name=annoDict["id"],y_range_name="y2")  # +"_annotaion
            else:

                newAnno = BoxAnnotation(top=max, bottom=min,
                                        fill_color=color,
                                        fill_alpha=globalThresholdsAlpha,
                                        level = globalThresholdsLevel,
                                        name=annoDict["id"])  # +"_annotaion

            self.add_renderers([newAnno])

            self.renderers[annoDict["id"]] = {"renderer": newAnno, "info": copy.deepcopy(annoDict)}  # we keep this renderer to speed up later


        except Exception as ex:
            self.logger.error("error draw threshold "+str(annoDict["id"])+str(ex))


    def draw_threshold2(self, anno,visible=False):
        """ draw the boxannotation for a threshold
            Args:
                 anno
        """

        try:
            tag = anno["tags"][0]

            if linePath:
                color = self.lines[linePath].glyph.line_color
            else:
                color ="blue"

            min = annotations[modelPath]["min"]
            max = annotations[modelPath]["max"]
            if min>max:
                max,min = min,max # swap them

            # print("draw new anno",color,start,end,modelPath)

            newAnno = BoxAnnotation(top=max, bottom=min,
                                    fill_color=color,
                                    fill_alpha=globalAlpha,
                                    name=modelPath)  # +"_annotaion

            self.add_renderers([newAnno])
        except Exception as ex:
            self.logger.error("error draw threshold "+str(modelPath)+ " "+linePath+" "+str(ex))


    def make_background_entries(self, data, roundValues = True):
        """
            create background entries from background colum of a table:
            we iterate through the data and create a list of entries
            {"start":startTime,"end":time,"value":value of the data,"color":color from the colormap}
            those entries can directly be used to draw backgrounds
            Args:
                data: dict with {backgroundId: list of data , __time: list of data
                roundValue [bool] if true, we round the values to int, floats are not useful for table lookups
            Returns:
                list of dict entries derived from the data
        """
        backGroundNodeId = self.server.get_settings()["background"]["background"]
        colorMap = self.server.get_settings()["background"]["backgroundMap"]

        startTime = None
        backgrounds = []
        defaultColor = "grey"
        if "default" in colorMap:
            defaultColor = colorMap["default"]

        if roundValues:
            # round the values, it is not useful to have float values here, we use the background value
            # for lookup of coloring, so we need int
            #self.logger.debug(f"before round {data[backGroundNodeId]}")
            data[backGroundNodeId]=[ round(value) if numpy.isfinite(value) else value for value in data[backGroundNodeId] ]
            #self.logger.debug(f"after round {data[backGroundNodeId]}")


        for value, time in zip(data[backGroundNodeId], data[backGroundNodeId+"__time"]):
            # must set the startTime?
            if not startTime:
                if not numpy.isfinite(value):
                    continue  # can't use inf/nan
                else:
                    startTime = time
                    currentBackGroundValue = value
            else:
                # now we are inside a region, let's see when it ends
                if value != currentBackGroundValue:
                    # a new entry starts, finish the last and add it to the list of background
                    try:
                        color = colorMap[str(int(currentBackGroundValue))]
                    except:
                        color = defaultColor
                    entry = {"start": startTime, "end": time, "value": currentBackGroundValue, "color": color}
                    self.logger.debug("ENTRY" + json.dumps(entry))
                    backgrounds.append(entry)
                    # now check if current value is finite, then we can start
                    if numpy.isfinite(value):
                        currentBackGroundValue = value
                        startTime = time
                    else:
                        startTime = None  # look for the next start
        # now also add the last, if we have one running
        if startTime:
            try:
                color = colorMap[str(int(currentBackGroundValue))]
            except:
                color = defaultColor
            entry = {"start": startTime, "end": time, "value": currentBackGroundValue, "color": color}
            backgrounds.append(entry)

        return copy.deepcopy(backgrounds)


    def show_backgrounds(self,data=None):
        """
            show the current backgrounds
            Args:
                data(dict):  contains a dict holding the nodeid with of the background and the __time as keys and the lists of data
                    if te data is not given, we get the backgrounds fresh from the data server
        """
        self.showBackgrounds=True

        try:
            self.logger.debug("show_backgrounds()")
            backGroundNodeId = self.server.get_settings()["background"]["background"]

            if not data:
                #we have to pick up the background data first
                self.logger.debug("get fresh background data from the model server %s",backGroundNodeId)
                bins = self.server.get_settings()["bins"]
                getData = self.server.get_data([backGroundNodeId], start=self.rangeStart, end=self.rangeEnd,
                                               bins=bins)  # for debug
                data = getData

            #now make the new backgrounds
            backgrounds = self.make_background_entries(data)
            #now we have a list of backgrounds
            self.logger.info("have %i background entries",len(backgrounds))
            #now plot them

            boxes =[]

            self.backgrounds=[]

            for back in backgrounds:
                name = "__background"+str('%8x'%random.randrange(16**8))
                newBack = BoxAnnotation(left=back["start"], right=back["end"],
                                        fill_color=back["color"],
                                        fill_alpha=globalBackgroundsAlpha,
                                        level = globalBackgroundsLevel,
                                        name=name)  # +"_annotaion
                boxes.append(newBack)
                back["rendererName"] = name
                self.backgrounds.append(back)  # put it in the list of backgrounds for later look up for streaming

            self.plot.renderers.extend(boxes)
        except Exception as ex:
            self.logger.error(f"problem duringshow_backgrounds {ex} ")

    def hide_backgrounds(self):
        self.showBackgrounds = False
        """ remove all background from the plot """
        self.remove_renderers(deleteMatch="__background")


    def show_scores(self):
        self.logger.debug("show_scores()")
        #adjust the current selected variables that they also contain the scores if they have any
        # scores are variables that are listed under "score variables" and have a node name like
        # name_*score
        # if the "name" node is the corresponding node, found via the name of the node
        # the score node must have a "score" string in the end of its name

        self.showScores=True

        additionalScores = [] # the list of score variables that should be displayed
        additionalScoresY2 = []
        currentVariables = self.server.get_variables_selected()
        currentVarNames = [path.split('.')[-1] for path in currentVariables]
        #now check if we need to add some scores
        for scoreVarName in self.server.get_score_variables():
            scoreNodeName = scoreVarName.split('.')[-1]
            splitted = scoreNodeName.split('_')
            ending = splitted[-1].upper()
            scoreName = '_'.join(splitted[:-1])
            if scoreName in currentVarNames and "SCORE" in ending:
                if self.server.has_y2() and self.server.is_y2_variable(scoreVarName):
                    additionalScoresY2.append(scoreVarName)
                else:
                    additionalScores.append(scoreVarName)

        #now we have in additionalScores the missing variables to add
        #write it to the backend and wait for the event to plot them
        if additionalScores !=[]:
            currentVariables.extend(additionalScores)
            currentVariables = list(set(currentVariables)) # del duplicates
            self.server.set_variables_selected(currentVariables,updateLocalNow=False)
        if additionalScoresY2:
            self.server.add_variables_selected(addList=[],addListY2=additionalScoresY2)

    def hide_scores(self):
        #remove the "score vars" from the selected in the backend
        #hide the scores
        self.logger.debug("hide_scores()")
        self.showScores=False

        currentVariables = self.server.get_variables_selected()
        scoreVars = self.server.get_score_variables()  # we assume ending in _score

        newVars = [var for var in currentVariables if var not in scoreVars]
        if newVars != currentVariables:
            self.server.set_variables_selected(newVars)


    def convert_y1_to_y2(self,y):
        factor = (y-self.plot.y_range.start)/(self.plot.y_range.end-self.plot.y_range.start)
        y2 = self.plot.extra_y_ranges["y2"].start + factor *(self.plot.extra_y_ranges["y2"].end -self.plot.extra_y_ranges["y2"].start)
        return y2

    def convert_y2_to_y1(self,y):
        factor = (y - self.plot.extra_y_ranges["y2"].start) / (self.plot.extra_y_ranges["y2"].end - self.plot.extra_y_ranges["y2"].start)
        y1 = self.plot.y_range.start + factor * (
                    self.plot.y_range.end - self.plot.y_range.start)
        return y1




    #called when the user dreates/removes annotations
    def edit_annotation_cb(self,start,end,tag,min,max):
        """
            call as a callback from the UI when a user adds or removes an annotation
            Args:
                start(float): the start time in epoch ms
                end (float): the end time in epoch ms
                tag (string): the currently selected tag by the UI, for erase there is the "-erase-" tag
        """
        self.logger.debug("edit anno %s %s %s",str(start),str(end),str(tag))
        """
        if tag == '-erase-':
            #remove all annotations which are inside the time
            deleteList = []
            annotations=self.server.get_annotations()
            for annoPath,annotation in annotations.items():
                if annotation["type"] == "time":
                    if annotation["startTime"]>start and annotation["startTime"] < end:
                        self.logger.debug("delete "+annoPath)
                        deleteList.append(annoPath)
            #now hide the boxes
            self.remove_annotations(deleteList)
        elif tag =="-erase threshold-":
            # remove all annotations which are inside the limits are are currently visible
            deleteList = []
            annotations = self.server.get_annotations()
            currentThresholds = [] # the list of threshold annotation currently visible
            for r in self.plot.renderers:
                if r.name in annotations:
                    #this annotation is currenlty visible
                    if annotations[r.name]["type"] == "threshold":
                        #this annotation is a threshold
                        currentThresholds.append(r.name)

            #now check if we have to delete it
            deleteList =[]
            for threshold in currentThresholds:
                tMin = annotations[threshold]["min"]
                tMax = annotations[threshold]["max"]
                if tMin>tMax:
                    tMin,tMax = tMax,tMin
                if tMax < =max and tMax>=min: # we check against the top line of the annotation
                    #must delete this one
                    deleteList.append(threshold)
            # now hide the boxes
            self.logger.debug(f"deletelist {deleteList}")
            self.remove_renderers(deleteList=deleteList)
            self.server.delete_annotations(deleteList)
        """




        if tag =="motif":
            variable = self.currentAnnotationVariable
            newAnno = self.server.add_annotation(start,end,tag,type="motif",var=variable)
            self.draw_motif(newAnno)


        elif "threshold" not in tag:
            #create a time annotation one

            newAnno= self.server.add_annotation(start,end,tag,type="time")# this also adds the annotation to the server list and the backend
            #print("\n now draw"+newAnnotationPath)
            self.show_annotations(fetch=False,newAnno=newAnno)
            #self.draw_annotation(newAnno,visible=True)
            #print("\n draw done")
        else:
            #create a threshold annotation, but only if ONE variable is currently selected
            variable = None
            if self.currentAnnotationVariable == None: # no variable given from context menu
                variables = self.server.get_variables_selected()
                scoreVariables = self.server.get_score_variables()
                vars=list(set(variables)-set(scoreVariables))
                if len(vars) != 1:
                    self.logger.error("can't create threshold anno, len(vars"+str(len(vars)))
                    return
                variable = vars[0]
            else:
                variable = self.currentAnnotationVariable

            if self.server.is_y2_variable(variable):
                min = self.convert_y1_to_y2(min)
                max = self.convert_y1_to_y2(max)


            newAnnotation  = self.server.add_annotation(start,end,tag,type ="threshold",min=min,max=max,var = variable )
            #self.currentAnnotationVariable = None
            self.draw_threshold(newAnnotation)# ,vars[0])

    def find_score_variable(self,variablePath):
        scoreVariables = self.server.get_score_variables()



    def session_destroyed_cb(self,context):
        # this still doesn't work
        self.id=self.id+"destroyed"
        self.logger.debug(f"SEESION_DETROYED CB {self.id} id{self}")
        self.server.sse_stop()




if __name__ == '__main__':

    ts_server = TimeSeriesWidgetDataServer('http://localhost:6001/',"root.visualization.widgets.timeseriesOne")
    t=TimeSeriesWidget(ts_server)