Repository: bokeh/bokeh

Issue:
[BUG] Warn about using the same layout element (here: plot) multiple times
#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
bokeh                     2.0.0                    py37_0  
python                    3.7.7           hcf32534_0_cpython 
WSL Ubuntu 18.04 (hosted on win10
Firefox 75.0 (64-bit)
running the app with command `bokeh serve testapp.py --dev`
#### Description of expected behavior and the observed behavior
Using an array of the same plot twice, i.e.
`rows=row( children=[plot1,plot1], width=800)`
does not show the plot twice. Using a deepcopy of plot1 will show the same behavior.
## Expected behavior
![image](https://user-images.githubusercontent.com/56429408/80697318-17335d80-8ad9-11ea-8fe9-032c4ad45299.png)

## Actual behavior
![image](https://user-images.githubusercontent.com/56429408/80697456-41851b00-8ad9-11ea-80a8-44bdcbcc745b.png)

#### Complete, minimal, self-contained example code that reproduces the issue

```
# minimal example for a bokeh dashboard featurigng the same element twice 
# (here: a plot)
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.plotting import figure

from copy import deepcopy
import numpy as np

# Set up data
x = np.linspace(0, 15, 200)
y = np.sin(x)

# Set up plots
plot1 = figure(plot_height=400, plot_width=400)
plot1.line(x, y,  line_width=3, line_alpha=0.6)
# plot2 = figure(plot_height=400, plot_width=400)
# plot2.line(x, y,  line_width=3, line_alpha=0.6)
# plot2=deepcopy(plot1) # can we just copy the first plot?

# set up the layout
rows=row( children=[plot1,plot1], width=800) # <= use plot 1 & 2 to get the "expected" behavior
curdoc().add_root(rows)
```

#### Stack traceback and/or browser JavaScript console output
no errors

#### Screenshots or screencasts of the bug in action
see above



Discussion:
@Dr-Nuke This has been explicitly not-supported for a long time. May I ask why you would ever want to put the same plot in a layout twice?
aha ok explicitly not-supported.
Then I guess it's purely my intuition and idea of object-oriented  programming that wants me to use the same object twice. Maybe the example of a graph is too specific, it could be any (cosmetic) object on a dashboard

The technical reason for this not being supported is that the mapping between a model's view and its children, is a simple `model.id` -> model view mapping. So e.g. `Row.children` can't have duplicated entries, though it should at least warn you about it. However, you can work around this by nesting a layout like this:
```py
from bokeh.io import save
from bokeh.plotting import figure
from bokeh.layouts import column, row

import numpy as np

x = np.linspace(0, 15, 200)
y = np.sin(x)

plot1 = figure(plot_height=400, plot_width=400)
plot1.line(x, y,  line_width=3, line_alpha=0.6)
r = row([plot1, row([plot1])])
save(r)
```
This will actually show two plots. However, by reusing the same model and all its referenced models, you are effectively linking those two plots. This results in e.g. panning one plot would pan the other one as well. I doubt this is the expected behavior.

If you have two very similar plots and you would like to save yourself some typing, then the best option would be do deep clone a model, but that's not supported currently. The most reliable alternative, that we use all across our documentation and examples, is to wrap code that produces a plot in a function, and then modify the results to the specific needs.
> `# plot2=deepcopy(plot1) # can we just copy the first plot?`

Unfortunately `deepcopy()` would copy identities of all involved models, so you would be back to square one, if it can copy bokeh's models reliably at all.
I will consider this as a bug, because you should be at least warned (preferably an exception should be thrown) that duplicated entries are prohibited.
I think a warning/ exception makes sense. Would have prevented me from going this far :)
I also understand why the `deepcopy` would not help.
nice workaround though!

I do think adding a `.clone` API to create copies (with new model ids) would be reasonable and useful 

We could also try to error/warn if a model is used twice in a layout, tho it's not completely obvious that there is a simple/clean way to do that. I don't think it can be done generically during `all_models` collection since models can be shared (e.g. linked axes). I think it would require special casing specifically around layouts with children. 

Resolving patch:
diff --git a/bokeh/core/validation/errors.py b/bokeh/core/validation/errors.py
--- a/bokeh/core/validation/errors.py
+++ b/bokeh/core/validation/errors.py
@@ -95,6 +95,9 @@
 1026 *(INCOMPATIBLE_LINE_EDIT_INTERSECTION_RENDERER)*
     The ``LineEditTool`` intersection_enderer may only reference ``LineGlyph`` models.
 
+1027 *(REPEATED_LAYOUT_CHILD)*
+    The same model can't be used multiple times in a layout.
+
 9999 *(EXT)*
     Indicates that a custom error check has failed.
 
@@ -141,6 +144,7 @@
     1024: ("CDSVIEW_FILTERS_WITH_CONNECTED",                     "CDSView filters are not compatible with glyphs with connected topology such as Line or Patch"),
     1025: ("INCOMPATIBLE_LINE_EDIT_RENDERER",                    "LineEditTool renderers may only reference MultiLine and Line glyph models"),
     1026: ("INCOMPATIBLE_LINE_EDIT_INTERSECTION_RENDERER",       "LineEditTool intersection_renderer may only reference LineGlyph models"),
+    1027: ("REPEATED_LAYOUT_CHILD",                              "The same model can't be used multiple times in a layout"),
     9999: ("EXT",                                                "Custom extension reports error"),
 }
 
diff --git a/bokeh/models/layouts.py b/bokeh/models/layouts.py
--- a/bokeh/models/layouts.py
+++ b/bokeh/models/layouts.py
@@ -39,7 +39,11 @@
     Tuple,
 )
 from ..core.validation import error, warning
-from ..core.validation.errors import MIN_PREFERRED_MAX_HEIGHT, MIN_PREFERRED_MAX_WIDTH
+from ..core.validation.errors import (
+    MIN_PREFERRED_MAX_HEIGHT,
+    MIN_PREFERRED_MAX_WIDTH,
+    REPEATED_LAYOUT_CHILD,
+)
 from ..core.validation.warnings import (
     BOTH_CHILD_AND_ROOT,
     EMPTY_LAYOUT,
@@ -355,6 +359,12 @@ class GridBox(LayoutDOM):
     respectively.
     """)
 
+    @error(REPEATED_LAYOUT_CHILD)
+    def _check_repeated_layout_children(self):
+        children = [ child[0] for child in self.children ]
+        if len(children) != len(set(children)):
+            return str(self)
+
 @abstract
 class Box(LayoutDOM):
     ''' Abstract base class for Row and Column. Do not use directly.
@@ -387,6 +397,11 @@ def _check_child_is_also_root(self):
         else:
             return None
 
+    @error(REPEATED_LAYOUT_CHILD)
+    def _check_repeated_layout_children(self):
+        if len(self.children) != len(set(self.children)):
+            return str(self)
+
     children = List(Instance(LayoutDOM), help="""
     The list of children, which can be other components including plots, rows, columns, and widgets.
     """)


Test patch:
diff --git a/tests/unit/bokeh/test_layouts.py b/tests/unit/bokeh/test_layouts.py
--- a/tests/unit/bokeh/test_layouts.py
+++ b/tests/unit/bokeh/test_layouts.py
@@ -14,9 +14,13 @@
 # Imports
 #-----------------------------------------------------------------------------
 
+# External imports
+import mock
+
 # Bokeh imports
+from bokeh.core.validation import check_integrity
 from bokeh.layouts import GridSpec, column, grid, gridplot, layout, row
-from bokeh.models import Column, GridBox, Row, Spacer
+from bokeh.models import Column, GridBox, LayoutDOM, Row, Spacer
 from bokeh.plotting import figure
 
 #-----------------------------------------------------------------------------
@@ -164,6 +168,25 @@ def test_grid() -> None:
         +---------+----+----+
         """)
 
+def test_repeated_children() -> None:
+    def test(layout: LayoutDOM) -> None:
+        with mock.patch("bokeh.core.validation.check.log") as mock_logger:
+            check_integrity([layout])
+        assert mock_logger.error.call_count == 1
+        assert mock_logger.error.call_args[0][0].startswith("E-1027 (REPEATED_LAYOUT_CHILD): The same model can't be used multiple times in a layout")
+
+    p0 = figure()
+    p1 = figure()
+
+    test(Row(children=[p0, p1, p0]))
+    test(row(p0, p1, p0))
+
+    test(Column(children=[p0, p1, p0]))
+    test(column(p0, p1, p0))
+
+    test(GridBox(children=[(p0, 0, 0), (p1, 1, 0), (p0, 2, 0)]))
+    test(gridplot([[p0], [p1], [p0]], toolbar_location=None))
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------