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

61 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-14 03:39 -0800

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 boolKwargs = DictField[str, bool]( 

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

43 default={}, 

44 optional=True, 

45 ) 

46 

47 numKwargs = DictField[str, float]( 

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

49 default={}, 

50 optional=True, 

51 ) 

52 

53 strKwargs = DictField[str, str]( 

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

55 default={}, 

56 optional=True, 

57 ) 

58 

59 xAxisLabel = Field[str]( 

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

61 default="x", 

62 ) 

63 

64 yAxisLabel = Field[str]( 

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

66 default="y", 

67 ) 

68 

69 xScale = ChoiceField[str]( 

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

71 default="linear", 

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

73 ) 

74 

75 yScale = ChoiceField[str]( 

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

77 default="linear", 

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

79 ) 

80 

81 xLinThresh = Field[float]( 

82 doc=( 

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

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

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

86 ), 

87 default=1e-6, 

88 optional=True, 

89 ) 

90 

91 yLinThresh = Field[float]( 

92 doc=( 

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

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

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

96 ), 

97 default=1e-6, 

98 optional=True, 

99 ) 

100 

101 def setDefaults(self): 

102 super().setDefaults() 

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

104 

105 def validate(self): 

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

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

108 ): 

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

110 

111 super().validate() 

112 

113 def getInputSchema(self) -> KeyedDataSchema: 

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

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

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

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

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

119 return base 

120 

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

122 self._validateInput(data) 

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

124 

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

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

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

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

129 

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

131 """Make the plot. 

132 

133 Parameters 

134 ---------- 

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

136 The catalog containing various rho statistics. 

137 **kwargs 

138 Additional keyword arguments to pass to the plot 

139 """ 

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

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

142 if fig is None: 

143 fig = plt.figure(dpi=300) 

144 ax = fig.add_subplot(111) 

145 else: 

146 ax = fig.gca() 

147 

148 ax.errorbar( 

149 data["x"], 

150 data["y"], 

151 xerr=data["xerr"], 

152 yerr=data["yerr"], 

153 **self.boolKwargs, # type: ignore 

154 **self.numKwargs, # type: ignore 

155 **self.strKwargs, # type: ignore 

156 ) 

157 ax.set_xlabel(self.xAxisLabel) 

158 ax.set_ylabel(self.yAxisLabel) 

159 if self.xScale == "symlog": 

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

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

162 else: 

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

164 if self.yScale == "symlog": 

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

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

167 else: 

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

169 

170 if plotInfo is not None: 

171 fig = addPlotInfo(fig, plotInfo) 

172 

173 return fig