Adding a Rectangle Patch and Text Patch to 3D Collection in Matplotlib - matplotlib

Problem Statement
I'm attempting to add two patches -- a rectangle patch and a text patch -- to the same space within a 3D plot. The ultimate goal is to annotate the rectangle patch with a corresponding value (about 20 rectangles across 4 planes -- see Figure 3). The following code does not get all the way there, but does demonstrate a rendering issue where sometimes the text patch is completely visible and sometimes it isn't -- interestingly, if the string doesn't extend outside the rectangle patch, it never seems to become visible at all. The only difference between Figures 1 and 2 is the rotation of the plot viewer image. I've left the cmap code in the example below because it's a requirement of the project (and just in case it affects the outcome).
Things I've Tried
Reversing the order that the patches are drawn.
Applying zorder values -- I think art3d.pathpatch_2d_to_3d is overriding that.
Creating a patch collection -- I can't seem to find a way to add the rectangle patch and the text patch to the same 3D collection.
Conclusion
I suspect that setting zorder to each patch before adding them to a 3D collection may be the solution, but I can't seem to find a way to get to that outcome. Similar questions suggest this, but I haven't been able to apply their answers to this problem specifically.
Environment
macOS: Big Sur 11.2.3
Python 3.8
Matplotlib 3.3.4
Figure 1
Figure 2
Figure 3
The Code
Generates Figures 1 and 2 (not 3).
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
from matplotlib.patches import Rectangle, PathPatch
from matplotlib.text import TextPath
from matplotlib.transforms import Affine2D
import mpl_toolkits.mplot3d.art3d as art3d
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
plt.style.use('dark_background')
fig = plt.figure()
ax = fig.gca(projection='3d')
cmap = plt.cm.bwr
norm = Normalize(vmin=50, vmax=80)
base_color = cmap(norm(50))
# Draw box
box = Rectangle((25, 25), width=50, height=50, color=cmap(norm(62)), ec='black', alpha=1)
ax.add_patch(box)
art3d.pathpatch_2d_to_3d(box, z=1, zdir="z")
# Draw text
text_path = TextPath((60, 50), "xxxx", size=10)
trans = Affine2D().rotate(0).translate(0, 1)
p1 = PathPatch(trans.transform_path(text_path))
ax.add_patch(p1)
art3d.pathpatch_2d_to_3d(p1, z=1, zdir="z")
ax.set_xlabel('x')
ax.set_xlim(0, 100)
ax.set_xticklabels([])
ax.xaxis.set_pane_color(base_color)
ax.set_ylabel('y')
ax.set_ylim(0, 100)
ax.set_yticklabels([])
ax.yaxis.set_pane_color(base_color)
ax.set_zlabel('z')
ax.set_zlim(1, 4)
ax.set_zticks([1, 2, 3, 4])
ax.zaxis.set_pane_color(base_color)
ax.set_zticklabels([])
plt.show()

This is a well-known problem with matplotlib 3D plotting: objects are drawn in a particular order, and those plotted last appear on "top" of the others, regardless of which should be in front in a "true" 3D plot.
See the FAQ here: https://matplotlib.org/mpl_toolkits/mplot3d/faq.html#my-3d-plot-doesn-t-look-right-at-certain-viewing-angles
My 3D plot doesn’t look right at certain viewing angles
This is probably the most commonly reported issue with mplot3d. The problem is that – from some viewing angles – a 3D object would appear in front of another object, even though it is physically behind it. This can result in plots that do not look “physically correct.”
Unfortunately, while some work is being done to reduce the occurrence of this artifact, it is currently an intractable problem, and can not be fully solved until matplotlib supports 3D graphics rendering at its core.
The problem occurs due to the reduction of 3D data down to 2D + z-order scalar. A single value represents the 3rd dimension for all parts of 3D objects in a collection. Therefore, when the bounding boxes of two collections intersect, it becomes possible for this artifact to occur. Furthermore, the intersection of two 3D objects (such as polygons or patches) can not be rendered properly in matplotlib’s 2D rendering engine.
This problem will likely not be solved until OpenGL support is added to all of the backends (patches are greatly welcomed). Until then, if you need complex 3D scenes, we recommend using MayaVi.

Related

Netgraph dot layout avoiding node labels overlap: howto?

I am generating simple syntactic dependecies trees such as in the following example:
Best layout is "dot" but on the top row things get very crowded. Is there any parameter or other way to avoid node labels overlapping with neighbouring nodes (and ideally edges)?
Maybe some matplotlib parameter to "draw on a very wide canvas"? (sorry totally matplotlib ignorant).
Here is the code I am currently using:
import platform
import matplotlib
import matplotlib.pyplot as plt
import stanza
import networkx as nx
from netgraph import InteractiveGraph
if platform.system() == "Darwin":
matplotlib.use("TkAgg") # MacOSX backend is bugged; use TkAgg
def display_graph(G: nx.DiGraph, title: str):
"""Use netgraph to show the graph"""
nodecolors = nx.get_node_attributes(G, "nodecol")
nodelabels = nx.get_node_attributes(G, "lemma")
nodeshapes = nx.get_node_attributes(G, "nodeshape")
edgelabels = nx.get_edge_attributes(G, "label")
# now plot the graph
plot_instance = InteractiveGraph(
G,
node_layout="dot",
node_labels=nodelabels,
node_color=nodecolors,
node_label_fontdict=dict(size=12),
node_shape=nodeshapes,
node_alpha=0.5, # node transparency
edge_labels=edgelabels,
edge_layout="curved",
arrows=True,
)
plt.suptitle(title, wrap=True)
plt.show()
return
Is there any parameter or other way to avoid node labels overlapping with neighbouring nodes (and ideally edges)?
If you set the parameter node_label_offset to some float, e.g. node_label_offset=0.05, then netgraph will find the position 0.05 away from the node center that is furthest away from all other nodes and edges. However, there is no guarantee that there aren't any label overlaps as the space may simply be too crowded.
Maybe some matplotlib parameter to "draw on a very wide canvas"? (sorry totally matplotlib ignorant).
The width and height of the canvas are controlled by the shape parameter. The default is shape=(1, 1). So shape=(2, 2) would double the size of the canvas.
However, as fontsizes in matplotlib (and hence netgraph) are defined in display units, you would also have to increase the size of the matplotlib figure to decrease the size of the text relative to the whole figure.
fig, ax = plt.subplots(figsize=(2*6.4, 2*4.8)) # default is 6.4, 4.8
plot_instance = InteractiveGraph(G, node_layout='dot', shape=(2, 2), ax=ax, ...)
Further suggestions:
Remove the node edges. node_edge_color=nodecolors or node_edge_width=0..
I would set the node alpha to one, as otherwise the underlying edge becomes visible. Use lighter node colors if needed.
Basically, you want to remove any source of contrast that distract from the text if you want the labels to "pop".

GeoViews: Applying matplotlib styling parameters to Polygons elements

Installed packages
holoviews 1.14.4, geoviews 1.9.1., matplotlib 3.4.2.
What I'm trying to do
I am trying to apply simple per-feature styles using GeoViews and the matplolib backend. I cannot figure out how to apply different edgecolor= parameters to different gv.Polygons elements in the same overlay. For some reason, they're always lightblue...
Similarly, facecolor= seems to have no effect.
Reproducible code sample
This uses a very small sample of the full dataset.
import pandas as pd
import geopandas as gpd
import geoviews as gv
from geoviews import opts
# loading both extensions as the full script calls for user input
# to choose between an interactive or static output
gv.extension('bokeh', 'matplotlib')
d1 = {'use': {0: 'Residential', 1: 'Residential'},
'geometry': {0: 'POLYGON ((13.80961103741604 51.04076975651729, 13.80965521888065 51.04079016168103, 13.80963851766593 51.04080454197601, 13.80959433642561 51.04078412781548, 13.80961103741604 51.04076975651729))',
1: 'POLYGON ((13.80977831740752 51.04313480566009, 13.80987122363639 51.04306085051974, 13.8099989591537 51.04312462457182, 13.80995486494384 51.04315973323087, 13.8099651184249 51.04316486464228, 13.80991634926543 51.04320371166482, 13.80977831740752 51.04313480566009))'}}
gdf1 = gpd.GeoDataFrame(pd.DataFrame(d1), geometry=gpd.GeoSeries.from_wkt(pd.DataFrame(d1)['geometry']), crs="EPSG:4326")
d2 = {'geometry': {1: 'POLYGON ((13.80894179055831 51.04544128170094, 13.80952887156242 51.0450399782091, 13.80954152432486 51.04504668985658, 13.80896834397535 51.04545611172818, 13.80894179055831 51.04544128170094))'}}
gdf2 = gpd.GeoDataFrame(pd.DataFrame(d2), geometry=gpd.GeoSeries.from_wkt(pd.DataFrame(d2)['geometry']), crs="EPSG:4326")
layout = gv.Polygons(gdf1, group="group1") * gv.Polygons(gdf2, group="group2")
layout.opts(
opts.Polygons('group1', cmap=['red'], edgecolor='black', linewidth=0.5, xaxis=None, yaxis=None, backend="matplotlib"),
opts.Polygons('group2', cmap=['lightblue'], edgecolor='blue', linewidth=0.5, backend="matplotlib"),
opts.Overlay(fig_size=500, backend='matplotlib')
)
gv.output(layout, backend='matplotlib')
gv.save(layout, "test.svg", dpi=600, backend='matplotlib')
Screenshot of the observed behaviour
This is a screen from the full dataset.
Expected behaviour
The red fill polygons belong to gdf1 and should have a black edgecolor but it's light blue instead. The blue fill polygon belongs to gdf2 and should have a lightblue fill and blue edgecolor, though the same color seems to be applied to both fill and edge.
What I've tried
Instead of using the group= parameter to specify styling for each of the Polygon elements (which I accidentally stumbled upon through the datashader documentation), I tried making multiple opts calls 'in-line' as suggested in the documentation for HoloViews here. This also has no effect.
Also, cmap=['color'] is the only method I've found to work to have GeoViews not use the automatically detected 'use' column in gdf1 as a vdim for color mapping. Is this the canonical approach and/or expected behaviour? color= or facecolor= seems to have no effect even though they are listed when calling gv.help(gv.opts.Polygons).
In short, I don't understand how to apply these particular styling parameters for the matplotlib backend and would very much appreciate any pointers.
2-Aug-21 Edit
Another strange behaviour seems to be that the figure in the, in my case VSCode-Python, interpreter, where the symbology seems to be faithfully represented, looks different from the .svg output generated by gv.save(layout, "test.svg", dpi=600, backend='matlplotlib'). The below images are outputs from the same run of the script.
Interpreter output:
gv.save() output:

Difference between matplotlib.countourf and matlab.contourf() - odd sharp edges in matplotlib

I am a recent migrant from Matlab to Python and have recently worked with Numpy and Matplotlib. I recoded one of my scripts from Matlab, which employs Matlab's contourf-function, into Python using matplotlib's corresponding contourf-function. I managed to replicate the output in Python, apart that the contourf-plots are not exacly the same, for a reason that is unknown to me. As I run the contourf-function in matplotlib, I get this otherwise nice figure but it has these sharp edges on the contour-levels on top and bottom, which should not be there (see Figure 1 below, matplotlib-output). Now, when I export the arrays I used in Python to Matlab (i.e. the exactly same data set that was used to generate the matplotlib-contourf-plot) and use Matlab's contourf-function, I get a slightly different output, without those sharp contour-level edges (see Figure 2 below, Matlab-output). I used the same number of levels in both figures. In figure 3 I have made a scatterplot of the same data, which shows that there are no such sharp edges in the data as shown in the contourf-plot (I added contour-lines just for reference). Example dataset can be downloaded through Dropbox-link given below. The data set contains three txt-files: X, Y, Z. Each of them are an 500x500 arrays, which can be directly used with contourf(), i.e. plt.contourf(X,Y,Z,...). The code that used was
plt.contourf(X,Y,Z,10, cmap=plt.cm.jet)
plt.contour(X,Y,Z,10,colors='black', linewidths=0.5)
plt.axis('equal')
plt.axis('off')
Does anyone have an idea why this happens? I would appreciate any insight on this!
Cheers,
Jussi
Below are the details of my setup:
Python 3.7.0
IPython 6.5.0
matplotlib 2.2.3
Matplotlib output
Matlab output
Matplotlib-scatter
Link to data set
The confusing thing about the matlab plot is that its colorbar shows much more levels than there are actually in the plot. Hence you don't see the actual intervals that are contoured.
You would achieve the same result in matplotlib by choosing 12 instead of 11 levels.
import numpy as np
import matplotlib.pyplot as plt
X, Y, Z = [np.loadtxt("data/roundcontourdata/{}.txt".format(i)) for i in list("XYZ")]
levels = np.linspace(Z.min(), Z.max(), 12)
cntr = plt.contourf(X,Y,Z,levels, cmap=plt.cm.jet)
plt.contour(X,Y,Z,levels,colors='black', linewidths=0.5)
plt.colorbar(cntr)
plt.axis('equal')
plt.axis('off')
plt.show()
So in conclusion, both plots are correct and show the same data. Just the levels being automatically chosen are different. This can be circumvented by choosing custom levels depending on the desired visual appearance.

How do I use colourmaps with variable alpha in a Seaborn kdeplot without seeing the contour lines?

Python version: 3.6.4 (Anaconda on Windows)
Seaborn: 0.8.1
Matplotlib: 2.1.2
I'm trying to create a 2D Kernel Density plot using Seaborn but I want each step in the colourmap to have a different alpha value. I had a look at this question to create a matplotlib colourmap with alpha values: Add alpha to an existing matplotlib colormap.
I have a problem in that the lines between contours are visible. The result I get is here:
I thought that I had found the answer when I found this question: Hide contour linestroke on pyplot.contourf to get only fills. I tried the method outlined in the answer (using set_edgecolor("face") but it did not work in this case. That question also seemed to be related to vector graphics formats and I am just writing out a PNG.
Here is my script:
import numpy as np
import seaborn as sns
import matplotlib.colors as cols
import matplotlib.pyplot as plt
def alpha_cmap(cmap):
my_cmap = cmap(np.arange(cmap.N))
# Set a square root alpha.
x = np.linspace(0, 1, cmap.N)
my_cmap[:,-1] = x ** (0.5)
my_cmap = cols.ListedColormap(my_cmap)
return my_cmap
xs = np.random.uniform(size=100)
ys = np.random.uniform(size=100)
kplot = sns.kdeplot(data=xs, data2=ys,
cmap=alpha_cmap(plt.cm.viridis),
shade=True,
shade_lowest=False,
n_levels=30)
plt.savefig("example_plot.png")
Guided by some comments on this question I have tried some other methods that have been successful when this problem has come up. Based on this question (Matplotlib Contourf Plots Unwanted Outlines when Alpha < 1) I have tried altering the plot call to:
sns.kdeplot(data=xs, data2=ys,
cmap=alpha_cmap(plt.cm.viridis),
shade=True,
shade_lowest=False,
n_levels=30,
antialiased=True)
With antialiased=True the lines between contours are replaced by a narrow white line:
I have also tried an approach similar to this question - Pyplot pcolormesh confused when alpha not 1. This approach is based on looping over the PathCollections in kplot.collections and tuning the parameters of the edges so that they become invisible. I have tried adding this code and tweaking the linewidth -
for thing in kplot.collections:
thing.set_edgecolor("face")
thing.set_linewidth(0.01)
fig.canvas.draw()
This results in a mix of white and dark lines - .
I believe that I will not be able to tune the line width to make the lines disappear because of the variable width of the contour bands.
Using both methods (antialiasing + linewidth) makes this version, which looks cool but isn't quite what I want:
I also found this question - Changing Transparency of/Remove Contour Lines in Matplotlib
This one suggests overplotting a second plot with a different number of contour levels on the same axis, like:
kplot = sns.kdeplot(data=xs, data2=ys,
ax=ax,
cmap=alpha_cmap(plt.cm.viridis),
shade=True,
shade_lowest=False,
n_levels=30,
antialiased=True)
kplot = sns.kdeplot(data=xs, data2=ys,
ax=ax,
cmap=alpha_cmap(plt.cm.viridis),
shade=True,
shade_lowest=False,
n_levels=35,
antialiased=True)
This results in:
This is better, and almost works. The problem here is I need variable (and non-linear) alpha throughout the colourmap. The variable banding and lines seem to be a result of the combinations of alpha when contours are plotted over each other. I also still see some clear/white lines in the result.

How can I draw axes with a 45 degree rotation?

I have a set of 7x4 plots arranged in a grid using subplot. I now want to add diagonal axes on top of these.
I know you can superpose axes on top of previously made subplots by setting the background to 'none':
ax = fig.add_subplot(111)
ax.set_axis_bgcolor('none')
But I can't find a rotated axis thing. Currently I'm trying to use a top view 3D axes, but I'm far from a usable solution there.
I'm willing to accept drawing the axis+ticks by hand, if this is the only way possible.
EDIT: using the floating_axis module, I was able to draw rotated (and sheared) axes, but unable to edit the ticks, which is very necessary for what I need. The following snippet demonstrates adding a floating_axis to an existing figure fig. Any manipulation of the axes' ticks fails.
from matplotlib.transforms import Affine2D
import mpl_toolkits.axisartist.floating_axes as floating_axes
trafo = Affine2D().skew_deg(-10,-10).rotate_deg(45)
grid_helper = floating_axes.GridHelperCurveLinear(trafo, extremes=(0, 4, 0, 4))
artistax = floating_axes.FloatingSubplot(fig, 111, grid_helper=grid_helper)
artistax.set_axis_bgcolor('none')
artistax.axis["top"].set_visible(False)
artistax.axis["right"].set_visible(False)
fig.add_subplot(artistax)