Coverage for python/lsst/utils/plotting/limits.py: 14%

34 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-01 02:29 -0700

1# This file is part of utils. 

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# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14from collections.abc import Sequence 

15from typing import Callable, Iterable, Optional 

16 

17import numpy as np 

18 

19 

20def calculate_safe_plotting_limits( 

21 data_series: Sequence, 

22 percentile: float = 99.9, 

23 constant_extra: Optional[float] = None, 

24 symmetric_around_zero: bool = False, 

25) -> tuple[float, float]: 

26 """Calculate the right limits for plotting for one or more data series. 

27 

28 Given one or more data series with potential outliers, calculated the 

29 values to pass for ymin, ymax so that extreme outliers don't ruin the 

30 plot. If you are plotting several series on a single axis, pass them 

31 all in and the overall plotting range will be given. 

32 

33 Parameters 

34 ---------- 

35 data_series : `iterable` or `iterable` of `iterable` 

36 One or more data series which will be going on the same axis, and 

37 therefore want to have their common plotting limits calculated. 

38 

39 Returns 

40 ------- 

41 ymin : `float` 

42 The value to set the ylim minimum to. 

43 ymax : `float` 

44 The value to set the ylim maximum to. 

45 """ 

46 localFunc = make_calculate_safe_plotting_limits(percentile, constant_extra, symmetric_around_zero) 

47 return localFunc(data_series) 

48 

49 

50def make_calculate_safe_plotting_limits( 

51 percentile: float = 99.9, 

52 constant_extra: Optional[float] = None, 

53 symmetric_around_zero: bool = False, 

54) -> Callable[[Sequence], tuple[float, float]]: 

55 """Make a ``calculate_safe_plotting_limits`` closure to get the common 

56 limits when not all data series are available initially. 

57 

58 Parameters 

59 ---------- 

60 percentile : `float`, optional 

61 The percentile used to clip the outliers from the data. 

62 constant_extra : `float`, optional 

63 The amount that's added on each side of the range so that data does not 

64 quite touch the axes. If the default ``None`` is left then 5% of the 

65 data range is added for cosmetics, but if zero is set this will 

66 overrides this behaviour and zero you will get. 

67 symmetric_around_zero : `bool`, optional 

68 Make the limits symmetric around zero? 

69 

70 Returns 

71 ------- 

72 calculate_safe_plotting_limits : `callable` 

73 The calculate_safe_plotting_limits function to pass the data series to. 

74 """ 

75 memory: list[Sequence] = [] 

76 

77 def calculate_safe_plotting_limits( 

78 data_series: Sequence, # a sequence of sequences is still a sequence 

79 ) -> tuple[float, float]: 

80 """Calculate the right limits for plotting for one or more data series. 

81 

82 Given one or more data series with potential outliers, calculated the 

83 values to pass for ymin, ymax so that extreme outliers don't ruin the 

84 plot. If you are plotting several series on a single axis, pass them 

85 all in and the overall plotting range will be given. 

86 

87 Parameters 

88 ---------- 

89 data_series : `iterable` or `iterable` of `iterable` 

90 One or more data series which will be going on the same axis, and 

91 therefore want to have their common plotting limits calculated. 

92 

93 Returns 

94 ------- 

95 ymin : `float` 

96 The value to set the ylim minimum to. 

97 ymax : `float` 

98 The value to set the ylim maximum to. 

99 """ 

100 nonlocal constant_extra 

101 nonlocal percentile 

102 nonlocal symmetric_around_zero 

103 

104 if not isinstance(data_series, Iterable): 

105 raise TypeError("data_series must be either an iterable, or an iterable of iterables") 

106 

107 # now we're sure we have an iterable, if it's just one make it a list 

108 # of it lsst.utils.ensure_iterable is not suitable here as we already 

109 # have one, we would need ensure_iterable_of_iterables here 

110 

111 # np.array are Iterable but not Sequence so isinstance that 

112 if not isinstance(data_series[0], Iterable): 

113 # we have a single data series, not multiple, wrap in [] so we can 

114 # iterate over it as if we were given many 

115 data_series = [data_series] 

116 

117 memory.extend(data_series) 

118 

119 mins = [] 

120 maxs = [] 

121 

122 for dataSeries in memory: 

123 max_val = np.nanpercentile(dataSeries, percentile) 

124 min_val = np.nanpercentile(dataSeries, 100.0 - percentile) 

125 

126 if constant_extra is None: 

127 data_range = max_val - min_val 

128 constant_extra = 0.05 * data_range 

129 

130 max_val += constant_extra 

131 min_val -= constant_extra 

132 

133 maxs.append(max_val) 

134 mins.append(min_val) 

135 

136 max_val = max(maxs) 

137 min_val = min(mins) 

138 

139 if symmetric_around_zero: 

140 biggest_abs = max(abs(min_val), abs(max_val)) 

141 return -biggest_abs, biggest_abs 

142 

143 return min_val, max_val 

144 

145 return calculate_safe_plotting_limits