Repository: matplotlib/matplotlib
Pull request: Fix TypeError when plotting stacked bar chart with decimal
## PR Summary
Hi, I'm opening this as part of coursework from University of Toronto Scarborough's CSCD01 taught by Dr. Anya Tafliovich @atafliovich. (https://www.utsc.utoronto.ca/~atafliovich/cscd01/index.html)


Fixes #10788 

### Bug Analysis and Technical Commentary:
When try to plot a bar graph with, a TypeError exception will be raised if values for bar bases are of type Decimal.

The problem is the width and height used to calculate the bar location are always of type float or int. There is no operation defined between float/int and Decimal.

The fix is adding a type conversion right before starting calculate the bar location according to align and orientation. If the input values for bar bases are of type Decimal, then convert all elements in the width/height array into Decimal. With further investigation, we cannot postpone the type conversion until the calculation time since the height/width array will later be used to create Patches (Artist).

### Acceptance Test:
create a bar chart with Decimal values as input
using plt.show() to view the generaed chart
Result:
A bar chart is successfully rendered without TypeError

### Example code:
```
from decimal import Decimal
import matplotlib.pyplot as plt

if __name__ == "__main__":
    plt.subplot(111)
    x = [Decimal(x) for x in range(10)]
    y = [Decimal(x) for x in range(10)]
    plt.bar(x, y)
    plt.show()

```
## PR Checklist
- [X] Has Pytest style unit tests
- [X] Code is [Flake 8](http://flake8.pycqa.org/en/latest/) compliant
- [ ] New features are documented, with examples if plot related
- [ ] Documentation is sphinx and numpydoc compliant
- [ ] Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
- [ ] Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at
  https://matplotlib.org/devel/gitwash/development_workflow.html.

- Do not create the PR out of master, but out of a separate branch.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
TypeError when plotting stacked bar chart with decimal
### Bug report

**Bug summary**

When plotting bar graph, TypeError exception is raised if values for bar bases are Decimal types.

**Code for reproduction**

```python
from decimal import Decimal
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
x = list(range(10))
y = [Decimal(x) for x in range(10)]
bar1 = ax.bar(x, y)
bar2 = ax.bar(x, y, bottom=y)
```

**Actual outcome**

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/user/dev/venv/lib/python3.6/site-packages/matplotlib/__init__.py", line 1717, in inner
    return func(ax, *args, **kwargs)
  File "/home/user/dev/venv/lib/python3.6/site-packages/matplotlib/axes/_axes.py", line 2137, in bar
    self.autoscale_view()
  File "/home/user/dev/venv/lib/python3.6/site-packages/matplotlib/axes/_base.py", line 2351, in autoscale_view
    'minposy', self.yaxis, self._ymargin, y_stickies, self.set_ybound)
  File "/home/user/dev/venv/lib/python3.6/site-packages/matplotlib/axes/_base.py", line 2330, in handle_single_axis
    do_lower_margin = not np.any(np.isclose(x0, stickies))
  File "/home/user/dev/venv/lib/python3.6/site-packages/numpy/core/numeric.py", line 2333, in isclose
    yfin = isfinite(y)
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
```

**Expected outcome**

No exception is raised, bar2 line behaves the same way it would if y contained ints or floats.

**Matplotlib version**
  * Operating system: Ubuntu 16.04
  * Matplotlib version: 2.1.1
  * Matplotlib backend: TkAgg
  * Python version: 3.6.4
  * Decimal version: 1.70

python installed via pyenv, matplotlib via pip

Hints from issue discussion:
First, this can be made even simpler:

```python
from decimal import Decimal
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
x = [Decimal(x) for x in range(10)]
y = [Decimal(x) for x in range(10)]
bar1 = ax.bar(x, y)
```
yields:

```
 File "/Users/jklymak/matplotlib/lib/matplotlib/axes/_axes.py", line 2250, in bar
    left = x - width / 2
TypeError: unsupported operand type(s) for -: 'decimal.Decimal' and 'float'
```

The problem is that we have to do some math on `x` to make bars, and we can't do mixed math on Decimal.  (this is unlike other functions like plot or scatter, which don't seem to mind iterating over the values in `x`, be they decimal or numpy...)

We can't just force it to float, because other values of x need not be floats (i.e. datetime, categorical). 

A possible solution is to write a unit converter for `decimal` and add to the default converters.  Of course this would just make everything np.float, in which case, why not just cast it to float at the user level.  
This also affects `datetime.time` values on the x axis. Here's an example that gives a similar error:
```
import datetime
import matplotlib.pyplot as plt
xs = [datetime.time(x) for x in range(10)]
ys = list(range(10))
plt.bar(xs, ys)
```
yields
```
/usr/local/envs/py3env/lib/python3.5/site-packages/matplotlib/axes/_axes.py in bar(self, *args, **kwargs)
   2064         if align == 'center':
   2065             if orientation == 'vertical':
-> 2066                 left = x - width / 2
   2067                 bottom = y
   2068             elif orientation == 'horizontal':

TypeError: unsupported operand type(s) for /: 'list' and 'int'
```
But it works fine using `plt.plot`.

Matplotlib version 2.1.2
@Stigjb Your issue is different. You are trying to plot `time` values which matplotlib in general does not support. For a workaround, use `datetime` instead of `time` values.  

In case you want matplotlib to add support for `time` values natively, I would suggest you open a new issue with this feature request.  
I am a student and is new to Matplotlib. For the problem about Decimal and float type error, I think it is because the height and width in bar(...) function in _axes.py (based on my current understanding) are always of type float (or integer). Further more, when creating patches, the width and height may be used in constructors. Here is my thought about how to (kind of) fix it:
Right before we compute alignment, we add a check:
```
if orientation=='vertical' and x.size > 0 and isInstance(x[0], Decimal):
     width = np.array([Decimal(itm) for itm in width ])
elif orientation=='horizontal' and y.size > 0 and isInstance(y[0], Decimal):
     height = np.array([Decimal(itm) for itm in height])

# Insert above code right before the following code
if align == 'center':
    if orientation == 'vertical':
        try:
             left = x - width / 2
# the rest code unchanged
```
Here are some bar plots I can produced with Decimal x and y after the above change:
![image](https://user-images.githubusercontent.com/21251857/53702632-fa1cd500-3dd6-11e9-879f-80973b5812cd.png)
Since I am quite new to Matplotlib, I am wondering whether this can be a fix and whether it has any side effect.
Please do pardon my beginner knowledge.

Commit message: Add DecimalConverter in units.py

In axis.py, instead of first checking whether input is number like, direct handle them as a united quantity.
Since Decimal is a kind of Number, so DecimalConvert doesn't need to provide extra AxisInfo or default unit.

Patch to review:
diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py
index 1855af151f27..d23be7c4346d 100644
--- a/lib/matplotlib/axis.py
+++ b/lib/matplotlib/axis.py
@@ -1543,10 +1543,6 @@ def have_units(self):
         return self.converter is not None or self.units is not None
 
     def convert_units(self, x):
-        # If x is already a number, doesn't need converting
-        if munits.ConversionInterface.is_numlike(x):
-            return x
-
         if self.converter is None:
             self.converter = munits.registry.get_converter(x)
 
@@ -1555,8 +1551,16 @@ def convert_units(self, x):
         try:
             ret = self.converter.convert(x, self.units, self)
         except Exception as e:
-            raise munits.ConversionError('Failed to convert value(s) to axis '
-                                         f'units: {x!r}') from e
+            # If x is already number like, converters other than
+            # DecimalConverter may raise error, so check here to walk around.
+            if munits.ConversionInterface.is_numlike(x):
+                return x
+            # Try convert again
+            try:
+                ret = self.converter.convert(x, self.units, self)
+            except Exception as e2:
+                raise munits.ConversionError('Failed to convert value(s) to '
+                                         f'axis units: {x!r}') from e2
         return ret
 
     def set_units(self, u):
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index b72c47839856..01556f72907c 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -11,6 +11,7 @@
 import numpy as np
 from numpy import ma
 from cycler import cycler
+from decimal import Decimal
 import pytest
 
 import warnings
@@ -1506,6 +1507,34 @@ def test_bar_tick_label_multiple_old_alignment():
            align='center')
 
 
+@check_figures_equal(extensions=["png"])
+def test_bar_decimal_center(fig_test, fig_ref):
+    ax = fig_test.subplots()
+    x0 = [1.5, 8.4, 5.3, 4.2]
+    y0 = [1.1, 2.2, 3.3, 4.4]
+    x = [Decimal(x) for x in x0]
+    y = [Decimal(y) for y in y0]
+    # Test image - vertical, align-center bar chart with Decimal() input
+    ax.bar(x, y, align='center')
+    # Reference image
+    ax = fig_ref.subplots()
+    ax.bar(x0, y0, align='center')
+
+
+@check_figures_equal(extensions=["png"])
+def test_barh_decimal_center(fig_test, fig_ref):
+    ax = fig_test.subplots()
+    x0 = [1.5, 8.4, 5.3, 4.2]
+    y0 = [1.1, 2.2, 3.3, 4.4]
+    x = [Decimal(x) for x in x0]
+    y = [Decimal(y) for y in y0]
+    # Test image - horizontal, align-center bar chart with Decimal() input
+    ax.barh(x, y, height=[0.5, 0.5, 1, 1], align='center')
+    # Reference image
+    ax = fig_ref.subplots()
+    ax.barh(x0, y0, height=[0.5, 0.5, 1, 1], align='center')
+
+
 def test_bar_color_none_alpha():
     ax = plt.gca()
     rects = ax.bar([1, 2], [2, 4], alpha=0.3, color='none', edgecolor='r')
diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py
index 4cbac7c226ab..194ba3d01e33 100644
--- a/lib/matplotlib/units.py
+++ b/lib/matplotlib/units.py
@@ -45,6 +45,8 @@ def default_units(x, axis):
 from numbers import Number
 
 import numpy as np
+from numpy import ma
+from decimal import Decimal
 
 from matplotlib import cbook
 
@@ -133,6 +135,43 @@ def is_numlike(x):
             return isinstance(x, Number)
 
 
+class DecimalConverter(ConversionInterface):
+    """
+    Converter for decimal.Decimal data to float.
+    """
+    @staticmethod
+    def convert(value, unit, axis):
+        """
+        Convert Decimals to floats.
+
+        The *unit* and *axis* arguments are not used.
+
+        Parameters
+        ----------
+        value : decimal.Decimal or iterable
+            Decimal or list of Decimal need to be converted
+        """
+        # If value is a Decimal
+        if isinstance(value, Decimal):
+            return np.float(value)
+        # else x is a list of Decimal
+        else:
+            converter = np.asarray
+            if isinstance(value, ma.MaskedArray):
+                converter = ma.asarray
+            return converter(value, dtype=np.float)
+
+    @staticmethod
+    def axisinfo(unit, axis):
+        # Since Decimal is a kind of Number, don't need specific axisinfo.
+        return AxisInfo()
+
+    @staticmethod
+    def default_units(x, axis):
+        # Return None since Decimal is a kind of Number.
+        return None
+
+
 class Registry(dict):
     """Register types with conversion interface."""
 
@@ -164,3 +203,4 @@ def get_converter(self, x):
 
 
 registry = Registry()
+registry[Decimal] = DecimalConverter()


Code review comments:
diff_hunk: @@ -1555,8 +1551,16 @@ def convert_units(self, x):
         try:
             ret = self.converter.convert(x, self.units, self)
         except Exception as e:
-            raise munits.ConversionError('Failed to convert value(s) to axis '
-                                         f'units: {x!r}') from e
+            # If x is already number like, converters other than
+            # DecimalConverter may raise error, so check here to walk around.
+            if munits.ConversionInterface.is_numlike(x):
+                return x
+            # Try convert again
path: lib/matplotlib/axis.py
text: @user1:
in what case can the conversion succeed on the second try but not the first?

@author:
@user1 Thanks for pointing this out. The second try is not needed. I have updated the code.