Coverage for python / lsst / analysis / tools / actions / plot / plotUtils.py: 10%

290 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-19 01:45 -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/>. 

21from __future__ import annotations 

22 

23__all__ = ("PanelConfig", "sortAllArrays") 

24 

25from collections.abc import Iterable, Mapping 

26from typing import TYPE_CHECKING 

27 

28import esutil 

29import matplotlib 

30import numpy as np 

31from matplotlib import cm, colors 

32from matplotlib.collections import PatchCollection 

33from matplotlib.patches import Rectangle 

34from scipy.stats import binned_statistic_2d 

35 

36from lsst.geom import Box2D, SpherePoint, degrees 

37from lsst.pex.config import Config, Field 

38 

39from ...math import nanMedian, nanSigmaMad 

40 

41if TYPE_CHECKING: 

42 from matplotlib.figure import Figure 

43 

44null_formatter = matplotlib.ticker.NullFormatter() 

45 

46 

47def generateSummaryStats(data, skymap, plotInfo): 

48 """Generate a summary statistic in each patch or detector. 

49 

50 Parameters 

51 ---------- 

52 data : `dict` 

53 A dictionary of the data to be plotted. 

54 skymap : `lsst.skymap.BaseSkyMap` 

55 The skymap associated with the data. 

56 plotInfo : `dict` 

57 A dictionary of the plot information. 

58 

59 Returns 

60 ------- 

61 patchInfoDict : `dict` 

62 A dictionary of the patch information. 

63 """ 

64 tractInfo = skymap.generateTract(plotInfo["tract"]) 

65 tractWcs = tractInfo.getWcs() 

66 

67 # For now also convert the gen 2 patchIds to gen 3 

68 if "y" in data.keys(): 

69 yCol = "y" 

70 elif "yStars" in data.keys(): 

71 yCol = "yStars" 

72 elif "yGalaxies" in data.keys(): 

73 yCol = "yGalaxies" 

74 elif "yUnknowns" in data.keys(): 

75 yCol = "yUnknowns" 

76 

77 patchInfoDict = {} 

78 maxPatchNum = tractInfo.num_patches.x * tractInfo.num_patches.y 

79 patches = np.arange(0, maxPatchNum, 1) 

80 

81 # Histogram (group) the patch values, and return an array of 

82 # "reverse indices" which is a specially encoded array of where 

83 # every patch is in the overall array. 

84 if len(data["patch"]) == 0: 

85 rev = np.full(maxPatchNum + 2, maxPatchNum + 2) 

86 else: 

87 _, rev = esutil.stat.histogram(data["patch"], min=0, max=maxPatchNum - 1, rev=True) 

88 

89 for patch in patches: 

90 # Pull out the onPatch indices 

91 onPatch = rev[rev[patch] : rev[patch + 1]] 

92 

93 if len(onPatch) == 0: 

94 stat = np.nan 

95 else: 

96 stat = nanMedian(data[yCol][onPatch]) 

97 try: 

98 patchTuple = (int(patch.split(",")[0]), int(patch.split(",")[-1])) 

99 patchInfo = tractInfo.getPatchInfo(patchTuple) 

100 gen3PatchId = tractInfo.getSequentialPatchIndex(patchInfo) 

101 except AttributeError: 

102 # For native gen 3 tables the patches don't need converting 

103 # When we are no longer looking at the gen 2 -> gen 3 

104 # converted repos we can tidy this up 

105 gen3PatchId = patch 

106 patchInfo = tractInfo.getPatchInfo(patch) 

107 

108 corners = Box2D(patchInfo.getInnerBBox()).getCorners() 

109 skyCoords = tractWcs.pixelToSky(corners) 

110 

111 patchInfoDict[gen3PatchId] = (skyCoords, stat) 

112 

113 tractCorners = Box2D(tractInfo.getBBox()).getCorners() 

114 skyCoords = tractWcs.pixelToSky(tractCorners) 

115 patchInfoDict["tract"] = (skyCoords, np.nan) 

116 

117 return patchInfoDict 

118 

119 

120def generateSummaryStatsVisit(cat, colName, visitSummaryTable): 

121 """Generate a summary statistic in each patch or detector. 

122 

123 Parameters 

124 ---------- 

125 cat : `pandas.core.frame.DataFrame` 

126 A dataframe of the data to be plotted. 

127 colName : `str` 

128 The name of the column to be plotted. 

129 visitSummaryTable : `pandas.core.frame.DataFrame` 

130 A dataframe of the visit summary table. 

131 

132 Returns 

133 ------- 

134 visitInfoDict : `dict` 

135 A dictionary of the visit information. 

136 """ 

137 visitInfoDict = {} 

138 for ccd in cat.detector.unique(): 

139 if ccd is None: 

140 continue 

141 onCcd = cat["detector"] == ccd 

142 stat = nanMedian(cat[colName].values[onCcd]) 

143 

144 sumRow = visitSummaryTable["id"] == ccd 

145 corners = zip(visitSummaryTable["raCorners"][sumRow][0], visitSummaryTable["decCorners"][sumRow][0]) 

146 cornersOut = [] 

147 for ra, dec in corners: 

148 corner = SpherePoint(ra, dec, units=degrees) 

149 cornersOut.append(corner) 

150 

151 visitInfoDict[ccd] = (cornersOut, stat) 

152 

153 return visitInfoDict 

154 

155 

156# Inspired by matplotlib.testing.remove_ticks_and_titles 

157def get_and_remove_axis_text(ax) -> tuple[list[str], list[np.ndarray]]: 

158 """Remove text from an Axis and its children and return with line points. 

159 

160 Parameters 

161 ---------- 

162 ax : `plt.Axis` 

163 A matplotlib figure axis. 

164 

165 Returns 

166 ------- 

167 texts : `List[str]` 

168 A list of all text strings (title and axis/legend/tick labels). 

169 line_xys : `List[numpy.ndarray]` 

170 A list of all line ``_xy`` attributes (arrays of shape ``(N, 2)``). 

171 """ 

172 line_xys = [line._xy for line in ax.lines] 

173 texts = [text.get_text() for text in (ax.title, ax.xaxis.label, ax.yaxis.label)] 

174 ax.set_title("") 

175 ax.set_xlabel("") 

176 ax.set_ylabel("") 

177 

178 try: 

179 texts_legend = ax.get_legend().texts 

180 texts.extend(text.get_text() for text in texts_legend) 

181 for text in texts_legend: 

182 text.set_alpha(0) 

183 except AttributeError: 

184 pass 

185 

186 for idx in range(len(ax.texts)): 

187 texts.append(ax.texts[idx].get_text()) 

188 ax.texts[idx].set_text("") 

189 

190 ax.xaxis.set_major_formatter(null_formatter) 

191 ax.xaxis.set_minor_formatter(null_formatter) 

192 ax.yaxis.set_major_formatter(null_formatter) 

193 ax.yaxis.set_minor_formatter(null_formatter) 

194 try: 

195 ax.zaxis.set_major_formatter(null_formatter) 

196 ax.zaxis.set_minor_formatter(null_formatter) 

197 except AttributeError: 

198 pass 

199 for child in ax.child_axes: 

200 texts_child, lines_child = get_and_remove_axis_text(child) 

201 texts.extend(texts_child) 

202 

203 return texts, line_xys 

204 

205 

206def get_and_remove_figure_text(figure: Figure): 

207 """Remove text from a Figure and its Axes and return with line points. 

208 

209 Parameters 

210 ---------- 

211 figure : `matplotlib.pyplot.Figure` 

212 A matplotlib figure. 

213 

214 Returns 

215 ------- 

216 texts : `List[str]` 

217 A list of all text strings (title and axis/legend/tick labels). 

218 line_xys : `List[numpy.ndarray]`, (N, 2) 

219 A list of all line ``_xy`` attributes (arrays of shape ``(N, 2)``). 

220 """ 

221 texts = [str(figure._suptitle)] 

222 lines = [] 

223 figure.suptitle("") 

224 

225 texts.extend(text.get_text() for text in figure.texts) 

226 figure.texts = [] 

227 

228 for ax in figure.get_axes(): 

229 texts_ax, lines_ax = get_and_remove_axis_text(ax) 

230 texts.extend(texts_ax) 

231 lines.extend(lines_ax) 

232 

233 return texts, lines 

234 

235 

236def parsePlotInfo(plotInfo: Mapping[str, str]) -> str: 

237 """Extract information from the plotInfo dictionary and parses it into 

238 a meaningful string that can be added to a figure. 

239 

240 Parameters 

241 ---------- 

242 plotInfo : `dict`[`str`, `str`] 

243 A plotInfo dictionary containing useful information to 

244 be included on a figure. 

245 

246 Returns 

247 ------- 

248 infoText : `str` 

249 A string containing the plotInfo information, parsed in such a 

250 way that it can be included on a figure. 

251 """ 

252 photocalibDataset = "None" 

253 astroDataset = "None" 

254 

255 run = plotInfo["run"] 

256 datasetsUsed = f"\nPhotoCalib: {photocalibDataset}, Astrometry: {astroDataset}" 

257 tableType = f"\nTable: {plotInfo['tableName']}" 

258 

259 dataIdText = "" 

260 if "tract" in plotInfo.keys(): 

261 dataIdText += f", Tract: {plotInfo['tract']}" 

262 if "visit" in plotInfo.keys(): 

263 dataIdText += f", Visit: {plotInfo['visit']}" 

264 

265 bandText = "" 

266 for band in plotInfo["bands"]: 

267 bandText += band + ", " 

268 bandsText = f", Bands: {bandText[:-2]}" 

269 infoText = f"\n{run}{datasetsUsed}{tableType}{dataIdText}{bandsText}" 

270 

271 # Find S/N and mag keys, if present. 

272 snKeys = [] 

273 magKeys = [] 

274 selectionKeys = [] 

275 selectionPrefix = "Selection: " 

276 for key, value in plotInfo.items(): 

277 if "SN" in key or "S/N" in key: 

278 snKeys.append(key) 

279 elif "Mag" in key: 

280 magKeys.append(key) 

281 elif key.startswith(selectionPrefix): 

282 selectionKeys.append(key) 

283 # Add S/N and mag values to label, if present. 

284 # TODO: Do something if there are multiple sn/mag keys. Log? Warn? 

285 newline = "\n" 

286 if snKeys: 

287 infoText = f"{infoText}{newline if magKeys else ', '}{snKeys[0]}{plotInfo.get(snKeys[0])}" 

288 if magKeys: 

289 infoText = f"{infoText}, {magKeys[0]}{plotInfo.get(magKeys[0])}" 

290 if selectionKeys: 

291 nPrefix = len(selectionPrefix) 

292 selections = ", ".join(f"{key[nPrefix:]}: {plotInfo[key]}" for key in selectionKeys) 

293 infoText = f"{infoText}, Selections: {selections}" 

294 

295 return infoText 

296 

297 

298def addPlotInfo(fig: Figure, plotInfo: Mapping[str, str]) -> Figure: 

299 """Add useful information to the plot. 

300 

301 Parameters 

302 ---------- 

303 fig : `matplotlib.figure.Figure` 

304 The figure to add the information to. 

305 plotInfo : `dict` 

306 A dictionary of the plot information. 

307 

308 Returns 

309 ------- 

310 fig : `matplotlib.figure.Figure` 

311 The figure with the information added. 

312 """ 

313 fig.text(0.01, 0.99, plotInfo["plotName"], fontsize=7, transform=fig.transFigure, ha="left", va="top") 

314 infoText = parsePlotInfo(plotInfo) 

315 fig.text(0.01, 0.984, infoText, fontsize=6, transform=fig.transFigure, alpha=0.6, ha="left", va="top") 

316 

317 return fig 

318 

319 

320def mkColormap(colorNames): 

321 """Make a colormap from the list of color names. 

322 

323 Parameters 

324 ---------- 

325 colorNames : `list` 

326 A list of strings that correspond to matplotlib named colors. 

327 

328 Returns 

329 ------- 

330 cmap : `matplotlib.colors.LinearSegmentedColormap` 

331 A colormap stepping through the supplied list of names. 

332 """ 

333 blues = [] 

334 greens = [] 

335 reds = [] 

336 alphas = [] 

337 

338 if len(colorNames) == 1: 

339 # Alpha is between 0 and 1 really but 

340 # using 1.5 saturates out the top of the 

341 # colorscale, this looks good for ComCam data 

342 # but might want to be changed in the future. 

343 alphaRange = [0.3, 1.0] 

344 nums = np.linspace(0, 1, len(alphaRange)) 

345 r, g, b = colors.colorConverter.to_rgb(colorNames[0]) 

346 for num, alpha in zip(nums, alphaRange): 

347 blues.append((num, b, b)) 

348 greens.append((num, g, g)) 

349 reds.append((num, r, r)) 

350 alphas.append((num, alpha, alpha)) 

351 

352 else: 

353 nums = np.linspace(0, 1, len(colorNames)) 

354 if len(colorNames) == 3: 

355 alphaRange = [1.0, 0.3, 1.0] 

356 elif len(colorNames) == 5: 

357 alphaRange = [1.0, 0.7, 0.3, 0.7, 1.0] 

358 else: 

359 alphaRange = np.ones(len(colorNames)) 

360 

361 for num, color, alpha in zip(nums, colorNames, alphaRange): 

362 r, g, b = colors.colorConverter.to_rgb(color) 

363 blues.append((num, b, b)) 

364 greens.append((num, g, g)) 

365 reds.append((num, r, r)) 

366 alphas.append((num, alpha, alpha)) 

367 

368 colorDict = {"blue": blues, "red": reds, "green": greens, "alpha": alphas} 

369 cmap = colors.LinearSegmentedColormap("newCmap", colorDict) 

370 return cmap 

371 

372 

373def extremaSort(xs): 

374 """Return the IDs of the points reordered so that those furthest from the 

375 median, in absolute terms, are last. 

376 

377 Parameters 

378 ---------- 

379 xs : `np.array` 

380 An array of the values to sort 

381 

382 Returns 

383 ------- 

384 ids : `np.array` 

385 """ 

386 med = nanMedian(xs) 

387 dists = np.abs(xs - med) 

388 ids = np.argsort(dists) 

389 return ids 

390 

391 

392def sortAllArrays(arrsToSort, sortArrayIndex=0): 

393 """Sort one array and then return all the others in the associated order. 

394 

395 Parameters 

396 ---------- 

397 arrsToSort : `list` [`np.array`] 

398 A list of arrays to be simultaneously sorted based on the array in 

399 the list position given by ``sortArrayIndex`` (defaults to be the 

400 first array in the list). 

401 sortArrayIndex : `int`, optional 

402 Zero-based index indicating the array on which to base the sorting. 

403 

404 Returns 

405 ------- 

406 arrsToSort : `list` [`np.array`] 

407 The list of arrays sorted on array in list index ``sortArrayIndex``. 

408 """ 

409 ids = extremaSort(arrsToSort[sortArrayIndex]) 

410 for i, arr in enumerate(arrsToSort): 

411 arrsToSort[i] = arr[ids] 

412 return arrsToSort 

413 

414 

415def addSummaryPlot(fig, loc, sumStats, label): 

416 """Add a summary subplot to the figure. 

417 

418 Parameters 

419 ---------- 

420 fig : `matplotlib.figure.Figure` 

421 The figure that the summary plot is to be added to. 

422 loc : `matplotlib.gridspec.SubplotSpec` or `int` or `(int, int, index` 

423 Describes the location in the figure to put the summary plot, 

424 can be a gridspec SubplotSpec, a 3 digit integer where the first 

425 digit is the number of rows, the second is the number of columns 

426 and the third is the index. This is the same for the tuple 

427 of int, int, index. 

428 sumStats : `dict` 

429 A dictionary where the patchIds are the keys which store the R.A. 

430 and the dec of the corners of the patch, along with a summary 

431 statistic for each patch. 

432 label : `str` 

433 The label to be used for the colorbar. 

434 

435 Returns 

436 ------- 

437 fig : `matplotlib.figure.Figure` 

438 """ 

439 # Add the subplot to the relevant place in the figure 

440 # and sort the axis out 

441 axCorner = fig.add_subplot(loc) 

442 axCorner.yaxis.tick_right() 

443 axCorner.yaxis.set_label_position("right") 

444 axCorner.xaxis.tick_top() 

445 axCorner.xaxis.set_label_position("top") 

446 axCorner.set_aspect("equal") 

447 

448 # Plot the corners of the patches and make the color 

449 # coded rectangles for each patch, the colors show 

450 # the median of the given value in the patch 

451 patches = [] 

452 colors = [] 

453 for dataId in sumStats.keys(): 

454 (corners, stat) = sumStats[dataId] 

455 ra = corners[0][0].asDegrees() 

456 dec = corners[0][1].asDegrees() 

457 xy = (ra, dec) 

458 width = corners[2][0].asDegrees() - ra 

459 height = corners[2][1].asDegrees() - dec 

460 patches.append(Rectangle(xy, width, height)) 

461 colors.append(stat) 

462 ras = [ra.asDegrees() for (ra, dec) in corners] 

463 decs = [dec.asDegrees() for (ra, dec) in corners] 

464 axCorner.plot(ras + [ras[0]], decs + [decs[0]], "k", lw=0.5) 

465 cenX = ra + width / 2 

466 cenY = dec + height / 2 

467 if dataId != "tract": 

468 axCorner.annotate(dataId, (cenX, cenY), color="k", fontsize=4, ha="center", va="center") 

469 

470 # Set the bad color to transparent and make a masked array 

471 cmapPatch = cm.coolwarm.copy() 

472 cmapPatch.set_bad(color="none") 

473 colors = np.ma.array(colors, mask=np.isnan(colors)) 

474 collection = PatchCollection(patches, cmap=cmapPatch) 

475 collection.set_array(colors) 

476 axCorner.add_collection(collection) 

477 

478 # Add some labels 

479 axCorner.set_xlabel("R.A. (deg)", fontsize=7) 

480 axCorner.set_ylabel("Dec. (deg)", fontsize=7) 

481 axCorner.tick_params(axis="both", labelsize=6, length=0, pad=1.5) 

482 axCorner.invert_xaxis() 

483 

484 # Add a colorbar 

485 pos = axCorner.get_position() 

486 yOffset = (pos.y1 - pos.y0) / 3 

487 cax = fig.add_axes([pos.x0, pos.y1 + yOffset, pos.x1 - pos.x0, 0.025]) 

488 fig.colorbar(collection, cax=cax, orientation="horizontal") 

489 cornerLabel = "Median of patch\nvalues for\ny axis" 

490 axCorner.text(1.1, 1.3, cornerLabel, transform=axCorner.transAxes, fontsize=6) 

491 cax.tick_params( 

492 axis="x", labelsize=6, labeltop=True, labelbottom=False, bottom=False, top=True, pad=0.5, length=2 

493 ) 

494 

495 return fig 

496 

497 

498def shorten_list(numbers: Iterable[int], *, range_indicator: str = "-", range_separator: str = ",") -> str: 

499 """Shorten an iterable of integers. 

500 

501 Parameters 

502 ---------- 

503 numbers : `~collections.abc.Iterable` [`int`] 

504 Any iterable (list, set, tuple, numpy.array) of integers. 

505 range_indicator : `str`, optional 

506 The string to use to indicate a range of numbers. 

507 range_separator : `str`, optional 

508 The string to use to separate ranges of numbers. 

509 

510 Returns 

511 ------- 

512 result : `str` 

513 A shortened string representation of the list. 

514 

515 Examples 

516 -------- 

517 >>> shorten_list([1,2,3,5,6,8]) 

518 "1-3,5-6,8" 

519 

520 >>> shorten_list((1,2,3,5,6,8,9,10,11), range_separator=", ") 

521 "1-3, 5-6, 8-11" 

522 

523 >>> shorten_list(range(4), range_indicator="..") 

524 "0..3" 

525 """ 

526 # Sort the list in ascending order. 

527 numbers = sorted(numbers) 

528 

529 if not numbers: # empty container 

530 return "" 

531 

532 # Initialize an empty list to hold the results to be returned. 

533 result = [] 

534 

535 # Initialize variables to track the current start and end of a list. 

536 start = 0 

537 end = 0 # initialize to 0 to handle single element lists. 

538 

539 # Iterate through the sorted list of numbers 

540 for end in range(1, len(numbers)): 

541 # If the current number is the same or consecutive to the previous 

542 # number, skip to the next iteration. 

543 if numbers[end] > numbers[end - 1] + 1: # > is used to handle duplicates, if any. 

544 # If the current number is not consecutive to the previous number, 

545 # add the current range to the result and reset the start to end. 

546 if start == end - 1: 

547 result.append(str(numbers[start])) 

548 else: 

549 result.append(range_indicator.join((str(numbers[start]), str(numbers[end - 1])))) 

550 

551 # Update start. 

552 start = end 

553 

554 # Add the final range to the result. 

555 if start == end: 

556 result.append(str(numbers[start])) 

557 else: 

558 result.append(range_indicator.join((str(numbers[start]), str(numbers[end])))) 

559 

560 # Return the shortened string representation. 

561 return range_separator.join(result) 

562 

563 

564class PanelConfig(Config): 

565 """Configuration options for the plot panels used by DiaSkyPlot. 

566 

567 The defaults will produce a good-looking single panel plot. 

568 The subplot2grid* fields correspond to matplotlib.pyplot.subplot2grid. 

569 """ 

570 

571 topSpinesVisible = Field[bool]( 

572 doc="Draw line and ticks on top of panel?", 

573 default=False, 

574 ) 

575 bottomSpinesVisible = Field[bool]( 

576 doc="Draw line and ticks on bottom of panel?", 

577 default=True, 

578 ) 

579 leftSpinesVisible = Field[bool]( 

580 doc="Draw line and ticks on left side of panel?", 

581 default=True, 

582 ) 

583 rightSpinesVisible = Field[bool]( 

584 doc="Draw line and ticks on right side of panel?", 

585 default=True, 

586 ) 

587 subplot2gridShapeRow = Field[int]( 

588 doc="Number of rows of the grid in which to place axis.", 

589 default=10, 

590 ) 

591 subplot2gridShapeColumn = Field[int]( 

592 doc="Number of columns of the grid in which to place axis.", 

593 default=10, 

594 ) 

595 subplot2gridLocRow = Field[int]( 

596 doc="Row of the axis location within the grid.", 

597 default=1, 

598 ) 

599 subplot2gridLocColumn = Field[int]( 

600 doc="Column of the axis location within the grid.", 

601 default=1, 

602 ) 

603 subplot2gridRowspan = Field[int]( 

604 doc="Number of rows for the axis to span downwards.", 

605 default=5, 

606 ) 

607 subplot2gridColspan = Field[int]( 

608 doc="Number of rows for the axis to span to the right.", 

609 default=5, 

610 ) 

611 

612 

613def plotProjectionWithBinning( 

614 ax, 

615 xs, 

616 ys, 

617 zs, 

618 cmap, 

619 xMin, 

620 xMax, 

621 yMin, 

622 yMax, 

623 xNumBins=45, 

624 yNumBins=None, 

625 fixAroundZero=False, 

626 nPointBinThresh=5000, 

627 isSorted=False, 

628 vmin=None, 

629 vmax=None, 

630 showExtremeOutliers=True, 

631 scatPtSize=7, 

632): 

633 """Plot color-mapped data in projection and with binning when appropriate. 

634 

635 Parameters 

636 ---------- 

637 ax : `matplotlib.axes.Axes` 

638 Axis on which to plot the projection data. 

639 xs, ys : `np.array` 

640 Arrays containing the x and y positions of the data. 

641 zs : `np.array` 

642 Array containing the scaling value associated with the (``xs``, ``ys``) 

643 positions. 

644 cmap : `matplotlib.colors.Colormap` 

645 Colormap for the ``zs`` values. 

646 xMin, xMax, yMin, yMax : `float` 

647 Data limits within which to compute bin sizes. 

648 xNumBins : `int`, optional 

649 The number of bins along the x-axis. 

650 yNumBins : `int`, optional 

651 The number of bins along the y-axis. If `None`, this is set to equal 

652 ``xNumBins``. 

653 nPointBinThresh : `int`, optional 

654 Threshold number of points above which binning will be implemented 

655 for the plotting. If the number of data points is lower than this 

656 threshold, a basic scatter plot will be generated. 

657 isSorted : `bool`, optional 

658 Whether the data have been sorted in ``zs`` (the sorting is to 

659 accommodate the overplotting of points in the upper and lower 

660 extrema of the data). 

661 vmin, vmax : `float`, optional 

662 The min and max limits for the colorbar. 

663 showExtremeOutliers: `bool`, default True 

664 Use overlaid scatter points to show the x-y positions of the 15% 

665 most extreme values. 

666 scatPtSize : `float`, optional 

667 The point size to use if just plotting a regular scatter plot. 

668 

669 Returns 

670 ------- 

671 plotOut : `matplotlib.collections.PathCollection` 

672 The plot object with ``ax`` updated with data plotted here. 

673 """ 

674 med = nanMedian(zs) 

675 mad = nanSigmaMad(zs) 

676 if vmin is None: 

677 vmin = med - 2 * mad 

678 if vmax is None: 

679 vmax = med + 2 * mad 

680 if fixAroundZero: 

681 scaleEnd = np.max([np.abs(vmin), np.abs(vmax)]) 

682 vmin = -1 * scaleEnd 

683 vmax = scaleEnd 

684 

685 yNumBins = xNumBins if yNumBins is None else yNumBins 

686 

687 xBinEdges = np.linspace(xMin, xMax, xNumBins + 1) 

688 yBinEdges = np.linspace(yMin, yMax, yNumBins + 1) 

689 finiteMask = np.isfinite(zs) 

690 xs = xs[finiteMask] 

691 ys = ys[finiteMask] 

692 zs = zs[finiteMask] 

693 binnedStats, xEdges, yEdges, binNums = binned_statistic_2d( 

694 xs, ys, zs, statistic="median", bins=(xBinEdges, yBinEdges) 

695 ) 

696 

697 if len(xs) >= nPointBinThresh: 

698 s = min(10, max(0.5, nPointBinThresh / 10 / (len(xs) ** 0.5))) 

699 lw = (s**0.5) / 10 

700 plotOut = ax.imshow( 

701 binnedStats.T, 

702 cmap=cmap, 

703 extent=[xEdges[0], xEdges[-1], yEdges[-1], yEdges[0]], 

704 vmin=vmin, 

705 vmax=vmax, 

706 ) 

707 if not isSorted: 

708 sortedArrays = sortAllArrays([zs, xs, ys]) 

709 zs, xs, ys = sortedArrays[0], sortedArrays[1], sortedArrays[2] 

710 if len(xs) > 1: 

711 if showExtremeOutliers: 

712 # Find the most extreme 15% of points. The list is ordered 

713 # by the distance from the median, this is just the 

714 # head/tail 15% of points. 

715 extremes = int(np.floor(len(xs) / 100) * 85) 

716 plotOut = ax.scatter( 

717 xs[extremes:], 

718 ys[extremes:], 

719 c=zs[extremes:], 

720 s=s, 

721 cmap=cmap, 

722 vmin=vmin, 

723 vmax=vmax, 

724 edgecolor="white", 

725 linewidths=lw, 

726 ) 

727 else: 

728 plotOut = ax.scatter( 

729 xs, 

730 ys, 

731 c=zs, 

732 cmap=cmap, 

733 s=scatPtSize, 

734 vmin=vmin, 

735 vmax=vmax, 

736 edgecolor="white", 

737 linewidths=0.2, 

738 ) 

739 return plotOut