Coverage for cc_modules/cc_plot.py: 91%

23 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/cc_modules/cc_plot.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

11 CamCOPS is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CamCOPS is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26**Plotting functions.** 

27 

28PROPER WAY TO USE MATPLOTLIB: 

29 

30- https://jbarillari.blogspot.co.uk/2009/09/threadsafety-and-matplotlibpylab.html?m=1 

31- https://sjohannes.wordpress.com/2010/06/11/using-matplotlib-in-a-web-application/amp/ 

32- https://matplotlib.org/faq/howto_faq.html#howto-webapp 

33- https://matplotlib.org/examples/api/agg_oo.html#api-agg-oo 

34 

35In summary: matplotlib is easy to use in a way that has global state, but that 

36will break in a threading application. Using the Figure() API is safe. Thus: 

37 

38.. code-block:: python 

39 

40 from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 

41 from matplotlib.figure import Figure 

42 

43 fig = Figure() 

44 canvas = FigureCanvas(fig) 

45 ax = fig.add_subplot(111) 

46 ax.plot([1, 2, 3]) 

47 ax.set_title('hi mom') 

48 ax.grid(True) 

49 ax.set_xlabel('time') 

50 ax.set_ylabel('volts') 

51 canvas.print_figure('test') 

52 

53""" # noqa 

54 

55# ============================================================================= 

56# Basic imports 

57# ============================================================================= 

58 

59import atexit 

60import logging 

61import os 

62import shutil 

63import tempfile 

64 

65from cardinal_pythonlib.logs import BraceStyleAdapter 

66 

67log = BraceStyleAdapter(logging.getLogger(__name__)) 

68 

69 

70# ============================================================================= 

71# Constants 

72# ============================================================================= 

73 

74ENVVAR_HOME = "HOME" 

75ENVVAR_MPLCONFIGDIR = "MPLCONFIGDIR" 

76 

77# ============================================================================= 

78# Import matplotlib 

79# ============================================================================= 

80 

81# We need to use os.environ, since per-request stuff won't be initialized yet. 

82# That goes for anything that affects imports (to avoid the complexity of 

83# delayed imports). 

84if ENVVAR_MPLCONFIGDIR in os.environ: 

85 # 1+2. Use a writable static directory (speeds pyplot loads hugely). 

86 _mpl_config_dir = os.environ[ENVVAR_MPLCONFIGDIR] 

87else: 

88 # 1. Make a temporary directory (must be a directory per process, I'm sure) 

89 _mpl_config_dir = tempfile.mkdtemp() 

90 # 2. Ensure temporary directory is removed when this process exits. 

91 atexit.register(lambda: shutil.rmtree(_mpl_config_dir, ignore_errors=True)) 

92 

93# 3. Tell matplotlib about this directory prior to importing it 

94# http://matplotlib.org/faq/environment_variables_faq.html 

95os.environ[ENVVAR_MPLCONFIGDIR] = _mpl_config_dir 

96 

97# 4. Another nasty matplotlib hack 

98# matplotlib.font_manager reads os.environ.get('HOME') directly, and 

99# searches ~/.fonts for fonts. That's fine unless a user is calling with 

100# sudo -u USER, leaving $HOME as it was but removing the permissions - then 

101# matplotlib crashes out with e.g. 

102# PermissionError: [Errno 13] Permission denied: '/home/rudolf/.fonts/SABOI___.TTF' # noqa 

103# Note that an empty string won't help either, since the check is 

104# "is not None". 

105# You can't assign None to an os.environ member; see 

106# http://stackoverflow.com/questions/3575165; do this: 

107if ENVVAR_HOME in os.environ: 

108 _old_home = os.environ[ENVVAR_HOME] 

109 del os.environ[ENVVAR_HOME] 

110else: 

111 _old_home = None 

112 

113# 5. Import matplotlib 

114log.debug( 

115 "Importing matplotlib (can be slow) (MPLCONFIGDIR={})...", _mpl_config_dir 

116) 

117# noinspection PyUnresolvedReferences 

118import matplotlib # noqa: E402, F401 

119 

120# 6. Restore $HOME 

121if _old_home is not None: 

122 os.environ[ENVVAR_HOME] = _old_home 

123 

124# 7. Set the backend 

125# REPLACED BY OO METHOD # matplotlib.use("Agg") # also the default backend 

126# ... http://matplotlib.org/faq/usage_faq.html#what-is-a-backend 

127# ... http://matplotlib.org/faq/howto_faq.html 

128# matplotlib.use("cairo") # cairo backend corrupts some SVG figures 

129 

130# Load this once so we can tell the user we're importing it and it's slow 

131# REPLACED BY OO METHOD # import matplotlib.pyplot 

132 

133log.debug("... finished importing matplotlib") 

134 

135# REPLACED BY OO METHOD # # THEN DO e.g. # import matplotlib.pyplot as plt