Repository: matplotlib/matplotlib

Issue:
add rc param for centimeter support
It would be great if I could specify a figsize in centimeters. I would like to write

```
import matplotlib as mpl
mpl.rcParams['lengthunit'] = 'c'
figure(figsize=(12, 8))
```

instead of

```
figure(figsize=(12/2.54, 8/2.54))
```

I think this would be a great addition for the non-anglo-saxon world ;)



Discussion:
I agree -- requiring inches there is rather archaic.  However, I don't think an rcParam is the right way to do it.  I'd rather see the unit be part of the parameter, e.g.:

   figsize=((12, 'cm', (8, 'cm'))

or

   figsize=('12cm', '8cm')

Neither of those are perfect (more thinking needs to be done), but I think changing global state for this could be too problematic.

Absolutely love the idea of using "unit-aware" objects instead of numbers for defining lengths and sizes as also stated in #1109.

@mdboom, would you be in favour of a unit-class that supports arithmetic operators like `__add__`, `__mul__`, `__sub__`, etc? So for example one could define `figsize = (12*cm, 8*cm)` or `figsize = (12*cm, 8*cm + 1*mm)`.

Perhaps something like this?

http://packages.python.org/quantities/user/tutorial.html

@WeatherGod, Thanks for the hint! I think I was looking for something like that for quite some time ^^. That tight integration with numpy seems nice. Is anyone imagining the plotting of such data with unit aware axis labelling too? ;)

@pwuertz I don't see the benefit of unit aware plotting/axis labelling. It requires almost no effort to append `'(cm)'` or `'(s)'` to my x-axis label. Date/time units are useful, and already supported. What is requested here is nothing to do with unit-aware plotting but physical figure attributes. Perhaps I'm overlooking something.

@dmcdougall Of course this is about figure attributes and my original question to mdboom was if he is in favor of a unit class that would support arithmetic operations with ints and floats.

WeatherGod came up with that project that takes this one step further. If matplotlib adapted this package, the idea of supporting it at plot level came to my mind. Think about plotting an array of times. When you decide that you want the plot to be displayed in `us` instead of `ms` you could change that at a single position in the code. Rescaling all of the plotted data and adding the correct unit to the label would be handled automatically. Just an idea, completely off-topic.

> When you decide that you want the plot to be displayed in `us` instead of `ms` you could change that at a single position in the code.

That's a fair point.

@pwuertz : I think it is worthy of a MEP. I love the idea, and would like to see this idea get developed further and discussed before we can make a sound decision about it. Would you be willing to write a MEP?

EDIT: Fixed typo from PEP->MEP. Doh!

> Would you be willing to write a PEP?

MEP?

> MEP?

Ja ja. Sorry :smiley:.

A MEP for general support of physical units that focuses on properties like `figsize` and discusses optional features like plotting of unit-aware data?

I think such a MEP would be great.  I see the two pieces: one for parameters like `figsize`, possible font size as well) and one for unitized data.  These pieces can be implemented somewhat independently (i.e. we can do one without the other), but it might be worth discussing them at the same time to contemplate some consistency between the two.  We already have lots of the infrastructure for unitized data that is currently used for dates, and it would be worth investigating how that could be extended.  For a long time, I've felt good docs on how to add a new unit would be very helpful -- it's not a part of the code that I personally understand very well.

> it's not a part of the code that I personally understand very well.

Likewise. I wonder who does?

On Thursday, October 18, 2012, Phil Elson wrote:

> it's not a part of the code that I personally understand very well.
> 
> Likewise. I wonder who does?
> 
> Sadly, I think it was John, but I don't think he implemented it.  I know it
> was added to address needs from NASA for the MSL lander (or was that the
> spline filter for paths?).  There was a email in the past six months where
> he discusses the history of various parts of mpl.  In fact, it might have
> been in response to my email awhile back about how mpl got me a job -- in
> case anyone wants to go searching.

Ben Root


When I got a little more time I'd like to work on these layout managing ideas and possibly write a MEP for that. As pointed out, this would also include support for units. The question is whether to wait for that layout management project (which might be a really long time) or to implement an unit aware `figsize` option as a more "short term" solution.

+1 for quantities integration

-1 on quantities integration. The promise is there, but it (and other
similar projects all have short-comings) See this very informative and
illuminating presentation at SciPy 2013

http://bekolay.org/scipy2013-quantities (hit S to see notes)
http://conference.scipy.org/scipy2013/presentation_detail.php?id=174

I also think quantities are out of scope.
But being able to give lengths in centimeters would be really helpful.

Benjamin Root notifications@github.com schrieb:

> -1 on quantities integration. The promise is there, but it (and other
> similar projects all have short-comings) See this very informative and
> illuminating presentation at SciPy 2013
> 
> http://bekolay.org/scipy2013-quantities (hit S to see notes)
> http://conference.scipy.org/scipy2013/presentation_detail.php?id=174
> 
> ---
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/matplotlib/matplotlib/issues/1369#issuecomment-22403321

@WeatherGod thanks for the reference.  I came across this page because I remember reading in the documentation some time ago about units in matplotlib, but then couldn't find anything that jumped out at me, only to realize that unit support had not been fully integrated in matplotlib.  

What matplotlib calls "unit support" is really the automatic conversion of complex datatypes (specifically `datetime` objects at this time) into floating point for plotting.  It could become the basis for handling any of the available unit/quantity packages available for matplotlib, but it doesn't address any of those at this time.

:+1:  I think just adding support for cm (at least as a short term fix) would make life a bit easier.

See #5104 for a possible implementation of this.
Another +1 for implementing this in rcparams. Would be great to be able to 'set and forget' metric units.
There is even a semi-abandoned MEP: #9226

I think the easiest least-painful idea would be a new method that was able to parse strings for width and height: `fig.set_size(width='15cm', height='2in')`.  Some will complain about hokey parsing, but...
This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear.  Thanks for your help!
This would still be useful in quite a lot of situations. The most progressed PR seems to be #12415 with quite a lot of discussions about the API.
I came away from that process convinced this is just they way it is.  Figure size is one thing, but points and dpi are deeply embedded in the library. 



Patch:
diff --git a/doc/users/next_whats_new/figsize_unit.rst b/doc/users/next_whats_new/figsize_unit.rst
new file mode 100644
index 000000000000..ded95d9930a5
--- /dev/null
+++ b/doc/users/next_whats_new/figsize_unit.rst
@@ -0,0 +1,9 @@
+Figure size units
+-----------------
+
+When creating figures, it is now possible to define figure sizes in cm or pixel.
+
+Up to now the figure size is specified via ``plt.figure(..., figsize=(6, 4))``,
+and the given numbers are interpreted as inches. It is now possible to add a
+unit string to the tuple, i.e. ``plt.figure(..., figsize=(600, 400, "px"))``.
+Supported unit strings are "in", "cm", "px".
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index bf24193f380a..e1d26a9b58d8 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -2475,8 +2475,13 @@ def __init__(self,
         """
         Parameters
         ----------
-        figsize : 2-tuple of floats, default: :rc:`figure.figsize`
-            Figure dimension ``(width, height)`` in inches.
+        figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
+            The figure dimensions. This can be
+
+            - a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
+              "cm" (centimenter), "px" (pixel).
+            - a tuple ``(width, height)``, which is interpreted in inches, i.e. as
+              ``(width, height, "in")``.
 
         dpi : float, default: :rc:`figure.dpi`
             Dots per inch.
@@ -2612,6 +2617,8 @@ def __init__(self,
         edgecolor = mpl._val_or_rc(edgecolor, 'figure.edgecolor')
         frameon = mpl._val_or_rc(frameon, 'figure.frameon')
 
+        figsize = _parse_figsize(figsize, dpi)
+
         if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any():
             raise ValueError('figure size must be positive finite not '
                              f'{figsize}')
@@ -3713,3 +3720,46 @@ def figaspect(arg):
     # the min/max dimensions (we don't want figures 10 feet tall!)
     newsize = np.clip(newsize, figsize_min, figsize_max)
     return newsize
+
+
+def _parse_figsize(figsize, dpi):
+    """
+    Convert a figsize expression to (width, height) in inches.
+
+    Parameters
+    ----------
+    figsize : (float, float) or (float, float, str)
+        This can be
+
+        - a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
+          "cm" (centimenter), "px" (pixel).
+        - a tuple ``(width, height)``, which is interpreted in inches, i.e. as
+          ``(width, height, "in")``.
+
+    dpi : float
+        The dots-per-inch; used for converting 'px' to 'in'.
+    """
+    num_parts = len(figsize)
+    if num_parts == 2:
+        return figsize
+    elif num_parts == 3:
+        x, y, unit = figsize
+        if unit == 'in':
+            pass
+        elif unit == 'cm':
+            x /= 2.54
+            y /= 2.54
+        elif unit == 'px':
+            x /= dpi
+            y /= dpi
+        else:
+            raise ValueError(
+                f"Invalid unit {unit!r} in 'figsize'; "
+                "supported units are 'in', 'cm', 'px'"
+            )
+        return x, y
+    else:
+        raise ValueError(
+            "Invalid figsize format, expected (x, y) or (x, y, unit) but got "
+            f"{figsize!r}"
+        )
diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi
index 064503c91384..c048ad556036 100644
--- a/lib/matplotlib/figure.pyi
+++ b/lib/matplotlib/figure.pyi
@@ -318,7 +318,9 @@ class Figure(FigureBase):
     subplotpars: SubplotParams
     def __init__(
         self,
-        figsize: tuple[float, float] | None = ...,
+        figsize: tuple[float, float]
+        | tuple[float, float, Literal["in", "cm", "px"]]
+        | None = ...,
         dpi: float | None = ...,
         *,
         facecolor: ColorType | None = ...,
@@ -421,3 +423,8 @@ class Figure(FigureBase):
 def figaspect(
     arg: float | ArrayLike,
 ) -> np.ndarray[tuple[Literal[2]], np.dtype[np.float64]]: ...
+
+def _parse_figsize(
+    figsize: tuple[float, float] | tuple[float, float, Literal["in", "cm", "px"]],
+    dpi: float
+) -> tuple[float, float]: ...
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index ea48fa2b180e..999b8e42120e 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -875,7 +875,9 @@ def figure(
     # autoincrement if None, else integer from 1-N
     num: int | str | Figure | SubFigure | None = None,
     # defaults to rc figure.figsize
-    figsize: ArrayLike | None = None,
+    figsize: ArrayLike  # a 2-element ndarray is accepted as well
+             | tuple[float, float, Literal["in", "cm", "px"]]
+             | None = None,
     # defaults to rc figure.dpi
     dpi: float | None = None,
     *,
@@ -908,8 +910,12 @@ def figure(
         window title is set to this value.  If num is a ``SubFigure``, its
         parent ``Figure`` is activated.
 
-    figsize : (float, float), default: :rc:`figure.figsize`
-        Width, height in inches.
+    figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
+        The figure dimensions. This can be
+
+        - a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
+          "px".
+        - a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
 
     dpi : float, default: :rc:`figure.dpi`
         The resolution of the figure in dots-per-inch.


Test patch:
diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py
index 0c873934ebcb..014eb2cf23d0 100644
--- a/lib/matplotlib/tests/test_figure.py
+++ b/lib/matplotlib/tests/test_figure.py
@@ -1819,3 +1819,19 @@ def test_subfigure_stale_propagation():
     sfig2.stale = True
     assert sfig1.stale
     assert fig.stale
+
+
+@pytest.mark.parametrize("figsize, figsize_inches", [
+    ((6, 4), (6, 4)),
+    ((6, 4, "in"), (6, 4)),
+    ((5.08, 2.54, "cm"), (2, 1)),
+    ((600, 400, "px"), (6, 4)),
+])
+def test_figsize(figsize, figsize_inches):
+    fig = plt.figure(figsize=figsize, dpi=100)
+    assert tuple(fig.get_size_inches()) == figsize_inches
+
+
+def test_figsize_invalid_unit():
+    with pytest.raises(ValueError, match="Invalid unit 'um'"):
+        plt.figure(figsize=(6, 4, "um"))