hv.Arrow Problems. Any fixes?

There are a number of unanswered questions re Arrows here.
I just ran into yet another problem that could be alleviated with specifying start and endpoints
for the annotation.

import holoviews as hv; hv.extension('bokeh',logo=False)
hv.Curve([y*y for y in np.linspace(0,1,100)])*hv.Arrow( 60, 0.36, 'here','<')

shows the problem. Actually, I ran into this, with the label outside the frame

Here is a hack to add arrows and labels to a plot:

G = nx.Graph()
G.add_edges_from([[0,1],[0,2],[0,3]])
nx.set_node_attributes(G, {0: {"text": ""}, 1: {"text": "a"}, 2: {"text": "b"}, 3: {"text": "c"}} )
pos   = { 0: np.array([0,0]), 1: np.array([1,0]),  2: np.array([0,1]), 3: np.array([0.5,0.2]) }

graph  = hv.Graph.from_networkx(G, pos).opts(directed=True, node_alpha=0, arrowhead_length=0.05)
labels = hv.Labels( graph.nodes, ['x', 'y'], "text")

x = np.linspace(-1,1,20)
hv.Curve( (x,x*x)).opts(padding=0.1) * graph * labels.opts(xoffset=0.08, yoffset=0.01)

Packaged this…

class ArrowsWithLabels:
    def __init__(self):
        self.reset()

    def reset(self):
        self.G      = nx.Graph()
        self.E      = {}
        self.pos    = {}
        self.text   = {}
        self.color  = {}

    def _add( self, point_id, point_pos, lbl_id, lbl_pos, text, at_start=False, color='black' ):
        self.E[(point_id,lbl_id)]     = text
        self.color[(point_id,lbl_id)] = color
        self.pos[point_id]            = np.array(point_pos)
        self.pos[lbl_id]              = np.array(lbl_pos)
        self.text[point_id if at_start else lbl_id] = {"text" : text, "color": color}

    def _add_lbl( self, lbl_id, point_id, delta, text, color="black" ):
        #print( f"adding {delta} for {lbl_id} to current {point_id} at {self.pos[point_id]}")
        self.G.add_node(lbl_id)
        self.pos[lbl_id]  = self.pos[point_id] + delta
        self.text[lbl_id] = {"text" : text, "color": color}

    def add( self, point_pos, lbl_pos, text, at_start=False, offset=None, color='black'):
        cur = len( self.pos )+1
        if offset is None:
            self._add( cur, point_pos, cur+1, lbl_pos, text, at_start, color )
        else:
            self._add( cur, point_pos, cur+1, lbl_pos, "", at_start, color )
            diff     = self.pos[cur+1] - self.pos[cur]
            diff_pos = offset * diff / np.linalg.norm( diff)
            if at_start:
                self._add_lbl( cur+2, cur, -diff_pos, text, color)
            else:
                self._add_lbl( cur+2, cur+1, diff_pos, text, color)
                
    def graph(self):
        _ = [self.text.__setitem__(k,{"text": "", "color": "black"}) for k in self.pos.keys() if self.text.get(k,None) is None]

        #self.G.add_edges_from(self.E.keys())
        for e in self.E.keys():
            self.G.add_edge( *e, color=self.color[e])
        nx.set_node_attributes( self.G, self.text )
        graph  = hv.Graph.from_networkx(self.G, self.pos).opts(directed=True, node_alpha=0, arrowhead_length=0.05)
        lbls   = hv.Labels( graph.nodes, ['x', 'y'], ["text","color"]).opts(text_color='color')
        return graph,lbls
    
    def _dbg(self):
        print("Pos")
        for i in self.pos.keys():
            print( f".  {i, self.pos[i]}")
        print("Edge")
        for i in self.E.keys():
            print( f".  {i, self.E[i]}")
        print("Text")
        for i in self.text.keys():
            print( f".  {i, self.text[i]}")

pl = ArrowsWithLabels()
pl.add( (0,0), (1,0), "X", offset=0.1,  color='green')
pl.add( (0,0), (0,1), "Y", offset=0.05, color='green')
pl.add( (0,0), (1,1), "q_1", offset=0.1)
pl.add( (0.8,0.5*0.5), (0.5,0.5*0.5), "??", at_start=True, offset=0.1, color="red")

arrows,lbls = pl.graph()
#pl._dbg()

x = np.linspace(-1,1,20)
(hv.Curve( (x,x*x)) * arrows * lbls)\
  .opts(hv.opts.Graph( edge_color="color"),
           hv.opts.Curve( xlim = (-1,1.5), ylim = (-0.05,1.4), width=400 ))