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

266 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-01 12:37 +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/>. 

21from __future__ import annotations 

22 

23__all__ = ("PanelConfig",) 

24 

25from typing import TYPE_CHECKING, Iterable, List, Mapping, Tuple 

26 

27import matplotlib 

28import matplotlib.pyplot as plt 

29import numpy as np 

30from lsst.geom import Box2D, SpherePoint, degrees 

31from lsst.pex.config import Config, Field 

32from matplotlib import colors 

33from matplotlib.collections import PatchCollection 

34from matplotlib.patches import Rectangle 

35from scipy.stats import binned_statistic_2d 

36 

37from ...math import nanMedian, nanSigmaMad 

38 

39if TYPE_CHECKING: 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true

40 from matplotlib.figure import Figure 

41 

42null_formatter = matplotlib.ticker.NullFormatter() 

43 

44 

45def generateSummaryStats(data, skymap, plotInfo): 

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

47 

48 Parameters 

49 ---------- 

50 data : `dict` 

51 A dictionary of the data to be plotted. 

52 skymap : `lsst.skymap.BaseSkyMap` 

53 The skymap associated with the data. 

54 plotInfo : `dict` 

55 A dictionary of the plot information. 

56 

57 Returns 

58 ------- 

59 patchInfoDict : `dict` 

60 A dictionary of the patch information. 

61 """ 

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

63 tractWcs = tractInfo.getWcs() 

64 

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

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

67 yCol = "y" 

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

69 yCol = "yStars" 

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

71 yCol = "yGalaxies" 

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

73 yCol = "yUnknowns" 

74 

75 patchInfoDict = {} 

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

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

78 for patch in patches: 

79 if patch is None: 

80 continue 

81 # Once the objectTable_tract catalogues are using gen 3 patches 

82 # this will go away 

83 onPatch = data["patch"] == patch 

84 if sum(onPatch) == 0: 

85 stat = np.nan 

86 else: 

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

88 try: 

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

90 patchInfo = tractInfo.getPatchInfo(patchTuple) 

91 gen3PatchId = tractInfo.getSequentialPatchIndex(patchInfo) 

92 except AttributeError: 

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

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

95 # converted repos we can tidy this up 

96 gen3PatchId = patch 

97 patchInfo = tractInfo.getPatchInfo(patch) 

98 

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

100 skyCoords = tractWcs.pixelToSky(corners) 

101 

102 patchInfoDict[gen3PatchId] = (skyCoords, stat) 

103 

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

105 skyCoords = tractWcs.pixelToSky(tractCorners) 

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

107 

108 return patchInfoDict 

109 

110 

111def generateSummaryStatsVisit(cat, colName, visitSummaryTable): 

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

113 

114 Parameters 

115 ---------- 

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

117 A dataframe of the data to be plotted. 

118 colName : `str` 

119 The name of the column to be plotted. 

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

121 A dataframe of the visit summary table. 

122 

123 Returns 

124 ------- 

125 visitInfoDict : `dict` 

126 A dictionary of the visit information. 

127 """ 

128 visitInfoDict = {} 

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

130 if ccd is None: 

131 continue 

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

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

134 

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

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

137 cornersOut = [] 

138 for ra, dec in corners: 

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

140 cornersOut.append(corner) 

141 

142 visitInfoDict[ccd] = (cornersOut, stat) 

143 

144 return visitInfoDict 

145 

146 

147# Inspired by matplotlib.testing.remove_ticks_and_titles 

148def get_and_remove_axis_text(ax) -> Tuple[List[str], List[np.ndarray]]: 

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

150 

151 Parameters 

152 ---------- 

153 ax : `plt.Axis` 

154 A matplotlib figure axis. 

155 

156 Returns 

157 ------- 

158 texts : `List[str]` 

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

160 line_xys : `List[numpy.ndarray]` 

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

162 """ 

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

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

165 ax.set_title("") 

166 ax.set_xlabel("") 

167 ax.set_ylabel("") 

168 

169 try: 

170 texts_legend = ax.get_legend().texts 

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

172 for text in texts_legend: 

173 text.set_alpha(0) 

174 except AttributeError: 

175 pass 

176 

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

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

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

180 

181 ax.xaxis.set_major_formatter(null_formatter) 

182 ax.xaxis.set_minor_formatter(null_formatter) 

183 ax.yaxis.set_major_formatter(null_formatter) 

184 ax.yaxis.set_minor_formatter(null_formatter) 

185 try: 

186 ax.zaxis.set_major_formatter(null_formatter) 

187 ax.zaxis.set_minor_formatter(null_formatter) 

188 except AttributeError: 

189 pass 

190 for child in ax.child_axes: 

191 texts_child, lines_child = get_and_remove_axis_text(child) 

192 texts.extend(texts_child) 

193 

194 return texts, line_xys 

195 

196 

197def get_and_remove_figure_text(figure: Figure): 

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

199 

200 Parameters 

201 ---------- 

202 figure : `matplotlib.pyplot.Figure` 

203 A matplotlib figure. 

204 

205 Returns 

206 ------- 

207 texts : `List[str]` 

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

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

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

211 """ 

212 texts = [str(figure._suptitle)] 

213 lines = [] 

214 figure.suptitle("") 

215 

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

217 figure.texts = [] 

218 

219 for ax in figure.get_axes(): 

220 texts_ax, lines_ax = get_and_remove_axis_text(ax) 

221 texts.extend(texts_ax) 

222 lines.extend(lines_ax) 

223 

224 return texts, lines 

225 

226 

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

228 """Add useful information to the plot. 

229 

230 Parameters 

231 ---------- 

232 fig : `matplotlib.figure.Figure` 

233 The figure to add the information to. 

234 plotInfo : `dict` 

235 A dictionary of the plot information. 

236 

237 Returns 

238 ------- 

239 fig : `matplotlib.figure.Figure` 

240 The figure with the information added. 

241 """ 

242 # TO DO: figure out how to get this information 

243 photocalibDataset = "None" 

244 astroDataset = "None" 

245 

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

247 

248 run = plotInfo["run"] 

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

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

251 

252 dataIdText = "" 

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

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

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

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

257 

258 bandText = "" 

259 for band in plotInfo["bands"]: 

260 bandText += band + ", " 

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

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

263 

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

265 snKeys = [] 

266 magKeys = [] 

267 selectionKeys = [] 

268 selectionPrefix = "Selection: " 

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

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

271 snKeys.append(key) 

272 elif "Mag" in key: 

273 magKeys.append(key) 

274 elif key.startswith(selectionPrefix): 

275 selectionKeys.append(key) 

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

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

278 newline = "\n" 

279 if snKeys: 

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

281 if magKeys: 

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

283 if selectionKeys: 

284 nPrefix = len(selectionPrefix) 

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

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

287 

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

289 

290 return fig 

291 

292 

293def mkColormap(colorNames): 

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

295 

296 Parameters 

297 ---------- 

298 colorNames : `list` 

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

300 

301 Returns 

302 ------- 

303 cmap : `matplotlib.colors.LinearSegmentedColormap` 

304 A colormap stepping through the supplied list of names. 

305 """ 

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

307 blues = [] 

308 greens = [] 

309 reds = [] 

310 for num, color in zip(nums, colorNames): 

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

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

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

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

315 

316 colorDict = {"blue": blues, "red": reds, "green": greens} 

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

318 return cmap 

319 

320 

321def extremaSort(xs): 

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

323 median, in absolute terms, are last. 

324 

325 Parameters 

326 ---------- 

327 xs : `np.array` 

328 An array of the values to sort 

329 

330 Returns 

331 ------- 

332 ids : `np.array` 

333 """ 

334 med = nanMedian(xs) 

335 dists = np.abs(xs - med) 

336 ids = np.argsort(dists) 

337 return ids 

338 

339 

340def sortAllArrays(arrsToSort, sortArrayIndex=0): 

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

342 

343 Parameters 

344 ---------- 

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

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

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

348 first array in the list). 

349 sortArrayIndex : `int`, optional 

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

351 

352 Returns 

353 ------- 

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

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

356 """ 

357 ids = extremaSort(arrsToSort[sortArrayIndex]) 

358 for i, arr in enumerate(arrsToSort): 

359 arrsToSort[i] = arr[ids] 

360 return arrsToSort 

361 

362 

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

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

365 

366 Parameters 

367 ---------- 

368 fig : `matplotlib.figure.Figure` 

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

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

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

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

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

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

375 of int, int, index. 

376 sumStats : `dict` 

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

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

379 statistic for each patch. 

380 label : `str` 

381 The label to be used for the colorbar. 

382 

383 Returns 

384 ------- 

385 fig : `matplotlib.figure.Figure` 

386 """ 

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

388 # and sort the axis out 

389 axCorner = fig.add_subplot(loc) 

390 axCorner.yaxis.tick_right() 

391 axCorner.yaxis.set_label_position("right") 

392 axCorner.xaxis.tick_top() 

393 axCorner.xaxis.set_label_position("top") 

394 axCorner.set_aspect("equal") 

395 

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

397 # coded rectangles for each patch, the colors show 

398 # the median of the given value in the patch 

399 patches = [] 

400 colors = [] 

401 for dataId in sumStats.keys(): 

402 (corners, stat) = sumStats[dataId] 

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

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

405 xy = (ra, dec) 

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

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

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

409 colors.append(stat) 

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

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

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

413 cenX = ra + width / 2 

414 cenY = dec + height / 2 

415 if dataId != "tract": 

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

417 

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

419 cmapPatch = plt.cm.coolwarm.copy() 

420 cmapPatch.set_bad(color="none") 

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

422 collection = PatchCollection(patches, cmap=cmapPatch) 

423 collection.set_array(colors) 

424 axCorner.add_collection(collection) 

425 

426 # Add some labels 

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

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

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

430 axCorner.invert_xaxis() 

431 

432 # Add a colorbar 

433 pos = axCorner.get_position() 

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

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

436 plt.colorbar(collection, cax=cax, orientation="horizontal") 

437 cax.text( 

438 0.5, 

439 0.48, 

440 label, 

441 color="k", 

442 transform=cax.transAxes, 

443 rotation="horizontal", 

444 horizontalalignment="center", 

445 verticalalignment="center", 

446 fontsize=6, 

447 ) 

448 cax.tick_params( 

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

450 ) 

451 

452 return fig 

453 

454 

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

456 """Shorten an iterable of integers. 

457 

458 Parameters 

459 ---------- 

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

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

462 range_indicator : `str`, optional 

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

464 range_separator : `str`, optional 

465 The string to use to separate ranges of numbers. 

466 

467 Returns 

468 ------- 

469 result : `str` 

470 A shortened string representation of the list. 

471 

472 Examples 

473 -------- 

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

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

476 

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

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

479 

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

481 "0..3" 

482 """ 

483 # Sort the list in ascending order. 

484 numbers = sorted(numbers) 

485 

486 if not numbers: # empty container 

487 return "" 

488 

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

490 result = [] 

491 

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

493 start = 0 

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

495 

496 # Iterate through the sorted list of numbers 

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

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

499 # number, skip to the next iteration. 

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

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

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

503 if start == end - 1: 

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

505 else: 

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

507 

508 # Update start. 

509 start = end 

510 

511 # Add the final range to the result. 

512 if start == end: 

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

514 else: 

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

516 

517 # Return the shortened string representation. 

518 return range_separator.join(result) 

519 

520 

521class PanelConfig(Config): 

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

523 

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

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

526 """ 

527 

528 topSpinesVisible = Field[bool]( 

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

530 default=False, 

531 ) 

532 bottomSpinesVisible = Field[bool]( 

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

534 default=True, 

535 ) 

536 leftSpinesVisible = Field[bool]( 

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

538 default=True, 

539 ) 

540 rightSpinesVisible = Field[bool]( 

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

542 default=True, 

543 ) 

544 subplot2gridShapeRow = Field[int]( 

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

546 default=10, 

547 ) 

548 subplot2gridShapeColumn = Field[int]( 

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

550 default=10, 

551 ) 

552 subplot2gridLocRow = Field[int]( 

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

554 default=1, 

555 ) 

556 subplot2gridLocColumn = Field[int]( 

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

558 default=1, 

559 ) 

560 subplot2gridRowspan = Field[int]( 

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

562 default=5, 

563 ) 

564 subplot2gridColspan = Field[int]( 

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

566 default=5, 

567 ) 

568 

569 

570def plotProjectionWithBinning( 

571 ax, 

572 xs, 

573 ys, 

574 zs, 

575 cmap, 

576 xMin, 

577 xMax, 

578 yMin, 

579 yMax, 

580 xNumBins=45, 

581 yNumBins=None, 

582 fixAroundZero=False, 

583 nPointBinThresh=5000, 

584 isSorted=False, 

585 vmin=None, 

586 vmax=None, 

587 showExtremeOutliers=True, 

588 scatPtSize=7, 

589): 

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

591 

592 Parameters 

593 ---------- 

594 ax : `matplotlib.axes.Axes` 

595 Axis on which to plot the projection data. 

596 xs, ys : `np.array` 

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

598 zs : `np.array` 

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

600 positions. 

601 cmap : `matplotlib.colors.Colormap` 

602 Colormap for the ``zs`` values. 

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

604 Data limits within which to compute bin sizes. 

605 xNumBins : `int`, optional 

606 The number of bins along the x-axis. 

607 yNumBins : `int`, optional 

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

609 ``xNumBins``. 

610 nPointBinThresh : `int`, optional 

611 Threshold number of points above which binning will be implemented 

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

613 threshold, a basic scatter plot will be generated. 

614 isSorted : `bool`, optional 

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

616 accommodate the overplotting of points in the upper and lower 

617 extrema of the data). 

618 vmin, vmax : `float`, optional 

619 The min and max limits for the colorbar. 

620 showExtremeOutliers: `bool`, default True 

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

622 most extreme values. 

623 scatPtSize : `float`, optional 

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

625 

626 Returns 

627 ------- 

628 plotOut : `matplotlib.collections.PathCollection` 

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

630 """ 

631 med = nanMedian(zs) 

632 mad = nanSigmaMad(zs) 

633 if vmin is None: 

634 vmin = med - 2 * mad 

635 if vmax is None: 

636 vmax = med + 2 * mad 

637 if fixAroundZero: 

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

639 vmin = -1 * scaleEnd 

640 vmax = scaleEnd 

641 

642 yNumBins = xNumBins if yNumBins is None else yNumBins 

643 

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

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

646 binnedStats, xEdges, yEdges, binNums = binned_statistic_2d( 

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

648 ) 

649 if len(xs) >= nPointBinThresh: 

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

651 lw = (s**0.5) / 10 

652 plotOut = ax.imshow( 

653 binnedStats.T, 

654 cmap=cmap, 

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

656 vmin=vmin, 

657 vmax=vmax, 

658 ) 

659 if not isSorted: 

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

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

662 if len(xs) > 1: 

663 if showExtremeOutliers: 

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

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

666 # head/tail 15% of points. 

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

668 plotOut = ax.scatter( 

669 xs[extremes:], 

670 ys[extremes:], 

671 c=zs[extremes:], 

672 s=s, 

673 cmap=cmap, 

674 vmin=vmin, 

675 vmax=vmax, 

676 edgecolor="white", 

677 linewidths=lw, 

678 ) 

679 else: 

680 plotOut = ax.scatter( 

681 xs, 

682 ys, 

683 c=zs, 

684 cmap=cmap, 

685 s=scatPtSize, 

686 vmin=vmin, 

687 vmax=vmax, 

688 edgecolor="white", 

689 linewidths=0.2, 

690 ) 

691 return plotOut