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-22 09:32 +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/>. 

21 

22__all__ = ("DiaSkyPanel", "DiaSkyPlot") 

23 

24from collections.abc import Mapping 

25 

26import matplotlib.pyplot as plt 

27from matplotlib.figure import Figure 

28 

29from lsst.pex.config import ConfigDictField, Field, ListField 

30 

31from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Vector 

32from .plotUtils import PanelConfig 

33 

34 

35class DiaSkyPanel(PanelConfig): 

36 """Configuration options for DiaSkyPlot panels.""" 

37 

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 ) 

76 

77 

78class DiaSkyPlot(PlotAction): 

79 """Generic pseudo base class for plotting DiaSources 

80 (or DiaObjects) on the sky. 

81 """ 

82 

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 ) 

89 

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) 

99 

100 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure: 

101 return self.makePlot(data, **kwargs) 

102 

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. 

106 

107 Parameters 

108 ---------- 

109 data : `lsst.analysis.tools.interfaces.KeyedData` 

110 

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 

125 

126 def _makeAxes(self, fig): 

127 """Determine axes layout for main figure. 

128 

129 Use matplotlib's subplot2grid to determine the panel geometry, 

130 which calls gridspec. 

131 

132 Parameters 

133 ---------- 

134 fig : `matplotlib.figure.Figure` 

135 

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 

153 

154 def _makePanel(self, data, panel, ax, **kwargs): 

155 """Plot a single panel. 

156 

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. 

178 

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)