Reputation: 803
I would like to add a layer with a gradient (from white to transparent) on top of my matplotlib image and be able to save the result as an SVG eventually (I have some more shapes on my actual image than sketched below).
It is unclear to me, if this is possible with matplotlib + svg. Does anybody know or has a solution for this?
Here is some example code:
plt.figure(figsize=[6, 6])
x = np.arange(0, 100, 0.00001)
y = np.sin(0.1* np.pi * x)
plt.plot(y)
plt.axis('off')
plt.gca().set_position([0, 0, 1, 1])
# TODO: Add white -> transparent layer
plt.savefig("temp.svg", bbox_inches=0)
The result should look like this:
Before:
After:
Upvotes: 2
Views: 1032
Reputation: 1164
You can adapt your code, to use as the example shown in the matplotlib
documentation
More especifically, you can modify the gradient_bar
function (which also uses the gradient_image
function) as here explained:
def gradient_bar(ax, x, y, width=0.5, bottom=0):
for left, top in zip(x, y):
right = left + width
gradient_image(
ax,
#here you are forcing it to be from top to bottom
extent=(left, right, top, bottom),
#here you are forcing it to be vertical
direction=0,
cmap=plt.cm.Blues,
cmap_range=(0, 1.0))
Upvotes: 1
Reputation: 1533
Here I present two solutions. The first one is fast solution, will bloat (just a little) the SVG size, but will yield better looking results than using a raster image. The second one produces what you'd expect for an SVG with gradients, but it is slower.
First option
As matplotlib
currently does not support gradients (see here), you can mimic a gradient using a not so large number of semitransparent rectangles. You can choose how many rectangles to use, where the transparency ends, and the color of the transparency:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
def add_veil(ax, top, n_steps=20, color='white'):
starts, height = np.linspace(0, top, n_steps, endpoint=False, retstep=True)
rects = [Rectangle((0, s), 1, height, facecolor=color, zorder=2.01,
alpha=(1-(n_a/n_steps)), transform=ax.transAxes)
for n_a, s in enumerate(starts)]
for r in rects:
ax.add_patch(r)
plt.figure(figsize=[6, 6])
x = np.arange(0, 100, 0.00001)
y = np.sin(0.1* np.pi * x)
plt.plot(x, y)
plt.axis('off')
ax = plt.gca()
ax.set_position([0, 0, 1, 1])
# DONE: Add white -> transparent layer
add_veil(ax, 1, 100)
plt.savefig("temp.svg", bbox_inches=0)
Result:
I had very good results with 20 rectangles or less. I guess you can adapt it yo use a non linear gradient, which usually looks better.
Second option The second option saves the SVG to memory, parses the XML, edits it and saves it. It is much slower since it has to parse the data. I made a class, so the data used for defining the gradient is generated only once.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import xml.etree.ElementTree as ET
import matplotlib
from io import BytesIO
class GradientAdder():
def __init__(self):
# First, define a linear gradient.
lingrad = ET.Element('linearGradient')
lingrad.set('id', "grad1")
lingrad.set('x1', "0%")
lingrad.set('y1', "100%")
lingrad.set('x2', "0%")
lingrad.set('y2', "0%")
stop1 = ET.Element('stop')
stop1.set('offset',"0%")
stop1.set('style', "stop-color:rgb(255,255,255);stop-opacity:1")
stop2 = ET.Element('stop')
stop2.set('offset',"100%")
stop2.set('style', "stop-color:rgb(255,255,255);stop-opacity:0")
lingrad.append(stop1)
lingrad.append(stop2)
self._defs = ET.Element('defs')
self._defs.append(lingrad)
self._defs.set('id', 'veilid')
def __call__(self, ax: matplotlib.axes.Axes, out_file: str):
rect = Rectangle((0, 0), 1, 1, zorder=2.01, color=(1,1,1,0),
transform=ax.transAxes)
ax.add_patch(rect)
rect.set_gid("whiteVeil") # We create a named patch
# Save SVG in a fake file object.
f = BytesIO()
ax.figure.savefig(f, format="svg", bbox_inches=0)
# This next line will break someday
ET.register_namespace('', "http://www.w3.org/2000/svg")
root, xmlid = ET.XMLID(f.getvalue())
root.append(self._defs)
rect = xmlid['whiteVeil'][0]
rect.set('style', "fill:url(#grad1);fill-opacity:1") #
ET.ElementTree(root).write(out_file)
g = GradientAdder()
x = np.arange(0, 100, 0.00001)
y = np.sin(0.1* np.pi * x)
plt.figure(figsize=[6, 6])
plt.plot(x, y)
plt.axis('off')
ax = plt.gca()
ax.set_position([0, 0, 1, 1])
g(ax, "/tmp/test.svg")
With this option, ypou have to stick to SVG's linear gradients. Note that it adds a transparent Rectangle
over your figure
Other option
To obtain best results you can add a Rectangle
patch over the Axes
, and then fill it with a gradient using Inkscape in headless mode. See here and here. The documentation is scattered and missing.
Upvotes: 1
Reputation: 2550
To achieve transition effect, You can plot one layer on top of Your existing figure with bicubic interpolation and custom color map.
First You have to create colormap having just two colors.
White color with alpha channel = 0 and other white with alpha channel = 1.
Then we add another figure filled with new color map from top to the bottom.
Here is code that does it:
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=[6, 6])
x = np.arange(0, 100, 0.00001)
y = np.sin(0.1 * np.pi * x)
plt.plot(y)
plt.axis('off')
plt.gca().set_position([0, 0, 1, 1])
# Add new subplot on top of what is there already
ax = fig.add_subplot()
ax.axis('off')
ax.set_position([0, 0, 1, 1])
# Create colormap of two white colors, one with alpha=0 and other one with alpha=1
colors = [(1, 1, 1, 0), (1, 1, 1, 1)]
# Create new colormap from our two colors
cmap = mcolors.LinearSegmentedColormap.from_list('mycmap', colors)
# Use imshow with bicubic interpolation to create transition from top to bottom
ax.imshow([[0., 0.], [1., 1.]],
cmap=cmap, interpolation='bicubic', aspect='auto'
)
# Save as .svg file
plt.savefig("temp.svg", bbox_inches=0)
Upvotes: 1