3
Current matplotlib graph How it should look like
enter image description here enter image description here

I want to change the graph color and gradient direction in the parts where graph goes below zero. Alternative image for illustration: enter image description here

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: enter image description here

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.
Ishan Jindal
  • 198
  • 1
  • 2
  • 18
  • Please provide an [mre](https://stackoverflow.com/help/minimal-reproducible-example) – tomjn Jul 22 '22 at 08:20
  • see https://stackoverflow.com/a/68004084/3944322, using a diverging colormap, e.g. `'bwr'`. – Stef Jul 22 '22 at 10:40

1 Answers1

2

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.

gradient negative parts matplotlib style cyberpunk

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()
Leonard
  • 2,510
  • 18
  • 37