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-07 02:02 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-07 02:02 -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/>.
22from __future__ import annotations
24__all__ = ("XYPlot",)
26from typing import TYPE_CHECKING, Any, Mapping
28import matplotlib.pyplot as plt
29from lsst.pex.config import ChoiceField, DictField, Field, FieldValidationError
31from ...interfaces import PlotAction, Vector
32from .plotUtils import addPlotInfo
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
37 from ...interfaces import KeyedData, KeyedDataSchema
40class XYPlot(PlotAction):
41 """Make a plot (with errorbars) of one quantity (X) vs another (Y)."""
43 boolKwargs = DictField[str, bool](
44 doc="Keyword arguments to ax.errorbar that take boolean values",
45 default={},
46 optional=True,
47 )
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 )
55 strKwargs = DictField[str, str](
56 doc="Keyword arguments to ax.errorbar that take string values",
57 default={},
58 optional=True,
59 )
61 xAxisLabel = Field[str](
62 doc="The label to use for the x-axis.",
63 default="x",
64 )
66 yAxisLabel = Field[str](
67 doc="The label to use for the y-axis.",
68 default="y",
69 )
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 )
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 )
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 )
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 )
103 def setDefaults(self):
104 super().setDefaults()
105 self.strKwargs = {"fmt": "o"}
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")
113 super().validate()
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
123 def __call__(self, data: KeyedData, **kwargs) -> Figure:
124 self._validateInput(data)
125 return self.makePlot(data, **kwargs)
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()}")
132 def makePlot(self, data: KeyedData, plotInfo: Mapping[str, str] | None = None, **kwargs: Any) -> Figure:
133 """Make the plot.
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
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()
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
177 if plotInfo is not None:
178 fig = addPlotInfo(fig, plotInfo)
180 return fig