Line id plot

[Update 2012/04/22: Following suggestions from Jane Rigby (see comments below), I have created a couple of functions for changing properties of labels. These exists as separate code in the following Github Gist: https://gist.github.com/2326396 .]

Manually placing labels in a plot, especially if it is crowded, is cumbersome.

The Python module lineid_plot, has functions for automatically placing labels in a plot, in such a way that the labels do not overlap with each other. This is very useful, for example, in creating plots that have labels identifying features in a spectrum.

This Python module is an adaptation of the IDL module lineid_plot.pro, in the IDLAstro library.

The code that adjusts the label positions is a direct translation of the appropriate section of the IDL code. Other features are implemented using the facilities provided by Matplotlib.

The module can be installed using pip or easy_install:

$ easy_install lineid_plot

or

$ pip install lineid_plot

The module can also be downloaded from http://pypi.python.org/pypi/lineid_plot/. Documentation is available at http://packages.python.org/lineid_plot/.

The source code repository for the module is available on Github: http://github.com/phn/lineid_plot.

An example is show below. The plot can be customized in several ways, as shown in the documentation.

import numpy as np

from matplotlib import pyplot as plt
import lineid_plot

wave = 1240 + np.arange(300) * 0.1

flux = np.random.normal(size=300)

line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35]
line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II']

lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1)

plt.show()
LineID plot example.

Example showing automatic placement of labels in a plot.

Advertisements
This entry was posted in Astronomy, Python and tagged , . Bookmark the permalink.

13 Responses to Line id plot

  1. That’s pretty neat… I’ve been trying to resolve the problem of crowded line labels for a while now. I’ll try to incorporate it in pyspeckit (pyspeckit.bitbucket.org).

  2. Jane Rigby says:

    Prasanth, this is wonderful! One quick question: I have tried getting lineid_plot to label my lines when I’m using subplot to plot multiple spectra on one figure. But lineid_plot doesn’t seem to play well with the subplots. It tries to make a new figure, instead. Is there an easy way to fix this?

    • Prasanth says:

      Hello,

      Glad to know that you found it useful.

      The following is an example plot that illustrates usage of subplots:

      http://phn.github.com/lineid_plot/#multiple-plots-using-user-provided-axes-instances

      One can pass an Axes instance to the plot_line_ids function:

      fig = plt.figure()

      # First Axes
      ax = fig.add_axes([0.1,0.06, 0.85, 0.35])
      ax.plot(wave, flux)

      # Pass the Axes instance to the plot_line_ids function.
      lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax)

      # Second Axes
      ax1 = fig.add_axes([0.1, 0.55, 0.85, 0.35])
      ax1.plot(wave, flux)

      # Pass the Axes instance to the plot_line_ids function.
      lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax1)

      One can also create an Figure instance and pass that using the fig parameter; the current Axes of the Figure will be used.

      Let me know if that answers your question.

      Prasanth

  3. Jane Rigby says:

    Prasanth, thank you for that! Please forgive a newbie followup question. What I like about subplot is that it automatically figures out how to subdivide the axes, so I don’t have to. I just say, divide the plot into 3×2 panels, and it does. So, is there a way to tell lineid_plot to just use the axis subdivisions that subplot chose, without manually setting them? Is that what you mean by “pass that using the fig parameter?”

    • Prasanth says:

      Hello,

      > Is that what you mean by “pass that using the fig parameter?”

      No; this is if you want to place labels on a specific Figure, without plot_line_ids creating a new Figure.

      Just to make sure that I understand you correctly:

      To create a 3×2 panel one needs to issue “subplot(3,2, 1)”, i.e., create a 3×2 panel if not present and then select the first as the current Axes. The command “subplot(3,2,4)” says make the 4th Axes as the current Axes. After this “plot(…)” will plot the data on the selected panel (i.e., Axes); panel 1 in the former case and panel 4 in the latter.

      With lineid_plot you want to able to do something like:

      from matplotlib import pyplot as plt
      plt.subplot(2,1, 1)
      plt.plot(…) # plot your data
      lineid_plot.plot_line_ids(….) # place labels in panel 1
      plt.subplot(2,1,2)
      plt.plot(…) # plot your data
      lineid_plot_line_ids(…) # place labels in panel 2

      and instead of creating a new figure (since no Axes or Figure is given), plot_line_ids() should just use the current Axes. The first command should draw labels in panel 1 and second in panel 2.

      Is this correct?

      If so, you can achieve it as follows:

      from matplotlib import pyplot as plt
      plt.subplot(2,1,1)
      plt.plot(…) # plot your data
      lineid_plot.plot_line_ids(…., ax=plt.gca()) # pass the current Axes
      plt.subplot(2,1,2)
      plt.plot(…) # plot your data
      lineid_plot_line_ids(…, ax=plt.gca()) # pass the current Axes

      But, the above code by itself will very likely result in labels overlapping into adjust plots. You can adjust the placement of labels using the various parameters of plot_line_ids, as illustrated in the documentation. For example:

      http://packages.python.org/lineid_plot/#custom-y-axis-location-for-annotation-points-arrow-tips

      and

      http://packages.python.org/lineid_plot/#custom-y-axis-location-for-label-boxes

      The function plot_line_ids purposefully doesn’t do anything to the Axes, such as adjusting space between panels or changing x and y axis limits. I made this choice because, my guess is that most users will want to add stuff to plots in addition to labels. Manipulating Axes spaces and the x and y axis limits from within plot_line_ids() will change what the user has done.

      Let me know if I understand you correctly, and if this helps.

      Prasanth

  4. Jane Rigby says:

    Prasanth, that’s 100% what I needed! Thanks for helping a newbie.

    One additional question: Is there a way to assign colors to different line labels to different lines on the same plot? Example use cases: A) label emission in black and absorption in grey; B) label redshifted lines in red and blueshifted lines in blue; C) distinguish wind, photospheric, nebular, and ISM lines in a galaxy with 4 different colored labels.

    • Prasanth says:

      Hello,

      Yes you can! But not using plot_line_ids() directly.

      See the following example for accessing the text boxes (Matplotlib Annotation objects) and dotted lines (Matplotlib Line2D objects), that are created by plot_line_ids():

      http://packages.python.org/lineid_plot/#accessing-a-specific-label

      Each of the text boxes and dotted lines are assigned a unique label by plot_line_ids(). For example if the labels for spectral features are:

      [‘N V’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’] ,

      then the text boxes are labelled:

      [‘N V’, ‘Si II_num_1’, ‘Si II_num_2’, ‘Si II_num_3’, ‘Si II_num_4’, ‘Si II_num_5’, ‘Si II_num_6’]

      and the dotted lines are labelled the same as above but with an additional suffix “_line” .

      We can use the Matplotlib Axes method findobj() method to get a reference to the label and line we are interested in:

      ax = plt.gca()
      a = ax.findobj(mpl.text.Annotation) # mpl is “import matplotlib as mpl”

      for i in a:
      if i.get_label() == “Si II_num_4”:
      break

      Now the name “i” hold reference to the box for the 3rd Si II feature.

      We can set any property on the “i” object and then call

      ax.canvas.figure.draw()

      to update the drawing.

      For the case you mention, we can do something like the following:

      i.set_color(“red”) # the text “Si II” set to red
      i.arrow_patch.set_color(“red”) # the black line attached to text set to red
      ax.figure.canvas.draw() # update to reflect the changes

      Now we can do similar procedure for the dotted line:

      a = ax.findobj(mpl.lines.Line2D) # All lines.
      for i in a:
      if i.get_label() == “Si II_num_4_line”:
      break

      i.set_color(“red”) # dotted line set to red
      ax.figure.canvas.draw() # update

      You can changes any property of the objects, not just color.

      I don’t know if there is a shorter way of doing this. At-least, you can probably put these in a function and then call it as needed.

      Let me know if this helps.

      Prasanth

  5. Jane Riby says:

    Last question, I promise: if I had an array containing all the colors, is there a way to set all the colors at once, something like the psudeocode “a.set_color(array_of_colors)”, where a and array_of_colors are the same size?

    • Prasanth says:

      My problem is not getting enough feedback; please do ask!!

      I think your situation can be a very common one. So I have created two functions that will do all that is needed to change the colors of the text and the dotted lines.

      I have posted these two functions as a Github Gist at https://gist.github.com/2326396.

      I will add these into the lineid_plot module in the near future. For now, download the file, put it in a directory where Python can find it (for example the “current” directory) and then import it.

      After plotting spectrum, and creating labels using lineid_plot.plot_line_ids(), create a list of valid Matplotlib color names. Then call the color_text_boxes() function, passing in the Axes, the labels used, and the colors. This will color the text boxes corresponding to the given label.

      >>> line_labels = [‘N V’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’, ‘Si II’]
      >>> label_colors = [“red”, “green”, “blue”, “green”, “yellow”, “cyan”, “red”]
      >>> lineid_utils.color_text_boxes(plt.gca(), line_labels, label_colors)

      If for some reason you don’t want to color the solid line attached to the text box use:

      >>> lineid_utils.color_text_boxes(plt.gca(), line_labels, label_colors, color_arrow=False)

      To color the dotted lines use the color_lines() function:

      >>> lineid_utils.color_lines(plt.gca(), line_labels, label_colors)

      Note that in both the function calls above I pass the current Axes; but you can pass any Axes instance. This is useful when there are multiple panels.

      You can pass only those labels for which you need to change color; only requirement is that the number of labels and number of colors must be the same.

      Let me know if this works.

      Prasanth

  6. Tsana says:

    Hi, this module is awesome and is saving me a lot of time placing labels manually. I just have one small problem, which might be an issue with my system/install in which case, sorry for bothering you!

    All the lines and labels are appearing, but they tend to overlap. I get this error message:
    Traceback (most recent call last):
    File “”, line 1, in
    File “/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/lineid_plot.py”, line 405, in plot_line_ids
    b_ext = box.get_window_extent()
    File “/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/matplotlib/text.py”, line 745, in get_window_extent
    raise RuntimeError(‘Cannot get window extent w/o renderer’)
    RuntimeError: Cannot get window extent w/o renderer

    Any idea how to fix this? I have a temporary workaround involving blank labels and cheating with \n but it would be awesome to get it working properly.

    In any case, thank-you for creating this module.

    • Prasanth says:

      Hello,

      Are you using the “GTKAgg” backend? This is a known issue https://github.com/phn/lineid_plot/issues/1 . Try other backends such as WXAgg or Qt4Agg. If you are finding it hard to install these backends, then try the Enthought Python Distribution or the Annaconda distribution from Continuum Analytics.

      Unfortunately I can’t look into this in more detail.

      • Tsana says:

        Thanks for your help. It all works perfectly with the QT4AGG backend. (Previously I was using the MacOSX (ie default for me) backend.)

  7. rex says:

    My love for you right now is infinite. :)

    Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s