├── .gitignore ├── .nojekyll ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── annotate_and_plot_kwargs.png ├── customize_box_and_lines.png ├── doc ├── Makefile ├── conf.py └── index.rst ├── lineid_plot ├── __init__.py ├── lineid_plot.py ├── utils.py └── version.py ├── multi_axes.png ├── requirements.txt ├── setup.cfg ├── setup.py ├── simple_plot.png ├── tests ├── baseline │ ├── test_access_a_specific_label.png │ ├── test_annotate_kwargs_and_plot_kwargs.png │ ├── test_custom_y_loc_for_annotation_point.png │ ├── test_custom_y_loc_for_annotation_point_each_label_sep_loc.png │ ├── test_custom_y_loc_for_label_boxes.png │ ├── test_custom_y_loc_for_label_boxes_each_box_sep_loc.png │ ├── test_customize_box_and_line.png │ ├── test_max_iter_large.png │ ├── test_max_iter_small.png │ ├── test_minimal_plot.png │ ├── test_multi_plot_user_axes.png │ ├── test_no_line_from_annotation_to_flux.png │ └── test_small_change_to_y_loc_of_label.png ├── matplotlibrc └── test_lineid_plot.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | build 3 | *.pyc 4 | *.html 5 | dist/ 6 | .cache/ 7 | .tox/ 8 | fontList.* 9 | MANIFEST 10 | *.egg-info/ 11 | 12 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/.nojekyll -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Prasanth Nair 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Line identification plots using Matplotlib 2 | ========================================== 3 | 4 | .. _lineid_plot: http://idlastro.gsfc.nasa.gov/ftp/pro/plot/lineid_plot.pro 5 | .. _IDL Astronomy User's Library: http://idlastro.gsfc.nasa.gov/ 6 | .. _pip: http://pypi.python.org/pypi/pip 7 | 8 | Manually labeling features in a crowed plot can be very time consuming. 9 | Functions in this module can be used to automatically place labels without the 10 | labels overlapping each other. This is useful, for example, in creating plots of 11 | a spectrum with spectral lines identified with labels. 12 | 13 | For more details see http://lineid-plot.readthedocs.io. 14 | 15 | Installation 16 | ============ 17 | 18 | Use `pip`_:: 19 | 20 | $ pip install lineid_plot 21 | 22 | Examples 23 | ======== 24 | 25 | Detailed examples are provided at http://lineid-plot.readthedocs.io . 26 | 27 | A basic plot can be created by calling the function 28 | ``plot_line_ids()``, and passing labels and x-axis locations of 29 | features. 30 | 31 | .. image:: simple_plot.png?raw=true 32 | :scale: 75% 33 | 34 | .. code-block:: python 35 | 36 | >>> import numpy as np 37 | >>> from matplotlib import pyplot as plt 38 | >>> import lineid_plot 39 | 40 | >>> wave = 1240 + np.arange(300) * 0.1 41 | >>> flux = np.random.normal(size=300) 42 | 43 | >>> line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 44 | >>> line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 45 | 46 | >>> lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 47 | >>> plt.show() 48 | 49 | 50 | The ``plot_line_ids()`` function also accepts Axes and/or Figure 51 | instances where labels are to be draw. 52 | 53 | .. image:: multi_axes.png?raw=true 54 | :scale: 75% 55 | 56 | .. code-block:: python 57 | 58 | >>> import numpy as np 59 | >>> from matplotlib import pyplot as plt 60 | >>> import lineid_plot 61 | 62 | >>> wave = 1240 + np.arange(300) * 0.1 63 | >>> flux = np.random.normal(size=300) 64 | >>> line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 65 | >>> line_flux = np.interp(line_wave, wave, flux) 66 | >>> line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 67 | >>> label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 68 | 69 | >>> fig = plt.figure(1) 70 | 71 | >>> ax = fig.add_axes([0.1,0.06, 0.85, 0.35]) 72 | >>> ax.plot(wave, flux) 73 | >>> lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax) 74 | 75 | >>> ax1 = fig.add_axes([0.1, 0.55, 0.85, 0.35]) 76 | >>> ax1.plot(wave, flux) 77 | >>> lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax1) 78 | 79 | 80 | The label text and the short line extending down from the text are created using 81 | the ``annotate`` method of a matplotlib Axes object. The longer line extending 82 | down to a point in the spectrum is created using the ``plot`` method on a 83 | matplotlib Axes instance. The ``plot_line_ids`` function accepts keywords to 84 | pass directly to these methods, ``annotate_kwargs`` and ``plot_kwargs``, 85 | respectively. But the best method for customizing boxes and lines is by 86 | obtaining a reference to it as shown in another example below. 87 | 88 | .. image:: annotate_and_plot_kwargs.png?raw=true 89 | :scale: 75% 90 | 91 | .. code-block:: python 92 | 93 | >>> import numpy as np 94 | >>> from matplotlib import pyplot as plt 95 | >>> import lineid_plot 96 | 97 | >>> wave = 1240 + np.arange(300) * 0.1 98 | >>> flux = np.random.normal(size=300) 99 | 100 | >>> line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 101 | >>> line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 102 | 103 | >>> ak = lineid_plot.initial_annotate_kwargs() 104 | >>> ak 105 | {'arrowprops': {'arrowstyle': '->', 'relpos': (0.5, 0.0)}, 106 | 'horizontalalignment': 'center', 107 | 'rotation': 90, 108 | 'textcoords': 'data', 109 | 'verticalalignment': 'center', 110 | 'xycoords': 'data'} 111 | >>> ak['arrowprops']['arrowstyle'] = "->" 112 | 113 | >>> pk = lineid_plot.initial_plot_kwargs() 114 | >>> pk 115 | {'color': 'k', 'linestyle': '--'} 116 | >>> pk['color'] = "red" 117 | 118 | >>> lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, annotate_kwargs=ak, plot_kwargs=pk) 119 | >>> plt.show() 120 | 121 | 122 | The boxes and the lines extending to the flux level both have their label set to 123 | a unique value. If the input contains identical labels then the function will 124 | construct unique lables by appending text. These can be used to quickly identify 125 | them. 126 | 127 | .. code-block:: python 128 | 129 | >>> for i in ax.texts: 130 | ....: print i.get_label() 131 | ....: 132 | N V 133 | Si II_num_1 134 | Si II_num_2 135 | Si II_num_3 136 | Si II_num_4 137 | Si II_num_5 138 | Si II_num_6 139 | >>> for i in ax.lines: 140 | ....: print i.get_label() 141 | ....: 142 | _line0 143 | N V_line 144 | Si II_num_1_line 145 | Si II_num_2_line 146 | Si II_num_3_line 147 | Si II_num_4_line 148 | Si II_num_5_line 149 | Si II_num_6_line 150 | 151 | 152 | The label ``_line0`` corresponds to the data plot and was assigned by 153 | Matplotlib. 154 | 155 | We can get a reference to an annotation box or a line using the ``Axes.findobj`` 156 | method. Once we get a reference we can change its properties. This is the best 157 | method for customizing boxes and lines. 158 | 159 | 160 | .. image:: customize_box_and_lines.png?raw=true 161 | :scale: 75% 162 | 163 | .. code-block:: python 164 | 165 | >>> import numpy as np 166 | >>> from matplotlib import pyplot as plt 167 | >>> import lineid_plot 168 | 169 | >>> wave = 1240 + np.arange(300) * 0.1 170 | >>> flux = np.random.normal(size=300) 171 | 172 | >>> line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 173 | >>> line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 174 | 175 | >>> fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 176 | 177 | >>> b = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1')[0] 178 | >>> b.set_rotation(0) 179 | >>> b.set_text("Si II$\lambda$1260.42") 180 | 181 | >>> line = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1_line')[0] 182 | >>> line.set_color("red") 183 | >>> line.set_linestyle("-") 184 | 185 | >>> plt.show() 186 | 187 | Adding a label to lines can cause problems when using ``plt.legend()``: the 188 | legend will include the lines drawn from text box to spectrum location. There 189 | are two ways of overcoming this. First is to provide explicit artists and texts 190 | to ``plt.legend()``. Second is to tell ``lineid_plot`` not to add these labels 191 | by passing in ``add_label_to_artists=False``. Of-course, if we use the second 192 | option then we can't use the above method for finding text and lines. 193 | 194 | .. code-block:: python 195 | 196 | fig, ax = lineid_plot.plot_line_ids( 197 | wave, flux, line_wave, line_label1, max_iter=300, add_label_to_artists=False 198 | ) 199 | 200 | Details 201 | ======= 202 | 203 | The placements are calculated using a simple, iterative algorithm adapted from 204 | the procedure `lineid_plot`_ in the NASA `IDL Astronomy User's Library`_. 205 | Matplotlib makes most of the other computations, such as extracting width of 206 | label boxes, re-positioning them etc., very easy. 207 | 208 | The main function in the module is ``plot_line_ids()``. Labeled plots can be 209 | created by passing the x and y coordinates, for example wavelength and flux, 210 | along with the x coordinates of the features and their labels. The x coordinates 211 | are adjusted until the labels, of given size, do not overlap, or when the 212 | iteration limit is reached. 213 | 214 | Users can provide the Axes instance or the Figure instance on which plots are to 215 | be made. If an Axes instance is provided, then the data is not plotted; only the 216 | labels are marked. This allows the user to separate plotting from labeling. For 217 | example, the user can create multiple Axes on a figure and then pass the Axes on 218 | which labels are to be marked. No changes are made to the existing layout. 219 | 220 | The labels and a short line for each label are create using matplotlib's 221 | ``Axes.annotate`` method. The longer lines extending down into the plot are 222 | created using matplotlib's ``Axes.plot`` method. 223 | 224 | The y axis locations of labels and annotation points i.e., arrow tips, can also 225 | be passed to the ``plot_line_ids()`` function. Minor changes can be passed using 226 | the ``box_axes_space`` keyword, where as major changes can be passed using the 227 | ``arrow_tip`` and ``box_loc`` keywords. The former is in figure fraction units 228 | and the latter two are in data coordinates. The latter two can be specified 229 | separately for each label. This is very useful in crowded regions. These 230 | features along with the ability to pass an Axes instance gives the program a lot 231 | of flexibility. 232 | 233 | An extension line from the annotation point to the y data value at the location 234 | of the identification i.e., flux level at the line, is drawn by default. The 235 | flux at the line is calculated using linear interpolation. This can be turned 236 | off using the ``extend`` keyword. This keyword can be set separately for each 237 | feature. 238 | 239 | The boxes containing text label and the line extending down can be customized by 240 | paasing ``annotate_kwargs`` and ``plot_kwargs`` respectively. Use 241 | ``initial_annotate_kwargs()`` and ``initial_plot_kwargs()`` to obtain the 242 | default dictionaries used. We can customize these dictionaries and pass them to 243 | ``plot_line_ids``. Further customizations can be performed by obtaining a 244 | reference to the annotation or line and using the matplotlib API. 245 | 246 | The ``plot_line_ids()`` function returns the Figure and Axes instances used. 247 | Additional customizations, such as manual adjustments to positions, can be 248 | carried out using these references. To easily identify the ojects, each label 249 | box and extension line have its ``label`` property set to a string that depends 250 | on the label text provided. Identifying the Matplotlib objects corresponding to 251 | these and customizing them are made easy by the many features provided by 252 | Matplotlib. 253 | 254 | The maximum number of iterations to use while calculating label positions can be 255 | supplied using the ``max_iter`` keyword. The amount of adjustment to be made in 256 | each iteration and when to change the adjustment factor can also be supplied. 257 | The defaults for these should be enough for most cases. 258 | 259 | License 260 | ======= 261 | 262 | Released under BSD; see http://www.opensource.org/licenses/bsd-license.php. 263 | 264 | Credits 265 | ======= 266 | 267 | Code here is adapted from `lineid_plot`_ procedure in the 268 | `IDL Astronomy User's Library`_ (IDLASTRO) IDL code distributed by NASA. 269 | 270 | For comments and suggestions, email to user prasanthhn in the gmail.com domain. 271 | 272 | 273 | .. LocalWords: lineid IDL idlastro gsfc nasa 274 | 275 | 276 | -------------------------------------------------------------------------------- /annotate_and_plot_kwargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/annotate_and_plot_kwargs.png -------------------------------------------------------------------------------- /customize_box_and_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/customize_box_and_lines.png -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LineIdentificationPlots.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LineIdentificationPlots.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/LineIdentificationPlots" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LineIdentificationPlots" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Line Identification Plots documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Sep 18 11:26:57 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['matplotlib.sphinxext.plot_directive', 29 | 'matplotlib.sphinxext.only_directives'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Line Identification Plots' 45 | copyright = u'2011, Prasanth Nair' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | 51 | # Read version.py 52 | __version__ = None 53 | curdir = os.path.dirname(__file__) 54 | with open(os.path.join(curdir, '..', 'lineid_plot', 'version.py')) as f: 55 | exec(f.read()) 56 | 57 | # The short X.Y version. 58 | version = __version__ 59 | # The full version, including alpha/beta/rc tags. 60 | release = __version__ 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'haiku' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | html_theme_options = {'nosidebar': True} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | #html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | #html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | #html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | #html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | #html_domain_indices = True 149 | 150 | # If false, no index is generated. 151 | #html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | #html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | #html_show_sourcelink = True 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | #html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | #html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | #html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | #html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = 'LineIdentificationPlotsdoc' 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | # The paper size ('letter' or 'a4'). 180 | #latex_paper_size = 'letter' 181 | 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | #latex_font_size = '10pt' 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'LineIdentificationPlots.tex', u'Line Identification Plots Documentation', 189 | u'Prasanth Nair', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Additional stuff for the LaTeX preamble. 207 | #latex_preamble = '' 208 | 209 | # Documents to append as an appendix to all manuals. 210 | #latex_appendices = [] 211 | 212 | # If false, no module index is generated. 213 | #latex_domain_indices = True 214 | 215 | 216 | # -- Options for manual page output -------------------------------------------- 217 | 218 | # One entry per manual page. List of tuples 219 | # (source start file, name, description, authors, manual section). 220 | man_pages = [ 221 | ('index', 'lineidentificationplots', u'Line Identification Plots Documentation', 222 | [u'Prasanth Nair'], 1) 223 | ] 224 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Line Identification Plots documentation master file, created by 2 | sphinx-quickstart on Sun Sep 18 11:26:57 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Line identification plots with automatic label layout 7 | ===================================================== 8 | 9 | .. contents:: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Manually placing labels in plots of spectrum is cumbersome. Functions 15 | in this module attempt to automatically position labels, in such a way 16 | that they do not overlap with each other. 17 | 18 | The module can be installed using `pip` and `easy_install`:: 19 | 20 | $ pip install lineid_plot 21 | 22 | The source code is available at https://github.com/phn/lineid_plot . 23 | 24 | The function `plot_line_ids()` takes several keyword parameters that 25 | can be used to customized the placement of labels. The labels are 26 | generated using the `annotate` function in Matplotlib. The function 27 | returns the Figure and Axes instances used, so that further 28 | customizations can be performed. 29 | 30 | Some example are shown below. 31 | 32 | Minimal plot with automatic label layout 33 | ---------------------------------------- 34 | 35 | .. plot:: 36 | :include-source: 37 | 38 | import numpy as np 39 | from matplotlib import pyplot as plt 40 | import lineid_plot 41 | 42 | wave = 1240 + np.arange(300) * 0.1 43 | flux = np.random.normal(size=300) 44 | 45 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 46 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 47 | 48 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 49 | 50 | plt.show() 51 | 52 | 53 | Customize label and line using keyword arguments 54 | ------------------------------------------------ 55 | 56 | .. plot:: 57 | :include-source: 58 | 59 | import numpy as np 60 | from matplotlib import pyplot as plt 61 | import lineid_plot 62 | 63 | wave = 1240 + np.arange(300) * 0.1 64 | flux = np.random.normal(size=300) 65 | 66 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 67 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 68 | 69 | ak = lineid_plot.initial_annotate_kwargs() 70 | ak['arrowprops']['arrowstyle'] = "->" 71 | 72 | pk = lineid_plot.initial_plot_kwargs() 73 | pk['color'] = "red" 74 | 75 | lineid_plot.plot_line_ids( 76 | wave, flux, line_wave, line_label1, annotate_kwargs=ak, plot_kwargs=pk) 77 | 78 | plt.show() 79 | 80 | 81 | Customize label and line using reference to matplotlib objects 82 | -------------------------------------------------------------- 83 | 84 | .. plot:: 85 | :include-source: 86 | 87 | import numpy as np 88 | from matplotlib import pyplot as plt 89 | import lineid_plot 90 | 91 | wave = 1240 + np.arange(300) * 0.1 92 | flux = np.random.normal(size=300) 93 | 94 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 95 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 96 | 97 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 98 | 99 | b = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1')[0] 100 | b.set_rotation(0) 101 | b.set_text("Si II$\lambda$1260.42") 102 | 103 | line = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1_line')[0] 104 | line.set_color("red") 105 | line.set_linestyle("-") 106 | 107 | plt.show() 108 | 109 | Plot without line from annotation point to flux level 110 | ----------------------------------------------------- 111 | 112 | .. plot:: 113 | :include-source: 114 | 115 | import numpy as np 116 | from matplotlib import pyplot as plt 117 | import lineid_plot 118 | 119 | wave = 1240 + np.arange(300) * 0.1 120 | flux = np.random.normal(size=300) 121 | 122 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 123 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 124 | 125 | # Set extend=False. 126 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, extend=False) 127 | 128 | plt.show() 129 | 130 | Multiple plots using user provided Axes instances 131 | ------------------------------------------------- 132 | 133 | .. plot:: 134 | :include-source: 135 | 136 | import numpy as np 137 | from matplotlib import pyplot as plt 138 | import lineid_plot 139 | 140 | wave = 1240 + np.arange(300) * 0.1 141 | flux = np.random.normal(size=300) 142 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 143 | line_flux = np.interp(line_wave, wave, flux) 144 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 145 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 146 | 147 | fig = plt.figure() 148 | 149 | # First Axes 150 | ax = fig.add_axes([0.1,0.06, 0.85, 0.35]) 151 | ax.plot(wave, flux) 152 | 153 | # Pass the Axes instance to the plot_line_ids function. 154 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax) 155 | 156 | # Second Axes 157 | ax1 = fig.add_axes([0.1, 0.55, 0.85, 0.35]) 158 | ax1.plot(wave, flux) 159 | 160 | # Pass the Axes instance to the plot_line_ids function. 161 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax1) 162 | 163 | plt.show() 164 | 165 | 166 | Applying small change to y axis location of label 167 | ------------------------------------------------- 168 | 169 | By default the annotation point is placed on the y-axes upper bound and 170 | the box is placed 0.06 *figure fraction* units above the annotation 171 | point. Figure fraction units is used so that the appearance doesn't 172 | depend on the y range of data. The default box location i.e., 0.06 can 173 | be changed using the keyword parameter `box_axes_space`. 174 | 175 | It is better not to use large values for this keyword. Large values can 176 | lead to `RuntimeError` when Matplotlib tries to render the plot. 177 | 178 | Examples of changing y axis locations of annotation points and label 179 | locations using data coordinates are show later. These methods are 180 | preferred when major changes are required. 181 | 182 | .. code-block:: python 183 | 184 | # box_axes_space takes numbers in figure fraction units. Keep this 185 | # small. 186 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 187 | box_axes_space=0.08) 188 | 189 | .. plot:: 190 | 191 | import numpy as np 192 | from matplotlib import pyplot as plt 193 | import lineid_plot 194 | 195 | wave = 1240 + np.arange(300) * 0.1 196 | flux = np.random.normal(size=300) 197 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 198 | line_flux = np.interp(line_wave, wave, flux) 199 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 200 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 201 | 202 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 203 | box_axes_space=0.08) 204 | 205 | plt.show() 206 | 207 | Custom y axis location for annotation points (arrow tips) 208 | --------------------------------------------------------- 209 | 210 | Use arrow_tip keyword to alter the annotation point. When assigning 211 | custom y axis locations, it is best to plot the data, set appropriate 212 | range for the Axes and then pass the Axes instance to 213 | `plot_line_ids()`. 214 | 215 | .. code-block:: python 216 | 217 | # Use arrow_tip keyword. 218 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 219 | arrow_tip=3.3, ax=ax) 220 | 221 | .. plot:: 222 | 223 | import numpy as np 224 | from matplotlib import pyplot as plt 225 | import lineid_plot 226 | 227 | wave = 1240 + np.arange(300) * 0.1 228 | flux = np.random.normal(size=300) 229 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 230 | line_flux = np.interp(line_wave, wave, flux) 231 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 232 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 233 | 234 | fig = plt.figure() 235 | ax = fig.add_subplot(111) 236 | ax.plot(wave, flux) 237 | ax.axis([1240, 1270, -3, 5]) 238 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 239 | arrow_tip=3.3, ax=ax) 240 | 241 | plt.show() 242 | 243 | Each label can have its own annotation point. 244 | 245 | .. code-block:: python 246 | 247 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 248 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 249 | arrow_tip=arrow_tips, ax=ax) 250 | 251 | .. plot:: 252 | 253 | import numpy as np 254 | from matplotlib import pyplot as plt 255 | import lineid_plot 256 | 257 | wave = 1240 + np.arange(300) * 0.1 258 | flux = np.random.normal(size=300) 259 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 260 | line_flux = np.interp(line_wave, wave, flux) 261 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 262 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 263 | 264 | fig = plt.figure() 265 | ax = fig.add_subplot(111) 266 | ax.plot(wave, flux) 267 | ax.axis([1240, 1270, -3, 5]) 268 | 269 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 270 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 271 | arrow_tip=arrow_tips, ax=ax) 272 | 273 | plt.show() 274 | 275 | 276 | Custom y axis location for label boxes 277 | -------------------------------------- 278 | 279 | The label boxes can be given a custom y axis location. The value given 280 | is taken as the y coordinate of the center of a box, in data 281 | coordinates. When assigning custom Y locations, it is best to plot the 282 | data, set appropriate range for the Axes and then pass the Axes 283 | instance to `plot_line_ids()`. 284 | 285 | .. code-block:: python 286 | 287 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 288 | arrow_tip=3.3, ax=ax, box_loc=4.3) 289 | 290 | .. plot:: 291 | 292 | import numpy as np 293 | from matplotlib import pyplot as plt 294 | import lineid_plot 295 | 296 | wave = 1240 + np.arange(300) * 0.1 297 | flux = np.random.normal(size=300) 298 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 299 | line_flux = np.interp(line_wave, wave, flux) 300 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 301 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 302 | 303 | fig = plt.figure() 304 | ax = fig.add_subplot(111) 305 | ax.plot(wave, flux) 306 | ax.axis([1240, 1270, -3, 5]) 307 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 308 | arrow_tip=3.3, ax=ax, box_loc=4.3) 309 | 310 | plt.show() 311 | 312 | 313 | Each box can be assigned a separate Y box location. 314 | 315 | .. code-block:: python 316 | 317 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 318 | box_loc = [4.3, 4.3, 4.3, 4.4, 4.5, 4.4, 4.3] 319 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 320 | arrow_tip=arrow_tips, box_loc=box_loc, ax=ax) 321 | 322 | .. plot:: 323 | 324 | import numpy as np 325 | from matplotlib import pyplot as plt 326 | import lineid_plot 327 | 328 | wave = 1240 + np.arange(300) * 0.1 329 | flux = np.random.normal(size=300) 330 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 331 | line_flux = np.interp(line_wave, wave, flux) 332 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 333 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 334 | 335 | fig = plt.figure() 336 | ax = fig.add_subplot(111) 337 | ax.plot(wave, flux) 338 | ax.axis([1240, 1270, -3, 5]) 339 | 340 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 341 | box_loc = [4.3, 4.3, 4.3, 4.4, 4.5, 4.4, 4.3] 342 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 343 | arrow_tip=arrow_tips, box_loc=box_loc, ax=ax) 344 | 345 | plt.show() 346 | 347 | 348 | Accessing a specific label 349 | -------------------------- 350 | 351 | Each box has a property named `label`. These are identical to the input 352 | labels, except when there are duplicated. The duplicate texts are given 353 | a numeric suffix, based on the order in which the duplicates occur in 354 | the input. These are generated using the `unique_labels` function. 355 | 356 | .. code-block:: python 357 | 358 | line_label1 359 | ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 360 | lineid_plot.unique_labels(line_label1) 361 | ['N V', 362 | 'Si II_num_1', 363 | 'Si II_num_2', 364 | 'Si II_num_3', 365 | 'Si II_num_4', 366 | 'Si II_num_5', 367 | 'Si II_num_6'] 368 | 369 | Each line extending from the annotation point to the flux level, is 370 | also assigned a label properties. The value is the above label 371 | property suffixed with "_line". 372 | 373 | Matplotlib ``Figure`` and ``Axes`` instances have a method 374 | ``findobj()`` which can be used to find objects in it, that satisfy 375 | certain conditions. For example, all ``Annotation`` objects. It will 376 | accept a function, and any object that will cause this function to 377 | return True, will be returned by ``findobj()``. 378 | 379 | In the following one of the "Si II" boxes and line extending to the 380 | flux level are made invisible. 381 | 382 | .. plot:: 383 | :include-source: 384 | 385 | import numpy as np 386 | from matplotlib import pyplot as plt 387 | import matplotlib as mpl 388 | import lineid_plot 389 | 390 | wave = 1240 + np.arange(300) * 0.1 391 | flux = np.random.normal(size=300) 392 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 393 | line_flux = np.interp(line_wave, wave, flux) 394 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 395 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 396 | 397 | fig = plt.figure() 398 | ax = fig.add_subplot(111) 399 | ax.plot(wave, flux) 400 | ax.axis([1240, 1270, -3, 5]) 401 | 402 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 403 | box_loc = [4.3, 4.3, 4.3, 4.4, 4.5, 4.4, 4.3] 404 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 405 | arrow_tip=arrow_tips, box_loc=box_loc, ax=ax) 406 | 407 | a = ax.findobj(mpl.text.Annotation) 408 | for i in a: 409 | if i.get_label() == "Si II_num_4": 410 | i.set_visible(False) 411 | 412 | a = ax.findobj(mpl.lines.Line2D) 413 | for i in a: 414 | if i.get_label() == "Si II_num_4_line": 415 | i.set_visible(False) 416 | 417 | plt.show() 418 | 419 | Adding a label to lines can cause problems when using ``plt.legend()``: the 420 | legend will include the lines drawn from text box to spectrum location. There 421 | are two ways of overcoming this. First is to provide explicit artists and texts 422 | to ``plt.legend()``. Second is to tell ``lineid_plot`` not to add these labels 423 | by passing in ``add_label_to_artists=False``. Of-course, if we use the second 424 | option then we can't use the above method for finding text and lines. 425 | 426 | .. code-block:: python 427 | 428 | fig, ax = lineid_plot.plot_line_ids( 429 | wave, flux, line_wave, line_label1, max_iter=300, add_label_to_artists=False 430 | ) 431 | 432 | 433 | Customizing box location adjustment parameters 434 | ---------------------------------------------- 435 | 436 | The parameter `max_iter` fixes the maximum number of adjustments to 437 | perform. Parameter `adjust_factor` sets the factor by which the current 438 | separation of the boxes must be increased or decreased. After `fd_p` 439 | percentage of `max_iter` the `adjust_factor` is reduced by a factor of 440 | `factor_decrement`. The defaults for all these should be enough in most 441 | situations. 442 | 443 | The example below shows, for the data used, a low value of `max_iter` 444 | doesn't separate the labels well enough. 445 | 446 | .. code-block:: python 447 | 448 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 449 | max_iter=10) 450 | 451 | .. plot:: 452 | 453 | import numpy as np 454 | from matplotlib import pyplot as plt 455 | import matplotlib as mpl 456 | import lineid_plot 457 | 458 | wave = 1240 + np.arange(300) * 0.1 459 | flux = np.random.normal(size=300) 460 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 461 | line_flux = np.interp(line_wave, wave, flux) 462 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 463 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 464 | 465 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 466 | max_iter=10) 467 | 468 | plt.show() 469 | 470 | Using a value of 300 works. Note that the default is 1000. 471 | 472 | .. code-block:: python 473 | 474 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 475 | max_iter=300) 476 | 477 | .. plot:: 478 | 479 | import numpy as np 480 | from matplotlib import pyplot as plt 481 | import matplotlib as mpl 482 | import lineid_plot 483 | 484 | wave = 1240 + np.arange(300) * 0.1 485 | flux = np.random.normal(size=300) 486 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 487 | line_flux = np.interp(line_wave, wave, flux) 488 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 489 | label1_sizes = np.array([12, 12, 12, 12, 12, 12, 12]) 490 | 491 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, 492 | max_iter=300) 493 | 494 | plt.show() 495 | 496 | 497 | .. Indices and tables 498 | .. ================== 499 | .. 500 | .. * :ref:`genindex` 501 | .. * :ref:`modindex` 502 | .. * :ref:`search` 503 | 504 | -------------------------------------------------------------------------------- /lineid_plot/__init__.py: -------------------------------------------------------------------------------- 1 | """Automatic placement of labels for features in a plot.""" 2 | 3 | from .lineid_plot import * 4 | from .version import __author__, __license__, __version__ 5 | -------------------------------------------------------------------------------- /lineid_plot/lineid_plot.py: -------------------------------------------------------------------------------- 1 | """Automatic placement of labels for features in a plot. 2 | 3 | Depends on Numpy and Matplotlib. 4 | """ 5 | from __future__ import division, print_function 6 | 7 | import warnings 8 | import numpy as np 9 | from matplotlib import pyplot as plt 10 | 11 | __all__ = ['plot_line_ids', 'initial_annotate_kwargs', 'initial_plot_kwargs', 12 | 'unique_labels', 'get_line_flux', 'get_box_loc', 'adjust_boxes', 13 | 'prepare_axes'] 14 | 15 | 16 | def _convert_to_array(x, size, name): 17 | """Check length of array or convert scalar to array. 18 | 19 | Check to see is `x` has the given length `size`. If this is true 20 | then return Numpy array equivalent of `x`. If not then raise 21 | ValueError, using `name` as an idnetification. If len(x) returns 22 | TypeError, then assume it is a scalar and create a Numpy array of 23 | length `size`. Each item of this array will have the value as `x`. 24 | """ 25 | try: 26 | l = len(x) 27 | if l != size: 28 | raise ValueError( 29 | "{0} must be scalar or of length {1}".format( 30 | name, size)) 31 | except TypeError: 32 | # Only one item 33 | xa = np.array([x] * size) # Each item is a diff. object. 34 | else: 35 | xa = np.array(x) 36 | 37 | return xa 38 | 39 | 40 | def get_line_flux(line_wave, wave, flux, **kwargs): 41 | """Interpolated flux at a given wavelength (calls np.interp).""" 42 | return np.interp(line_wave, wave, flux, **kwargs) 43 | 44 | 45 | def unique_labels(line_labels): 46 | """If a label occurs more than once, add num. as suffix.""" 47 | from collections import defaultdict 48 | d = defaultdict(int) 49 | for i in line_labels: 50 | d[i] += 1 51 | d = dict((i, k) for i, k in d.items() if k != 1) 52 | line_labels_u = [] 53 | for lab in reversed(line_labels): 54 | c = d.get(lab, 0) 55 | if c >= 1: 56 | v = lab + "_num_" + str(c) 57 | d[lab] -= 1 58 | else: 59 | v = lab 60 | line_labels_u.insert(0, v) 61 | 62 | return line_labels_u 63 | 64 | 65 | def get_box_loc(fig, ax, line_wave, arrow_tip, box_axes_space=0.06): 66 | """Box loc in data coords, given Fig. coords offset from arrow_tip. 67 | 68 | Parameters 69 | ---------- 70 | fig: matplotlib Figure artist 71 | Figure on which the boxes will be placed. 72 | ax: matplotlib Axes artist 73 | Axes on which the boxes will be placed. 74 | arrow_tip: list or array of floats 75 | Location of tip of arrow, in data coordinates. 76 | box_axes_space: float 77 | Vertical space between arrow tip and text box in figure 78 | coordinates. Default is 0.06. 79 | 80 | Returns 81 | ------- 82 | box_loc: list of floats 83 | Box locations in data coordinates. 84 | 85 | Notes 86 | ----- 87 | Note that this function is not needed if user provides both arrow 88 | tip positions and box locations. The use case is when the program 89 | has to automatically find positions of boxes. In the automated 90 | plotting case, the arrow tip is set to be the top of the Axes 91 | (outside this function) and the box locations are determined by 92 | `box_axes_space`. 93 | 94 | In Matplotlib annotate function, both the arrow tip and the box 95 | location can be specified. While calculating automatic box 96 | locations, it is not ideal to use data coordinates to calculate 97 | box location, since plots will not have a uniform appearance. Given 98 | locations of arrow tips, and a spacing in figure fraction, this 99 | function will calculate the box locations in data 100 | coordinates. Using this boxes can be placed in a uniform manner. 101 | 102 | """ 103 | # Plot boxes in their original x position, at a height given by the 104 | # key word box_axes_spacing above the arrow tip. The default 105 | # is set to 0.06. This is in figure fraction so that the spacing 106 | # doesn't depend on the data y range. 107 | box_loc = [] 108 | fig_inv_trans = fig.transFigure.inverted() 109 | for w, a in zip(line_wave, arrow_tip): 110 | # Convert position of tip of arrow to figure coordinates, add 111 | # the vertical space between top edge and text box in figure 112 | # fraction. Convert this text box position back to data 113 | # coordinates. 114 | display_coords = ax.transData.transform((w, a)) 115 | figure_coords = fig_inv_trans.transform(display_coords) 116 | figure_coords[1] += box_axes_space 117 | display_coords = fig.transFigure.transform(figure_coords) 118 | ax_coords = ax.transData.inverted().transform(display_coords) 119 | box_loc.append(ax_coords) 120 | 121 | return box_loc 122 | 123 | 124 | def adjust_boxes(line_wave, box_widths, left_edge, right_edge, 125 | max_iter=1000, adjust_factor=0.35, 126 | factor_decrement=3.0, fd_p=0.75): 127 | """Ajdust given boxes so that they don't overlap. 128 | 129 | Parameters 130 | ---------- 131 | line_wave: list or array of floats 132 | Line wave lengths. These are assumed to be the initial y (wave 133 | length) location of the boxes. 134 | box_widths: list or array of floats 135 | Width of box containing labels for each line identification. 136 | left_edge: float 137 | Left edge of valid data i.e., wave length minimum. 138 | right_edge: float 139 | Right edge of valid data i.e., wave lengths maximum. 140 | max_iter: int 141 | Maximum number of iterations to attempt. 142 | adjust_factor: float 143 | Gap between boxes are reduced or increased by this factor after 144 | each iteration. 145 | factor_decrement: float 146 | The `adjust_factor` itself if reduced by this factor, after 147 | certain number of iterations. This is useful for crowded 148 | regions. 149 | fd_p: float 150 | Percentage, given as a fraction between 0 and 1, after which 151 | adjust_factor must be reduced by a factor of 152 | `factor_decrement`. Default is set to 0.75. 153 | 154 | Returns 155 | ------- 156 | wlp, niter, changed: (float, float, float) 157 | The new y (wave length) location of the text boxes, the number 158 | of iterations used and a flag to indicated whether any changes to 159 | the input locations were made or not. 160 | 161 | Notes 162 | ----- 163 | This is a direct translation of the code in lineid_plot.pro file in 164 | NASA IDLAstro library. 165 | 166 | Positions are returned either when the boxes no longer overlap or 167 | when `max_iter` number of iterations are completed. So if there are 168 | many boxes, there is a possibility that the final box locations 169 | overlap. 170 | 171 | References 172 | ---------- 173 | + http://idlastro.gsfc.nasa.gov/ftp/pro/plot/lineid_plot.pro 174 | + http://idlastro.gsfc.nasa.gov/ 175 | 176 | """ 177 | # Adjust positions. 178 | niter = 0 179 | changed = True 180 | nlines = len(line_wave) 181 | 182 | wlp = line_wave[:] 183 | while changed: 184 | changed = False 185 | for i in range(nlines): 186 | if i > 0: 187 | diff1 = wlp[i] - wlp[i - 1] 188 | separation1 = (box_widths[i] + box_widths[i - 1]) / 2.0 189 | else: 190 | diff1 = wlp[i] - left_edge + box_widths[i] * 1.01 191 | separation1 = box_widths[i] 192 | if i < nlines - 2: 193 | diff2 = wlp[i + 1] - wlp[i] 194 | separation2 = (box_widths[i] + box_widths[i + 1]) / 2.0 195 | else: 196 | diff2 = right_edge + box_widths[i] * 1.01 - wlp[i] 197 | separation2 = box_widths[i] 198 | 199 | if diff1 < separation1 or diff2 < separation2: 200 | if wlp[i] == left_edge: 201 | diff1 = 0 202 | if wlp[i] == right_edge: 203 | diff2 = 0 204 | if diff2 > diff1: 205 | wlp[i] = wlp[i] + separation2 * adjust_factor 206 | wlp[i] = wlp[i] if wlp[i] < right_edge else \ 207 | right_edge 208 | else: 209 | wlp[i] = wlp[i] - separation1 * adjust_factor 210 | wlp[i] = wlp[i] if wlp[i] > left_edge else \ 211 | left_edge 212 | changed = True 213 | niter += 1 214 | if niter == max_iter * fd_p: 215 | adjust_factor /= factor_decrement 216 | if niter >= max_iter: 217 | break 218 | 219 | return wlp, changed, niter 220 | 221 | 222 | def prepare_axes(wave, flux, fig=None, ax_lower=(0.1, 0.1), 223 | ax_dim=(0.85, 0.65)): 224 | """Create fig and axes if needed and layout axes in fig.""" 225 | # Axes location in figure. 226 | if not fig: 227 | fig = plt.figure() 228 | ax = fig.add_axes([ax_lower[0], ax_lower[1], ax_dim[0], ax_dim[1]]) 229 | ax.plot(wave, flux) 230 | return fig, ax 231 | 232 | 233 | def initial_annotate_kwargs(): 234 | """Return default parameters passed to Axes.annotate to create labels.""" 235 | return dict( 236 | xycoords="data", textcoords="data", 237 | rotation=90, horizontalalignment="center", verticalalignment="center", 238 | arrowprops=dict(arrowstyle="-", relpos=(0.5, 0.0)) 239 | ) 240 | 241 | 242 | def initial_plot_kwargs(): 243 | """Return default parameters passed to Axes.plot to create line from label into plot.""" 244 | return dict(linestyle="--", color="k",) 245 | 246 | 247 | def plot_line_ids(wave, flux, line_wave, line_label1, label1_size=None, 248 | extend=True, annotate_kwargs=None, plot_kwargs=None, 249 | **kwargs): 250 | """Label features with automatic layout of labels. 251 | 252 | Parameters 253 | ---------- 254 | wave: list or array of floats 255 | Wave lengths of data. 256 | flux: list or array of floats 257 | Flux at each wavelength. 258 | line_wave: list or array of floats 259 | Wave length of features to be labelled. 260 | line_label1: list of strings 261 | Label text for each line. 262 | label1_size: list of floats 263 | Font size in points. If not given the default value in 264 | Matplotlib is used. This is typically 12. 265 | extend: boolean or list of boolean values 266 | For those lines for which this keyword is True, a dashed line 267 | will be drawn from the tip of the annotation to the flux at the 268 | line. 269 | annotate_kwargs : dict 270 | Keyword arguments to pass to `annotate`, e.g. color. 271 | 272 | Default value is obtained by calling ``initial_annotate_kwargs()``. 273 | plot_kwargs : dict 274 | Keyword arguments to pass to `plot`, e.g. color. 275 | 276 | Default value is obtained by calling ``initial_plot_kwargs()``. 277 | kwargs: key value pairs 278 | All of these keywords are optional. 279 | 280 | The following keys are recognized: 281 | 282 | ax : Matplotlib Axes 283 | The Axes in which the labels are to be placed. If not 284 | given a new Axes is created. 285 | fig: Matplotlib Figure 286 | The figure in which the labels are to be placed. If `ax` 287 | if given then keyword is then ignored. The figure 288 | associated with `ax` is used. If `fig` and `ax` are not 289 | given then a new figure is created and an axes is added 290 | to it. 291 | arrow_tip: scalar or list of floats 292 | The location of the annotation point, in data coords. If 293 | the value is scalar then it is used for all. Default 294 | value is the upper bound of the Axes, at the time of 295 | plotting. 296 | box_loc: scalar or list of floats 297 | The y axis location of the text label boxes, in data 298 | units. The default is to place it above the `arrow_tip` 299 | by `box_axes_space` units in figure fraction length. 300 | box_axes_space: float 301 | If no `box_loc` is given then the y position of label 302 | boxes is set to `arrow_tip` + this many figure fraction 303 | units. The default is 0.06. This ensures that the label 304 | layout appearance is independent of the y data range. 305 | max_iter: int 306 | Maximum iterations to use. Default is set to 1000. 307 | add_label_to_artists: boolean 308 | If True (default is True) then add unique labels to artists, both 309 | text labels and line extending from text label to spectrum. If 310 | False then don't add such labels. 311 | Returns 312 | ------- 313 | fig, ax: Matplotlib Figure, Matplotlib Axes 314 | Figure instance on which the labels were placed and the Axes 315 | instance on which the labels were placed. These can be used for 316 | further customizations. For example, some labels can be hidden 317 | by accessing the corresponding `Text` instance form the 318 | `ax.texts` list. 319 | 320 | Notes 321 | ----- 322 | + By default the labels are placed along the top of the Axes. The 323 | annotation point is on the top boundary of the Axes at the y 324 | location of the line. The y location of the boxes are 0.06 figure 325 | fraction units above the annotation location. This value can be 326 | customized using the `box_axes_space` parameter. The value must 327 | be in figure fractions units. Y location of both labels and 328 | annotation points can be changed using `arrow_tip` and `box_loc` 329 | parameters. 330 | + If `arrow_tip` parameter is given then it is used as the 331 | annotation point. This can be a list in which case each line can 332 | have its own annotation point. 333 | + If `box_loc` is given, then the boxes are placed at this 334 | position. This too can be a list. 335 | + `arrow_tip` and `box_loc` are the "y" components of `xy` and 336 | `xyann` parameters accepted by the `annotate` function in 337 | Matplotlib. 338 | + If the `extend` keyword is True then a line is drawn from the 339 | annotation point to the flux at the line wavelength. The flux is 340 | calculated by linear interpolation. This parameter can be a list, 341 | with one value for each line. 342 | + The maximum iterations to be used can be customized using the 343 | `max_iter` keyword parameter. 344 | + add_label_to_artists: Adding labels to artists makes it very easy to get 345 | reference to an artist using Figure.findobj. But a call to plt.legend() 346 | will display legend for the lines. Setting add_label_to_artists=False, 347 | will not add labels to text or lines and solves this issue. We can all 348 | plt.legend() with artists and labels to only display legends for the 349 | specified artists. 350 | 351 | """ 352 | wave = np.array(wave) 353 | flux = np.array(flux) 354 | line_wave = np.array(line_wave) 355 | line_label1 = np.array(line_label1) 356 | 357 | nlines = len(line_wave) 358 | assert nlines == len(line_label1), "Each line must have a label." 359 | 360 | if label1_size is None: 361 | label1_size = np.array([12] * nlines) 362 | label1_size = _convert_to_array(label1_size, nlines, "lable1_size") 363 | 364 | extend = _convert_to_array(extend, nlines, "extend") 365 | 366 | # Sort. 367 | indx = np.argsort(wave) 368 | wave[:] = wave[indx] 369 | flux[:] = flux[indx] 370 | indx = np.argsort(line_wave) 371 | line_wave[:] = line_wave[indx] 372 | line_label1[:] = line_label1[indx] 373 | label1_size[:] = label1_size[indx] 374 | 375 | # Flux at the line wavelengths. 376 | line_flux = get_line_flux(line_wave, wave, flux) 377 | 378 | # Figure and Axes. If Axes is given then use it. If not, create 379 | # figure, if not given, and add Axes to it using a default 380 | # layout. Also plot the data in the Axes. 381 | ax = kwargs.get("ax", None) 382 | if not ax: 383 | fig = kwargs.get("fig", None) 384 | fig, ax = prepare_axes(wave, flux, fig) 385 | else: 386 | fig = ax.figure 387 | 388 | # Find location of the tip of the arrow. Either the top edge of the 389 | # Axes or the given data coordinates. 390 | ax_bounds = ax.get_ybound() 391 | arrow_tip = kwargs.get("arrow_tip", ax_bounds[1]) 392 | arrow_tip = _convert_to_array(arrow_tip, nlines, "arrow_tip") 393 | 394 | # The y location of boxes from the arrow tips. Either given heights 395 | # in data coordinates or use `box_axes_space` in figure 396 | # fraction. The latter has a default value which is used when no 397 | # box locations are given. Figure coordiantes are used so that the 398 | # y location does not dependent on the data y range. 399 | box_loc = kwargs.get("box_loc", None) 400 | if not box_loc: 401 | box_axes_space = kwargs.get("box_axes_space", 0.06) 402 | box_loc = get_box_loc(fig, ax, line_wave, arrow_tip, box_axes_space) 403 | else: 404 | box_loc = _convert_to_array(box_loc, nlines, "box_loc") 405 | box_loc = tuple(zip(line_wave, box_loc)) 406 | 407 | # If any labels are repeated add "_num_#" to it. If there are 3 "X" 408 | # then the first gets "X_num_3". The result is passed as the label 409 | # parameter of annotate. This makes it easy to find the box 410 | # corresponding to a label using Figure.findobj. But the downside is that a 411 | # call to plt.legend() will display legends for the lines (from text to 412 | # spectrum location). So we don't add the label to artists if the user 413 | # doesn't want to. 414 | al = kwargs.get('add_label_to_artists', True) 415 | label_u = unique_labels(line_label1) if al else [None for _ in line_label1] 416 | label_u_line = [i + "_line" for i in label_u] if al else label_u 417 | 418 | if annotate_kwargs is None: 419 | annotate_kwargs = {} 420 | if plot_kwargs is None: 421 | plot_kwargs = {} 422 | ak = initial_annotate_kwargs() 423 | ak.update(annotate_kwargs) 424 | pk = initial_plot_kwargs() 425 | pk.update(plot_kwargs) 426 | # Draw boxes at initial (x, y) location. 427 | for i in range(nlines): 428 | ax.annotate(line_label1[i], xy=(line_wave[i], arrow_tip[i]), 429 | xytext=(box_loc[i][0], 430 | box_loc[i][1]), 431 | 432 | fontsize=label1_size[i], 433 | label=label_u[i], 434 | **ak) 435 | if extend[i]: 436 | ax.plot([line_wave[i]] * 2, [arrow_tip[i], line_flux[i]], 437 | scalex=False, scaley=False, 438 | label=label_u_line[i], 439 | **pk) 440 | 441 | # Draw the figure so that get_window_extent() below works. 442 | fig.canvas.draw() 443 | 444 | # Get annotation boxes and convert their dimensions from display 445 | # coordinates to data coordinates. Specifically, we want the width 446 | # in wavelength units. For each annotation box, transform the 447 | # bounding box into data coordinates and extract the width. 448 | ax_inv_trans = ax.transData.inverted() # display to data 449 | box_widths = [] # box width in wavelength units. 450 | for box in ax.texts: 451 | b_ext = box.get_window_extent() 452 | box_widths.append(b_ext.transformed(ax_inv_trans).width) 453 | 454 | # Find final x locations of boxes so that they don't overlap. 455 | # Function adjust_boxes uses a direct translation of the equivalent 456 | # code in lineid_plot.pro in IDLASTRO. 457 | max_iter = kwargs.get('max_iter', 1000) 458 | adjust_factor = kwargs.get('adjust_factor', 0.35) 459 | factor_decrement = kwargs.get('factor_decrement', 3.0) 460 | wlp, niter, changed = adjust_boxes(line_wave, box_widths, 461 | np.min(wave), np.max(wave), 462 | adjust_factor=adjust_factor, 463 | factor_decrement=factor_decrement, 464 | max_iter=max_iter) 465 | 466 | # Redraw the boxes at their new x location. 467 | for i in range(nlines): 468 | box = ax.texts[i] 469 | if hasattr(box, 'xyann'): 470 | box.xyann = (wlp[i], box.xyann[1]) 471 | elif hasattr(box, 'xytext'): 472 | box.xytext = (wlp[i], box.xytext[1]) 473 | else: 474 | warnings.warn("Warning: missing xyann and xytext attributes. " 475 | "Your matplotlib version may not be compatible " 476 | "with lineid_plot.") 477 | 478 | # Update the figure 479 | fig.canvas.draw() 480 | 481 | # Return Figure and Axes so that they can be used for further 482 | # manual customization. 483 | return fig, ax 484 | -------------------------------------------------------------------------------- /lineid_plot/utils.py: -------------------------------------------------------------------------------- 1 | """Some utility functions.""" 2 | import matplotlib as mpl 3 | import lineid_plot 4 | from lineid_plot import unique_labels 5 | 6 | 7 | def get_labels(labels): 8 | """Create unique labels.""" 9 | label_u = unique_labels(labels) 10 | label_u_line = [i + "_line" for i in label_u] 11 | return label_u, label_u_line 12 | 13 | 14 | def get_boxes_and_lines(ax, labels): 15 | """Get boxes and lines using labels as id.""" 16 | labels_u, labels_u_line = get_labels(labels) 17 | boxes = ax.findobj(mpl.text.Annotation) 18 | lines = ax.findobj(mpl.lines.Line2D) 19 | lineid_boxes = [] 20 | lineid_lines = [] 21 | 22 | for box in boxes: 23 | l = box.get_label() 24 | try: 25 | loc = labels_u.index(l) 26 | except ValueError: 27 | # this box is either one not added by lineidplot or has no label. 28 | continue 29 | lineid_boxes.append(box) 30 | 31 | for line in lines: 32 | l = line.get_label() 33 | try: 34 | loc = labels_u_line.index(l) 35 | except ValueError: 36 | # this line is either one not added by lineidplot or has no label. 37 | continue 38 | lineid_lines.append(line) 39 | 40 | return lineid_boxes, lineid_lines 41 | 42 | 43 | def color_text_boxes(ax, labels, colors, color_arrow=True): 44 | """Color text boxes. 45 | 46 | Instead of this function, one can pass annotate_kwargs and plot_kwargs to 47 | plot_line_ids function. 48 | """ 49 | assert len(labels) == len(colors), \ 50 | "Equal no. of colors and lables must be given" 51 | boxes = ax.findobj(mpl.text.Annotation) 52 | box_labels = lineid_plot.unique_labels(labels) 53 | for box in boxes: 54 | l = box.get_label() 55 | try: 56 | loc = box_labels.index(l) 57 | except ValueError: 58 | continue # No changes for this box 59 | box.set_color(colors[loc]) 60 | if color_arrow: 61 | box.arrow_patch.set_color(colors[loc]) 62 | 63 | ax.figure.canvas.draw() 64 | 65 | 66 | def color_lines(ax, labels, colors): 67 | """Color lines. 68 | 69 | Instead of this function, one can pass annotate_kwargs and plot_kwargs to 70 | plot_line_ids function. 71 | """ 72 | assert len(labels) == len(colors), \ 73 | "Equal no. of colors and lables must be given" 74 | lines = ax.findobj(mpl.lines.Line2D) 75 | line_labels = [i + "_line" for i in lineid_plot.unique_labels(labels)] 76 | for line in lines: 77 | l = line.get_label() 78 | try: 79 | loc = line_labels.index(l) 80 | except ValueError: 81 | continue # No changes for this line 82 | line.set_color(colors[loc]) 83 | 84 | ax.figure.canvas.draw() 85 | -------------------------------------------------------------------------------- /lineid_plot/version.py: -------------------------------------------------------------------------------- 1 | """Version information.""" 2 | 3 | # -*- coding:utf-8 -*- 4 | 5 | __version__ = '0.6' 6 | __author__ = "Prasanth Nair" 7 | __license__ = 'BSD' 8 | -------------------------------------------------------------------------------- /multi_axes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/multi_axes.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Lineid_plot setup.py.""" 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | # Read version.py 6 | __version__ = None 7 | curdir = os.path.dirname(__file__) 8 | with open(os.path.join(curdir, 'lineid_plot', 'version.py')) as f: 9 | exec(f.read()) 10 | 11 | setup( 12 | name="lineid_plot", 13 | version=__version__, 14 | description="Automatic placement of labels in a plot.", 15 | license='BSD', 16 | author="Prasanth Nair", 17 | author_email="prasanthhn@gmail.com", 18 | url='https://github.com/phn/lineid_plot', 19 | packages=find_packages(), 20 | classifiers=[ 21 | 'Development Status :: 6 - Mature', 22 | 'Intended Audience :: Science/Research', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Topic :: Scientific/Engineering :: Astronomy', 25 | 'Topic :: Scientific/Engineering :: Physics', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Programming Language :: Python :: 3.6', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /simple_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/simple_plot.png -------------------------------------------------------------------------------- /tests/baseline/test_access_a_specific_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_access_a_specific_label.png -------------------------------------------------------------------------------- /tests/baseline/test_annotate_kwargs_and_plot_kwargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_annotate_kwargs_and_plot_kwargs.png -------------------------------------------------------------------------------- /tests/baseline/test_custom_y_loc_for_annotation_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_custom_y_loc_for_annotation_point.png -------------------------------------------------------------------------------- /tests/baseline/test_custom_y_loc_for_annotation_point_each_label_sep_loc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_custom_y_loc_for_annotation_point_each_label_sep_loc.png -------------------------------------------------------------------------------- /tests/baseline/test_custom_y_loc_for_label_boxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_custom_y_loc_for_label_boxes.png -------------------------------------------------------------------------------- /tests/baseline/test_custom_y_loc_for_label_boxes_each_box_sep_loc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_custom_y_loc_for_label_boxes_each_box_sep_loc.png -------------------------------------------------------------------------------- /tests/baseline/test_customize_box_and_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_customize_box_and_line.png -------------------------------------------------------------------------------- /tests/baseline/test_max_iter_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_max_iter_large.png -------------------------------------------------------------------------------- /tests/baseline/test_max_iter_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_max_iter_small.png -------------------------------------------------------------------------------- /tests/baseline/test_minimal_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_minimal_plot.png -------------------------------------------------------------------------------- /tests/baseline/test_multi_plot_user_axes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_multi_plot_user_axes.png -------------------------------------------------------------------------------- /tests/baseline/test_no_line_from_annotation_to_flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_no_line_from_annotation_to_flux.png -------------------------------------------------------------------------------- /tests/baseline/test_small_change_to_y_loc_of_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phn/lineid_plot/7c7a1af53fe439b3a7c5a57f01680575837fb978/tests/baseline/test_small_change_to_y_loc_of_label.png -------------------------------------------------------------------------------- /tests/matplotlibrc: -------------------------------------------------------------------------------- 1 | ### MATPLOTLIBRC FORMAT 2 | 3 | # This is a sample matplotlib configuration file - you can find a copy 4 | # of it on your system in 5 | # site-packages/matplotlib/mpl-data/matplotlibrc. If you edit it 6 | # there, please note that it will be overridden in your next install. 7 | # If you want to keep a permanent local copy that will not be 8 | # over-written, place it in HOME/.matplotlib/matplotlibrc (unix/linux 9 | # like systems) and C:\Documents and Settings\yourname\.matplotlib 10 | # (win32 systems). 11 | # 12 | # This file is best viewed in a editor which supports python mode 13 | # syntax highlighting. Blank lines, or lines starting with a comment 14 | # symbol, are ignored, as are trailing comments. Other lines must 15 | # have the format 16 | # key : val # optional comment 17 | # 18 | # Colors: for the color values below, you can either use - a 19 | # matplotlib color string, such as r, k, or b - an rgb tuple, such as 20 | # (1.0, 0.5, 0.0) - a hex string, such as ff00ff or #ff00ff - a scalar 21 | # grayscale intensity such as 0.75 - a legal html color name, eg red, 22 | # blue, darkslategray 23 | 24 | #### CONFIGURATION BEGINS HERE 25 | 26 | # the default backend; one of GTK GTKAgg GTKCairo CocoaAgg FltkAgg 27 | # MacOSX QtAgg Qt4Agg TkAgg WX WXAgg Agg Cairo GDK PS PDF SVG Template 28 | # You can also deploy your own backend outside of matplotlib by 29 | # referring to the module name (which must be in the PYTHONPATH) as 30 | # 'module://my_backend' 31 | backend : Agg 32 | 33 | # if you are runing pyplot inside a GUI and your backend choice 34 | # conflicts, we will automatically try and find a compatible one for 35 | # you if backend_fallback is True 36 | #backend_fallback: True 37 | #interactive : False 38 | #toolbar : toolbar2 # None | classic | toolbar2 39 | #timezone : UTC # a pytz timezone string, eg US/Central or Europe/Paris 40 | 41 | # Where your matplotlib data lives if you installed to a non-default 42 | # location. This is where the matplotlib fonts, bitmaps, etc reside 43 | #datapath : /home/jdhunter/mpldata 44 | 45 | 46 | ### LINES 47 | # See http://matplotlib.sourceforge.net/api/artist_api.html#module-matplotlib.lines for more 48 | # information on line properties. 49 | #lines.linewidth : 1.0 # line width in points 50 | #lines.linestyle : - # solid line 51 | #lines.color : blue 52 | #lines.marker : None # the default marker 53 | #lines.markeredgewidth : 0.5 # the line width around the marker symbol 54 | #lines.markersize : 6 # markersize, in points 55 | #lines.dash_joinstyle : miter # miter|round|bevel 56 | #lines.dash_capstyle : butt # butt|round|projecting 57 | #lines.solid_joinstyle : miter # miter|round|bevel 58 | #lines.solid_capstyle : projecting # butt|round|projecting 59 | #lines.antialiased : True # render lines in antialised (no jaggies) 60 | 61 | ### PATCHES 62 | # Patches are graphical objects that fill 2D space, like polygons or 63 | # circles. See 64 | # http://matplotlib.sourceforge.net/api/artist_api.html#module-matplotlib.patches 65 | # information on patch properties 66 | #patch.linewidth : 1.0 # edge width in points 67 | #patch.facecolor : blue 68 | #patch.edgecolor : black 69 | #patch.antialiased : True # render patches in antialised (no jaggies) 70 | 71 | ### FONT 72 | # 73 | # font properties used by text.Text. See 74 | # http://matplotlib.sourceforge.net/api/font_manager_api.html for more 75 | # information on font properties. The 6 font properties used for font 76 | # matching are given below with their default values. 77 | # 78 | # The font.family property has five values: 'serif' (e.g. Times), 79 | # 'sans-serif' (e.g. Helvetica), 'cursive' (e.g. Zapf-Chancery), 80 | # 'fantasy' (e.g. Western), and 'monospace' (e.g. Courier). Each of 81 | # these font families has a default list of font names in decreasing 82 | # order of priority associated with them. 83 | # 84 | # The font.style property has three values: normal (or roman), italic 85 | # or oblique. The oblique style will be used for italic, if it is not 86 | # present. 87 | # 88 | # The font.variant property has two values: normal or small-caps. For 89 | # TrueType fonts, which are scalable fonts, small-caps is equivalent 90 | # to using a font size of 'smaller', or about 83% of the current font 91 | # size. 92 | # 93 | # The font.weight property has effectively 13 values: normal, bold, 94 | # bolder, lighter, 100, 200, 300, ..., 900. Normal is the same as 95 | # 400, and bold is 700. bolder and lighter are relative values with 96 | # respect to the current weight. 97 | # 98 | # The font.stretch property has 11 values: ultra-condensed, 99 | # extra-condensed, condensed, semi-condensed, normal, semi-expanded, 100 | # expanded, extra-expanded, ultra-expanded, wider, and narrower. This 101 | # property is not currently implemented. 102 | # 103 | # The font.size property is the default font size for text, given in pts. 104 | # 12pt is the standard value. 105 | # 106 | #font.family : sans-serif 107 | #font.style : normal 108 | #font.variant : normal 109 | #font.weight : medium 110 | #font.stretch : normal 111 | # note that font.size controls default text sizes. To configure 112 | # special text sizes tick labels, axes, labels, title, etc, see the rc 113 | # settings for axes and ticks. Special text sizes can be defined 114 | # relative to font.size, using the following values: xx-small, x-small, 115 | # small, medium, large, x-large, xx-large, larger, or smaller 116 | #font.size : 12.0 117 | #font.serif : Bitstream Vera Serif, New Century Schoolbook, Century Schoolbook L, Utopia, ITC Bookman, Bookman, Nimbus Roman No9 L, Times New Roman, Times, Palatino, Charter, serif 118 | #font.sans-serif : Bitstream Vera Sans, Lucida Grande, Verdana, Geneva, Lucid, Arial, Helvetica, Avant Garde, sans-serif 119 | #font.cursive : Apple Chancery, Textile, Zapf Chancery, Sand, cursive 120 | #font.fantasy : Comic Sans MS, Chicago, Charcoal, Impact, Western, fantasy 121 | #font.monospace : Bitstream Vera Sans Mono, Andale Mono, Nimbus Mono L, Courier New, Courier, Fixed, Terminal, monospace 122 | 123 | ### TEXT 124 | # text properties used by text.Text. See 125 | # http://matplotlib.sourceforge.net/api/artist_api.html#module-matplotlib.text for more 126 | # information on text properties 127 | 128 | #text.color : black 129 | 130 | ### LaTeX customizations. See http://www.scipy.org/Wiki/Cookbook/Matplotlib/UsingTex 131 | #text.usetex : False # use latex for all text handling. The following fonts 132 | # are supported through the usual rc parameter settings: 133 | # new century schoolbook, bookman, times, palatino, 134 | # zapf chancery, charter, serif, sans-serif, helvetica, 135 | # avant garde, courier, monospace, computer modern roman, 136 | # computer modern sans serif, computer modern typewriter 137 | # If another font is desired which can loaded using the 138 | # LaTeX \usepackage command, please inquire at the 139 | # matplotlib mailing list 140 | #text.latex.unicode : False # use "ucs" and "inputenc" LaTeX packages for handling 141 | # unicode strings. 142 | #text.latex.preamble : # IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES 143 | # AND IS THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP 144 | # IF THIS FEATURE DOES NOT DO WHAT YOU EXPECT IT TO. 145 | # preamble is a comma separated list of LaTeX statements 146 | # that are included in the LaTeX document preamble. 147 | # An example: 148 | # text.latex.preamble : \usepackage{bm},\usepackage{euler} 149 | # The following packages are always loaded with usetex, so 150 | # beware of package collisions: color, geometry, graphicx, 151 | # type1cm, textcomp. Adobe Postscript (PSSNFS) font packages 152 | # may also be loaded, depending on your font settings 153 | 154 | #text.dvipnghack : None # some versions of dvipng don't handle alpha 155 | # channel properly. Use True to correct 156 | # and flush ~/.matplotlib/tex.cache 157 | # before testing and False to force 158 | # correction off. None will try and 159 | # guess based on your dvipng version 160 | 161 | #text.markup : 'plain' # Affects how text, such as titles and labels, are 162 | # interpreted by default. 163 | # 'plain': As plain, unformatted text 164 | # 'tex': As TeX-like text. Text between $'s will be 165 | # formatted as a TeX math expression. 166 | # This setting has no effect when text.usetex is True. 167 | # In that case, all text will be sent to TeX for 168 | # processing. 169 | 170 | # The following settings allow you to select the fonts in math mode. 171 | # They map from a TeX font name to a fontconfig font pattern. 172 | # These settings are only used if mathtext.fontset is 'custom'. 173 | # Note that this "custom" mode is unsupported and may go away in the 174 | # future. 175 | #mathtext.cal : cursive 176 | #mathtext.rm : serif 177 | #mathtext.tt : monospace 178 | #mathtext.it : serif:italic 179 | #mathtext.bf : serif:bold 180 | #mathtext.sf : sans 181 | #mathtext.fontset : cm # Should be 'cm' (Computer Modern), 'stix', 182 | # 'stixsans' or 'custom' 183 | #mathtext.fallback_to_cm : True # When True, use symbols from the Computer Modern 184 | # fonts when a symbol can not be found in one of 185 | # the custom math fonts. 186 | 187 | #mathtext.default : it # The default font to use for math. 188 | # Can be any of the LaTeX font names, including 189 | # the special name "regular" for the same font 190 | # used in regular text. 191 | 192 | ### AXES 193 | # default face and edge color, default tick sizes, 194 | # default fontsizes for ticklabels, and so on. See 195 | # http://matplotlib.sourceforge.net/api/axes_api.html#module-matplotlib.axes 196 | #axes.hold : True # whether to clear the axes by default on 197 | #axes.facecolor : white # axes background color 198 | #axes.edgecolor : black # axes edge color 199 | #axes.linewidth : 1.0 # edge linewidth 200 | #axes.grid : False # display grid or not 201 | #axes.titlesize : large # fontsize of the axes title 202 | #axes.labelsize : medium # fontsize of the x any y labels 203 | #axes.labelcolor : black 204 | #axes.axisbelow : False # whether axis gridlines and ticks are below 205 | # the axes elements (lines, text, etc) 206 | #axes.formatter.limits : -7, 7 # use scientific notation if log10 207 | # of the axis range is smaller than the 208 | # first or larger than the second 209 | #axes.unicode_minus : True # use unicode for the minus symbol 210 | # rather than hypen. See http://en.wikipedia.org/wiki/Plus_sign#Plus_sign 211 | 212 | #polaraxes.grid : True # display grid on polar axes 213 | #axes3d.grid : True # display grid on 3d axes 214 | 215 | ### TICKS 216 | # see http://matplotlib.sourceforge.net/api/axis_api.html#matplotlib.axis.Tick 217 | #xtick.major.size : 4 # major tick size in points 218 | #xtick.minor.size : 2 # minor tick size in points 219 | #xtick.major.pad : 4 # distance to major tick label in points 220 | #xtick.minor.pad : 4 # distance to the minor tick label in points 221 | #xtick.color : k # color of the tick labels 222 | #xtick.labelsize : medium # fontsize of the tick labels 223 | #xtick.direction : in # direction: in or out 224 | 225 | #ytick.major.size : 4 # major tick size in points 226 | #ytick.minor.size : 2 # minor tick size in points 227 | #ytick.major.pad : 4 # distance to major tick label in points 228 | #ytick.minor.pad : 4 # distance to the minor tick label in points 229 | #ytick.color : k # color of the tick labels 230 | #ytick.labelsize : medium # fontsize of the tick labels 231 | #ytick.direction : in # direction: in or out 232 | 233 | 234 | ### GRIDS 235 | #grid.color : black # grid color 236 | #grid.linestyle : : # dotted 237 | #grid.linewidth : 0.5 # in points 238 | 239 | ### Legend 240 | #legend.fancybox : False # if True, use a rounded box for the 241 | # legend, else a rectangle 242 | #legend.isaxes : True 243 | #legend.numpoints : 2 # the number of points in the legend line 244 | #legend.fontsize : large 245 | #legend.pad : 0.0 # deprecated; the fractional whitespace inside the legend border 246 | #legend.borderpad : 0.5 # border whitspace in fontsize units 247 | #legend.markerscale : 1.0 # the relative size of legend markers vs. original 248 | # the following dimensions are in axes coords 249 | #legend.labelsep : 0.010 # the vertical space between the legend entries 250 | #legend.handlelen : 0.05 # the length of the legend lines 251 | #legend.handletextsep : 0.02 # the space between the legend line and legend text 252 | #legend.axespad : 0.02 # the border between the axes and legend edge 253 | #legend.shadow : False 254 | 255 | ### FIGURE 256 | # See http://matplotlib.sourceforge.net/api/figure_api.html#matplotlib.figure.Figure 257 | #figure.figsize : 8, 6 # figure size in inches 258 | #figure.dpi : 80 # figure dots per inch 259 | #figure.facecolor : 0.75 # figure facecolor; 0.75 is scalar gray 260 | #figure.edgecolor : white # figure edgecolor 261 | 262 | # The figure subplot parameters. All dimensions are fraction of the 263 | # figure width or height 264 | #figure.subplot.left : 0.125 # the left side of the subplots of the figure 265 | #figure.subplot.right : 0.9 # the right side of the subplots of the figure 266 | #figure.subplot.bottom : 0.1 # the bottom of the subplots of the figure 267 | #figure.subplot.top : 0.9 # the top of the subplots of the figure 268 | #figure.subplot.wspace : 0.2 # the amount of width reserved for blank space between subplots 269 | #figure.subplot.hspace : 0.2 # the amount of height reserved for white space between subplots 270 | 271 | ### IMAGES 272 | #image.aspect : equal # equal | auto | a number 273 | #image.interpolation : bilinear # see help(imshow) for options 274 | #image.cmap : jet # gray | jet etc... 275 | #image.lut : 256 # the size of the colormap lookup table 276 | image.origin : upper # lower | upper 277 | #image.resample : False 278 | 279 | ### CONTOUR PLOTS 280 | #contour.negative_linestyle : dashed # dashed | solid 281 | 282 | ### Agg rendering 283 | ### Warning: experimental, 2008/10/10 284 | #agg.path.chunksize : 0 # 0 to disable; values in the range 285 | # 10000 to 100000 can improve speed slightly 286 | # and prevent an Agg rendering failure 287 | # when plotting very large data sets, 288 | # especially if they are very gappy. 289 | # It may cause minor artifacts, though. 290 | # A value of 20000 is probably a good 291 | # starting point. 292 | ### SAVING FIGURES 293 | #path.simplify : False # When True, simplify paths by removing "invisible" 294 | # points to reduce file size and increase rendering 295 | # speed 296 | #path.simplify_threshold : 0.1 # The threshold of similarity below which 297 | # vertices will be removed in the simplification 298 | # process 299 | 300 | # the default savefig params can be different from the display params 301 | # Eg, you may want a higher resolution, or to make the figure 302 | # background white 303 | #savefig.dpi : 100 # figure dots per inch 304 | #savefig.facecolor : white # figure facecolor when saving 305 | #savefig.edgecolor : white # figure edgecolor when saving 306 | 307 | #cairo.format : png # png, ps, pdf, svg 308 | 309 | # tk backend params 310 | #tk.window_focus : False # Maintain shell focus for TkAgg 311 | #tk.pythoninspect : False # tk sets PYTHONINSEPCT 312 | 313 | # ps backend params 314 | #ps.papersize : letter # auto, letter, legal, ledger, A0-A10, B0-B10 315 | #ps.useafm : False # use of afm fonts, results in small files 316 | #ps.usedistiller : False # can be: None, ghostscript or xpdf 317 | # Experimental: may produce smaller files. 318 | # xpdf intended for production of publication quality files, 319 | # but requires ghostscript, xpdf and ps2eps 320 | #ps.distiller.res : 6000 # dpi 321 | #ps.fonttype : 3 # Output Type 3 (Type3) or Type 42 (TrueType) 322 | 323 | # pdf backend params 324 | #pdf.compression : 6 # integer from 0 to 9 325 | # 0 disables compression (good for debugging) 326 | #pdf.fonttype : 3 # Output Type 3 (Type3) or Type 42 (TrueType) 327 | 328 | # svg backend params 329 | #svg.image_inline : True # write raster image data directly into the svg file 330 | #svg.image_noscale : False # suppress scaling of raster data embedded in SVG 331 | #svg.embed_char_paths : True # embed character outlines in the SVG file 332 | 333 | # docstring params 334 | #docstring.hardcopy = False # set this when you want to generate hardcopy docstring 335 | 336 | # Set the verbose flags. This controls how much information 337 | # matplotlib gives you at runtime and where it goes. The verbosity 338 | # levels are: silent, helpful, debug, debug-annoying. Any level is 339 | # inclusive of all the levels below it. If your setting is "debug", 340 | # you'll get all the debug and helpful messages. When submitting 341 | # problems to the mailing-list, please set verbose to "helpful" or "debug" 342 | # and paste the output into your report. 343 | # 344 | # The "fileo" gives the destination for any calls to verbose.report. 345 | # These objects can a filename, or a filehandle like sys.stdout. 346 | # 347 | # You can override the rc default verbosity from the command line by 348 | # giving the flags --verbose-LEVEL where LEVEL is one of the legal 349 | # levels, eg --verbose-helpful. 350 | # 351 | # You can access the verbose instance in your code 352 | # from matplotlib import verbose. 353 | #verbose.level : silent # one of silent, helpful, debug, debug-annoying 354 | #verbose.fileo : sys.stdout # a log filename, sys.stdout or sys.stderr 355 | -------------------------------------------------------------------------------- /tests/test_lineid_plot.py: -------------------------------------------------------------------------------- 1 | """Some tests for lineid_plot.""" 2 | import numpy as np 3 | import matplotlib as mpl 4 | from matplotlib import pyplot as plt 5 | import pytest 6 | 7 | import lineid_plot 8 | 9 | RFLUX = np.random.RandomState(seed=123).normal(size=300) 10 | 11 | 12 | def test_unique_labels(): 13 | """Make sure we can create unique labels.""" 14 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 15 | x = ['N V', 'Si II_num_1', 'Si II_num_2', 'Si II_num_3', 'Si II_num_4', 16 | 'Si II_num_5', 'Si II_num_6'] 17 | assert lineid_plot.unique_labels(line_label1) == x 18 | 19 | 20 | @pytest.mark.mpl_image_compare 21 | def test_minimal_plot(): 22 | """Test a minimal plot.""" 23 | wave = 1240 + np.arange(300) * 0.1 24 | flux = RFLUX 25 | 26 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 27 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 28 | 29 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 30 | 31 | return fig 32 | 33 | 34 | @pytest.mark.mpl_image_compare 35 | def test_no_line_from_annotation_to_flux(): 36 | """Must create plot with no line from annotation to flux point.""" 37 | wave = 1240 + np.arange(300) * 0.1 38 | flux = RFLUX 39 | 40 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 41 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 42 | 43 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, extend=False) 44 | return fig 45 | 46 | 47 | @pytest.mark.mpl_image_compare 48 | def test_multi_plot_user_axes(): 49 | """User can supply custom axes.""" 50 | wave = 1240 + np.arange(300) * 0.1 51 | flux = RFLUX 52 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 53 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 54 | 55 | fig = plt.figure() 56 | 57 | # First Axes 58 | ax = fig.add_axes([0.1, 0.06, 0.85, 0.35]) 59 | ax.plot(wave, flux) 60 | 61 | # Pass the Axes instance to the plot_line_ids function. 62 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax) 63 | 64 | # Second Axes 65 | ax1 = fig.add_axes([0.1, 0.55, 0.85, 0.35]) 66 | ax1.plot(wave, flux) 67 | 68 | # Pass the Axes instance to the plot_line_ids function. 69 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, ax=ax1) 70 | 71 | return fig 72 | 73 | 74 | @pytest.mark.mpl_image_compare 75 | def test_annotate_kwargs_and_plot_kwargs(): 76 | """User can supply custom annotate and plot kwargs.""" 77 | wave = 1240 + np.arange(300) * 0.1 78 | flux = RFLUX 79 | 80 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 81 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 82 | 83 | ak = lineid_plot.initial_annotate_kwargs() 84 | ak['arrowprops']['arrowstyle'] = "->" 85 | 86 | pk = lineid_plot.initial_plot_kwargs() 87 | pk['color'] = "red" 88 | 89 | fig, ax = lineid_plot.plot_line_ids( 90 | wave, flux, line_wave, line_label1, annotate_kwargs=ak, plot_kwargs=pk) 91 | 92 | return fig 93 | 94 | 95 | @pytest.mark.mpl_image_compare 96 | def test_customize_box_and_line(): 97 | """User can change box and line aspects after plotting.""" 98 | wave = 1240 + np.arange(300) * 0.1 99 | flux = RFLUX 100 | 101 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 102 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 103 | 104 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1) 105 | 106 | b = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1')[0] 107 | b.set_rotation(0) 108 | b.set_text("Si II$\lambda$1260.42") 109 | 110 | line = ax.findobj(match=lambda x: x.get_label() == 'Si II_num_1_line')[0] 111 | line.set_color("red") 112 | line.set_linestyle("-") 113 | 114 | return fig 115 | 116 | 117 | @pytest.mark.mpl_image_compare 118 | def test_small_change_to_y_loc_of_label(): 119 | """User can make small changes to y_loc_of_label.""" 120 | wave = 1240 + np.arange(300) * 0.1 121 | flux = RFLUX 122 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 123 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 124 | 125 | fig, ax = lineid_plot.plot_line_ids( 126 | wave, flux, line_wave, line_label1, 127 | box_axes_space=0.08) 128 | 129 | return fig 130 | 131 | 132 | @pytest.mark.mpl_image_compare 133 | def test_custom_y_loc_for_annotation_point(): 134 | """User cna supply custom y_loc for annotation point.""" 135 | wave = 1240 + np.arange(300) * 0.1 136 | flux = RFLUX 137 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 138 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 139 | 140 | fig = plt.figure() 141 | ax = fig.add_subplot(111) 142 | ax.plot(wave, flux) 143 | ax.axis([1240, 1270, -3, 5]) 144 | lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, arrow_tip=3.3, ax=ax) 145 | 146 | return fig 147 | 148 | 149 | @pytest.mark.mpl_image_compare 150 | def test_custom_y_loc_for_annotation_point_each_label_sep_loc(): 151 | """User can specific y_loc for each label.""" 152 | wave = 1240 + np.arange(300) * 0.1 153 | flux = RFLUX 154 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 155 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 156 | 157 | fig = plt.figure() 158 | ax = fig.add_subplot(111) 159 | ax.plot(wave, flux) 160 | ax.axis([1240, 1270, -3, 5]) 161 | 162 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 163 | lineid_plot.plot_line_ids( 164 | wave, flux, line_wave, line_label1, arrow_tip=arrow_tips, ax=ax) 165 | 166 | return fig 167 | 168 | 169 | @pytest.mark.mpl_image_compare 170 | def test_custom_y_loc_for_label_boxes(): 171 | """User can specify y_loc for label box.""" 172 | wave = 1240 + np.arange(300) * 0.1 173 | flux = RFLUX 174 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 175 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 176 | 177 | fig = plt.figure() 178 | ax = fig.add_subplot(111) 179 | ax.plot(wave, flux) 180 | ax.axis([1240, 1270, -3, 5]) 181 | lineid_plot.plot_line_ids( 182 | wave, flux, line_wave, line_label1, arrow_tip=3.3, ax=ax, box_loc=4.3) 183 | 184 | return fig 185 | 186 | 187 | @pytest.mark.mpl_image_compare 188 | def test_custom_y_loc_for_label_boxes_each_box_sep_loc(): 189 | """User can specify y_loc for each box.""" 190 | wave = 1240 + np.arange(300) * 0.1 191 | flux = RFLUX 192 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 193 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 194 | 195 | fig = plt.figure() 196 | ax = fig.add_subplot(111) 197 | ax.plot(wave, flux) 198 | ax.axis([1240, 1270, -3, 5]) 199 | 200 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 201 | box_loc = [4.3, 4.3, 4.3, 4.4, 4.5, 4.4, 4.3] 202 | lineid_plot.plot_line_ids( 203 | wave, flux, line_wave, line_label1, 204 | arrow_tip=arrow_tips, box_loc=box_loc, ax=ax) 205 | 206 | return fig 207 | 208 | 209 | @pytest.mark.mpl_image_compare 210 | def test_access_a_specific_label(): 211 | """User can access each box and line using label.""" 212 | wave = 1240 + np.arange(300) * 0.1 213 | flux = RFLUX 214 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 215 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 216 | 217 | fig = plt.figure() 218 | ax = fig.add_subplot(111) 219 | ax.plot(wave, flux) 220 | ax.axis([1240, 1270, -3, 5]) 221 | 222 | arrow_tips = [3.3, 3.3, 3.3, 3.4, 3.5, 3.4, 3.3] 223 | box_loc = [4.3, 4.3, 4.3, 4.4, 4.5, 4.4, 4.3] 224 | lineid_plot.plot_line_ids( 225 | wave, flux, line_wave, line_label1, 226 | arrow_tip=arrow_tips, box_loc=box_loc, ax=ax) 227 | 228 | a = ax.findobj(mpl.text.Annotation) 229 | for i in a: 230 | if i.get_label() == "Si II_num_4": 231 | i.set_visible(False) 232 | 233 | a = ax.findobj(mpl.lines.Line2D) 234 | for i in a: 235 | if i.get_label() == "Si II_num_4_line": 236 | i.set_visible(False) 237 | 238 | return fig 239 | 240 | 241 | @pytest.mark.mpl_image_compare 242 | def test_max_iter_small(): 243 | """User can specify small max_iter.""" 244 | wave = 1240 + np.arange(300) * 0.1 245 | flux = RFLUX 246 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 247 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 248 | 249 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, max_iter=10) 250 | 251 | return fig 252 | 253 | 254 | @pytest.mark.mpl_image_compare 255 | def test_max_iter_large(): 256 | """User can specify large max_iter.""" 257 | wave = 1240 + np.arange(300) * 0.1 258 | flux = RFLUX 259 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 260 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 261 | 262 | fig, ax = lineid_plot.plot_line_ids(wave, flux, line_wave, line_label1, max_iter=300) 263 | 264 | return fig 265 | 266 | 267 | def test_dont_add_label_to_artists(): 268 | """User can choose to not add labels to artists.""" 269 | wave = 1240 + np.arange(300) * 0.1 270 | flux = RFLUX 271 | line_wave = [1242.80, 1260.42, 1264.74, 1265.00, 1265.2, 1265.3, 1265.35] 272 | line_label1 = ['N V', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II', 'Si II'] 273 | 274 | fig, ax = lineid_plot.plot_line_ids( 275 | wave, flux, line_wave, line_label1, max_iter=300, add_label_to_artists=False 276 | ) 277 | 278 | labels = lineid_plot.unique_labels(line_label1) 279 | for label in labels: 280 | assert fig.findobj(match=lambda x: x.get_label() == label) == [] 281 | assert fig.findobj(match=lambda x: x.get_label() == label + "_line") == [] 282 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,36}-mpl{21,22} 3 | 4 | [testenv] 5 | setenv = MPLCONFIGDIR=./tests 6 | deps = 7 | numpy 8 | pytest 9 | pytest-mpl 10 | mpl15: matplotlib==2.1.0 11 | mpl22: matplotlib==2.2.0 12 | commands = py.test --mpl --mpl-baseline-path=tests/baseline tests/ 13 | 14 | --------------------------------------------------------------------------------