Coverage for python/lsst/analysis/tools/actions/plot/diaSkyPlot.py: 26%

68 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-26 04:09 -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/>. 

21 

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

23 

24from typing import Mapping 

25 

26import matplotlib.pyplot as plt 

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

28from matplotlib.figure import Figure 

29 

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

31from .plotUtils import PanelConfig 

32 

33 

34class DiaSkyPanel(PanelConfig): 

35 """Configuration options for DiaSkyPlot panels.""" 

36 

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 ) 

75 

76 

77class DiaSkyPlot(PlotAction): 

78 """Generic pseudo base class for plotting DiaSources 

79 (or DiaObjects) on the sky. 

80 """ 

81 

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 ) 

88 

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) 

98 

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

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

101 

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. 

105 

106 Parameters 

107 ---------- 

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

109 

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 

124 

125 def _makeAxes(self, fig): 

126 """Determine axes layout for main figure. 

127 

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

129 which calls gridspec. 

130 

131 Parameters 

132 ---------- 

133 fig : `matplotlib.figure.Figure` 

134 

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 

152 

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

154 """Plot a single panel. 

155 

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. 

177 

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)