N8_Coder
N8_Coder

Reputation: 803

White to Transparent Layer in matplotlib figure (SVG)

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:
Before
After:
After

Upvotes: 2

Views: 1032

Answers (3)

nferreira78
nferreira78

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

azelcer
azelcer

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:

Final 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

Domarm
Domarm

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

Related Questions