Coverage for python/lsst/analysis/tools/actions/plot/colorColorFitPlot.py: 11%
237 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 04:37 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 04:37 -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__ = ("ColorColorFitPlot",)
26from typing import Mapping, cast
28import matplotlib.patheffects as pathEffects
29import matplotlib.pyplot as plt
30import numpy as np
31import scipy.stats
32from lsst.pex.config import Field, ListField, RangeField
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 def getInputSchema(self, **kwargs) -> KeyedDataSchema:
99 base: list[tuple[str, type[Vector] | type[Scalar]]] = []
100 base.append(("x", Vector))
101 base.append(("y", Vector))
102 base.append(("mag", Vector))
103 base.append(("approxMagDepth", Scalar))
104 base.append((f"{self.plotName}_sigmaMAD", Scalar))
105 base.append((f"{self.plotName}_median", Scalar))
106 base.append(("mODR", Scalar))
107 base.append(("bODR", Scalar))
108 base.append(("bPerpMin", Scalar))
109 base.append(("bPerpMax", Scalar))
110 base.append(("mPerp", Scalar))
112 return base
114 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure:
115 self._validateInput(data, **kwargs)
116 return self.makePlot(data, **kwargs)
118 def _validateInput(self, data: KeyedData, **kwargs) -> None:
119 """NOTE currently can only check that something is not a scalar, not
120 check that data is consistent with Vector
121 """
122 needed = self.getInputSchema(**kwargs)
123 if remainder := {key.format(**kwargs) for key, _ in needed} - {
124 key.format(**kwargs) for key in data.keys()
125 }:
126 raise ValueError(f"Task needs keys {remainder} but they were not in input")
127 for name, typ in needed:
128 isScalar = issubclass((colType := type(data[name.format(**kwargs)])), Scalar)
129 if isScalar and typ != Scalar:
130 raise ValueError(f"Data keyed by {name} has type {colType} but action requires type {typ}")
132 def makePlot(
133 self,
134 data: KeyedData,
135 plotInfo: Mapping[str, str],
136 **kwargs,
137 ) -> Figure:
138 """Make stellar locus plots using pre fitted values.
140 Parameters
141 ----------
142 data : `KeyedData`
143 The data to plot the points from, for more information
144 please see the notes section.
145 plotInfo : `dict`
146 A dictionary of information about the data being plotted
147 with keys:
149 * ``"run"``
150 The output run for the plots (`str`).
151 * ``"skymap"``
152 The type of skymap used for the data (`str`).
153 * ``"filter"``
154 The filter used for this data (`str`).
155 * ``"tract"``
156 The tract that the data comes from (`str`).
158 Returns
159 -------
160 fig : `matplotlib.figure.Figure`
161 The resulting figure.
163 Notes
164 -----
165 The axis labels are given by `self.xAxisLabel` and `self.yAxisLabel`.
166 The perpendicular distance of the points to the fit line is given in a
167 histogram in the second panel.
169 For the code to work it expects various quantities to be present in
170 the `data` that it is given.
172 The quantities that are expected to be present are:
174 * Statistics that are shown on the plot or used by the plotting code:
175 * ``approxMagDepth``
176 The approximate magnitude corresponding to the SN cut used.
177 * ``f"{self.plotName}_sigmaMAD"``
178 The sigma mad of the distances to the line fit.
179 * ``f"{self.plotName or ''}_median"``
180 The median of the distances to the line fit.
182 * Parameters from the fitting code that are illustrated on the plot:
183 * ``"bFixed"``
184 The fixed intercept to fall back on.
185 * ``"mFixed"``
186 The fixed gradient to fall back on.
187 * ``"bODR"``
188 The intercept calculated by the final orthogonal distance
189 regression fitting.
190 * ``"mODR"``
191 The gradient calculated by the final orthogonal distance
192 regression fitting.
193 * ``"xMin`"``
194 The x minimum of the box used in the fit.
195 * ``"xMax"``
196 The x maximum of the box used in the fit.
197 * ``"yMin"``
198 The y minimum of the box used in the fit.
199 * ``"yMax"``
200 The y maximum of the box used in the fit.
201 * ``"mPerp"``
202 The gradient of the line perpendicular to the line from
203 the second ODR fit.
204 * ``"bPerpMin"``
205 The intercept of the perpendicular line that goes through
206 xMin.
207 * ``"bPerpMax"``
208 The intercept of the perpendicular line that goes through
209 xMax.
210 * ``"goodPoints"``
211 The points that passed the initial set of cuts (typically
212 in fluxType S/N, extendedness, magnitude, and isfinite).
213 * ``"fitPoints"``
214 The points use in the final fit.
216 * The main inputs to plot:
217 x, y, mag
219 Examples
220 --------
221 An example of the plot produced from this code is here:
223 .. image:: /_static/analysis_tools/stellarLocusExample.png
225 For a detailed example of how to make a plot from the command line
226 please see the
227 :ref:`getting started guide<analysis-tools-getting-started>`.
228 """
229 paramDict = data.pop("paramDict")
230 # Points to use for the fit.
231 fitPoints = data.pop("fitPoints")
232 # Points with finite values for x, y, and mag.
233 goodPoints = data.pop("goodPoints")
235 # TODO: Make a no data fig function and use here.
236 if sum(fitPoints) < paramDict["minObjectForFit"]:
237 fig = plt.figure(dpi=120)
238 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62])
239 ax.tick_params(labelsize=7)
240 noDataText = (
241 "Number of objects after cuts ({})\nis less than the minimum required\nby "
242 "paramDict[minObjectForFit] ({})".format(sum(fitPoints), int(paramDict["minObjectForFit"]))
243 )
244 plt.text(0.5, 0.5, noDataText, ha="center", va="center", fontsize=8)
245 fig = addPlotInfo(plt.gcf(), plotInfo)
246 return fig
248 # Define new colormaps.
249 newBlues = mkColormap(["darkblue", "paleturquoise"])
250 newGrays = mkColormap(["lightslategray", "white"])
252 # Make a figure with three panels.
253 fig = plt.figure(dpi=300)
254 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62])
255 if self.doPlotDistVsColor:
256 axLowerRight = fig.add_axes([0.65, 0.11, 0.26, 0.34])
257 else:
258 axLowerRight = fig.add_axes([0.65, 0.11, 0.3, 0.34])
259 axHist = fig.add_axes([0.65, 0.55, 0.3, 0.32])
261 xs = cast(Vector, data["x"])
262 ys = cast(Vector, data["y"])
263 mags = cast(Vector, data["mag"])
265 # Plot the initial fit box.
266 (initialBox,) = ax.plot(
267 [paramDict["xMin"], paramDict["xMax"], paramDict["xMax"], paramDict["xMin"], paramDict["xMin"]],
268 [paramDict["yMin"], paramDict["yMin"], paramDict["yMax"], paramDict["yMax"], paramDict["yMin"]],
269 "k",
270 alpha=0.3,
271 label="Initial selection",
272 )
274 # Add some useful information to the plot.
275 bbox = dict(alpha=0.9, facecolor="white", edgecolor="none")
276 infoText = "N Total: {}\nN Used: {}".format(sum(goodPoints), sum(fitPoints))
277 ax.text(0.04, 0.97, infoText, color="k", transform=ax.transAxes, fontsize=7, bbox=bbox, va="top")
279 # Calculate the point density for the Used and NotUsed subsamples.
280 xyUsed = np.vstack([xs[fitPoints], ys[fitPoints]])
281 xyNotUsed = np.vstack([xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints]])
282 zUsed = scipy.stats.gaussian_kde(xyUsed)(xyUsed)
283 zNotUsed = scipy.stats.gaussian_kde(xyNotUsed)(xyNotUsed)
285 notUsedScatter = ax.scatter(
286 xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints], c=zNotUsed, cmap=newGrays, s=0.3
287 )
288 fitScatter = ax.scatter(
289 xs[fitPoints], ys[fitPoints], c=zUsed, cmap=newBlues, s=0.3, label="Used for Fit"
290 )
292 # Add colorbars.
293 cbAx = fig.add_axes([0.12, 0.07, 0.43, 0.04])
294 plt.colorbar(fitScatter, cax=cbAx, orientation="horizontal")
295 cbKwargs = {
296 "color": "k",
297 "rotation": "horizontal",
298 "ha": "center",
299 "va": "center",
300 "fontsize": 7,
301 }
302 cbText = cbAx.text(
303 0.5,
304 0.5,
305 "Number Density (used in fit)",
306 transform=cbAx.transAxes,
307 **cbKwargs,
308 )
309 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()])
310 cbAx.set_xticks([np.min(zUsed), np.max(zUsed)], labels=["Less", "More"], fontsize=7)
311 cbAxNotUsed = fig.add_axes([0.12, 0.11, 0.43, 0.04])
312 plt.colorbar(notUsedScatter, cax=cbAxNotUsed, orientation="horizontal")
313 cbText = cbAxNotUsed.text(
314 0.5,
315 0.5,
316 "Number Density (not used in fit)",
317 transform=cbAxNotUsed.transAxes,
318 **cbKwargs,
319 )
320 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()])
321 cbAxNotUsed.set_xticks([])
323 ax.set_xlabel(self.xAxisLabel, fontsize=8)
324 ax.set_ylabel(self.yAxisLabel, fontsize=8)
325 ax.tick_params(labelsize=7)
327 # Set axis limits from configs if set, otherwise based on the data.
328 if self.xLims is not None:
329 ax.set_xlim(self.xLims[0], self.xLims[1])
330 else:
331 percsX = np.nanpercentile(xs[goodPoints], [0.5, 99.5])
332 x5 = (percsX[1] - percsX[0]) / 5
333 ax.set_xlim(percsX[0] - x5, percsX[1] + x5)
334 if self.yLims is not None:
335 ax.set_ylim(self.yLims[0], self.yLims[1])
336 else:
337 percsY = np.nanpercentile(ys[goodPoints], [0.5, 99.5])
338 y5 = (percsY[1] - percsY[0]) / 5
339 ax.set_ylim(percsY[0] - y5, percsY[1] + y5)
341 # Plot the fit lines.
342 if np.fabs(paramDict["mFixed"]) > 1:
343 ysFitLineFixed = np.array([paramDict["yMin"], paramDict["yMax"]])
344 xsFitLineFixed = (ysFitLineFixed - paramDict["bFixed"]) / paramDict["mFixed"]
345 ysFitLine = np.array([paramDict["yMin"], paramDict["yMax"]])
346 xsFitLine = (ysFitLine - data["bODR"]) / data["mODR"]
348 else:
349 xsFitLineFixed = np.array([paramDict["xMin"], paramDict["xMax"]])
350 ysFitLineFixed = paramDict["mFixed"] * xsFitLineFixed + paramDict["bFixed"]
351 xsFitLine = np.array([paramDict["xMin"], paramDict["xMax"]])
352 ysFitLine = np.array(
353 [
354 data["mODR"] * xsFitLine[0] + data["bODR"],
355 data["mODR"] * xsFitLine[1] + data["bODR"],
356 ]
357 )
359 ax.plot(xsFitLineFixed, ysFitLineFixed, "w", lw=1.5)
360 (lineFixed,) = ax.plot(xsFitLineFixed, ysFitLineFixed, "tab:green", lw=1, ls="--", label="Fixed")
361 ax.plot(xsFitLine, ysFitLine, "w", lw=1.5)
362 (lineOdrFit,) = ax.plot(xsFitLine, ysFitLine, "k", lw=1, ls="--", label="ODR Fit")
363 ax.legend(
364 handles=[initialBox, lineFixed, lineOdrFit], handlelength=1.5, fontsize=6, loc="lower right"
365 )
367 # Calculate the distances (in mmag) to the line for the data used in
368 # the fit. Two points are needed to characterize the lines we want
369 # to get the distances to.
370 p1 = np.array([xsFitLine[0], ysFitLine[0]])
371 p2 = np.array([xsFitLine[1], ysFitLine[1]])
373 p1Fixed = np.array([xsFitLineFixed[0], ysFitLineFixed[0]])
374 p2Fixed = np.array([xsFitLineFixed[1], ysFitLineFixed[1]])
376 # Convert to millimags.
377 statsUnitStr = "mmag"
378 distsFixed = np.array(perpDistance(p1Fixed, p2Fixed, zip(xs[fitPoints], ys[fitPoints]))) * 1000
379 dists = np.array(perpDistance(p1, p2, zip(xs[fitPoints], ys[fitPoints]))) * 1000
380 maxDist = np.abs(np.nanmax(dists)) / 1000 # These will be used to set the fit boundary line limits.
381 minDist = np.abs(np.nanmin(dists)) / 1000
382 # Now we have the information for the perpendicular line we can use it
383 # to calculate the points at the ends of the perpendicular lines that
384 # intersect at the box edges.
385 if np.fabs(paramDict["mFixed"]) > 1:
386 xMid = (paramDict["yMin"] - data["bODR"]) / data["mODR"]
387 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)])
388 ysFit = data["mPerp"] * xsFit + data["bPerpMin"]
389 else:
390 xsFit = np.array(
391 [
392 paramDict["xMin"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist),
393 paramDict["xMin"],
394 paramDict["xMin"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist),
395 ]
396 )
397 ysFit = xsFit * data["mPerp"] + data["bPerpMin"]
398 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1)
400 if np.fabs(paramDict["mFixed"]) > 1:
401 xMid = (paramDict["yMax"] - data["bODR"]) / data["mODR"]
402 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)])
403 ysFit = data["mPerp"] * xsFit + data["bPerpMax"]
404 else:
405 xsFit = np.array(
406 [
407 paramDict["xMax"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist),
408 paramDict["xMax"],
409 paramDict["xMax"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist),
410 ]
411 )
412 ysFit = xsFit * data["mPerp"] + data["bPerpMax"]
413 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1)
415 # Compute statistics for fit.
416 medDists = nanMedian(dists)
417 madDists = nanSigmaMad(dists)
418 meanDists = nanMean(dists)
419 rmsDists = np.sqrt(np.mean(np.array(dists) ** 2))
421 xMid = paramDict["xMin"] + 0.5 * (paramDict["xMax"] - paramDict["xMin"])
422 if self.doPlotRedBlueHists:
423 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"])
424 blueDists = dists[blueStars]
425 blueMedDists = nanMedian(blueDists)
426 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"])
427 redDists = dists[redStars]
428 redMedDists = nanMedian(redDists)
430 if self.doPlotRedBlueHists:
431 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"])
432 blueDists = dists[blueStars]
433 blueMedDists = nanMedian(blueDists)
434 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"])
435 redDists = dists[redStars]
436 redMedDists = nanMedian(redDists)
438 # Add a histogram.
439 axHist.set_ylabel("Number", fontsize=7)
440 axHist.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7)
441 axHist.tick_params(labelsize=7)
442 nSigToPlot = 3.5
443 axHist.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists)
444 lineMedian = axHist.axvline(
445 medDists, color="k", lw=1, alpha=0.5, label="Median: {:0.2f} {}".format(medDists, statsUnitStr)
446 )
447 lineMad = axHist.axvline(
448 medDists + madDists,
449 color="k",
450 ls="--",
451 lw=1,
452 alpha=0.5,
453 label=r"$\sigma_{MAD}$" + ": {:0.2f} {}".format(madDists, statsUnitStr),
454 )
455 axHist.axvline(medDists - madDists, color="k", ls="--", lw=1, alpha=0.5)
456 lineRms = axHist.axvline(
457 meanDists + rmsDists,
458 color="k",
459 ls=":",
460 lw=1,
461 alpha=0.3,
462 label="RMS: {:0.2f} {}".format(rmsDists, statsUnitStr),
463 )
464 axHist.axvline(meanDists - rmsDists, color="k", ls=":", lw=1, alpha=0.3)
465 if self.doPlotRedBlueHists:
466 lineBlueMedian = axHist.axvline(
467 blueMedDists,
468 color="blue",
469 ls=":",
470 lw=1.0,
471 alpha=0.5,
472 label="blueMed: {:0.2f}".format(blueMedDists),
473 )
474 lineRedMedian = axHist.axvline(
475 redMedDists,
476 color="red",
477 ls=":",
478 lw=1.0,
479 alpha=0.5,
480 label="redMed: {:0.2f}".format(redMedDists),
481 )
482 linesForLegend = [lineMedian, lineMad, lineRms, lineBlueMedian, lineRedMedian]
483 else:
484 linesForLegend = [lineMedian, lineMad, lineRms]
485 fig.legend(
486 handles=linesForLegend,
487 handlelength=1.0,
488 fontsize=6,
489 loc="lower right",
490 bbox_to_anchor=(0.955, 0.89),
491 bbox_transform=fig.transFigure,
492 ncol=2,
493 )
495 axHist.hist(dists, bins=100, histtype="stepfilled", label="ODR Fit", color="k", ec="k", alpha=0.3)
496 axHist.hist(distsFixed, bins=100, histtype="step", label="Fixed", color="tab:green", alpha=1.0)
497 if self.doPlotRedBlueHists:
498 axHist.hist(blueDists, bins=100, histtype="stepfilled", color="blue", ec="blue", alpha=0.3)
499 axHist.hist(redDists, bins=100, histtype="stepfilled", color="red", ec="red", alpha=0.3)
501 handles = [Rectangle((0, 0), 1, 1, color="k", alpha=0.4)]
502 handles.append(Rectangle((0, 0), 1, 1, color="none", ec="tab:green", alpha=1.0))
503 labels = ["ODR Fit", "Fixed"]
504 if self.doPlotRedBlueHists:
505 handles.append(Rectangle((0, 0), 1, 1, color="blue", alpha=0.3))
506 handles.append(Rectangle((0, 0), 1, 1, color="red", alpha=0.3))
507 labels = ["ODR Fit", "Blue Stars", "Red Stars", "Fixed"]
508 axHist.legend(handles, labels, fontsize=5, loc="upper right")
510 if self.doPlotDistVsColor:
511 axLowerRight.axhline(0.0, color="k", ls="-", lw=0.8, zorder=-1)
512 # Compute and plot a running median of dists vs. color.
513 if np.fabs(data["mODR"]) > 1.0:
514 xRun = ys[fitPoints].copy()
515 axLowerRight.set_xlabel(self.yAxisLabel, fontsize=7)
516 else:
517 xRun = xs[fitPoints].copy()
518 axLowerRight.set_xlabel(self.xAxisLabel, fontsize=7)
519 lowerRightPlot = axLowerRight.scatter(xRun, dists, c=mags[fitPoints], cmap=newBlues, s=0.2)
520 yRun = dists.copy()
521 xySorted = zip(xRun, yRun)
522 xySorted = sorted(xySorted)
523 xSorted = [x for x, y in xySorted]
524 ySorted = [y for x, y in xySorted]
525 nCumulate = int(max(3, len(xRun) // 10))
526 yRunMedian = median_filter(ySorted, size=nCumulate)
527 axLowerRight.plot(xSorted, yRunMedian, "w", lw=1.8)
528 axLowerRight.plot(xSorted, yRunMedian, c="purple", ls="-", lw=1.1, label="Running Median")
529 axLowerRight.set_ylim(-2.5 * madDists, 2.5 * madDists)
530 axLowerRight.set_ylabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7)
531 axLowerRight.legend(fontsize=4, loc="upper right", handlelength=1.0)
532 # Add colorbars.
533 cbAx = fig.add_axes([0.915, 0.11, 0.014, 0.34])
534 plt.colorbar(lowerRightPlot, cax=cbAx, orientation="vertical")
535 cbKwargs = {
536 "color": "k",
537 "rotation": "vertical",
538 "ha": "center",
539 "va": "center",
540 "fontsize": 4,
541 }
542 cbText = cbAx.text(
543 0.5,
544 0.5,
545 self.magLabel,
546 transform=cbAx.transAxes,
547 **cbKwargs,
548 )
549 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.2, foreground="w"), pathEffects.Normal()])
550 cbAx.tick_params(length=2, labelsize=3.5)
551 else:
552 # Add a contour plot showing the magnitude dependance of the
553 # distance to the fit.
554 axLowerRight.invert_yaxis()
555 axLowerRight.axvline(0.0, color="k", ls="--", zorder=-1)
556 percsDists = np.nanpercentile(dists, [4, 96])
557 minXs = -1 * np.min(np.fabs(percsDists))
558 maxXs = np.min(np.fabs(percsDists))
559 plotPoints = (dists < maxXs) & (dists > minXs)
560 xsContour = np.array(dists)[plotPoints]
561 ysContour = cast(Vector, cast(Vector, mags)[cast(Vector, fitPoints)])[cast(Vector, plotPoints)]
562 H, xEdges, yEdges = np.histogram2d(xsContour, ysContour, bins=(11, 11))
563 xBinWidth = xEdges[1] - xEdges[0]
564 yBinWidth = yEdges[1] - yEdges[0]
565 axLowerRight.contour(
566 xEdges[:-1] + xBinWidth / 2, yEdges[:-1] + yBinWidth / 2, H.T, levels=7, cmap=newBlues
567 )
568 axLowerRight.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=8)
569 axLowerRight.set_ylabel(self.magLabel, fontsize=8)
570 axLowerRight.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists)
571 axLowerRight.tick_params(labelsize=6)
573 fig = addPlotInfo(plt.gcf(), plotInfo)
575 return fig