Current matplotlib graph
How it should look like
I want to change the graph color and gradient direction in the parts where graph goes below zero. Alternative image for illustration:
I have tried it using this code
def add_gradient_fill(ax: Optional[plt.Axes] = None, alpha_gradientglow: float = 1.0):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
if not ax:
ax = plt.gca()
lines = ax.get_lines()
for line in lines:
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
fill_color = line.get_color()
zorder = line.get_zorder()
alpha = line.get_alpha()
alpha = 1.0 if alpha is None else alpha
rgb = mcolors.colorConverter.to_rgb(fill_color)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
z[:, :, -1] = np.linspace(0, alpha, 100)[:, None]
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
clip_path = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path)
im.set_clip_path(clip_path)
ax.autoscale(True)
This code is also a part of a matplotlib theming library called mplcyberpunk.
This provides great looks to the plot, but as mentioned earlier, I want that the sub-zero parts of the graphs be in different color with gradient direction reversed.
How can this be possibly achieved?
PS: Sincerely, my question is different from other graph gradient questions, please don't close this.
Edit
Minimal reproducible code
import matplotlib.pyplot as plt
import mplcyberpunk as mplcp
x = range(-10, 11)
y = [(i ** 2) - 50 for i in x]
plt.style.use('cyberpunk')
###### just for setting the theme, ignore these lines #########
for param in ['figure.facecolor', 'axes.facecolor', 'savefig.facecolor']:
plt.rcParams[param] = '#303030'
for param in ['text.color', 'axes.labelcolor', 'xtick.color', 'ytick.color']:
plt.rcParams[param] = '#ffffff'
plt.subplots()[1].grid(color='#404040')
##################################################################
plt.plot(x, y)
mplcp.make_lines_glow()
mplcp.add_gradient_fill()
plt.show()
Update:
Well I somehow figured it out, but there are some visual defects that need focus. Here are the functions and output:
from itertools import groupby
import numpy as np
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Polygon
def add_glow_effects(n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
change_line_color: bool = True,
color_positive: str = '#0000ff',
color_negative: str = '#ff0000',
alpha_gradientglow: float = 1.0, ):
make_lines_glow(n_glow_lines, diff_linewidth, alpha_line, change_line_color, color_positive, color_negative)
add_gradient_fill(alpha_gradientglow, color_positive, color_negative, )
def make_lines_glow(n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
change_line_color: bool = True,
color_positive: str = '#0000ff',
color_negative: str = '#ff0000'):
ax = plt.gca()
lines = ax.get_lines()
alpha_value = alpha_line / n_glow_lines
for line_element in lines:
if not isinstance(line_element, Line2D):
continue
x, y = line_element.get_data(orig=False)
x, y = optimize_lines(list(x), list(y))
lines_list = list_form(x, y)
for line in lines_list:
if change_line_color:
y_avg = sum(line[1]) / len(line[1])
if y_avg >= 0:
color = color_positive
else:
color = color_negative
else:
color = line_element.get_color()
line = Line2D(line[0], line[1], linewidth=line_element.get_linewidth(), color=color)
data = list(line.get_data(orig=False))
linewidth = line.get_linewidth()
ax.plot(data[0], data[1], color=color, linewidth=linewidth)
for n in range(1, n_glow_lines + 1):
glow_line, = ax.plot(*data)
glow_line.update_from(line)
# line properties are copied as seen in this solution: https://stackoverflow.com/a/54688412/3240855
glow_line.set_alpha(alpha_value)
glow_line.set_linewidth(linewidth + (diff_linewidth * n))
# mark the glow lines, to disregard them in the underglow function.
glow_line.is_glow_line = True
# noinspection PyArgumentList
def add_gradient_fill(alpha_gradientglow: float = 1.0,
color_positive: str = '#00ff00',
color_negative: str = '#ff0000'):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
ax = plt.gca()
lines = ax.get_lines()
for line_element in lines:
if not isinstance(line_element, Line2D):
continue
x, y = line_element.get_data(orig=False)
x, y = optimize_lines(list(x), list(y))
lines_list = list_form(x, y)
for line in lines_list:
y_avg = sum(line[1]) / len(line[1])
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
line = Line2D(line[0], line[1], linewidth=line_element.get_linewidth())
zorder = line.get_zorder()
alpha = line_element.get_alpha()
alpha = 1.0 if alpha is None else alpha
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
xy = np.column_stack([x, y])
if y_avg >= 0:
fill_color = color_positive
linspace = np.linspace(0, alpha, 100)[:, None]
xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
else:
fill_color = color_negative
linspace = np.linspace(alpha, 0, 100)[:, None]
xy = np.vstack([[xmin, ymax], xy, [xmax, ymax], [xmin, ymax]])
rgb = mcolors.colorConverter.to_rgb(fill_color)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
z[:, :, -1] = linspace
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
clip_path = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path)
im.set_clip_path(clip_path)
ax.autoscale(True)
def optimize_lines(x: list, y: list):
y = [list(element) for index, element in groupby(y, lambda a: a >= 0)]
indexes = [0]
for i in y:
indexes.append(len(i) + indexes[-1])
# from https://www.geeksforgeeks.org/python-group-consecutive-elements-by-sign/
x = [x[indexes[i]:indexes[i + 1]] for i, _ in enumerate(indexes) if i != len(indexes) - 1]
for i in range(len(y) - 1):
if y[i][-1] == 0 and y[i + 1][0] == 0:
continue
a = y[i][-1]
b = y[i + 1][0]
diff = abs(a) + abs(b)
a_ = (abs(0 - a)) / diff
b_ = abs(0 - b) / diff
x[i].append(x[i][-1] + a_)
x[i + 1].insert(0, x[i + 1][0] - b_)
y[i].append(0)
y[i + 1].insert(0, 0)
x = [list(i) for i in x]
y = [list(i) for i in y]
# input: x=[1,2,3,4,5], y=[1,2,-5,0,2]
# output: x=[[1, 2, 2.2857142857142856], [2.2857142857142856, 3, 4.0], [4.0, 4, 5]],
# y=[[1, 2, 0], [0, -5, 0], [0, 0, 2]]
return list(x), list(y)
def list_form(x: list[list], y: list[list]):
lst = []
for i in range(len(x)):
lst.append([x[i], y[i]])
return lst
The output is now this:
Notice how the glow from function is collected at the left side of graph. also, at the end of the graph these is a tiny purple triangle that is offset by one corner.
The title of this post has been changed to "Visual defects in matplotlib graph" from "Matplotlib graph gradient away from the x axis" for the purpose of relevance, keeping in mind the latest update to the post.
Interesting question. I have several ideas to help you there. I think the easiest solution will be to find an elegant way to "split" the data conditionally when zero-crossing occurs (but you need to detect the zero-crossings accurately for clean clipping masks).
The solution below is not yet finished, but it solves the first issue of having a two-color gradient and a compound path to get a positive/negative clipping mask. Now there is the line color that needs to be also split into + and - parts. So far, I just overlayed the line below zero on top of the existing line, and the glow of this line clearly mixes with the one of the first line.
I'll be back to it later; maybe this will help meanwhile.
import matplotlib.pyplot as plt
import mplcyberpunk as mplcp
import matplotlib.colors as mcolors
from matplotlib.path import Path
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.patches import Polygon, PathPatch
def add_gradient_fill(ax=None, alpha_gradientglow=1.0, negative_color="C1"):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
if not ax:
ax = plt.gca()
lines = ax.get_lines()
for line in lines:
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
fill_color = line.get_color()
zorder = line.get_zorder()
alpha = line.get_alpha()
alpha = 1.0 if alpha is None else alpha
rgb = mcolors.colorConverter.to_rgb(fill_color)
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = np.nanmin(x), np.nanmax(x)
ymin, ymax = np.nanmin(y), np.nanmax(y)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
# z[:, :, -1] = np.linspace(0, alpha, 100)[:, None]
ynorm = max(np.abs(ymin), np.abs(ymax))
ymin_norm = ymin / ynorm
ymax_norm = ymax / ynorm
ynorm = np.linspace(ymin_norm, ymax_norm, 100)
z[:, :, -1] = alpha * np.abs(ynorm[:, None])
rgb_neg = mcolors.colorConverter.to_rgb(negative_color)
z[ynorm < 0, :, :3] = rgb_neg
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
# Detect zero crossings
y_copy = y.copy()
y = y.clip(0, None)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, 0], xy, [xmax, 0], [xmin, 0]])
clip_path_1 = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
y = y_copy.copy()
y = y.clip(None, 0)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, 0], xy, [xmax, 0], [xmin, 0]])
clip_path_2 = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path_1)
ax.add_patch(clip_path_2)
clip_paths = clip_path_2, clip_path_1
vertices = np.concatenate([i.get_path().vertices for i in clip_paths])
codes = np.concatenate([i.get_path().codes for i in clip_paths])
clip_path = PathPatch(Path(vertices, codes), transform=ax.transData)
im.set_clip_path(clip_path)
ax.autoscale(True)
y = y_copy.copy()
y[y > 0] = np.nan
ax.plot(x, y)
def make_lines_glow(
ax=None,
n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
lines=None,
) -> None:
"""Add a glow effect to the lines in an axis object.
Each existing line is redrawn several times with increasing width and low alpha to create the glow effect.
"""
if not ax:
ax = plt.gca()
lines = ax.get_lines() if lines is None else lines
lines = [lines] if isinstance(lines, Line2D) else lines
alpha_value = alpha_line / n_glow_lines
for line in lines:
data = line.get_data(orig=False)
linewidth = line.get_linewidth()
try:
step_type = line.get_drawstyle().split('-')[1]
except:
step_type = None
for n in range(1, n_glow_lines + 1):
if step_type:
glow_line, = ax.step(*data)
else:
glow_line, = ax.plot(*data)
glow_line.update_from(line) # line properties are copied as seen in this solution: https://stackoverflow.com/a/54688412/3240855
glow_line.set_alpha(alpha_value)
glow_line.set_linewidth(linewidth + (diff_linewidth * n))
glow_line.is_glow_line = True # mark the glow lines, to disregard them in the underglow function.
x = np.arange(-10, 11)
y = np.array([(i ** 2) - 50 for i in x])
plt.style.use('cyberpunk')
for param in ['figure.facecolor', 'axes.facecolor', 'savefig.facecolor']:
plt.rcParams[param] = '#303030'
for param in ['text.color', 'axes.labelcolor', 'xtick.color', 'ytick.color']:
plt.rcParams[param] = '#ffffff'
plt.subplots()[1].grid(color='#404040')
plt.plot(x, y)
add_gradient_fill(negative_color="C1")
make_lines_glow()
plt.show()
Related
I have the following code:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-np.pi/2, np.pi/2, 30)
y = np.linspace(-np.pi/2, np.pi/2, 30)
x,y = np.meshgrid(x,y)
z = np.sin(x**2+y**2)[:-1,:-1]
fig,ax = plt.subplots()
ax.pcolormesh(x,y,z)
Which gives this image:
Now lets say I want to highlight the edge certain grid boxes:
highlight = (z > 0.9)
I could use the contour function, but this would result in a "smoothed" contour. I just want to highlight the edge of a region, following the edge of the grid boxes.
The closest I've come is adding something like this:
highlight = np.ma.masked_less(highlight, 1)
ax.pcolormesh(x, y, highlight, facecolor = 'None', edgecolors = 'w')
Which gives this plot:
Which is close, but what I really want is for only the outer and inner edges of that "donut" to be highlighted.
So essentially I am looking for some hybrid of the contour and pcolormesh functions - something that follows the contour of some value, but follows grid bins in "steps" rather than connecting point-to-point. Does that make sense?
Side note: In the pcolormesh arguments, I have edgecolors = 'w', but the edges still come out to be blue. Whats going on there?
EDIT:
JohanC's initial answer using add_iso_line() works for the question as posed. However, the actual data I'm using is a very irregular x,y grid, which cannot be converted to 1D (as is required for add_iso_line().
I am using data which has been converted from polar coordinates (rho, phi) to cartesian (x,y). The 2D solution posed by JohanC does not appear to work for the following case:
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
def pol2cart(rho, phi):
x = rho * np.cos(phi)
y = rho * np.sin(phi)
return(x, y)
phi = np.linspace(0,2*np.pi,30)
rho = np.linspace(0,2,30)
pp, rr = np.meshgrid(phi,rho)
xx,yy = pol2cart(rr, pp)
z = np.sin(xx**2 + yy**2)
scale = 5
zz = ndimage.zoom(z, scale, order=0)
fig,ax = plt.subplots()
ax.pcolormesh(xx,yy,z[:-1, :-1])
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xmin, xmax = xx.min(), xx.max()
ymin, ymax = yy.min(), yy.max()
ax.contour(np.linspace(xmin,xmax, zz.shape[1]) + (xmax-xmin)/z.shape[1]/2,
np.linspace(ymin,ymax, zz.shape[0]) + (ymax-ymin)/z.shape[0]/2,
np.where(zz < 0.9, 0, 1), levels=[0.5], colors='red')
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
This post shows a way to draw such lines. As it is not straightforward to adapt to the current pcolormesh, the following code demonstrates a possible adaption.
Note that the 2d versions of x and y have been renamed, as the 1d versions are needed for the line segments.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
x = np.linspace(-np.pi / 2, np.pi / 2, 30)
y = np.linspace(-np.pi / 2, np.pi / 2, 30)
xx, yy = np.meshgrid(x, y)
z = np.sin(xx ** 2 + yy ** 2)[:-1, :-1]
fig, ax = plt.subplots()
ax.pcolormesh(x, y, z)
def add_iso_line(ax, value, color):
v = np.diff(z > value, axis=1)
h = np.diff(z > value, axis=0)
l = np.argwhere(v.T)
vlines = np.array(list(zip(np.stack((x[l[:, 0] + 1], y[l[:, 1]])).T,
np.stack((x[l[:, 0] + 1], y[l[:, 1] + 1])).T)))
l = np.argwhere(h.T)
hlines = np.array(list(zip(np.stack((x[l[:, 0]], y[l[:, 1] + 1])).T,
np.stack((x[l[:, 0] + 1], y[l[:, 1] + 1])).T)))
lines = np.vstack((vlines, hlines))
ax.add_collection(LineCollection(lines, lw=1, colors=color))
add_iso_line(ax, 0.9, 'r')
plt.show()
Here is an adaption of the second answer, which can work with only 2d arrays:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from scipy import ndimage
x = np.linspace(-np.pi / 2, np.pi / 2, 30)
y = np.linspace(-np.pi / 2, np.pi / 2, 30)
x, y = np.meshgrid(x, y)
z = np.sin(x ** 2 + y ** 2)
scale = 5
zz = ndimage.zoom(z, scale, order=0)
fig, ax = plt.subplots()
ax.pcolormesh(x, y, z[:-1, :-1] )
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
ax.contour(np.linspace(xmin,xmax, zz.shape[1]) + (xmax-xmin)/z.shape[1]/2,
np.linspace(ymin,ymax, zz.shape[0]) + (ymax-ymin)/z.shape[0]/2,
np.where(zz < 0.9, 0, 1), levels=[0.5], colors='red')
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
plt.show()
I'll try to refactor add_iso_line method in order to make it more clear an open for optimisations. So, at first, there comes a must-do part:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
x = np.linspace(-np.pi/2, np.pi/2, 30)
y = np.linspace(-np.pi/2, np.pi/2, 30)
x, y = np.meshgrid(x,y)
z = np.sin(x**2+y**2)[:-1,:-1]
fig, ax = plt.subplots()
ax.pcolormesh(x,y,z)
xlim, ylim = ax.get_xlim(), ax.get_ylim()
highlight = (z > 0.9)
Now highlight is a binary array that looks like this:
After that we can extract indexes of True cells, look for False neighbourhoods and identify positions of 'red' lines. I'm not comfortable enough with doing it in a vectorised manner (like here in add_iso_line method) so just using simple loop:
lines = []
cells = zip(*np.where(highlight))
for x, y in cells:
if x == 0 or highlight[x - 1, y] == 0: lines.append(([x, y], [x, y + 1]))
if x == highlight.shape[0] or highlight[x + 1, y] == 0: lines.append(([x + 1, y], [x + 1, y + 1]))
if y == 0 or highlight[x, y - 1] == 0: lines.append(([x, y], [x + 1, y]))
if y == highlight.shape[1] or highlight[x, y + 1] == 0: lines.append(([x, y + 1], [x + 1, y + 1]))
And, finally, I resize and center coordinates of lines in order to fit with pcolormesh:
lines = (np.array(lines) / highlight.shape - [0.5, 0.5]) * [xlim[1] - xlim[0], ylim[1] - ylim[0]]
ax.add_collection(LineCollection(lines, colors='r'))
plt.show()
In conclusion, this is very similar to JohanC solution and, in general, slower. Fortunately, we can reduce amount of cells significantly, extracting contours only using python-opencv package:
import cv2
highlight = highlight.astype(np.uint8)
contours, hierarchy = cv2.findContours(highlight, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cells = np.vstack(contours).squeeze()
This is an illustration of cells being checked:
I'm plotting a simple 2D density map obtained with scipy.stats.gaussian_kde. There is always a plotting artifact towards the edges where the density appears to be lower:
I've tried every interpolation method in imshow() and none seems to be able to get rid of it. Is there a proper way to handle this?
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
x_data = np.random.uniform(1., 2000., 1000)
y_data = np.random.uniform(1., 2000., 1000)
xmin, xmax = np.min(x_data), np.max(x_data)
ymin, ymax = np.min(y_data), np.max(y_data)
values = np.vstack([x_data, y_data])
# Gaussian KDE.
kernel = stats.gaussian_kde(values, bw_method=.2)
# Grid density (number of points).
gd_c = complex(0, 50)
# Define x,y grid.
x_grid, y_grid = np.mgrid[xmin:xmax:gd_c, ymin:ymax:gd_c]
positions = np.vstack([x_grid.ravel(), y_grid.ravel()])
# Evaluate kernel in grid positions.
k_pos = kernel(positions)
ext_range = [xmin, xmax, ymin, ymax]
kde = np.reshape(k_pos.T, x_grid.shape)
im = plt.imshow(np.rot90(kde), cmap=plt.get_cmap('RdYlBu_r'), extent=ext_range)
plt.show()
After a while I found a way to address this issue applying a neat trick explained by Flabetvibes in this excellent answer.
I use the code shown there to mirror the data as shown in the first figure of the mentioned answer. The only modification I introduced is to trim the mirrored data to a perc padding (I set it to 10% by default) so as not to carry around a lot of unnecessary values.
The result is shown here, original non-mirrored data to the left, and mirrored data to the right:
As can be seen, the changes in the resulting density map are not trivial. I personally believe the mirrored-data KDE represents the actual density better.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
def in_box(towers, bounding_box):
return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0],
towers[:, 0] <= bounding_box[1]),
np.logical_and(bounding_box[2] <= towers[:, 1],
towers[:, 1] <= bounding_box[3]))
def dataMirror(towers, bounding_box, perc=.1):
# Select towers inside the bounding box
i = in_box(towers, bounding_box)
# Mirror points
points_center = towers[i, :]
points_left = np.copy(points_center)
points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0])
points_right = np.copy(points_center)
points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0])
points_down = np.copy(points_center)
points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2])
points_up = np.copy(points_center)
points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1])
points = np.append(points_center,
np.append(np.append(points_left,
points_right,
axis=0),
np.append(points_down,
points_up,
axis=0),
axis=0),
axis=0)
# Trim mirrored frame to withtin a 'perc' pad
xr, yr = np.ptp(towers.T[0]) * perc, np.ptp(towers.T[1]) * perc
xmin, xmax = bounding_box[0] - xr, bounding_box[1] + xr
ymin, ymax = bounding_box[2] - yr, bounding_box[3] + yr
msk = (points[:, 0] > xmin) & (points[:, 0] < xmax) &\
(points[:, 1] > ymin) & (points[:, 1] < ymax)
points = points[msk]
return points.T
def KDEplot(xmin, xmax, ymin, ymax, values):
# Gaussian KDE.
kernel = stats.gaussian_kde(values, bw_method=.2)
# Grid density (number of points).
gd_c = complex(0, 50)
# Define x,y grid.
x_grid, y_grid = np.mgrid[xmin:xmax:gd_c, ymin:ymax:gd_c]
positions = np.vstack([x_grid.ravel(), y_grid.ravel()])
# Evaluate kernel in grid positions.
k_pos = kernel(positions)
ext_range = [xmin, xmax, ymin, ymax]
kde = np.reshape(k_pos.T, x_grid.shape)
plt.imshow(np.rot90(kde), cmap=plt.get_cmap('RdYlBu_r'), extent=ext_range)
x_data = np.random.uniform(1., 2000., 1000)
y_data = np.random.uniform(1., 2000., 1000)
xmin, xmax = np.min(x_data), np.max(x_data)
ymin, ymax = np.min(y_data), np.max(y_data)
values = np.vstack([x_data, y_data])
# Plot non-mirrored data
plt.subplot(121)
KDEplot(xmin, xmax, ymin, ymax, values)
plt.scatter(*values, s=3, c='k')
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
# Plot mirrored data
bounding_box = (xmin, xmax, ymin, ymax)
values = dataMirror(values.T, bounding_box)
plt.subplot(122)
KDEplot(xmin, xmax, ymin, ymax, values)
plt.scatter(*values, s=3, c='k')
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
plt.show()
I have a set of 100 random 2D points (between 0 and 20) in a scatter plot with 2 sub plots surrounding the main. When I zoom in the main scatter plot, the range on the subplots gets shrunk, however I can see points from outside the zoom window region.
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import random
numPoints = 100
x = [random.uniform(0, 20) for i in range(numPoints)]
y = [random.uniform(0, 20) for i in range(numPoints)]
# Set up the axes with gridspec
fig = plt.figure(figsize=(6, 6), constrained_layout=True)
grid = fig.add_gridspec(ncols=2, nrows=2, width_ratios=[0.3, 5], height_ratios=[5, 0.3])
main_ax = fig.add_subplot(grid[:-1, 1:])
main_ax.plot(x, y, 'ok', markersize=3, alpha=0.2)
y_hist = fig.add_subplot(grid[:-1, 0], xticklabels=[], sharey=main_ax)
x_hist = fig.add_subplot(grid[-1, 1:], yticklabels=[], sharex=main_ax)
x_hist.plot(
x, [0 for i in x],
'ok',
color='gray'
)
x_hist.invert_yaxis()
y_hist.plot(
[0 for i in y], y,
'ok',
color='gray'
)
y_hist.invert_xaxis()
main_ax.grid(True, lw = 1, ls = '--', c = '.75')
x_hist.grid(True, axis="x", lw = 1, ls = '--', c = '.75')
y_hist.grid(True, axis="y", lw = 1, ls = '--', c = '.75')
plt.show()
I am trying to get the dots in the left and bottom sub plots of the above image to match just what you see in the main plot (3 points).
Instead they show everything in that direction. The Left subplot shows every point on the x axis between 0 and 2.5. The bottom subplot shows every point on the y axis between 10 and 12.5.
You would need to filter the data, depending on the limits of the main axes. One can connect callbacks on zoom events, see Matplotlib: Finding out xlim and ylim after zoom and connect them to a function that performs the filtering on the data.
import numpy as np
import matplotlib.pyplot as plt
numPoints = 100
x = np.random.rand(numPoints)*20
y = np.random.rand(numPoints)*20
zeros = np.zeros_like(x)
# Set up the axes with gridspec
fig = plt.figure(figsize=(6, 6), constrained_layout=True)
grid = fig.add_gridspec(ncols=2, nrows=2, width_ratios=[0.3, 5], height_ratios=[5, 0.3])
ax_main = fig.add_subplot(grid[:-1, 1:])
ax_y = fig.add_subplot(grid[:-1, 0], xticklabels=[], sharey=ax_main)
ax_x = fig.add_subplot(grid[-1, 1:], yticklabels=[], sharex=ax_main)
ax_main.plot(x, y, 'ok', markersize=3, alpha=0.2)
xline, = ax_x.plot(x, zeros, marker='o', ls="none", color='gray')
yline, = ax_y.plot(zeros, y, marker='o', ls="none", color='gray')
ax_main.grid(True, lw = 1, ls = '--', c = '.75')
ax_y.grid(True, axis="x", lw = 1, ls = '--', c = '.75')
ax_x.grid(True, axis="y", lw = 1, ls = '--', c = '.75')
def xchange(evt):
ymin, ymax = ax_main.get_ylim()
filt = (y <= ymax) & (y >= ymin)
xline.set_data(x[filt], zeros[filt])
def ychange(evt):
xmin, xmax = ax_main.get_xlim()
filt = (x <= xmax) & (x >= xmin)
yline.set_data(zeros[filt], y[filt])
ax_main.callbacks.connect('xlim_changed', ychange)
ax_main.callbacks.connect('ylim_changed', xchange)
plt.show()
I am try to work out with my atomic composition with ternary phase diagram, here is my picture
I wish to put my scale to the ticks on the ternary phase diagram (i.e. those triangular axis) instead of x and y axis. Is there a ways to put the scale on the tick at triangular axis instead of axis x and y? How to remove the x-axis and y-axis while still maintain its labels?
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as tri
def plot_ticks(start, stop, tick, n):
r = np.linspace(0, 1, n+1)
x = start[0] * (1 - r) + stop[0] * r
x = np.vstack((x, x + tick[0]))
y = start[1] * (1 - r) + stop[1] * r
y = np.vstack((y, y + tick[1]))
plt.plot(x, y, 'k', lw=1)
n = 5
tick_size = 0.1
margin = 0.05
# define corners of triangle
left = np.r_[0, 0]
right = np.r_[1, 0]
top = np.r_[0.5, np.sqrt(3)*0.576]
triangle = np.c_[left, right, top, left]
# define corners of triangle
left = np.r_[0, 0]
right = np.r_[1, 0]
top = np.r_[0.5, np.sqrt(3)*0.576]
triangle = np.c_[left, right, top, left]
# define vectors for ticks
bottom_tick = 0.8264*tick_size * (right - top) / n
right_tick = 0.8264*tick_size * (top - left) / n
left_tick = 0.8264*tick_size * (left - right) / n
# first load some data: format x1,x2,x3,value
test_data = np.array([[4,0,0,2.238],
[0,4,0,2.315],
[0,0,4,2.147],
[3,1,0,2.494],
[2,2,0,2.190],
[2,2,0,2.632],
[3,0,1,2.173],
[2,0,2,2.329],
[1,0,3,2.526],
[0,3,1,2.365],
[0,2,2,2.220],
[0,1,3,2.080],
[2,1,1,2.231],
[1,2,1,2.291],
[1,1,2,2.088]])
#Define twin axis
#ax = plt.gca()
fig, ax = plt.subplots()
plot_ticks(left, right, bottom_tick, n)
plot_ticks(right, top, right_tick, n)
plot_ticks(left, top, left_tick, n)
#ax2 = ax.twinx()
# barycentric coords: (a,b,c)
a=test_data[:,0]
b=test_data[:,1]
c=test_data[:,2]
# values is stored in the last column
v = test_data[:,-1]
# translate the data to cartesian corrds
x = 0.5 * ( 2.*b+c ) / ( a+b+c )
y = 0.576*np.sqrt(3) * c / (a+b+c)
# create a triangulation out of these points
T = tri.Triangulation(x,y)
# plot the contour
plt.tricontourf(x,y,T.triangles,v,cmap='jet')
# create the grid
corners = np.array([[0, 0], [1, 0], [0.5, np.sqrt(3)*0.576]])
triangle = tri.Triangulation(corners[:, 0], corners[:, 1])
# creating the grid
refiner = tri.UniformTriRefiner(triangle)
trimesh = refiner.refine_triangulation(subdiv=4)
#plotting the mesh and caliberate the axis
plt.triplot(trimesh,'k--')
#plt.title('Binding energy peratom of Al-Ti-Ni clusters')
ax.set_xlabel('Al-Ti',fontsize=12,color='black')
ax.set_ylabel('Ti-Ni',fontsize=12,color='black')
ax2 = ax.twinx()
ax2.set_ylabel('Al-Ni',fontsize=12,color='black')
plt.gcf().text(0.07, 0.05, 'Ti', fontsize=12,color='black')
plt.gcf().text(0.93, 0.05, 'Al', fontsize=12,color='black')
plt.gcf().text(0.5, 0.9, 'Ni', fontsize=12,color='black')
#set scale for axis
ax.set_xlim(1, 0)
ax.set_ylim(0, 1)
ax2.set_ylim(1, 0)
cax = plt.axes([0.75, 0.55, 0.055, 0.3])
plt.colorbar(cax=cax,format='%.3f')
plt.savefig("AID.png", dpi=1000)
plt.show()
As was mentioned in the comments you can make your own axis just by adding a text to the ticks you generate. Most of the time you need a little tweaking
to get the offsets right...
def plot_ticks(start, stop, tick, n, offset=(.0, .0)):
r = np.linspace(0, 1, n+1)
x = start[0] * (1 - r) + stop[0] * r
x = np.vstack((x, x + tick[0]))
y = start[1] * (1 - r) + stop[1] * r
y = np.vstack((y, y + tick[1]))
plt.plot(x, y, 'k', lw=1)
# add tick labels
for xx, yy, rr in zip(x[1], y[1], r):
plt.text(xx+offset[0], yy+offset[1], "{:.2}".format(rr))
# Note that the ordering from start to stop is important for the tick labels
plot_ticks(right, left, bottom_tick, n, offset=(0, -0.04))
plot_ticks(left, top, left_tick, n, offset=(-0.06, -0.0))
plot_ticks(top, right, right_tick, n)
In addition I switched the axis off via ax.set_axis_off() and I also deleted the twin axis, as you used these only to display the ticks and labels for the connections. These labels can also easily be placed via fig.text() as you did with the corners:
# Corners
fig.text(0.07, 0.05, 'Ti', fontsize=12, color='black')
fig.text(0.93, 0.05, 'Al', fontsize=12, color='black')
fig.text(0.50, 0.90, 'Ni', fontsize=12, color='black')
# Connections
fig.text(0.47, 0.05, 'Ti-Al', fontsize=12, color='black') # Note: not sure about
fig.text(0.72, 0.50, 'Al-Ni', fontsize=12, color='black') # the nomenclature;
fig.text(0.25, 0.50, 'Ti-Ni', fontsize=12, color='black') # might be switched
I am trying to color each individual face of a cylinder, however I am not sure how to go about it, I have tried the following:
for i in range(10):
col.append([])
for i in range(10):
for j in range(20):
col[i].append(plt.cm.Blues(0.4))
ax.plot_surface(X, Y, Z,facecolors = col,edgecolor = "red")
I want each face to be assigned its own color, so I would think I would supply an array of colors for each of the faces in a 2d array.
But this gives an error:
in plot_surface
colset.append(fcolors[rs][cs])
IndexError: list index out of range
Here is the full code:
import numpy as np
from matplotlib import cm
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.linalg import norm
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
origin = np.array([0, 0, 0])
#axis and radius
p0 = np.array([1, 3, 2])
p1 = np.array([8, 5, 9])
R = 5
#vector in direction of axis
v = p1 - p0
#find magnitude of vector
mag = norm(v)
#unit vector in direction of axis
v = v / mag
#make some vector not in the same direction as v
not_v = np.array([1, 0, 0])
if (v == not_v).all():
not_v = np.array([0, 1, 0])
#make vector perpendicular to v
n1 = np.cross(v, not_v)
#normalize n1
n1 /= norm(n1)
#make unit vector perpendicular to v and n1
n2 = np.cross(v, n1)
#surface ranges over t from 0 to length of axis and 0 to 2*pi
t = np.linspace(0, mag, 200)
theta = np.linspace(0, 2 * np.pi, 100)
#use meshgrid to make 2d arrays
t, theta = np.meshgrid(t, theta)
#generate coordinates for surface
X, Y, Z = [p0[i] + v[i] * t + R * np.sin(theta) * n1[i] + R * np.cos(theta) * n2[i] for i in [0, 1, 2]]
col = []
for i in range(10):
col.append([])
for i in range(10):
for j in range(20):
col[i].append(plt.cm.Blues(0.4))
ax.plot_surface(X, Y, Z,facecolors = col,edgecolor = "red")
#plot axis
ax.plot(*zip(p0, p1), color = 'red')
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_zlim(0, 10)
plt.axis('off')
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
plt.show()
Your Z array is of size 100x200, yet you are only specifying 10x20 colors. A quicker way to make col (with the right dimensions) might be something like:
col1 = plt.cm.Blues(np.linspace(0,1,200)) # linear gradient along the t-axis
col1 = np.repeat(col1[np.newaxis,:, :], 100, axis=0) # expand over the theta-axis
col2 = plt.cm.Blues(np.linspace(0,1,100)) # linear gradient along the theta-axis
col2 = np.repeat(col2[:, np.newaxis, :], 200, axis=1) # expand over the t-axis
ax=plt.subplot(121, projection='3d')
ax.plot_surface(X, Y, Z, facecolors=col1)
ax=plt.subplot(122, projection='3d')
ax.plot_surface(X, Y, Z, facecolors=col2)
Which produces: