Skip to content

Annotation

Sevans711 edited this page Sep 3, 2020 · 10 revisions

Annotation

On this page are examples of how QOL.plots can help you annotate plots:

Before running any of the following examples, make sure to do:

import matplotlib.pyplot as plt
import numpy as np
import QOL.plots as pqol

Known Issues with Annotation:

  • These functions to not play well when the default scale is adjusted, e.g. via plt.yscale('log').
    • Workaround: perform scale function on data instead of plot scale, e.g. plt.plot(np.log10(yy)) instead of plt.plot(yy) to have a logarithmic "scale".
    • Resource required to fix issue: knowledge of how to access parameters related to the active plot's scale. (E.g. I am looking for something like: plt.gcf()._yscale but that is not the correct name for it....) If you know how to help, please let me know at sevans7@bu.edu.

Create a horizontal or vertical line and label it with text:

(or, go back to top)
In its simplest form, this is accomplished by:
pqol.hline(y0, "hello") #line at y=y0 [data coords] labeled "hello"
   #or
pqol.vline(x0, "oh hi") #line at x=x0 [data coords] labeled "oh hi"

For more complicated usage, see the images and description below.

## Labeled hline, with a black vline for reference ##
plt.plot(range(10)) #initial plot
pqol.hline(3, "hello, world!", textloc=7, textspec="data") #hline at y=3, text at x=7.
                        #textspec = coord system for textloc. it is "axes" by default. 
pqol.vline(7, color="black") #vline at x=7.
plt.show()

## Labeled vline
plt.plot(range(10)) #initial plot
pqol.vline(3, ">LeftUp")            #default textside and textdirection.
pqol.vline(3, ">LeftDown",  textdirection='down')     #default textside.
pqol.vline(3, ">RightUp",   textside='right')    #default textdirection.
pqol.vline(3, ">RightDown", textside='right', textdirection='down')
plt.show()

There are many more ways to relatively easily customize text/line placement, text properties, line color/style, and more. For further information see the documentation for pqol.hline and pqol.vline, e.g. via help(pqol.hline) and help(pqol.vline).

Known issues: the margin is strange, for text which reads upwards on the right or downwards on the left. For now, the fix is to just edit the margin parameter to around 0.01 for such text, instead of its default of 0.002. I am not sure why it is different, but I may get around to fixing this one day....

Put a textbox or legend at an empty spot on the plot:

(or, go back to top)
In its simplest form, this is accomplished by:
pqol.text("Hello, World!")  #places textbox on plot, with text "Hello, World!"
   #or
pqol.legend()               #places legend on plot

For more complicated uses, check out the example image generated by the code below.

x = np.linspace(-4,4,20)

plt.plot(x, x, label='positive line')
annotate() #defined below
plt.show()

plt.plot(x, x,  label='positive line')
plt.plot(x, -x, label='negative line')
annotate() #defined below
plt.show()

plt.plot(x, x**2, label='parabola')
annotate() #defined below
plt.show()

def annotate():
    '''adds a legend and many textboxes.'''
    pqol.legend()
    pqol.text('look, a wild textbox!')
    pqol.text('this box has\na whole new line!')
    pqol.text('no border', bbox=None)
    pqol.text('orange & transparent', bbox=dict(facecolor='orange', alpha=0.5))
    pqol.text('small font', fontsize=10)
    pqol.text('large font', fontsize=24)
    pqol.text('this box is placed with extra care', iters=100)

Notes (for all users of these commands):

  • Each call of pqol.text or pqol.legend automatically searches for the best ('least cluttered') place to put the text or legend. This is especially useful when you have something you need on the plot, but don't care much where on the plot it ends up.
  • Any keyword arguments entered to pqol.text or pqol.legend will be passed to plt.text or plt.legend, respectively, so you can do any type of stylizations as you normally would, as demonstrated via the borderless, orange, small, and large text objects above.
  • When adding many text or legend objects to your plot, it is best practice (i.e. it will make overlapping objects least likely) to put them in the following order, since pqol.text and pqol.legend do not retroactively rearrange existing objects on the plot:
    • First, add anything with a location you want to specify.
    • Next, add objects from biggest to smallest.
  • If you have a textbox you need to place in a very specific location, you can use pqol.text('text', ax_xy=axes_coordinates_for_box), or plt.text(x, y, 'text') where x and y are in data coordinates.
    • pqol.text and pqol.legend will still take into account any text objects placed in specific locations. For example, if you place a textbox at the top right corner via plt.text, then call pqol.text, pqol.text will take this top-right-corner textbox into account while deciding where to add the new textbox.

More details (for more advanced use of these commands):

  • Each call to pqol.text or pqol.legend does a few iterations before finally placing the object in the best spot the code could find during those iterations. Each iteration involves placing the object at one grid point (on a 12x12 grid, by default), and deciding if that is a better spot for it than any of the previously tried spots. The code tries grid point anchors in order of least overlap with data/text/legends, to most overlap. By default, the code tries 20 iterations for a legend, and 40 iterations for a text object.
    • To improve selection, increase number of iterations via the iters parameter. For example, see the code for the box "placed with extra care" in the example above.
    • To reduce plotting time, reduce number of iterations via the iters parameter.
    • To iterate over all points in the grid, use iters=144 for the 12x12 grid, or use iters=-1 for any grid size.
  • By default, the 'emptiness' of a spot is resolved on a 12x12 grid by counting the amount of data points in each grid box, blurring this image via a 5x5 gaussian kernel with $\sigma$=0.5 grid boxes, then determining the overlap with existing text and legend objects, weighted by the fraction of each grid box that they cover.
    • This produces a behavior which seems close to what would be desirable, and for most users this should be left alone.
    • If you want to edit the parameters, you can do so by inputting optional keyword arguments to functions. Examine the documentation (docstrings) of the relevant functions to learn how to do this.
  • In its current implementation, pqol.text may still lead to overlap between outlines of objects. This is mainly because plt.gca().transData.inverted().transform( obj.get_window_extent(renderer=plt.gcf().canvas.get_renderer() ), for obj being text or legend, gives an imprecise box which does not actually align with the textbox or legend outline, but is "close enough" to be worth using. (If you know a better way to learn the size of a text or legend object, please contact me and I will try to get it implemented!)
    • To view the box which is actually being used to calculate overlap, you can do, for example, t=pqol.text('text') pqol.draw_bbox(pqol.bbox_corners(t, output='data'), color='red') before doing plt.show(), to draw the box in red. (Also note, you can get a list of all the text objects on the active plot by calling pqol.get_texts(), and you can get the legend object by calling pqol.get_legend().)
  • Mainly for debugging purposes: you can see the calculated "overlap" shown on top of the plot by doing pqol.imshow_overplot(pqol.total_overlap()).

Draw lines over an image:

(or, go back to top)
In its simplest form, this is accomplished by:
pqol.line_on_imshow(m=slope, b=y_intercept)

For more complicated usage, see the images and description below.

## make some data ##
yy, xx = np.indices((64, 128))/16
     #makes x & y values corresponding to the indices of a 64x128 grid, divided by 16.
data   = np.sin(xx+yy)*np.cos(yy) 

## plot the data ##
extent = [np.min(xx), np.max(xx), np.min(yy), np.max(yy)]
plt.imshow(data, extent=extent, cmap='terrain', alpha=0.7) #alpha is used here just to make the colors less bright.
pqol.colorbar()        #makes a nice colorbar. See Colors & Colorbars page of wiki for more details.

## begin to use pqol function: line_on_imshow ##
pqol.line_on_imshow(1, 0, color='red', ls='-', linewidth=4)
    #makes a red solid line with slope=1, y_intercept=0.
pqol.line_on_imshow(**pqol.linecalc([1.5, 7.5], [0.7, 3.7]), color='cyan', ls='--', linewidth=4)
    #makes a cyan dashed line going through the points (x1,y1)=(1.5,0.7), (x2,y2)=(7.5,3.7)

## note that you could use vline or hline to draw lines as well! Here is an example: ##
pqol.vline(3*np.pi/2, "x=3$\pi$/2", textparams={'color':'purple', 'fontsize':20, 'weight':'bold'}, \
           textloc=0.5, textside='right', textdirection='down', color='black', ls='-.', linewidth=4)
    #makes a vertical line at x=3pi/2, labels it with "x=3$\pi$/2" in purple bold fontsize=20 text,
    #with the start of the text located halfway up the y-axis, on the right side of the line,
    #with text going downwards. The line itself is black and dash-dotted. 

Notes:

  • The cyan line is created using pqol.linecalc() to determine its slope and y-intercept. pqol.linecalc(x1x2, y1y2) takes as inputs the x coordinates of two points and y coordinates of those same two points, and returns dict(m=slope, b=y_intercept).
  • You can plot lines outside of imshow as well. For example, make a plt.plot() then plot a line of best fit. That is a bit less difficult than plotting on top of images; you can either figure it out on your own, or use pqol.plotline(xdata, m, b, xb=0, **pltplotkwargs), with xdata=x-coordinates of points where you want the line to be plotted, m=slope, b=y-intercept, xb=x-coordinate where y=b (for a y-intercept, this is at x=0. Putting xb!=0 means b is no longer actually the y-intercept).
  • For more info on pqol.colorbar() and other color-related pqol functions, see Colors & Colorbars

Clone this wiki locally