Repository: holoviz/holoviews

Issue:
Set initial selected range for RangeToolLink
#### Is your feature request related to a problem? Please describe.

As described on [stackoverflow](https://stackoverflow.com/questions/73647097/predefine-selection-of-holoviews-rangetoollink), users want to adjust the initial selection range in the RangeToolLink, rather than having it select the entire range from the start. There is a solution using hooks (see the [accepted answer](https://stackoverflow.com/a/73649363/3149975)), however, there should be a simpler way to do it.

#### Describe the solution you'd like
For the axes being linked, allow a start and end value in some way. A complication is that more than one axis is allowed to be linked.

One possible solution that avoids axes' names from having to be args:
`RangeToolLink(source, target, axes=['x', 'y'], start = {'x':1, 'y':2}, end={'x':3, 'y':4})`




Discussion:

Resolving patch:
diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py
--- a/holoviews/plotting/bokeh/links.py
+++ b/holoviews/plotting/bokeh/links.py
@@ -138,10 +138,22 @@ class RangeToolLinkCallback(LinkCallback):
     def __init__(self, root_model, link, source_plot, target_plot):
         toolbars = list(root_model.select({'type': Toolbar}))
         axes = {}
-        if 'x' in link.axes:
-            axes['x_range'] = target_plot.handles['x_range']
-        if 'y' in link.axes:
-            axes['y_range'] = target_plot.handles['y_range']
+
+        for axis in ('x', 'y'):
+            if axis not in link.axes:
+                continue
+
+            axes[f'{axis}_range'] = target_plot.handles[f'{axis}_range']
+            bounds = getattr(link, f'bounds{axis}', None)
+            if bounds is None:
+                continue
+
+            start, end = bounds
+            if start is not None:
+                axes[f'{axis}_range'].start = start
+            if end is not None:
+                axes[f'{axis}_range'].end = end
+
         tool = RangeTool(**axes)
         source_plot.state.add_tools(tool)
         if bokeh3 and toolbars:
diff --git a/holoviews/plotting/links.py b/holoviews/plotting/links.py
--- a/holoviews/plotting/links.py
+++ b/holoviews/plotting/links.py
@@ -104,6 +104,12 @@ class RangeToolLink(Link):
     axes = param.ListSelector(default=['x'], objects=['x', 'y'], doc="""
         Which axes to link the tool to.""")
 
+    boundsx = param.Tuple(default=None, length=2, doc="""
+        (start, end) bounds for the x-axis""")
+
+    boundsy = param.Tuple(default=None, length=2, doc="""
+        (start, end) bounds for the y-axis""")
+
     _requires_target = True
 
 


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_links.py b/holoviews/tests/plotting/bokeh/test_links.py
--- a/holoviews/tests/plotting/bokeh/test_links.py
+++ b/holoviews/tests/plotting/bokeh/test_links.py
@@ -40,6 +40,32 @@ def test_range_tool_link_callback_both_axes(self):
         self.assertEqual(range_tool.x_range, tgt_plot.handles['x_range'])
         self.assertEqual(range_tool.y_range, tgt_plot.handles['y_range'])
 
+    def test_range_tool_link_callback_boundsx_arg(self):
+        array = np.random.rand(100, 2)
+        src = Curve(array)
+        target = Scatter(array)
+        x_start = 0.2
+        x_end = 0.3
+        RangeToolLink(src, target, axes=['x', 'y'], boundsx=(x_start, x_end))
+        layout = target + src
+        plot = bokeh_renderer.get_plot(layout)
+        tgt_plot = plot.subplots[(0, 0)].subplots['main']
+        self.assertEqual(tgt_plot.handles['x_range'].start, x_start)
+        self.assertEqual(tgt_plot.handles['x_range'].end, x_end)
+
+    def test_range_tool_link_callback_boundsy_arg(self):
+        array = np.random.rand(100, 2)
+        src = Curve(array)
+        target = Scatter(array)
+        y_start = 0.8
+        y_end = 0.9
+        RangeToolLink(src, target, axes=['x', 'y'], boundsy=(y_start, y_end))
+        layout = target + src
+        plot = bokeh_renderer.get_plot(layout)
+        tgt_plot = plot.subplots[(0, 0)].subplots['main']
+        self.assertEqual(tgt_plot.handles['y_range'].start, y_start)
+        self.assertEqual(tgt_plot.handles['y_range'].end, y_end)
+
     def test_data_link_dynamicmap_table(self):
         dmap = DynamicMap(lambda X: Points([(0, X)]), kdims='X').redim.range(X=(-1, 1))
         table = Table([(-1,)], vdims='y')