Coverage for python/lsst/analysis/tools/actions/plot/xyPlot.py: 36%

61 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-04 11:09 +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 

22from __future__ import annotations 

23 

24__all__ = ("XYPlot",) 

25 

26from typing import TYPE_CHECKING, Any, Mapping 

27 

28import matplotlib.pyplot as plt 

29from lsst.pex.config import ChoiceField, DictField, Field, FieldValidationError 

30 

31from ...interfaces import PlotAction, Vector 

32from .plotUtils import addPlotInfo 

33 

34if TYPE_CHECKING: 34 ↛ 35line 34 didn't jump to line 35, because the condition on line 34 was never true

35 from matplotlib.figure import Figure 

36 

37 from ...interfaces import KeyedData, KeyedDataSchema 

38 

39 

40class XYPlot(PlotAction): 

41 """Make a plot (with errorbars) of one quantity (X) vs another (Y).""" 

42 

43 boolKwargs = DictField[str, bool]( 

44 doc="Keyword arguments to ax.errorbar that take boolean values", 

45 default={}, 

46 optional=True, 

47 ) 

48 

49 numKwargs = DictField[str, float]( 

50 doc="Keyword arguments to ax.errorbar that take numerical (float or int) values", 

51 default={}, 

52 optional=True, 

53 ) 

54 

55 strKwargs = DictField[str, str]( 

56 doc="Keyword arguments to ax.errorbar that take string values", 

57 default={}, 

58 optional=True, 

59 ) 

60 

61 xAxisLabel = Field[str]( 

62 doc="The label to use for the x-axis.", 

63 default="x", 

64 ) 

65 

66 yAxisLabel = Field[str]( 

67 doc="The label to use for the y-axis.", 

68 default="y", 

69 ) 

70 

71 xScale = ChoiceField[str]( 

72 doc="The scale to use for the x-axis.", 

73 default="linear", 

74 allowed={scale: scale for scale in ("linear", "log", "symlog")}, 

75 ) 

76 

77 yScale = ChoiceField[str]( 

78 doc="The scale to use for the y-axis.", 

79 default="linear", 

80 allowed={scale: scale for scale in ("linear", "log", "symlog")}, 

81 ) 

82 

83 xLinThresh = Field[float]( 

84 doc=( 

85 "The value around zero where the scale becomes linear in x-axis " 

86 "when symlog is set as the scale. Sets the `linthresh` parameter " 

87 "of `~matplotlib.axes.set_xscale`." 

88 ), 

89 default=1e-6, 

90 optional=True, 

91 ) 

92 

93 yLinThresh = Field[float]( 

94 doc=( 

95 "The value around zero where the scale becomes linear in y-axis " 

96 "when symlog is set as the scale. Sets the `linthresh` parameter " 

97 "of `~matplotlib.axes.set_yscale`." 

98 ), 

99 default=1e-6, 

100 optional=True, 

101 ) 

102 

103 def setDefaults(self): 

104 super().setDefaults() 

105 self.strKwargs = {"fmt": "o"} 

106 

107 def validate(self): 

108 if (len(set(self.boolKwargs.keys()).intersection(self.numKwargs.keys())) > 0) or ( 

109 len(set(self.boolKwargs.keys()).intersection(self.strKwargs.keys())) > 0 

110 ): 

111 raise FieldValidationError(self.boolKwargs, self, "Keywords have been repeated") 

112 

113 super().validate() 

114 

115 def getInputSchema(self) -> KeyedDataSchema: 

116 base: list[tuple[str, type[Vector]]] = [] 

117 base.append(("x", Vector)) 

118 base.append(("y", Vector)) 

119 base.append(("xerr", Vector)) 

120 base.append(("yerr", Vector)) 

121 return base 

122 

123 def __call__(self, data: KeyedData, **kwargs) -> Figure: 

124 self._validateInput(data) 

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

126 

127 def _validateInput(self, data: KeyedData) -> None: 

128 needed = set(k[0] for k in self.getInputSchema()) 

129 if not needed.issubset(data.keys()): 

130 raise ValueError(f"Input data does not contain all required keys: {self.getInputSchema()}") 

131 

132 def makePlot(self, data: KeyedData, plotInfo: Mapping[str, str] | None = None, **kwargs: Any) -> Figure: 

133 """Make the plot. 

134 

135 Parameters 

136 ---------- 

137 data : `~pandas.core.frame.DataFrame` 

138 The catalog containing various rho statistics. 

139 **kwargs 

140 Additional keyword arguments to pass to the plot 

141 

142 Returns 

143 ------- 

144 fig : `~matplotlib.figure.Figure` 

145 The resulting figure. 

146 """ 

147 # Allow for multiple curves to lie on the same plot. 

148 fig = kwargs.get("fig", None) 

149 if fig is None: 

150 fig = plt.figure(dpi=300) 

151 ax = fig.add_subplot(111) 

152 else: 

153 ax = fig.gca() 

154 

155 ax.errorbar( 

156 data["x"], 

157 data["y"], 

158 xerr=data["xerr"], 

159 yerr=data["yerr"], 

160 **self.boolKwargs, # type: ignore 

161 **self.numKwargs, # type: ignore 

162 **self.strKwargs, # type: ignore 

163 ) 

164 ax.set_xlabel(self.xAxisLabel) 

165 ax.set_ylabel(self.yAxisLabel) 

166 if self.xScale == "symlog": 

167 ax.set_xscale("symlog", linthresh=self.xLinThresh) 

168 ax.fill_betweenx(y=data["y"], x1=-self.xLinThresh, x2=self.xLinThresh, color="gray", alpha=0.2) 

169 else: 

170 ax.set_xscale(self.xScale) # type: ignore 

171 if self.yScale == "symlog": 

172 ax.set_yscale("symlog", linthresh=self.yLinThresh) 

173 ax.fill_between(x=data["x"], y1=-self.yLinThresh, y2=self.yLinThresh, color="gray", alpha=0.2) 

174 else: 

175 ax.set_yscale(self.yScale) # type: ignore 

176 

177 if plotInfo is not None: 

178 fig = addPlotInfo(fig, plotInfo) 

179 

180 return fig