diff --git a/plugins/plotly-express/docs/sub-plots.md b/plugins/plotly-express/docs/sub-plots.md
index d34c71da7..3f58b916d 100644
--- a/plugins/plotly-express/docs/sub-plots.md
+++ b/plugins/plotly-express/docs/sub-plots.md
@@ -36,8 +36,110 @@ tipping_plots = dx.make_subplots(

-## API Reference
+### Share Axes
+
+Share axes between plots with the `shared_xaxes` and `shared_yaxes` parameters.
+
+#### Share All Axes
+
+When `shared_xaxes` or `shared_yaxes` is set to `"all"`, all axes of the same type are shared.
+When one axis is adjusted, all axes are adjusted to match.
+
+```python order=tipping_plots,lunch_tips,dinner_tips
+import deephaven.plot.express as dx
+tips = dx.data.tips() # import a ticking version of the Tips dataset
+
+# filter the tips dataset for separate lunch and dinner charts
+lunch_tips = tips.where("Time = `Lunch`")
+dinner_tips = tips.where("Time = `Dinner`")
+
+# create chart that shares all axes
+tipping_plots = dx.make_subplots(
+ dx.scatter(lunch_tips, x="TotalBill", y="Tip", labels={"Tip": "Lunch Tips"}),
+ dx.scatter(dinner_tips, x="TotalBill", y="Tip", labels={"Tip": "Dinner Tips"}),
+ rows=2, shared_yaxes="all", shared_xaxes="all"
+)
+```
+
+#### Share Y Axes
+
+When `shared_yaxis` is set to `True`, all y axes are shared along the same row.
+When one y-axis is adjusted, all axes along the same row are adjusted to match.
+
+```python order=tipping_plots,lunch_tips,dinner_tips
+import deephaven.plot.express as dx
+tips = dx.data.tips() # import a ticking version of the Tips dataset
+
+# filter the tips dataset for separate lunch and dinner charts
+lunch_tips = tips.where("Time = `Lunch`")
+dinner_tips = tips.where("Time = `Dinner`")
+
+# create chart that shares y axes along the row
+tipping_plots = dx.make_subplots(
+ dx.scatter(lunch_tips, x="TotalBill", y="Tip", labels={"Tip": "Lunch Tips"}),
+ dx.scatter(dinner_tips, x="TotalBill", y="Tip", labels={"Tip": "Dinner Tips"}),
+ cols=2, shared_yaxes=True
+)
+```
+
+To share the y axes along the same column, set `shared_yaxes` to `"columns"`.
+
+```python order=tipping_plots,lunch_tips,dinner_tips
+import deephaven.plot.express as dx
+tips = dx.data.tips() # import a ticking version of the Tips dataset
+# filter the tips dataset for separate lunch and dinner charts
+lunch_tips = tips.where("Time = `Lunch`")
+dinner_tips = tips.where("Time = `Dinner`")
+
+# create chart that shares y axes along the column
+tipping_plots = dx.make_subplots(
+ dx.scatter(lunch_tips, x="TotalBill", y="Tip", labels={"Tip": "Lunch Tips"}),
+ dx.scatter(dinner_tips, x="TotalBill", y="Tip", labels={"Tip": "Dinner Tips"}),
+ rows=2, shared_yaxes="columns"
+)
+```
+
+#### Share X Axes
+
+When `shared_xaxis` is set to `True`, all x axes are shared along the same column.
+When one x-axis is adjusted, all axes along the same column are adjusted to match.
+
+```python order=tipping_plots,lunch_tips,dinner_tips
+import deephaven.plot.express as dx
+tips = dx.data.tips() # import a ticking version of the Tips dataset
+
+# filter the tips dataset for separate lunch and dinner charts
+lunch_tips = tips.where("Time = `Lunch`")
+dinner_tips = tips.where("Time = `Dinner`")
+
+# create chart that shares x axes along the column
+tipping_plots = dx.make_subplots(
+ dx.scatter(lunch_tips, x="TotalBill", y="Tip", labels={"Tip": "Lunch Tips"}),
+ dx.scatter(dinner_tips, x="TotalBill", y="Tip", labels={"Tip": "Dinner Tips"}),
+ rows=2, shared_xaxes=True
+)
+```
+
+To share the x axes along the same column, set `shared_yaxes` to `"columns"`.
+
+```python order=tipping_plots,lunch_tips,dinner_tips
+import deephaven.plot.express as dx
+tips = dx.data.tips() # import a ticking version of the Tips dataset
+
+# filter the tips dataset for separate lunch and dinner charts
+lunch_tips = tips.where("Time = `Lunch`")
+dinner_tips = tips.where("Time = `Dinner`")
+
+# create chart that shares x axes along the row
+tipping_plots = dx.make_subplots(
+ dx.scatter(lunch_tips, x="TotalBill", y="Tip", labels={"Tip": "Lunch Tips"}),
+ dx.scatter(dinner_tips, x="TotalBill", y="Tip", labels={"Tip": "Dinner Tips"}),
+ cols=2, shared_xaxes="rows"
+)
+```
+
+## API Reference
```{eval-rst}
.. dhautofunction:: deephaven.plot.express.make_subplots
```
diff --git a/plugins/plotly-express/src/deephaven/plot/express/plots/_layer.py b/plugins/plotly-express/src/deephaven/plot/express/plots/_layer.py
index c93b6827f..9e1018f94 100644
--- a/plugins/plotly-express/src/deephaven/plot/express/plots/_layer.py
+++ b/plugins/plotly-express/src/deephaven/plot/express/plots/_layer.py
@@ -270,6 +270,9 @@ def match_axes(
# this is the base axis to match to, so matches is not added
return {}
if axis_index is not None:
+ if axis_index not in matches_axes[match_axis_key]:
+ # this is the first axis to match to, so add it
+ matches_axes[match_axis_key][axis_index] = new_trace_axis
return {"matches": matches_axes[match_axis_key][axis_index]}
return {}
diff --git a/plugins/plotly-express/src/deephaven/plot/express/plots/subplots.py b/plugins/plotly-express/src/deephaven/plot/express/plots/subplots.py
index 9d10065c5..ed126e0ef 100644
--- a/plugins/plotly-express/src/deephaven/plot/express/plots/subplots.py
+++ b/plugins/plotly-express/src/deephaven/plot/express/plots/subplots.py
@@ -357,9 +357,9 @@ def make_subplots(
calculated from rows and number of figs provided if not passed
but rows is.
One of rows or cols should be provided if passing figs directly.
- shared_xaxes: "rows", "cols"/True, "all" or None depending on what axes
+ shared_xaxes: "rows", "columns"/True, "all" or None depending on what axes
should be shared
- shared_yaxes: "rows"/True, "cols", "all" or None depending on what axes
+ shared_yaxes: "rows"/True, "columns", "all" or None depending on what axes
should be shared
grid: A grid (list of lists) of figures to draw. None can be
provided in a grid entry
diff --git a/plugins/plotly-express/test/deephaven/plot/express/plots/test_make_subplots.py b/plugins/plotly-express/test/deephaven/plot/express/plots/test_make_subplots.py
new file mode 100644
index 000000000..4310077eb
--- /dev/null
+++ b/plugins/plotly-express/test/deephaven/plot/express/plots/test_make_subplots.py
@@ -0,0 +1,546 @@
+import unittest
+
+from ..BaseTest import BaseTestCase
+
+
+class MakeSubplotsTestCase(BaseTestCase):
+ def setUp(self) -> None:
+ from deephaven import new_table
+ from deephaven.column import int_col
+
+ self.source = new_table(
+ [
+ int_col("X", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ int_col("Y", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
+ ]
+ )
+
+ def test_basic_make_subplots(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(chart, chart, rows=2).to_dict(self.exporter)
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 0.425],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.575, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+ def test_make_subplots_shared_axes_all(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(
+ chart, chart, rows=2, shared_xaxes="all", shared_yaxes="all"
+ ).to_dict(self.exporter)
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 0.425],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.575, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+ def test_make_subplots_shared_xaxes(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(chart, chart, rows=2, shared_xaxes=True).to_dict(
+ self.exporter
+ )
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 0.425],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.575, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+ def test_make_subplots_shared_xaxes_row(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(chart, chart, cols=2, shared_xaxes="rows").to_dict(
+ self.exporter
+ )
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 0.45],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.55, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ "matches": "x",
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.0, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+ def test_make_subplots_shared_yaxes(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(chart, chart, cols=2, shared_yaxes=True).to_dict(
+ self.exporter
+ )
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 0.45],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.55, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.0, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+ def test_make_subplots_shared_yaxes_col(self):
+ import src.deephaven.plot.express as dx
+ from deephaven.constants import NULL_INT
+
+ chart = dx.scatter(self.source, x="X", y="Y")
+ charts = dx.make_subplots(chart, chart, rows=2, shared_yaxes="columns").to_dict(
+ self.exporter
+ )
+
+ expected_data = [
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x",
+ "y": [NULL_INT],
+ "yaxis": "y",
+ },
+ {
+ "hovertemplate": "X=%{x}
Y=%{y}",
+ "legendgroup": "",
+ "marker": {"color": "#636efa", "symbol": "circle"},
+ "mode": "markers",
+ "name": "",
+ "showlegend": False,
+ "type": "scattergl",
+ "x": [NULL_INT],
+ "xaxis": "x2",
+ "y": [NULL_INT],
+ "yaxis": "y2",
+ },
+ ]
+
+ expected_layout = {
+ "legend": {"tracegroupgap": 0},
+ "xaxis": {
+ "anchor": "y",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "domain": [0.0, 1.0],
+ "side": "bottom",
+ "title": {"text": "X"},
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0.0, 0.425],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.575, 1.0],
+ "side": "left",
+ "title": {"text": "Y"},
+ "matches": "y",
+ },
+ }
+
+ expected_mappings = [
+ {
+ "data_columns": {"X": ["/plotly/data/0/x"], "Y": ["/plotly/data/0/y"]},
+ "table": 0,
+ },
+ {
+ "data_columns": {"X": ["/plotly/data/1/x"], "Y": ["/plotly/data/1/y"]},
+ "table": 0,
+ },
+ ]
+
+ self.assert_chart_equals(
+ charts,
+ expected_data=expected_data,
+ expected_layout=expected_layout,
+ expected_mappings=expected_mappings,
+ expected_is_user_set_template=False,
+ expected_is_user_set_color=False,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()