Repository: mwaskom/seaborn (2023-01-02 ~ 2023-02-22)

problem_statement: Figure title being removed by seaborn objects API when plotting on subfigures
I recently came across an odd behaviour with the seaborn objects API when using subfigures. Here is a minimal example : 
```
import seaborn as sns
import seaborn.objects as so
import matplotlib.pyplot as plt

fig = plt.figure(constrained_layout=True)
subfigs = fig.subfigures(1,2)
tips = sns.load_dataset("tips")
p = (
    so.Plot(tips, "total_bill")
    .add(so.Bars(), so.Hist())
)
p.on(subfigs[0]).plot()

ax = subfigs[1].subplots()
ax.scatter([1],[1])

fig.suptitle("Test title")
plt.show()
```
which results in the title missing from the image :
![title_issue_bad](https://user-images.githubusercontent.com/1338337/210242982-57262fb0-d1d4-4aab-b400-8f59cae522f3.png)

Commenting the `p.on(subfigs[0]).plot()` results in the title reappearing.
I have done a bit of digging and found that changing  line 186 from the _core/subplots.py file from `figure = target.figure` to `figure = target` seems to solve the issue. Is there a specific reason to why it fetches the parent figure currently, since Subfigure is supposed to be a drop-in replacement for Figure ? I also expect this will not have the intended behaviour if we deal with subfigures of subfigures.

date: 2023-01-02
patch: diff --git a/seaborn/_compat.py b/seaborn/_compat.py
--- a/seaborn/_compat.py
+++ b/seaborn/_compat.py
@@ -149,10 +149,14 @@ def set_layout_engine(fig, engine):
     if hasattr(fig, "set_layout_engine"):
         fig.set_layout_engine(engine)
     else:
+        # _version_predates(mpl, 3.6)
         if engine == "tight":
             fig.set_tight_layout(True)
         elif engine == "constrained":
             fig.set_constrained_layout(True)
+        elif engine == "none":
+            fig.set_tight_layout(False)
+            fig.set_constrained_layout(False)
 
 
 def share_axis(ax0, ax1, which):
diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py
--- a/seaborn/_core/plot.py
+++ b/seaborn/_core/plot.py
@@ -1662,6 +1662,11 @@ def _finalize_figure(self, p: Plot) -> None:
                 if axis_key in self._scales:  # TODO when would it not be?
                     self._scales[axis_key]._finalize(p, axis_obj)
 
-        engine_default = None if p._target is not None else "tight"
-        layout_engine = p._layout_spec.get("engine", engine_default)
-        set_layout_engine(self._figure, layout_engine)
+        if (engine := p._layout_spec.get("engine", default)) is not default:
+            # None is a valid arg for Figure.set_layout_engine, hence `default`
+            set_layout_engine(self._figure, engine)
+        elif p._target is None:
+            # Don't modify the layout engine if the user supplied their own
+            # matplotlib figure and didn't specify an engine through Plot
+            # TODO switch default to "constrained"?
+            set_layout_engine(self._figure, "tight")

test_patch: diff --git a/tests/_core/test_plot.py b/tests/_core/test_plot.py
--- a/tests/_core/test_plot.py
+++ b/tests/_core/test_plot.py
@@ -1133,11 +1133,30 @@ def test_on_axes_with_subplots_error(self):
         with pytest.raises(RuntimeError, match="Cannot create multiple subplots"):
             p2.plot()
 
-    def test_on_disables_layout_algo(self):
+    @pytest.mark.skipif(
+        _version_predates(mpl, "3.6"),
+        reason="Requires newer matplotlib layout engine API"
+    )
+    def test_on_layout_algo_default(self):
 
-        f = mpl.figure.Figure()
+        class MockEngine(mpl.layout_engine.ConstrainedLayoutEngine):
+            ...
+
+        f = mpl.figure.Figure(layout=MockEngine())
         p = Plot().on(f).plot()
-        assert not p._figure.get_tight_layout()
+        layout_engine = p._figure.get_layout_engine()
+        assert layout_engine.__class__.__name__ == "MockEngine"
+
+    @pytest.mark.skipif(
+        _version_predates(mpl, "3.6"),
+        reason="Requires newer matplotlib layout engine API"
+    )
+    def test_on_layout_algo_spec(self):
+
+        f = mpl.figure.Figure(layout="constrained")
+        p = Plot().on(f).layout(engine="tight").plot()
+        layout_engine = p._figure.get_layout_engine()
+        assert layout_engine.__class__.__name__ == "TightLayoutEngine"
 
     def test_axis_labels_from_constructor(self, long_df):
 
@@ -1383,7 +1402,7 @@ def test_layout_algo(self, algo):
         p = Plot().facet(["a", "b"]).limit(x=(.1, .9))
 
         p1 = p.layout(engine=algo).plot()
-        p2 = p.layout(engine=None).plot()
+        p2 = p.layout(engine="none").plot()
 
         # Force a draw (we probably need a method for this)
         p1.save(io.BytesIO())


----

problem_statement: Width computation after histogram slightly wrong with log scale
Note the slight overlap here:

```python
(
    so.Plot(tips, "total_bill")
    .add(so.Bars(alpha=.3, edgewidth=0), so.Hist(bins=4))
    .scale(x="log")
)
```
![image](https://user-images.githubusercontent.com/315810/178975852-d8fd830e-ae69-487d-be22-36531fca3f8f.png)

It becomes nearly imperceptible with more bins:

```
(
    so.Plot(tips, "total_bill")
    .add(so.Bars(alpha=.3, edgewidth=0), so.Hist(bins=8))
    .scale(x="log")
)
```
![image](https://user-images.githubusercontent.com/315810/178976113-7026b3ae-0b87-48df-adc0-00e90d5aea94.png)

This is not about `Bars`; `Bar` has it too:

```python
(
    so.Plot(tips, "total_bill")
    .add(so.Bar(alpha=.3, edgewidth=0, width=1), so.Hist(bins=4))
    .scale(x="log")
)
```
![image](https://user-images.githubusercontent.com/315810/178975910-484df65f-4ce6-482e-9992-5d02faf6b9ea.png)


date: 2023-01-09
patch: diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py
--- a/seaborn/_core/plot.py
+++ b/seaborn/_core/plot.py
@@ -1377,10 +1377,9 @@ def _unscale_coords(
     ) -> DataFrame:
         # TODO do we still have numbers in the variable name at this point?
         coord_cols = [c for c in df if re.match(r"^[xy]\D*$", str(c))]
-        drop_cols = [*coord_cols, "width"] if "width" in df else coord_cols
         out_df = (
             df
-            .drop(drop_cols, axis=1)
+            .drop(coord_cols, axis=1)
             .reindex(df.columns, axis=1)  # So unscaled columns retain their place
             .copy(deep=False)
         )
@@ -1396,12 +1395,6 @@ def _unscale_coords(
                 inverted = transform(values)
                 out_df.loc[values.index, str(var)] = inverted
 
-                if var == orient and "width" in view_df:
-                    width = view_df["width"]
-                    out_df.loc[values.index, "width"] = (
-                        transform(values + width / 2) - transform(values - width / 2)
-                    )
-
         return out_df
 
     def _generate_pairings(
diff --git a/seaborn/_marks/bar.py b/seaborn/_marks/bar.py
--- a/seaborn/_marks/bar.py
+++ b/seaborn/_marks/bar.py
@@ -29,17 +29,23 @@ class BarBase(Mark):
 
     def _make_patches(self, data, scales, orient):
 
+        transform = scales[orient]._matplotlib_scale.get_transform()
+        forward = transform.transform
+        reverse = transform.inverted().transform
+
+        other = {"x": "y", "y": "x"}[orient]
+
+        pos = reverse(forward(data[orient]) - data["width"] / 2)
+        width = reverse(forward(data[orient]) + data["width"] / 2) - pos
+
+        val = (data[other] - data["baseline"]).to_numpy()
+        base = data["baseline"].to_numpy()
+
         kws = self._resolve_properties(data, scales)
         if orient == "x":
-            kws["x"] = (data["x"] - data["width"] / 2).to_numpy()
-            kws["y"] = data["baseline"].to_numpy()
-            kws["w"] = data["width"].to_numpy()
-            kws["h"] = (data["y"] - data["baseline"]).to_numpy()
+            kws.update(x=pos, y=base, w=width, h=val)
         else:
-            kws["x"] = data["baseline"].to_numpy()
-            kws["y"] = (data["y"] - data["width"] / 2).to_numpy()
-            kws["w"] = (data["x"] - data["baseline"]).to_numpy()
-            kws["h"] = data["width"].to_numpy()
+            kws.update(x=base, y=pos, w=val, h=width)
 
         kws.pop("width", None)
         kws.pop("baseline", None)

test_patch: diff --git a/tests/_marks/test_bar.py b/tests/_marks/test_bar.py
--- a/tests/_marks/test_bar.py
+++ b/tests/_marks/test_bar.py
@@ -200,3 +200,13 @@ def test_unfilled(self, x, y):
         colors = p._theme["axes.prop_cycle"].by_key()["color"]
         assert_array_equal(fcs, to_rgba_array([colors[0]] * len(x), 0))
         assert_array_equal(ecs, to_rgba_array([colors[4]] * len(x), 1))
+
+    def test_log_scale(self):
+
+        x = y = [1, 10, 100, 1000]
+        p = Plot(x, y).add(Bars()).scale(x="log").plot()
+        ax = p._figure.axes[0]
+
+        paths = ax.collections[0].get_paths()
+        for a, b in zip(paths, paths[1:]):
+            assert a.vertices[1, 0] == pytest.approx(b.vertices[0, 0])


----

problem_statement: `heatmap(..., norm=None, ...)` has different behaviour than without specifying the `norm` argument
Hi,

I'm noticing a strange behaviour when passing `norm=None` to a heatmap and I believe it's due to these lines: https://github.com/mwaskom/seaborn/blob/3733590d86a7f2c2a95cd9940a34aa7df5f5a3d2/seaborn/matrix.py#L299-L303

Specifically, if I use `sns.heatmap(..., vmin=0.0, vmax=1.0, ...)` I get something like this:

![without-norm](https://user-images.githubusercontent.com/3457859/220935158-fdc86688-1780-4efd-8418-28523bdc5c24.png)

but when I use `sns.heatmap(..., vmin=0.0, vmax=1.0, norm=None, ...)`, `vmin` and `vmax` are lost:

![with-norm](https://user-images.githubusercontent.com/3457859/220935301-d8c4b1ce-d76b-4d58-add5-18d08529ab41.png)

I'm happy to send a PR if this issue isn't addressed anywhere.

Ștefan

date: 2023-02-22
patch: diff --git a/seaborn/matrix.py b/seaborn/matrix.py
--- a/seaborn/matrix.py
+++ b/seaborn/matrix.py
@@ -298,7 +298,7 @@ def plot(self, ax, cax, kws):
 
         # setting vmin/vmax in addition to norm is deprecated
         # so avoid setting if norm is set
-        if "norm" not in kws:
+        if kws.get("norm") is None:
             kws.setdefault("vmin", self.vmin)
             kws.setdefault("vmax", self.vmax)
 

test_patch: diff --git a/tests/test_matrix.py b/tests/test_matrix.py
--- a/tests/test_matrix.py
+++ b/tests/test_matrix.py
@@ -265,6 +265,20 @@ def test_cmap_with_properties(self):
         hm = mat._HeatMapper(self.df_unif, **kws)
         npt.assert_array_equal(cmap(np.inf), hm.cmap(np.inf))
 
+    def test_explicit_none_norm(self):
+
+        vals = np.linspace(.2, 1, 9)
+        cmap = mpl.cm.binary
+        _, (ax1, ax2) = plt.subplots(2)
+
+        mat.heatmap([vals], vmin=0, cmap=cmap, ax=ax1)
+        fc_default_norm = ax1.collections[0].get_facecolors()
+
+        mat.heatmap([vals], vmin=0, norm=None, cmap=cmap, ax=ax2)
+        fc_explicit_norm = ax2.collections[0].get_facecolors()
+
+        npt.assert_array_almost_equal(fc_default_norm, fc_explicit_norm, 2)
+
     def test_ticklabels_off(self):
         kws = self.default_kws.copy()
         kws['xticklabels'] = False