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

33 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-01 11:57 +0000

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 Callable, Iterable, Sequence 

15 

16import numpy as np 

17 

18 

19def calculate_safe_plotting_limits( 

20 data_series: Sequence, 

21 percentile: float = 99.9, 

22 constant_extra: float | None = None, 

23 symmetric_around_zero: bool = False, 

24) -> tuple[float, float]: 

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

26 

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

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

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

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

31 

32 Parameters 

33 ---------- 

34 data_series : `iterable` or `iterable` of `iterable` 

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

36 therefore want to have their common plotting limits calculated. 

37 percentile : `float`, optional 

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

39 constant_extra : `float` or `None`, optional 

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

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

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

43 overrides this behaviour and zero you will get. 

44 symmetric_around_zero : `bool`, optional 

45 Whether to make the limits symmetric around zero. 

46 

47 Returns 

48 ------- 

49 ymin : `float` 

50 The value to set the ylim minimum to. 

51 ymax : `float` 

52 The value to set the ylim maximum to. 

53 """ 

54 localFunc = make_calculate_safe_plotting_limits(percentile, constant_extra, symmetric_around_zero) 

55 return localFunc(data_series) 

56 

57 

58def make_calculate_safe_plotting_limits( 

59 percentile: float = 99.9, 

60 constant_extra: float | None = None, 

61 symmetric_around_zero: bool = False, 

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

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

64 limits when not all data series are available initially. 

65 

66 Parameters 

67 ---------- 

68 percentile : `float`, optional 

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

70 constant_extra : `float`, optional 

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

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

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

74 overrides this behaviour and zero you will get. 

75 symmetric_around_zero : `bool`, optional 

76 Whether to make the limits symmetric around zero. 

77 

78 Returns 

79 ------- 

80 calculate_safe_plotting_limits : `callable` 

81 The calculate_safe_plotting_limits function to pass the data series to. 

82 """ 

83 memory: list[Sequence] = [] 

84 

85 def calculate_safe_plotting_limits( 

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

87 ) -> tuple[float, float]: 

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

89 

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

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

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

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

94 

95 Parameters 

96 ---------- 

97 data_series : `iterable` or `iterable` of `iterable` 

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

99 therefore want to have their common plotting limits calculated. 

100 

101 Returns 

102 ------- 

103 ymin : `float` 

104 The value to set the ylim minimum to. 

105 ymax : `float` 

106 The value to set the ylim maximum to. 

107 """ 

108 nonlocal constant_extra 

109 nonlocal percentile 

110 nonlocal symmetric_around_zero 

111 

112 if not isinstance(data_series, Iterable): 

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

114 

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

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

117 # have one, we would need ensure_iterable_of_iterables here 

118 

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

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

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

122 # iterate over it as if we were given many 

123 data_series = [data_series] 

124 

125 memory.extend(data_series) 

126 

127 mins = [] 

128 maxs = [] 

129 

130 for dataSeries in memory: 

131 max_val = np.nanpercentile(dataSeries, percentile) 

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

133 

134 if constant_extra is None: 

135 data_range = max_val - min_val 

136 constant_extra = 0.05 * data_range 

137 

138 max_val += constant_extra 

139 min_val -= constant_extra 

140 

141 maxs.append(max_val) 

142 mins.append(min_val) 

143 

144 max_val = max(maxs) 

145 min_val = min(mins) 

146 

147 if symmetric_around_zero: 

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

149 return -biggest_abs, biggest_abs 

150 

151 return min_val, max_val 

152 

153 return calculate_safe_plotting_limits