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

34 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 08:37 +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 

14__all__ = ["calculate_safe_plotting_limits", "make_calculate_safe_plotting_limits"] 

15 

16from collections.abc import Callable, Iterable, Sequence 

17 

18import numpy as np 

19 

20 

21def calculate_safe_plotting_limits( 

22 data_series: Sequence, 

23 percentile: float = 99.9, 

24 constant_extra: float | None = None, 

25 symmetric_around_zero: bool = False, 

26) -> tuple[float, float]: 

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

28 

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

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

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

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

33 

34 Parameters 

35 ---------- 

36 data_series : `iterable` or `iterable` of `iterable` 

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

38 therefore want to have their common plotting limits calculated. 

39 percentile : `float`, optional 

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

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

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

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

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

45 overrides this behaviour and zero you will get. 

46 symmetric_around_zero : `bool`, optional 

47 Whether to make the limits symmetric around zero. 

48 

49 Returns 

50 ------- 

51 ymin : `float` 

52 The value to set the ylim minimum to. 

53 ymax : `float` 

54 The value to set the ylim maximum to. 

55 """ 

56 localFunc = make_calculate_safe_plotting_limits(percentile, constant_extra, symmetric_around_zero) 

57 return localFunc(data_series) 

58 

59 

60def make_calculate_safe_plotting_limits( 

61 percentile: float = 99.9, 

62 constant_extra: float | None = None, 

63 symmetric_around_zero: bool = False, 

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

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

66 limits when not all data series are available initially. 

67 

68 Parameters 

69 ---------- 

70 percentile : `float`, optional 

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

72 constant_extra : `float`, optional 

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

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

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

76 overrides this behaviour and zero you will get. 

77 symmetric_around_zero : `bool`, optional 

78 Whether to make the limits symmetric around zero. 

79 

80 Returns 

81 ------- 

82 calculate_safe_plotting_limits : `callable` 

83 The calculate_safe_plotting_limits function to pass the data series to. 

84 """ 

85 memory: list[Sequence] = [] 

86 

87 def calculate_safe_plotting_limits( 

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

89 ) -> tuple[float, float]: 

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

91 

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

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

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

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

96 

97 Parameters 

98 ---------- 

99 data_series : `iterable` or `iterable` of `iterable` 

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

101 therefore want to have their common plotting limits calculated. 

102 

103 Returns 

104 ------- 

105 ymin : `float` 

106 The value to set the ylim minimum to. 

107 ymax : `float` 

108 The value to set the ylim maximum to. 

109 """ 

110 nonlocal constant_extra 

111 nonlocal percentile 

112 nonlocal symmetric_around_zero 

113 

114 if not isinstance(data_series, Iterable): 

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

116 

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

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

119 # have one, we would need ensure_iterable_of_iterables here 

120 

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

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

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

124 # iterate over it as if we were given many 

125 data_series = [data_series] 

126 

127 memory.extend(data_series) 

128 

129 mins = [] 

130 maxs = [] 

131 

132 for dataSeries in memory: 

133 max_val = np.nanpercentile(dataSeries, percentile) 

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

135 

136 if constant_extra is None: 

137 data_range = max_val - min_val 

138 constant_extra = 0.05 * data_range 

139 

140 max_val += constant_extra 

141 min_val -= constant_extra 

142 

143 maxs.append(max_val) 

144 mins.append(min_val) 

145 

146 max_val = max(maxs) 

147 min_val = min(mins) 

148 

149 if symmetric_around_zero: 

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

151 return -biggest_abs, biggest_abs 

152 

153 return min_val, max_val 

154 

155 return calculate_safe_plotting_limits