Coverage for python/lsst/analysis/tools/actions/plot/diaSkyPlot.py: 26%
68 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-09 04:18 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-09 04:18 -0700
1# This file is part of analysis_tools.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ("DiaSkyPanel", "DiaSkyPlot")
24from typing import Mapping
26import matplotlib.pyplot as plt
27from lsst.pex.config import ConfigDictField, Field, ListField
28from matplotlib.figure import Figure
30from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Vector
31from .plotUtils import PanelConfig
34class DiaSkyPanel(PanelConfig):
35 """Configuration options for DiaSkyPlot panels."""
37 xlabel = Field[str](
38 doc="Panel x-axis label.",
39 default="RA (deg)",
40 )
41 ylabel = Field[str](
42 doc="Panel y-axis label.",
43 default="Dec (deg)",
44 )
45 invertXAxis = Field[bool](
46 doc="Invert x-axis?",
47 default=True,
48 )
49 size = Field[float](
50 doc="Point size",
51 default=20,
52 )
53 alpha = Field[float](
54 doc="Point transparency",
55 default=0.5,
56 )
57 # Eventually we might retrieve data from more columns to make the plot
58 # prettier/more information rich
59 ras = ListField[str](
60 doc="Names of RA columns",
61 optional=False,
62 )
63 decs = ListField[str](
64 doc="Names of Dec columns",
65 optional=False,
66 )
67 colorList = Field[str](
68 doc="Colors for the points",
69 optional=True,
70 )
71 legendLabels = ListField[str](
72 doc="Labels for the legend",
73 optional=True,
74 )
77class DiaSkyPlot(PlotAction):
78 """Generic pseudo base class for plotting DiaSources
79 (or DiaObjects) on the sky.
80 """
82 panels = ConfigDictField(
83 doc="A configurable dict describing the panels to be plotted (both data columns and layouts).",
84 keytype=str,
85 itemtype=DiaSkyPanel,
86 default={},
87 )
89 def getInputSchema(self, **kwargs) -> KeyedDataSchema:
90 """Defines the schema this plot action expects (the keys it looks
91 for and what type they should be). In other words, verifies that
92 the input data has the columns we are expecting with the right dtypes.
93 """
94 for ra in self.panels.ras.values():
95 yield (ra, Vector)
96 for dec in self.panels.decs.values():
97 yield (dec, Vector)
99 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure:
100 return self.makePlot(data, **kwargs)
102 def makePlot(self, data: KeyedData, **kwargs) -> Figure:
103 """Make an N-panel plot with locations of DiaSources or
104 DiaObjects displayed in each panel.
106 Parameters
107 ----------
108 data : `lsst.analysis.tools.interfaces.KeyedData`
110 Returns
111 -------
112 fig : `matplotlib.figure.Figure`
113 """
114 if "figsize" in kwargs:
115 figsize = kwargs.pop("figsize", "")
116 fig = plt.figure(figsize=figsize, dpi=300)
117 else:
118 fig = plt.figure(figsize=(12, 9), dpi=300)
119 axs = self._makeAxes(fig)
120 for panel, ax in zip(self.panels.values(), axs):
121 self._makePanel(data, panel, ax, **kwargs)
122 plt.draw()
123 return fig
125 def _makeAxes(self, fig):
126 """Determine axes layout for main figure.
128 Use matplotlib's subplot2grid to determine the panel geometry,
129 which calls gridspec.
131 Parameters
132 ----------
133 fig : `matplotlib.figure.Figure`
135 Returns
136 -------
137 axs : `list` containing one or more matplotlib axes, one for each panel
138 """
139 axs = []
140 for count, panel in enumerate(self.panels.values()):
141 subplot2gridShape = (panel.subplot2gridShapeRow, panel.subplot2gridShapeColumn)
142 subplot2gridLoc = (panel.subplot2gridLocRow, panel.subplot2gridLocColumn)
143 axs.append(
144 plt.subplot2grid(
145 subplot2gridShape,
146 subplot2gridLoc,
147 rowspan=panel.subplot2gridRowspan,
148 colspan=panel.subplot2gridColspan,
149 )
150 )
151 return axs
153 def _makePanel(self, data, panel, ax, **kwargs):
154 """Plot a single panel.
156 Parameters
157 ----------
158 data : `lsst.analysis.tools.interfaces.KeyedData`
159 panel : `DiaSkyPanel`
160 ax : matplotlib axis
161 color : `str`
162 """
163 artists = [] # Placeholder for each series being plotted
164 for idx, (ra, dec) in enumerate(zip(panel.ras, panel.decs)): # loop over column names (dict keys)
165 if panel.colorList:
166 color = panel.colorList[idx]
167 artist = ax.scatter(
168 data[ra], data[dec], s=panel.size, alpha=panel.alpha, marker=".", linewidths=0, c=color
169 )
170 else: # Use matplotlib default colors
171 artist = ax.scatter(
172 data[ra], data[dec], s=panel.size, alpha=panel.alpha, marker=".", linewidths=0
173 )
174 artists.append(artist)
175 # TODO DM-42768: implement lists of sizes, alphas, etc.
176 # and add better support for multi-panel plots.
178 ax.set_xlabel(panel.xlabel)
179 ax.set_ylabel(panel.ylabel)
180 if panel.legendLabels:
181 ax.legend(artists, panel.legendLabels)
182 if panel.invertXAxis:
183 ax.invert_xaxis()
184 if panel.topSpinesVisible:
185 ax.spines["top"].set_visible(True)
186 else:
187 ax.spines["top"].set_visible(False)
188 if not panel.bottomSpinesVisible: # default is True
189 ax.spines["bottom"].set_visible(False)
190 if not panel.leftSpinesVisible:
191 # Default is True; if False, also put ticks and labels on the right
192 ax.spines["left"].set_visible(False)
193 ax.yaxis.tick_right()
194 ax.yaxis.set_label_position("right")
195 if not panel.rightSpinesVisible: # default is True
196 ax.spines["right"].set_visible(False)