Coverage for python / lsst / analysis / tools / actions / plot / diaSkyPlot.py: 26%
68 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 09:01 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 09:01 +0000
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 collections.abc import Mapping
26import matplotlib.pyplot as plt
27from matplotlib.figure import Figure
29from lsst.pex.config import ConfigDictField, Field, ListField
31from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Vector
32from .plotUtils import PanelConfig
35class DiaSkyPanel(PanelConfig):
36 """Configuration options for DiaSkyPlot panels."""
38 xlabel = Field[str](
39 doc="Panel x-axis label.",
40 default="RA (deg)",
41 )
42 ylabel = Field[str](
43 doc="Panel y-axis label.",
44 default="Dec (deg)",
45 )
46 invertXAxis = Field[bool](
47 doc="Invert x-axis?",
48 default=True,
49 )
50 size = Field[float](
51 doc="Point size",
52 default=20,
53 )
54 alpha = Field[float](
55 doc="Point transparency",
56 default=0.5,
57 )
58 # Eventually we might retrieve data from more columns to make the plot
59 # prettier/more information rich
60 ras = ListField[str](
61 doc="Names of RA columns",
62 optional=False,
63 )
64 decs = ListField[str](
65 doc="Names of Dec columns",
66 optional=False,
67 )
68 colorList = Field[str](
69 doc="Colors for the points",
70 optional=True,
71 )
72 legendLabels = ListField[str](
73 doc="Labels for the legend",
74 optional=True,
75 )
78class DiaSkyPlot(PlotAction):
79 """Generic pseudo base class for plotting DiaSources
80 (or DiaObjects) on the sky.
81 """
83 panels = ConfigDictField(
84 doc="A configurable dict describing the panels to be plotted (both data columns and layouts).",
85 keytype=str,
86 itemtype=DiaSkyPanel,
87 default={},
88 )
90 def getInputSchema(self, **kwargs) -> KeyedDataSchema:
91 """Defines the schema this plot action expects (the keys it looks
92 for and what type they should be). In other words, verifies that
93 the input data has the columns we are expecting with the right dtypes.
94 """
95 for ra in self.panels.ras.values():
96 yield (ra, Vector)
97 for dec in self.panels.decs.values():
98 yield (dec, Vector)
100 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure:
101 return self.makePlot(data, **kwargs)
103 def makePlot(self, data: KeyedData, **kwargs) -> Figure:
104 """Make an N-panel plot with locations of DiaSources or
105 DiaObjects displayed in each panel.
107 Parameters
108 ----------
109 data : `lsst.analysis.tools.interfaces.KeyedData`
111 Returns
112 -------
113 fig : `matplotlib.figure.Figure`
114 """
115 if "figsize" in kwargs:
116 figsize = kwargs.pop("figsize", "")
117 fig = plt.figure(figsize=figsize, dpi=300)
118 else:
119 fig = plt.figure(figsize=(12, 9), dpi=300)
120 axs = self._makeAxes(fig)
121 for panel, ax in zip(self.panels.values(), axs):
122 self._makePanel(data, panel, ax, **kwargs)
123 plt.draw()
124 return fig
126 def _makeAxes(self, fig):
127 """Determine axes layout for main figure.
129 Use matplotlib's subplot2grid to determine the panel geometry,
130 which calls gridspec.
132 Parameters
133 ----------
134 fig : `matplotlib.figure.Figure`
136 Returns
137 -------
138 axs : `list` containing one or more matplotlib axes, one for each panel
139 """
140 axs = []
141 for count, panel in enumerate(self.panels.values()):
142 subplot2gridShape = (panel.subplot2gridShapeRow, panel.subplot2gridShapeColumn)
143 subplot2gridLoc = (panel.subplot2gridLocRow, panel.subplot2gridLocColumn)
144 axs.append(
145 plt.subplot2grid(
146 subplot2gridShape,
147 subplot2gridLoc,
148 rowspan=panel.subplot2gridRowspan,
149 colspan=panel.subplot2gridColspan,
150 )
151 )
152 return axs
154 def _makePanel(self, data, panel, ax, **kwargs):
155 """Plot a single panel.
157 Parameters
158 ----------
159 data : `lsst.analysis.tools.interfaces.KeyedData`
160 panel : `DiaSkyPanel`
161 ax : matplotlib axis
162 color : `str`
163 """
164 artists = [] # Placeholder for each series being plotted
165 for idx, (ra, dec) in enumerate(zip(panel.ras, panel.decs)): # loop over column names (dict keys)
166 if panel.colorList:
167 color = panel.colorList[idx]
168 artist = ax.scatter(
169 data[ra], data[dec], s=panel.size, alpha=panel.alpha, marker=".", linewidths=0, c=color
170 )
171 else: # Use matplotlib default colors
172 artist = ax.scatter(
173 data[ra], data[dec], s=panel.size, alpha=panel.alpha, marker=".", linewidths=0
174 )
175 artists.append(artist)
176 # TODO DM-42768: implement lists of sizes, alphas, etc.
177 # and add better support for multi-panel plots.
179 ax.set_xlabel(panel.xlabel)
180 ax.set_ylabel(panel.ylabel)
181 if panel.legendLabels:
182 ax.legend(artists, panel.legendLabels)
183 if panel.invertXAxis:
184 ax.invert_xaxis()
185 if panel.topSpinesVisible:
186 ax.spines["top"].set_visible(True)
187 else:
188 ax.spines["top"].set_visible(False)
189 if not panel.bottomSpinesVisible: # default is True
190 ax.spines["bottom"].set_visible(False)
191 if not panel.leftSpinesVisible:
192 # Default is True; if False, also put ticks and labels on the right
193 ax.spines["left"].set_visible(False)
194 ax.yaxis.tick_right()
195 ax.yaxis.set_label_position("right")
196 if not panel.rightSpinesVisible: # default is True
197 ax.spines["right"].set_visible(False)