yuzie007 / mpltern

Ternary plots as projections of Matplotlib
https://yuzie007.github.io/mpltern
MIT License
39 stars 2 forks source link

Inability to having NaN-values in tricontourf #12

Closed asbjos closed 1 year ago

asbjos commented 1 year ago

While matplotlib supports having nan-values in an array passed to contour(f), the mpltern analogue tricontour(f) raises a ValueError: z array must not contain non-finite values within the triangulation.

Could this be implemented please? (I'm doing least_squares over the ternary compositions, and if no solution is found by it, I want to mask out those concentrations by passing NaNs)

import numpy as np
import matplotlib.pyplot as plt
from mpltern.datasets import get_triangular_grid

#%% matplotlib
plt.figure()
ax1 = plt.subplot(121, projection="3d")

x = np.linspace(0, 1, 11)
y = np.linspace(0, 1, 11)
x, y = np.meshgrid(x, y)
z = 1 - x - y
T = x*y
T[np.where(z < 0)] = np.nan
T[2, 1] = np.nan

ax1.contourf(x, y, T)
ax1.text(x[2, 1], y[2, 1], 0, "nan", color="r")

#%% mpltern
ax2 = plt.subplot(122, projection="ternary")
x, y, z = get_triangular_grid(11)
T = x * y
T[3] = np.nan # results in error
ax2.text(x[3], y[3], z[3], "nan", color="r")
ax2.tricontourf(x, y, z, T) # Error

image

yuzie007 commented 1 year ago

Thank you @asbjos for using mpltern and reporting the issue you faced. So far I guess this is likely the expected behavior inherited from matplotlib; (tricontourf is NOT the original of mpltern but inherited from Matplotlib, and mpltern simply offers a wrapper for that.) You can take a look on: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.tricontourf.html They explicitly write "All values in z must be finite. Hence, nan and inf values must either be removed or set_mask be used." So, probably you can try to use the mask option.

asbjos commented 1 year ago

Thank you for the quick response, @yuzie007 ! Could you please guide me to how to implement such a mask? I tried setting a mask like this:

ax2.tricontourf(x, y, z, T, mask=np.isnan(T))

But get the ValueError that "mask array must have same length as triangles array", although x, y, z, T, and np.isnan(T) all have the same length (=66).

yuzie007 commented 1 year ago

Thank you @asbjos for your report. I found that the mask option works for triangles, not directly for given points. For this we need to make a Triangulation object on our side. Since this works so far only for Matplotlib xy space (not for mpltern ternary space), we need to convert the coordinates from ternary to the original xy coordinates. Then, we need to identify the triangle indices we want to color in white (for this you probably need to refer to https://matplotlib.org/stable/api/tri_api.html#matplotlib-tri).

The running example is as follows.

import numpy as np
import matplotlib.pyplot as plt
from mpltern.datasets import get_triangular_grid
from matplotlib.tri import Triangulation

#%% matplotlib
plt.figure()
ax1 = plt.subplot(121, projection="3d")

x = np.linspace(0, 1, 11)
y = np.linspace(0, 1, 11)
x, y = np.meshgrid(x, y)
z = 1 - x - y
T = x*y
T[np.where(z < 0)] = np.nan
T[2, 1] = np.nan

ax1.contourf(x, y, T)
ax1.text(x[2, 1], y[2, 1], 0, "nan", color="r")

#%% mpltern
ax2 = plt.subplot(122, projection="ternary")
x, y, z = get_triangular_grid(11)
T = x * y
# T[3] = np.nan # results in error
tlr = np.column_stack((x, y, z))
xx, yy = ax2.transProjection.transform(tlr).T  # coordinates in xy
tri = Triangulation(xx, yy)
mask = np.zeros(len(tri.triangles), dtype=bool)
mask[20] = 1  # you need to identify the triangle index you want to color in white
tri.set_mask(mask)
ax2.tricontourf(tri, T, transform=ax2.transData)  # plot in xy coordinates
plt.show()

Figure_1

If you want to give np.nan at triangle corner points as you originally wished, I would suggest to make an enhancement request to Matplotlib tricontourf (or any related methods working on triangular grids), as this is the limitation rather inherited from Matplotlib.