Coverage for python / lsst / analysis / tools / actions / plot / colorColorFitPlot.py: 10%
254 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:08 +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/>.
22from __future__ import annotations
24__all__ = ("ColorColorFitPlot",)
26from typing import Mapping, cast
28import matplotlib.patheffects as pathEffects
29import numpy as np
30import scipy.stats
31from lsst.pex.config import Field, ListField, RangeField
32from lsst.utils.plotting import make_figure, set_rubin_plotstyle
33from matplotlib.figure import Figure
34from matplotlib.patches import Rectangle
35from scipy.ndimage import median_filter
37from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Scalar, Vector
38from ...math import nanMean, nanMedian, nanSigmaMad
39from ..keyedData.stellarLocusFit import perpDistance
40from .plotUtils import addPlotInfo, mkColormap
43class ColorColorFitPlot(PlotAction):
44 """Make a color-color plot and overplot a prefited line to the fit region.
46 This is mostly used for the stellar locus plots and also includes panels
47 that illustrate the goodness of the given fit.
48 """
50 xAxisLabel = Field[str](doc="Label to use for the x axis", optional=False)
51 yAxisLabel = Field[str](doc="Label to use for the y axis", optional=False)
52 magLabel = Field[str](doc="Label to use for the magnitudes used to color code by", optional=False)
54 plotTypes = ListField[str](
55 doc="Selection of types of objects to plot. Can take any combination of"
56 " stars, galaxies, unknown, mag, any.",
57 default=["stars"],
58 )
60 plotName = Field[str](doc="The name for the plot.", optional=False)
61 minPointsForFit = RangeField[int](
62 doc="Minimum number of valid objects to bother attempting a fit.",
63 default=5,
64 min=1,
65 deprecated="This field is no longer used. The value should go as an "
66 "entry to the paramsDict keyed as minObjectForFit. Will be removed "
67 "after v27.",
68 )
70 xLims = ListField[float](
71 doc="Minimum and maximum x-axis limit to force (provided as a list of [xMin, xMax]). "
72 "If `None`, limits will be computed and set based on the data.",
73 dtype=float,
74 default=None,
75 optional=True,
76 )
78 yLims = ListField[float](
79 doc="Minimum and maximum y-axis limit to force (provided as a list of [yMin, yMax]). "
80 "If `None`, limits will be computed and set based on the data.",
81 dtype=float,
82 default=None,
83 optional=True,
84 )
86 doPlotRedBlueHists = Field[bool](
87 doc="Plot distance from fit histograms separated into blue and red star subsamples?",
88 default=False,
89 optional=True,
90 )
92 doPlotDistVsColor = Field[bool](
93 doc="Plot distance from fit as a function of color in lower right panel?",
94 default=True,
95 optional=True,
96 )
98 publicationStyle = Field[bool](
99 doc="Use a publication-quality plot style.",
100 default=False,
101 )
103 def getInputSchema(self, **kwargs) -> KeyedDataSchema:
104 base: list[tuple[str, type[Vector] | type[Scalar]]] = []
105 base.append(("x", Vector))
106 base.append(("y", Vector))
107 base.append(("mag", Vector))
108 base.append(("approxMagDepth", Scalar))
109 base.append((f"{self.plotName}_sigmaMAD", Scalar))
110 base.append((f"{self.plotName}_median", Scalar))
111 base.append(("mODR", Scalar))
112 base.append(("bODR", Scalar))
113 base.append(("bPerpMin", Scalar))
114 base.append(("bPerpMax", Scalar))
115 base.append(("mPerp", Scalar))
117 return base
119 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure:
120 self._validateInput(data, **kwargs)
121 return self.makePlot(data, **kwargs)
123 def _validateInput(self, data: KeyedData, **kwargs) -> None:
124 """NOTE currently can only check that something is not a scalar, not
125 check that data is consistent with Vector
126 """
127 needed = self.getInputSchema(**kwargs)
128 if remainder := {key.format(**kwargs) for key, _ in needed} - {
129 key.format(**kwargs) for key in data.keys()
130 }:
131 raise ValueError(f"Task needs keys {remainder} but they were not in input")
132 for name, typ in needed:
133 isScalar = issubclass((colType := type(data[name.format(**kwargs)])), Scalar)
134 if isScalar and typ != Scalar:
135 raise ValueError(f"Data keyed by {name} has type {colType} but action requires type {typ}")
137 def makePlot(
138 self,
139 data: KeyedData,
140 plotInfo: Mapping[str, str],
141 **kwargs,
142 ) -> Figure:
143 """Make stellar locus plots using pre fitted values.
145 Parameters
146 ----------
147 data : `KeyedData`
148 The data to plot the points from, for more information
149 please see the notes section.
150 plotInfo : `dict`
151 A dictionary of information about the data being plotted
152 with keys:
154 * ``"run"``
155 The output run for the plots (`str`).
156 * ``"skymap"``
157 The type of skymap used for the data (`str`).
158 * ``"filter"``
159 The filter used for this data (`str`).
160 * ``"tract"``
161 The tract that the data comes from (`str`).
163 Returns
164 -------
165 fig : `matplotlib.figure.Figure`
166 The resulting figure.
168 Notes
169 -----
170 The axis labels are given by `self.xAxisLabel` and `self.yAxisLabel`.
171 The perpendicular distance of the points to the fit line is given in a
172 histogram in the second panel.
174 For the code to work it expects various quantities to be present in
175 the `data` that it is given.
177 The quantities that are expected to be present are:
179 * Statistics that are shown on the plot or used by the plotting code:
180 * ``approxMagDepth``
181 The approximate magnitude corresponding to the SN cut used.
182 * ``f"{self.plotName}_sigmaMAD"``
183 The sigma mad of the distances to the line fit.
184 * ``f"{self.plotName or ''}_median"``
185 The median of the distances to the line fit.
187 * Parameters from the fitting code that are illustrated on the plot:
188 * ``"bFixed"``
189 The fixed intercept to fall back on.
190 * ``"mFixed"``
191 The fixed gradient to fall back on.
192 * ``"bODR"``
193 The intercept calculated by the final orthogonal distance
194 regression fitting.
195 * ``"mODR"``
196 The gradient calculated by the final orthogonal distance
197 regression fitting.
198 * ``"xMin`"``
199 The x minimum of the box used in the fit.
200 * ``"xMax"``
201 The x maximum of the box used in the fit.
202 * ``"yMin"``
203 The y minimum of the box used in the fit.
204 * ``"yMax"``
205 The y maximum of the box used in the fit.
206 * ``"mPerp"``
207 The gradient of the line perpendicular to the line from
208 the second ODR fit.
209 * ``"bPerpMin"``
210 The intercept of the perpendicular line that goes through
211 xMin.
212 * ``"bPerpMax"``
213 The intercept of the perpendicular line that goes through
214 xMax.
215 * ``"goodPoints"``
216 The points that passed the initial set of cuts (typically
217 in fluxType S/N, extendedness, magnitude, and isfinite).
218 * ``"fitPoints"``
219 The points use in the final fit.
221 * The main inputs to plot:
222 x, y, mag
224 Examples
225 --------
226 An example of the plot produced from this code is here:
228 .. image:: /_static/analysis_tools/stellarLocusExample.png
230 For a detailed example of how to make a plot from the command line
231 please see the
232 :ref:`getting started guide<analysis-tools-getting-started>`.
233 """
234 set_rubin_plotstyle()
235 paramDict = data.pop("paramDict")
236 # Points to use for the fit.
237 fitPoints = data.pop("fitPoints")
238 # Points with finite values for x, y, and mag.
239 goodPoints = data.pop("goodPoints")
241 # TODO: Make a no data fig function and use here.
242 if sum(fitPoints) < paramDict["minObjectForFit"]:
243 fig = make_figure(dpi=120)
244 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62])
245 ax.tick_params(labelsize=7)
246 noDataText = (
247 "Number of objects after cuts ({})\nis less than the minimum required\nby "
248 "paramDict[minObjectForFit] ({})".format(sum(fitPoints), int(paramDict["minObjectForFit"]))
249 )
250 fig.text(0.5, 0.5, noDataText, ha="center", va="center", fontsize=8)
251 fig = addPlotInfo(fig, plotInfo)
252 return fig
254 # Define new colormaps.
255 newBlues = mkColormap(["#3D5195"])
256 newGrays = mkColormap(["#CFCFCF", "#595959"])
258 # Make a figure with three panels.
259 fig = make_figure(dpi=300)
260 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62])
261 if self.doPlotDistVsColor:
262 axLowerRight = fig.add_axes([0.65, 0.11, 0.26, 0.34])
263 else:
264 axLowerRight = fig.add_axes([0.65, 0.11, 0.3, 0.34])
265 axHist = fig.add_axes([0.65, 0.55, 0.3, 0.32])
267 xs = cast(Vector, data["x"])
268 ys = cast(Vector, data["y"])
269 mags = cast(Vector, data["mag"])
271 # Plot the initial fit box.
272 (initialBox,) = ax.plot(
273 [paramDict["xMin"], paramDict["xMax"], paramDict["xMax"], paramDict["xMin"], paramDict["xMin"]],
274 [paramDict["yMin"], paramDict["yMin"], paramDict["yMax"], paramDict["yMax"], paramDict["yMin"]],
275 "k",
276 alpha=0.3,
277 label="Initial selection",
278 )
280 if not self.publicationStyle:
281 # Add some useful information to the plot.
282 bbox = dict(alpha=0.9, facecolor="white", edgecolor="none")
283 infoText = "N Total: {}\nN Used: {}".format(sum(goodPoints), sum(fitPoints))
284 ax.text(0.04, 0.97, infoText, color="k", transform=ax.transAxes, fontsize=7, bbox=bbox, va="top")
286 # Calculate the point density for the Used and NotUsed subsamples.
287 xyUsed = np.vstack([xs[fitPoints], ys[fitPoints]])
288 xyNotUsed = np.vstack([xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints]])
290 # Try using a Gaussian KDE to get color bars showing number density. If
291 # there are not enough points for KDE, use a constant color.
292 zUsed = np.ones(np.sum(fitPoints))
293 if len(xyUsed.ravel()) >= 2:
294 try:
295 zUsed = scipy.stats.gaussian_kde(xyUsed)(xyUsed)
296 except np.linalg.LinAlgError:
297 pass
299 zNotUsed = np.ones(np.sum(~fitPoints & goodPoints))
300 if len(xyNotUsed.ravel()) >= 2:
301 try:
302 zNotUsed = scipy.stats.gaussian_kde(xyNotUsed)(xyNotUsed)
303 except np.linalg.LinAlgError:
304 pass
306 notUsedScatter = ax.scatter(
307 xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints], c=zNotUsed, cmap=newGrays, s=0.3
308 )
309 fitScatter = ax.scatter(
310 xs[fitPoints], ys[fitPoints], c=zUsed, cmap=newBlues, s=0.3, label="Used for Fit"
311 )
313 # Add colorbars.
314 cbAx = fig.add_axes([0.12, 0.07, 0.43, 0.04])
315 fig.colorbar(fitScatter, cax=cbAx, orientation="horizontal")
316 cbKwargs = {
317 "color": "k",
318 "rotation": "horizontal",
319 "ha": "center",
320 "va": "center",
321 "fontsize": 7,
322 }
323 cbText = cbAx.text(
324 0.5,
325 0.5,
326 "Number Density (used in fit)",
327 transform=cbAx.transAxes,
328 **cbKwargs,
329 )
330 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()])
331 cbAx.set_xticks([np.min(zUsed), np.max(zUsed)], labels=["Less", "More"], fontsize=7)
332 cbAxNotUsed = fig.add_axes([0.12, 0.11, 0.43, 0.04])
333 fig.colorbar(notUsedScatter, cax=cbAxNotUsed, orientation="horizontal")
334 cbText = cbAxNotUsed.text(
335 0.5,
336 0.5,
337 "Number Density (not used in fit)",
338 transform=cbAxNotUsed.transAxes,
339 **cbKwargs,
340 )
341 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()])
342 cbAxNotUsed.set_xticks([])
344 ax.set_xlabel(self.xAxisLabel)
345 ax.set_ylabel(self.yAxisLabel)
346 ax.tick_params(labelsize=7)
348 # Set axis limits from configs if set, otherwise based on the data.
349 if self.xLims is not None:
350 ax.set_xlim(self.xLims[0], self.xLims[1])
351 else:
352 percsX = np.nanpercentile(xs[goodPoints], [0.5, 99.5])
353 x5 = (percsX[1] - percsX[0]) / 5
354 ax.set_xlim(percsX[0] - x5, percsX[1] + x5)
355 if self.yLims is not None:
356 ax.set_ylim(self.yLims[0], self.yLims[1])
357 else:
358 percsY = np.nanpercentile(ys[goodPoints], [0.5, 99.5])
359 y5 = (percsY[1] - percsY[0]) / 5
360 ax.set_ylim(percsY[0] - y5, percsY[1] + y5)
362 # Plot the fit lines.
363 if np.fabs(paramDict["mFixed"]) > 1:
364 ysFitLineFixed = np.array([paramDict["yMin"], paramDict["yMax"]])
365 xsFitLineFixed = (ysFitLineFixed - paramDict["bFixed"]) / paramDict["mFixed"]
366 ysFitLine = np.array([paramDict["yMin"], paramDict["yMax"]])
367 xsFitLine = (ysFitLine - data["bODR"]) / data["mODR"]
369 else:
370 xsFitLineFixed = np.array([paramDict["xMin"], paramDict["xMax"]])
371 ysFitLineFixed = paramDict["mFixed"] * xsFitLineFixed + paramDict["bFixed"]
372 xsFitLine = np.array([paramDict["xMin"], paramDict["xMax"]])
373 ysFitLine = np.array(
374 [
375 data["mODR"] * xsFitLine[0] + data["bODR"],
376 data["mODR"] * xsFitLine[1] + data["bODR"],
377 ]
378 )
380 ax.plot(xsFitLineFixed, ysFitLineFixed, "w", lw=1.5)
381 (lineFixed,) = ax.plot(xsFitLineFixed, ysFitLineFixed, "#C85200", lw=1, ls="--", label="Fixed")
382 ax.plot(xsFitLine, ysFitLine, "w", lw=1.5)
383 (lineOdrFit,) = ax.plot(xsFitLine, ysFitLine, "k", lw=1, ls="--", label="ODR Fit")
384 ax.legend(
385 handles=[initialBox, lineFixed, lineOdrFit], handlelength=1.5, fontsize=6, loc="lower right"
386 )
388 # Calculate the distances (in mmag) to the line for the data used in
389 # the fit. Two points are needed to characterize the lines we want
390 # to get the distances to.
391 p1 = np.array([xsFitLine[0], ysFitLine[0]])
392 p2 = np.array([xsFitLine[1], ysFitLine[1]])
394 p1Fixed = np.array([xsFitLineFixed[0], ysFitLineFixed[0]])
395 p2Fixed = np.array([xsFitLineFixed[1], ysFitLineFixed[1]])
397 # Convert to millimags.
398 statsUnitStr = "mmag"
399 distsFixed = np.array(perpDistance(p1Fixed, p2Fixed, zip(xs[fitPoints], ys[fitPoints]))) * 1000
400 dists = np.array(perpDistance(p1, p2, zip(xs[fitPoints], ys[fitPoints]))) * 1000
401 maxDist = np.abs(np.nanmax(dists)) / 1000 # These will be used to set the fit boundary line limits.
402 minDist = np.abs(np.nanmin(dists)) / 1000
403 # Now we have the information for the perpendicular line we can use it
404 # to calculate the points at the ends of the perpendicular lines that
405 # intersect at the box edges.
406 if np.fabs(paramDict["mFixed"]) > 1:
407 xMid = (paramDict["yMin"] - data["bODR"]) / data["mODR"]
408 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)])
409 ysFit = data["mPerp"] * xsFit + data["bPerpMin"]
410 else:
411 xsFit = np.array(
412 [
413 paramDict["xMin"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist),
414 paramDict["xMin"],
415 paramDict["xMin"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist),
416 ]
417 )
418 ysFit = xsFit * data["mPerp"] + data["bPerpMin"]
419 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1)
421 if np.fabs(paramDict["mFixed"]) > 1:
422 xMid = (paramDict["yMax"] - data["bODR"]) / data["mODR"]
423 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)])
424 ysFit = data["mPerp"] * xsFit + data["bPerpMax"]
425 else:
426 xsFit = np.array(
427 [
428 paramDict["xMax"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist),
429 paramDict["xMax"],
430 paramDict["xMax"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist),
431 ]
432 )
433 ysFit = xsFit * data["mPerp"] + data["bPerpMax"]
434 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1)
436 # Compute statistics for fit.
437 medDists = nanMedian(dists)
438 madDists = nanSigmaMad(dists)
439 meanDists = nanMean(dists)
440 rmsDists = np.sqrt(np.mean(np.array(dists) ** 2))
442 xMid = paramDict["xMin"] + 0.5 * (paramDict["xMax"] - paramDict["xMin"])
443 if self.doPlotRedBlueHists:
444 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"])
445 blueDists = dists[blueStars]
446 blueMedDists = nanMedian(blueDists)
447 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"])
448 redDists = dists[redStars]
449 redMedDists = nanMedian(redDists)
451 if self.doPlotRedBlueHists:
452 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"])
453 blueDists = dists[blueStars]
454 blueMedDists = nanMedian(blueDists)
455 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"])
456 redDists = dists[redStars]
457 redMedDists = nanMedian(redDists)
459 # Add a histogram.
460 axHist.set_ylabel("Number", fontsize=7)
461 axHist.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7)
462 axHist.tick_params(labelsize=7)
463 nSigToPlot = 3.5
464 axHist.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists)
465 lineMedian = axHist.axvline(
466 medDists, color="k", lw=1, alpha=0.5, label="Median: {:0.2f} {}".format(medDists, statsUnitStr)
467 )
468 lineMad = axHist.axvline(
469 medDists + madDists,
470 color="k",
471 ls="--",
472 lw=1,
473 alpha=0.5,
474 label=r"$\sigma_{MAD}$" + ": {:0.2f} {}".format(madDists, statsUnitStr),
475 )
476 axHist.axvline(medDists - madDists, color="k", ls="--", lw=1, alpha=0.5)
477 lineRms = axHist.axvline(
478 meanDists + rmsDists,
479 color="k",
480 ls=":",
481 lw=1,
482 alpha=0.3,
483 label="RMS: {:0.2f} {}".format(rmsDists, statsUnitStr),
484 )
485 axHist.axvline(meanDists - rmsDists, color="k", ls=":", lw=1, alpha=0.3)
486 if self.doPlotRedBlueHists:
487 lineBlueMedian = axHist.axvline(
488 blueMedDists,
489 color="blue",
490 ls=":",
491 lw=1.0,
492 alpha=0.5,
493 label="blueMed: {:0.2f}".format(blueMedDists),
494 )
495 lineRedMedian = axHist.axvline(
496 redMedDists,
497 color="red",
498 ls=":",
499 lw=1.0,
500 alpha=0.5,
501 label="redMed: {:0.2f}".format(redMedDists),
502 )
503 linesForLegend = [lineMedian, lineMad, lineRms, lineBlueMedian, lineRedMedian]
504 else:
505 linesForLegend = [lineMedian, lineMad, lineRms]
506 fig.legend(
507 handles=linesForLegend,
508 handlelength=1.0,
509 fontsize=6,
510 loc="lower right",
511 bbox_to_anchor=(0.955, 0.89),
512 bbox_transform=fig.transFigure,
513 ncol=2,
514 )
516 axHist.hist(dists, bins=100, histtype="stepfilled", label="ODR Fit", color="k", ec="k", alpha=0.3)
517 axHist.hist(distsFixed, bins=100, histtype="step", label="Fixed", color="#C85200", alpha=1.0)
518 if self.doPlotRedBlueHists:
519 axHist.hist(blueDists, bins=100, histtype="stepfilled", color="blue", ec="blue", alpha=0.3)
520 axHist.hist(redDists, bins=100, histtype="stepfilled", color="red", ec="red", alpha=0.3)
522 handles = [Rectangle((0, 0), 1, 1, color="k", alpha=0.4)]
523 handles.append(Rectangle((0, 0), 1, 1, color="none", ec="#C85200", alpha=1.0))
524 labels = ["ODR Fit", "Fixed"]
525 if self.doPlotRedBlueHists:
526 handles.append(Rectangle((0, 0), 1, 1, color="blue", alpha=0.3))
527 handles.append(Rectangle((0, 0), 1, 1, color="red", alpha=0.3))
528 labels = ["ODR Fit", "Blue Stars", "Red Stars", "Fixed"]
529 axHist.legend(handles, labels, fontsize=5, loc="upper right")
531 if self.doPlotDistVsColor:
532 axLowerRight.axhline(0.0, color="k", ls="-", lw=0.8, zorder=-1)
533 # Compute and plot a running median of dists vs. color.
534 if np.fabs(data["mODR"]) > 1.0:
535 xRun = ys[fitPoints].copy()
536 axLowerRight.set_xlabel(self.yAxisLabel, fontsize=7)
537 else:
538 xRun = xs[fitPoints].copy()
539 axLowerRight.set_xlabel(self.xAxisLabel, fontsize=7)
540 lowerRightPlot = axLowerRight.scatter(xRun, dists, c=mags[fitPoints], cmap=newBlues, s=0.2)
541 yRun = dists.copy()
542 xySorted = zip(xRun, yRun)
543 xySorted = sorted(xySorted)
544 xSorted = [x for x, y in xySorted]
545 ySorted = [y for x, y in xySorted]
546 nCumulate = int(max(3, len(xRun) // 10))
547 yRunMedian = median_filter(ySorted, size=nCumulate)
548 axLowerRight.plot(xSorted, yRunMedian, "w", lw=1.8)
549 axLowerRight.plot(xSorted, yRunMedian, c="#595959", ls="-", lw=1.1, label="Running Median")
550 axLowerRight.set_ylim(-2.5 * madDists, 2.5 * madDists)
551 axLowerRight.set_ylabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7)
552 axLowerRight.legend(fontsize=4, loc="upper right", handlelength=1.0)
553 # Add colorbars.
554 cbAx = fig.add_axes([0.915, 0.11, 0.014, 0.34])
555 fig.colorbar(lowerRightPlot, cax=cbAx, orientation="vertical")
556 cbKwargs = {
557 "color": "k",
558 "rotation": "vertical",
559 "ha": "center",
560 "va": "center",
561 "fontsize": 4,
562 }
563 cbText = cbAx.text(
564 0.5,
565 0.5,
566 self.magLabel,
567 transform=cbAx.transAxes,
568 **cbKwargs,
569 )
570 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.2, foreground="w"), pathEffects.Normal()])
571 cbAx.tick_params(length=2, labelsize=3.5)
572 else:
573 # Add a contour plot showing the magnitude dependance of the
574 # distance to the fit.
575 axLowerRight.invert_yaxis()
576 axLowerRight.axvline(0.0, color="k", ls="--", zorder=-1)
577 percsDists = np.nanpercentile(dists, [4, 96])
578 minXs = -1 * np.min(np.fabs(percsDists))
579 maxXs = np.min(np.fabs(percsDists))
580 plotPoints = (dists < maxXs) & (dists > minXs)
581 xsContour = np.array(dists)[plotPoints]
582 ysContour = cast(Vector, cast(Vector, mags)[cast(Vector, fitPoints)])[cast(Vector, plotPoints)]
583 H, xEdges, yEdges = np.histogram2d(xsContour, ysContour, bins=(11, 11))
584 xBinWidth = xEdges[1] - xEdges[0]
585 yBinWidth = yEdges[1] - yEdges[0]
586 axLowerRight.contour(
587 xEdges[:-1] + xBinWidth / 2, yEdges[:-1] + yBinWidth / 2, H.T, levels=7, cmap=newBlues
588 )
589 axLowerRight.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=8)
590 axLowerRight.set_ylabel(self.magLabel, fontsize=8)
591 axLowerRight.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists)
592 axLowerRight.tick_params(labelsize=6)
594 # This is here because matplotlib occasionally decides
595 # that there needs to be 10^15 minor ticks.
596 # This is probably unneeded and may make the plot look
597 # rather busy if it ever finds enough memory to render.
598 # If this ever becomes a problem it can be looked at in
599 # the future.
600 for ax in fig.get_axes():
601 ax.minorticks_off()
603 fig.canvas.draw()
604 if not self.publicationStyle:
605 fig = addPlotInfo(fig, plotInfo)
607 return fig