Stratigraphic column in matplotlib - matplotlib

My goal is to create a stratigraphic column (colored stacked rectangles) using matplotlib like the example below.
Data is in this format:
depth = [1,2,3,4,5,6,7,8,9,10] #depth (feet) below ground surface
lithotype = [4,4,4,5,5,5,6,6,6,2] #lithology type. 4 = clay, 6 = sand, 2 = silt
I tried matplotlib.patches.Rectangle but it's cumbersome. Wondering if someone has another suggestion.

Imho using Rectangle is not so difficult nor cumbersome.
from numpy import ones
from matplotlib.pyplot import show, subplots
from matplotlib.cm import get_cmap
from matplotlib.patches import Rectangle as r
# a simplification is to use, for the lithology types, a qualitative colormap
# here I use Paired, but other qualitative colormaps are displayed in
# https://matplotlib.org/stable/tutorials/colors/colormaps.html#qualitative
qcm = get_cmap('Paired')
# the data, augmented with type descriptions
# note that depths start from zero
depth = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # depth (feet) below ground surface
lithotype = [4, 4, 4, 5, 5, 5, 6, 1, 6, 2] # lithology type.
types = {1:'swiss cheese', 2:'silt', 4:'clay', 5:'silty sand', 6:'sand'}
# prepare the figure
fig, ax = subplots(figsize = (4, 8))
w = 2 # a conventional width, used to size the x-axis and the rectangles
ax.set(xlim=(0,2), xticks=[]) # size the x-axis, no x ticks
ax.set_ylim(ymin=0, ymax=depth[-1])
ax.invert_yaxis()
fig.suptitle('Soil Behaviour Type')
fig.subplots_adjust(right=0.5)
# plot a series of dots, that eventually will be covered by the Rectangle\s
# so that we can draw a legend
for lt in set(lithotype):
ax.scatter(lt, depth[1], color=qcm(lt), label=types[lt], zorder=0)
fig.legend(loc='center right')
ax.plot((1,1), (0,depth[-1]), lw=0)
# do the rectangles
for d0, d1, lt in zip(depth, depth[1:], lithotype):
ax.add_patch(
r( (0, d0), # coordinates of upper left corner
2, d1-d0, # conventional width on x, thickness of the layer
facecolor=qcm(lt), edgecolor='k'))
# That's all, folks!
show()
As you can see, placing the rectangles is not complicated, what is indeed cumbersome is to properly prepare the Figure and the Axes.
I know that I omitted part of the qualifying details from my solution, but I hope these omissions won't stop you from profiting from my answer.

I made a package called striplog for handling this sort of data and making these kinds of plots.
The tool can read CSV, LAS, and other formats directly (if the format is rather particular), but we can also construct a Striplog object manually. First let's set up the basic data:
depth = [1,2,3,4,5,6,7,8,9,10]
lithotype = [4,4,4,5,5,5,6,6,6,2]
KEY = {2: 'silt', 4: 'clay', 5: 'mud', 6: 'sand'}
Now you need to know that a Striplog is composed of Interval objects, each of which can have one or more Component elements:
from striplog import Striplog, Component, Interval
intervals = []
for top, base, lith in zip(depth, depth[1:], lithotype):
comp = Component({'lithology': KEY[lith]})
iv = Interval(top, base, components=[comp])
intervals.append(iv)
s = Striplog(intervals).merge_neighbours() # Merge like with like.
This results in Striplog(3 Intervals, start=1.0, stop=10.0). Now we'd like to make a plot using an appropriate Legend object.
from striplog import Legend
legend_csv = u"""colour, width, component lithology
#F7E9A6, 3, Sand
#A68374, 2.5, Silt
#99994A, 2, Mud
#666666, 1, Clay"""
legend = Legend.from_csv(text=legend_csv)
s.plot(legend=legend, aspect=2, label='lithology')
Which gives:
Admittedly the plotting is a little limited, but it's just matplotlib so you can always add more code. To be honest, if I were to build this tool today, I think I'd probably leave the plotting out entirely; it's often easier for the user to do their own thing.
Why go to all this trouble? Fair question. striplog lets you merge zones, make thickness or lithology histograms, make queries ("show me sandstone beds thicker than 2 m"), make 'flags', export LAS or CSV, and even do Markov chain sequence analysis. But even if it's not what you're looking for, maybe you can recycle some of the plotting code! Good luck.

Related

Creating a colorbar in matplotlib given the code below

I'm having a really hard time creating a colourbar given the code below. I had the idea of colouring the lines one by one as my code adds the plot lines for each cycle iteration. What matters is that for each main cycle iteration (i in stim_strength_list) a new line is added for each of the subplots. And the colour values for such line get continuously updated.
Now the issue of such a code is that I'm having an extremely hard time creating a colourbar sitting to the right of all the subplot. Most importantly because I have no ScalarMappable object that can be fed into the plt.colorbar() function. And I don't know of any other function that could get the job done properly.
I see a lot of tutorials on-line where no parameters are given at all to the function and everything works just fine but I don't know why it isn't the case for me. I just began programming recently so I'm really sure that there is something really simple flyin over my head, but despite having searched all over stack overflow I haven't managed to find anything that could fix my specific problem.
stim_strength_list = [int(x)*10 for x in range(0,81)]
stp_list = [int(x) for x in range(51,71)]
area_list = ['V1', 'TO', 'AT', 'PFL', 'PML', 'M1L', 'A1', 'AB', 'PB', 'PFI', 'PMI', 'M1I']
# CREATE THE FIGURE
fig, axis = plt.subplots(2, 6, sharey=True, sharex=True, squeeze=True, figsize=(30, 22.5))
fig.suptitle("Activation plots for each area at different levels of strength, spiking architecture", fontsize=25.0, y=0.99)
# COLOURS
red = 0.0
blue = 1.0
for i in stim_strength_list:
# CREATING VALUES TO BE PLOTTED
a = df_s.drop(df_s[df_s['stim_strength'] != i].index)
b = a.groupby('stp').mean()
for j in area_list: # here a create an empty list for each of the 12 areas
globals()[j]=[] # i.e.: V1 = [], TO = [], AT = [], ...
for j in stp_list: # here I cycle through the time steps..
for k in area_list:
globals()[k].append(b.loc[j, k]) # to append values to each area called upon
# PLOTTING
for row in range(0, 2): # given the plt.subplots() methods refers to the subplots via a matrix coordinate system...
for col in range (0, 6): # ...I use this nested loop to plot values in each list in its respective subplot
if row == 0:
axis[row, col].plot(stp_list, globals()[area_list[col]], color=(red, 0, blue), linewidth=1)
else: # about the col + 6 indexing, if I'm in the second row of the subplots I just add 6 to the current value of col to fetch the appropriate brain area this is just
axis[row, col].plot(stp_list, globals()[area_list[col + 6]], color=(red, 0, blue), linewidth=1)
blue -= 1/len(stim_strength_list) # this is to make it that the first line is fully blue...
red += 1/len(stim_strength_list) # ... and the final one is fully red
# TITLING
for row in range(0, 2): #same logic as above, I did an external nested loop simply because the titling would be done for len(stp_list)*12 times
for col in range (0, 6):
if row == 0:
axis[row, col].set_title(area_list[col], fontsize=25)
else:
axis[row, col].set_title(area_list[col + 6], fontsize=25)
# LABELLING
axis[0, 0].set_ylabel('Amount of cells', fontsize=20)
axis[1, 0].set_ylabel('Amount of cells', fontsize=20)
for i in range(0, 6):
axis[1, i].set_xlabel('Time step', fontsize=20)
fig.tight_layout()
plt.show()
This is the code which creates everything. What is missing is the dataframe from which data is taken but i think that you can get an idea by looking at the result.
Final plot
What I would love to achieve is to have a colorbar which displays the colors employed to color the different lines within each subplot. Such transition is determined by progressively increasing and decreasing the red and blue values within the color attribute in .plot() method.
If I just add plt.colorbar() I will get the following error:
Traceback (most recent call last):
  at block x, line y
  at /opt/python/envs/default/lib/python3.8/site-packages/matplotlib/pyplot.py, line 2084, in colorbar(mappable, cax, ax, **kw)
RuntimeError: No mappable was found to use for colorbar creation. First define a mappable such as an image (with imshow) or a contour set (with contourf).
What would you do with the code available to achieve the result I described above?

How to create volume from point cloud in spherical coordinates?

I have two sets of discrete points in spherical coordinates, each representing top and bottom surfaces of an object.
I am trying to create volume from these points to separate points which lies inside and outside the object. Any suggestions where to look or which library to use?
Blue and red points represents top and bottom surfaces. Red points are generated by shifting top surface radially downwards with some constant radius.
If I am right, the blue and red surfaces are meshed (and watertight). So for every point you can draw the line from the sphere center and look for intersections with the mesh. This is done by finding the two triangles such that the line pierces them (this can be done by looking at the angular coordinates only, using a point-in-triangle formula), then finding the intersection points. Then it is an easy matter to classify the point as before the red surface, after the blue or in between.
Exhaustive search for the triangles can be costly. You can speed it up for instance using a hierarchy of bounding boxes or similar device.
Here is a custom tinkered method which may works at the condition that the average distance between points in the original surface is much smaller than the thickness of the volume and than the irregularities on the surface contour. In other words, that there are a lot of points describing the blue surfaces.
import matplotlib.pylab as plt
import numpy as np
from scipy.spatial import KDTree
# Generate a test surface:
theta = np.linspace(3, 1, 38)
phi = np.zeros_like(theta)
r = 1 + 0.1*np.sin(8*theta)
surface_points = np.stack((r, theta, phi), axis=1) # n x 3 array
# Generate test points:
x_span, y_span = np.linspace(-1, 0.7, 26), np.linspace(0.1, 1.2, 22)
x_grid, y_grid = np.meshgrid(x_span, y_span)
r_test = np.sqrt(x_grid**2 + y_grid**2).ravel()
theta_test = np.arctan2(y_grid, x_grid).ravel()
phi_test = np.zeros_like(theta_test)
test_points = np.stack((r_test, theta_test, phi_test), axis=1) # n x 3 array
# Determine if the test points are in the volume:
volume_thickness = 0.2 # Distance between the two surfaces
angle_threshold = 0.05 # Angular threshold to determine for a point
# if the line from the origin to the point
# go through the surface
# Get the nearest point: (replace the interpolation)
get_nearest_points = KDTree(surface_points[:, 1:]) # keep only the angles
# This is based on the cartesian distance,
# and therefore not enterily valid for the angle between points on a sphere
# It could be better to project the points on a unit shpere, and convert
# all coordinates in cartesian frame in order to do the nearest point seach...
distance, idx = get_nearest_points.query(test_points[:, 1:])
go_through = distance < angle_threshold
nearest_surface_radius = surface_points[idx, 0]
is_in_volume = (go_through) & (nearest_surface_radius > test_points[:, 0]) \
& (nearest_surface_radius - volume_thickness < test_points[:, 0])
not_in_volume = np.logical_not(is_in_volume)
# Graph;
plt.figure(figsize=(10, 7))
plt.polar(test_points[is_in_volume, 1], test_points[is_in_volume, 0], '.r',
label='in volume');
plt.polar(test_points[not_in_volume, 1], test_points[not_in_volume, 0], '.k',
label='not in volume', alpha=0.2);
plt.polar(test_points[go_through, 1], test_points[go_through, 0], '.g',
label='go through', alpha=0.2);
plt.polar(surface_points[:, 1], surface_points[:, 0], '.b',
label='surface');
plt.xlim([0, np.pi]); plt.grid(False);plt.legend();
The result graph, for 2D case, is:
The idea is to look for each test point the nearest point in the surface, by considering only the direction and not the radius. Once this "same direction" point is found, it's possible to test both if the point is inside the volume along the radial direction (volume_thickness), and close enough to the surface using the parameter angle_threshold.
I think it would be better to mesh (non-convex) the blue surface and perform a proper interpolation, but I don't know Scipy method for this.

Annotate a data point with a graph

For the lack of better term, is there a way to annotate a data point with a graph? I include an example of what I am for below
Big black data point with a graph corresponding to it. Note that graph is rotated so its "x" axis (not shown) is perpendicular to the "y" axis of the scatter plot
annotation_box http://matplotlib.org/examples/pylab_examples/demo_annotation_box.html is the closest thing I can find at the moment, but even knowing the proper term for what I want to do, would make my life easier.
If I understood the problem correctly, what you need are floating axes that you can place as annotations over your plot. Unfortunately, this is not easily possible in matplotlib, as far I know.
An easy solution would be to just plot the points and graphs in the same axis, with the graphs scaled down and shifted close to the points.
import numpy as np
import scipy.stats as sps
import matplotlib.pyplot as plt
xp = [5, 1, 3]
yp = [2, 1, 4]
# just generate some curves
curves_x = np.array([np.linspace(0, 10, 100)] * 3)
curves_y = sps.gamma.pdf(curves_x[0], [[2], [5], [7]], 1)
plt.scatter(xp, yp, s=50)
for x, y, cx, cy in zip(xp, yp, curves_x, curves_y):
plt.plot(x + cy / np.max(cy) + 0.1 , y + cx / np.max(cx) - 0.5)
plt.show()
This is a very simplistic example. The numbers will have to be tuned to look nice with varying scale of the data.

How do I change the color of the axes of a matplotlib 3D plot?

I have set
import matplotlib as mpl
AXES_COLOR = '#333333'
mpl.rc('axes', edgecolor=AXES_COLOR, labelcolor=AXES_COLOR, grid=True)
mpl.rc('xtick', color=AXES_COLOR)
mpl.rc('ytick', color=AXES_COLOR)
mpl.rc('grid', color=AXES_COLOR)
The color of the axes labels and the ticks are properly set both in 2D and in 3D. However, the edgecolor doesn't apply to 3D axes and they remain black. Likewise, the grid isn't affected.
I think figured out how to access the individual axes of a 3D plot:
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d # Needed for 3d projection.
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.w_zaxis # <- the z axis
The documentation mentions a property that we can use until the developers have finished refactoring their 3D code:
import pprint
pprint.pprint(ax.w_xaxis._AXINFO)
{'x': {'color': (0.95, 0.95, 0.95, 0.5),
'i': 0,
'juggled': (1, 0, 2),
'tickdir': 1},
'y': {'color': (0.9, 0.9, 0.9, 0.5),
'i': 1,
'juggled': (0, 1, 2),
'tickdir': 0},
'z': {'color': (0.925, 0.925, 0.925, 0.5),
'i': 2,
'juggled': (0, 2, 1),
'tickdir': 0}}
However, the color parameter changes the color of the background of the axes planes (between the wired of the grid), not the color of the edges of these planes.
Am I digging too deep ?
Instead of changing axis3d.py try this: ax.w_xaxis.line.set_color("red")
Turns out it's impossible since these values are hard-coded. This archived email from the matplotlib-users mailing list helped me. Here's the relevant part:
Unfortunately, you have stumbled upon one of the ugliness of the mplot3d
implementation. I am hoping to have more control available for the next
release. But right now, there is no way to turn off the axes spines
(because they aren't implemented as spines). If you really want to dig into
the source code, you could change the color argument to the Line2D call in
the init3d() method in matplotlib/lib/mpl_toolkits/axis3d.py
Although this answer was addressing another concern, it sent me to the direction of axis3d.py. I found it in /usr/lib/pymodules/python2.7/mpl_toolkits/mplot3d. I made a backup of the original axis3d.py and I moved axis3d.pyc away.
Since the code is pretty short and fairly well written it didn't take long to locate the two lines I had to change.
To change the color of the edges of the individual axes, I modified the self.line=... in __init__: just replace color=(0, 0, 0, 1) by color=(1, 0, 0, 1) for a horribly flashy red. Components of the tuple are red, green, blue, alpha, all floats from 0 to 1.
To change the color of the grid, I modified the draw method. I replaced the color self.gridlines.set_color([(0.9,0.9,0.9,1)] * len(lines)) by something of my choosing.
And that's it, it just works. Not the most convenient, but it's not more work than editing a rc configuration file.
I did not recreate a .pyc file. It does not recreate itself because I do not run my python code as root. I don't mind the extra milliseconds that python needs to recompile the .py each time.

How can I plot points so they appear over top of the spines with matplotlib?

The following generates a plot with three data points, at (0, 0), (0, 0.5), and (1, 1). Only that portion of the plotted points (small circles) which lie inside the plot area is visible, so I see quarter-circles in the corners, and a half circle along the left spine.
Is there a trick I can use to make all the points fully visible, so they are not clipped within the axes frame?
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
fig = Figure()
canvas = FigureCanvas(fig)
ax = fig.add_subplot(111)
ax.plot([0, 0, 1], [0, 0.5, 1], 'o')
fig.canvas.print_figure('test.png')
Edit: Amro's suggestion -- the obvious approach -- is not the preferred approach as these are for ROC graphs (conventionally drawn with a box from 0 to 1 on both axes). If I could trick matplotlib into producing results similar to the many at http://www.google.com/search?q=roc+plot which have a box tightly around 0..1 on both axes, yet have points drawn on top of the axis lines as many of them do, that would be optimal.
Edit 2: I'm guessing this can be done using "spine placement" (new as of MPL 0.99), with the plot area enlarged slightly as Amro suggested, but then with the spines repositioned slightly to be along both 0 axes. I'll experiment with this and post an answer if it works, though feel free to beat me to it.
You can turn the clipping off, either in the plot command, or in the artist objects returned by a call to "plot".
First, here's the figure, with extra big symbols so it's clear:
In the plot command you can do
ax.plot([0, 0, 1], [0, 0.5, 1], 'o', clip_on=False, markersize=20)
or you could have
p = ax.plot([0, 0, 1], [0, 0.5, 1], 'o', markersize=20)
for m in p:
m.set_clip_on(False)
You can extend the axes limits a bit in all directions:
ax = fig.add_subplot(111, xlim=(-0.1,1.1), ylim=(-0.1,1.1))
I combined my idea using the new spine.set_position() capability with Amro's suggestion to expand the bounds slightly. The following works with only with matplotlib 1.0 or later, as it relies on the new spine.set_bounds() call. (I believe Amro's idea needed 1.0 or later as well, since the xlim/ylim kwargs did nothing for me with 0.99.1.)
fig = Figure()
canvas = FigureCanvas(fig)
ax = fig.add_subplot(111, xlim=(-0.1, 1.1), ylim=(-0.1, 1.1))
ax.plot([0, 0, 1], [0, 0.5, 1], 'o')
for side in 'left bottom top right'.split():
ax.spines[side].set_position('zero')
ax.spines[side].set_bounds(0, 1)
canvas.print_figure('test.png')
I'd still be quite interested to hear if there's a different approach, but my guess after much googling is that matplotlib has a basic restriction around this area: all data is tightly clipped by the region defined for the axis.