ari
ari

Reputation: 4523

Annotating ranges of data

How can I annotate a range of my data? E.g., say the data from x = 5 to x = 10 is larger than some cut-off, how could I indicate that on the graph. If I was annotating by hand, I would just draw a large bracket above the range and write my annotation above the bracket.

The closest I've seen is using arrowstyle='<->' and connectionstyle='bar', to make two arrows pointing to the edges of your data with a line connecting their tails. But that doesn't quite do the right thing; the text that you enter for the annotation will end up under one of the arrows, rather than above the bar.

Here is my attempt, along with it's results:

annotate(' ', xy=(1,.5),  xycoords='data',
            xytext=(190, .5), textcoords='data',
            arrowprops=dict(arrowstyle="<->",
                            connectionstyle="bar",
                            ec="k",
                            shrinkA=5, shrinkB=5,
                            )
            )

Annotation attempt

Another problem with my attempted solution is that the squared shape of the annotating bracket does not really make it clear that I am highlighting a range (unlike, e.g., a curly brace). But I suppose that's just being nitpicky at this point.

Upvotes: 14

Views: 8175

Answers (6)

premes
premes

Reputation: 381

I updated the previous answers to have some of the features I wanted, like an option for a vertical brace, that I wanted to place in multi-plot figures. One still has to futz with the beta_scale parameter sometimes depending on the scale of the data that one is applying this to.

enter image description here

def rotate_point(x, y, angle_rad):
    cos,sin = np.cos(angle_rad),np.sin(angle_rad)
    return cos*x-sin*y,sin*x+cos*y

def draw_brace(ax, span, position, text, text_pos, brace_scale=1.0, beta_scale=300., rotate=False, rotate_text=False):
    '''
        all positions and sizes are in axes units
        span: size of the curl
        position: placement of the tip of the curl
        text: label to place somewhere
        text_pos: position for the label
        beta_scale: scaling for the curl, higher makes a smaller radius
        rotate: true rotates to place the curl vertically
        rotate_text: true rotates the text vertically        
    '''
    # get the total width to help scale the figure
    ax_xmin, ax_xmax = ax.get_xlim()
    xax_span = ax_xmax - ax_xmin
    resolution = int(span/xax_span*100)*2+1 # guaranteed uneven
    beta = beta_scale/xax_span # the higher this is, the smaller the radius
    # center the shape at (0, 0)
    x = np.linspace(-span/2., span/2., resolution)
    # calculate the shape
    x_half = x[:int(resolution/2)+1]
    y_half_brace = (1/(1.+np.exp(-beta*(x_half-x_half[0])))
                + 1/(1.+np.exp(-beta*(x_half-x_half[-1]))))
    y = np.concatenate((y_half_brace, y_half_brace[-2::-1]))
    # put the tip of the curl at (0, 0)
    max_y = np.max(y)    
    min_y = np.min(y)
    y /= (max_y-min_y)
    y *= brace_scale
    y -= max_y
    # rotate the trace before shifting
    if rotate:
        x,y = rotate_point(x, y, np.pi/2)
    # shift to the user's spot   
    x += position[0]        
    y += position[1]
    ax.autoscale(False)
    ax.plot(x, y, color='black', lw=1, clip_on=False)
    # put the text
    ax.text(text_pos[0], text_pos[1], text, ha='center', va='bottom', rotation=90 if rotate_text else 0) 

Upvotes: 0

Nikaido
Nikaido

Reputation: 4629

a minor modification of the draw_brace of @Joooeey and @guezy to have also the brace upside down

+argument upsidedown

def draw_brace(ax, xspan, yy, text, upsidedown=False):
    """Draws an annotated brace on the axes."""
    # shamelessly copied from https://stackoverflow.com/questions/18386210/annotating-ranges-of-data-in-matplotlib
    xmin, xmax = xspan
    xspan = xmax - xmin
    ax_xmin, ax_xmax = ax.get_xlim()
    xax_span = ax_xmax - ax_xmin

    ymin, ymax = ax.get_ylim()
    yspan = ymax - ymin
    resolution = int(xspan/xax_span*100)*2+1 # guaranteed uneven
    beta = 300./xax_span # the higher this is, the smaller the radius

    x = np.linspace(xmin, xmax, resolution)
    x_half = x[:int(resolution/2)+1]
    y_half_brace = (1/(1.+np.exp(-beta*(x_half-x_half[0])))
                    + 1/(1.+np.exp(-beta*(x_half-x_half[-1]))))
    if upsidedown:
        y = np.concatenate((y_half_brace[-2::-1], y_half_brace))
    else:
        y = np.concatenate((y_half_brace, y_half_brace[-2::-1]))
    y = yy + (.05*y - .01)*yspan # adjust vertical position

    ax.autoscale(False)
    line = ax.plot(x, y, color='black', lw=1)

    if upsidedown:
        text = ax.text((xmax+xmin)/2., yy+-.07*yspan, text, ha='center', va='bottom',fontsize=7)
    else:
        text = ax.text((xmax+xmin)/2., yy+.07*yspan, text, ha='center', va='bottom',fontsize=7)
    return line, text

Upvotes: 1

GB Arivazhagan
GB Arivazhagan

Reputation: 51

Here is a minor modification to guzey and jooeey's answer to plot the flower braces outside the axes.

def draw_brace(ax, xspan, yy, text):
"""Draws an annotated brace outside the axes."""
    xmin, xmax = xspan
    xspan = xmax - xmin
    ax_xmin, ax_xmax = ax.get_xlim()
    xax_span = ax_xmax - ax_xmin

    ymin, ymax = ax.get_ylim()
    yspan = ymax - ymin
    resolution = int(xspan/xax_span*100)*2+1 # guaranteed uneven
    beta = 300./xax_span # the higher this is, the smaller the radius

    x = np.linspace(xmin, xmax, resolution)
    x_half = x[:int(resolution/2)+1]
    y_half_brace = (1/(1.+np.exp(-beta*(x_half-x_half[0])))
                + 1/(1.+np.exp(-beta*(x_half-x_half[-1]))))
    y = np.concatenate((y_half_brace, y_half_brace[-2::-1]))
    y = yy + (.05*y - .01)*yspan # adjust vertical position

    ax.autoscale(False)
    ax.plot(x, -y, color='black', lw=1, clip_on=False)

    ax.text((xmax+xmin)/2., -yy-.17*yspan, text, ha='center', va='bottom')

    
# Sample code
fmax = 1
fstart = -100
fend = 0
frise = 50
ffall = 20

def S(x):
   if x<=0:
       return 0
   elif x>=1:
       return 1
   else:
       return 1/(1+np.exp((1/(x-1))+(1/x)))

x = np.linspace(700,1000,500)
lam = [fmax*(S((i-880)/60)-S(((i-1000)/25)+1)) for i in x]
fig = plt.figure(1)
ax = fig.add_subplot(111)
plt.plot(x,lam)
plt.xlim([850,1000])
ax.set_aspect(50,adjustable='box')
plt.ylabel('$\lambda$')
plt.xlabel('$x$')
ax.xaxis.set_label_coords(0.5, -0.35)
draw_brace(ax, (900,950),0.2, 'rise')
draw_brace(ax, (980,1000),0.2, 'fall')
plt.text(822,0.95,'$(\lambda_{\mathrm{max}})$')

Sample output

Upvotes: 5

Joooeey
Joooeey

Reputation: 3886

As mentioned in this answer, you can construct curly brackets with sigmoidal functions. Below is a function that adds curly brackets just above the x-axis. The curly brackets it produces should look the same regardless of the axes limits, as long as the figure width and height don't vary.

import numpy as np
import matplotlib.pyplot as plt

def draw_brace(ax, xspan, text):
    """Draws an annotated brace on the axes."""
    xmin, xmax = xspan
    xspan = xmax - xmin
    ax_xmin, ax_xmax = ax.get_xlim()
    xax_span = ax_xmax - ax_xmin
    ymin, ymax = ax.get_ylim()
    yspan = ymax - ymin
    resolution = int(xspan/xax_span*100)*2+1 # guaranteed uneven
    beta = 300./xax_span # the higher this is, the smaller the radius

    x = np.linspace(xmin, xmax, resolution)
    x_half = x[:resolution//2+1]
    y_half_brace = (1/(1.+np.exp(-beta*(x_half-x_half[0])))
                    + 1/(1.+np.exp(-beta*(x_half-x_half[-1]))))
    y = np.concatenate((y_half_brace, y_half_brace[-2::-1]))
    y = ymin + (.05*y - .01)*yspan # adjust vertical position

    ax.autoscale(False)
    ax.plot(x, y, color='black', lw=1)

    ax.text((xmax+xmin)/2., ymin+.07*yspan, text, ha='center', va='bottom')

ax = plt.gca()
ax.plot(range(10))
draw_brace(ax, (0, 8), 'large brace')
draw_brace(ax, (8, 9), 'small brace')

Output:

enter image description here

Upvotes: 11

guzey
guzey

Reputation: 131

I modified Joooeey's answer to allow to change the vertical position of braces:

def draw_brace(ax, xspan, yy, text):
    """Draws an annotated brace on the axes."""
    xmin, xmax = xspan
    xspan = xmax - xmin
    ax_xmin, ax_xmax = ax.get_xlim()
    xax_span = ax_xmax - ax_xmin

    ymin, ymax = ax.get_ylim()
    yspan = ymax - ymin
    resolution = int(xspan/xax_span*100)*2+1 # guaranteed uneven
    beta = 300./xax_span # the higher this is, the smaller the radius

    x = np.linspace(xmin, xmax, resolution)
    x_half = x[:int(resolution/2)+1]
    y_half_brace = (1/(1.+np.exp(-beta*(x_half-x_half[0])))
                    + 1/(1.+np.exp(-beta*(x_half-x_half[-1]))))
    y = np.concatenate((y_half_brace, y_half_brace[-2::-1]))
    y = yy + (.05*y - .01)*yspan # adjust vertical position

    ax.autoscale(False)
    ax.plot(x, y, color='black', lw=1)

    ax.text((xmax+xmin)/2., yy+.07*yspan, text, ha='center', va='bottom')
ax = plt.gca()
ax.plot(range(10))
draw_brace(ax, (0, 8), -0.5, 'large brace')
draw_brace(ax, (8, 9), 3, 'small brace')

Output:

matplotlib braces

Also note that in Joooeey's answer, line

x_half = x[:resolution/2+1]

should be

x_half = x[:int(resolution/2)+1]

Otherwise, the number that the script tries to use as index here is a float.

Finally, note that right now the brace will not show up if you move it out of bounds. You need to add parameter clip_on=False, like this:

ax.plot(x, y, color='black', lw=1, clip_on=False)

Upvotes: 13

tacaswell
tacaswell

Reputation: 87486

You can just wrap it all up in a function:

def add_range_annotation(ax, start, end, txt_str, y_height=.5, txt_kwargs=None, arrow_kwargs=None):
    """
    Adds horizontal arrow annotation with text in the middle

    Parameters
    ----------
    ax : matplotlib.Axes
        The axes to draw to

    start : float
        start of line

    end : float
        end of line

    txt_str : string
        The text to add

    y_height : float
        The height of the line

    txt_kwargs : dict or None
        Extra kwargs to pass to the text

    arrow_kwargs : dict or None
        Extra kwargs to pass to the annotate

    Returns
    -------
    tuple
        (annotation, text)
    """

    if txt_kwargs is None:
        txt_kwargs = {}
    if arrow_kwargs is None:
        # default to your arrowprops
        arrow_kwargs = {'arrowprops':dict(arrowstyle="<->",
                            connectionstyle="bar",
                            ec="k",
                            shrinkA=5, shrinkB=5,
                            )}

    trans = ax.get_xaxis_transform()

    ann = ax.annotate('', xy=(start, y_height),
                        xytext=(end, y_height),
                        transform=trans,
                        **arrow_kwargs)
    txt = ax.text((start + end) / 2,
                  y_height + .05,
                  txt_str,
                  **txt_kwargs)


    if plt.isinteractive():
        plt.draw()
    return ann, txt

Alternately,

start, end = .6, .8
ax.axvspan(start, end, alpha=.2, color='r')
trans = ax.get_xaxis_transform()
ax.text((start + end) / 2, .5, 'test', transform=trans)

Upvotes: 6

Related Questions