You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

339 lines
11 KiB

11 months ago
  1. # coding: utf-8
  2. # flake8: noqa
  3. # # `adashof`: Functions used in the notebooks of the blog
  4. #
  5. # [Blog](http://werthmuller.org/blog)
  6. # [Repo](http://github.com/prisae/blog-notebooks)
  7. #
  8. # - circle : Create circle on figure with axes of different sizes.
  9. # - move_sn_y : Move scientific notation exponent from top to the side.
  10. # - fillgrid : Fill rectangular grid with colours or a colour and transparency.
  11. # - checksize : Check size of pdf figure, and adjust if required.
  12. # - cm2in : Convert centimetres to inches
  13. # In[1]:
  14. import numpy as np
  15. import matplotlib as mpl
  16. from matplotlib import cm
  17. import matplotlib.pyplot as plt
  18. # In[2]:
  19. def circle(xy, radius, kwargs=None):
  20. """Create circle on figure with axes of different sizes.
  21. Plots a circle on the current axes using `plt.Circle`, taking into account
  22. the figure size and the axes units.
  23. It is done by plotting in the figure coordinate system, taking the aspect
  24. ratio into account. In this way, the data dimensions do not matter.
  25. However, if you adjust `xlim` or `ylim` after plotting `circle`, it will
  26. screw them up; set `plt.axis` before calling `circle`.
  27. Parameters
  28. ----------
  29. xy, radius, kwars :
  30. As required for `plt.Circle`.
  31. """
  32. # Get current figure and axis
  33. fig = mpl.pyplot.gcf()
  34. ax = fig.gca()
  35. # Calculate figure dimension ratio width/height
  36. pr = fig.get_figwidth()/fig.get_figheight()
  37. # Get the transScale (important if one of the axis is in log-scale)
  38. tscale = ax.transScale + (ax.transLimits + ax.transAxes)
  39. ctscale = tscale.transform_point(xy)
  40. cfig = fig.transFigure.inverted().transform(ctscale)
  41. # Create circle
  42. if kwargs == None:
  43. circ = mpl.patches.Ellipse(cfig, radius, radius*pr,
  44. transform=fig.transFigure)
  45. else:
  46. circ = mpl.patches.Ellipse(cfig, radius, radius*pr,
  47. transform=fig.transFigure, **kwargs)
  48. # Draw circle
  49. ax.add_artist(circ)
  50. # In[3]:
  51. def move_sn_y(offs=0, dig=0, side='left', omit_last=False):
  52. """Move scientific notation exponent from top to the side.
  53. Additionally, one can set the number of digits after the comma
  54. for the y-ticks, hence if it should state 1, 1.0, 1.00 and so forth.
  55. Parameters
  56. ----------
  57. offs : float, optional; <0>
  58. Horizontal movement additional to default.
  59. dig : int, optional; <0>
  60. Number of decimals after the comma.
  61. side : string, optional; {<'left'>, 'right'}
  62. To choose the side of the y-axis notation.
  63. omit_last : bool, optional; <False>
  64. If True, the top y-axis-label is omitted.
  65. Returns
  66. -------
  67. locs : list
  68. List of y-tick locations.
  69. Note
  70. ----
  71. This is kind of a non-satisfying hack, which should be handled more
  72. properly. But it works. Functions to look at for a better implementation:
  73. ax.ticklabel_format
  74. ax.yaxis.major.formatter.set_offset_string
  75. """
  76. # Get the ticks
  77. locs, _ = mpl.pyplot.yticks()
  78. # Put the last entry into a string, ensuring it is in scientific notation
  79. # E.g: 123456789 => '1.235e+08'
  80. llocs = '%.3e' % locs[-1]
  81. # Get the magnitude, hence the number after the 'e'
  82. # E.g: '1.235e+08' => 8
  83. yoff = int(str(llocs).split('e')[1])
  84. # If omit_last, remove last entry
  85. if omit_last:
  86. slocs = locs[:-1]
  87. else:
  88. slocs = locs
  89. # Set ticks to the requested precision
  90. form = r'$%.'+str(dig)+'f$'
  91. mpl.pyplot.yticks(locs, list(map(lambda x: form % x, slocs/(10**yoff))))
  92. # Define offset depending on the side
  93. if side == 'left':
  94. offs = -.18 - offs # Default left: -0.18
  95. elif side == 'right':
  96. offs = 1 + offs # Default right: 1.0
  97. # Plot the exponent
  98. mpl.pyplot.text(offs, .98, r'$\times10^{%i}$' % yoff, transform =
  99. mpl.pyplot.gca().transAxes, verticalalignment='top')
  100. # Return the locs
  101. return locs
  102. # In[4]:
  103. def fillgrid(xval, yval, values, style='colour', cmap=cm.Spectral,
  104. unicol='#000000', lc='k', lw=0.5):
  105. """Fill rectangular grid with colours or a colour and transparency.
  106. Parameters
  107. ----------
  108. xval, yval : array
  109. Grid-points in x- and in y-direction.
  110. values : array, dimension: (x-1)-by-(y-1)
  111. Values between 0 and 1
  112. style : string, optional; {<'colour'>, 'alpha'}
  113. Defines if values represent colour or alpha.
  114. cmap : mpl.cm-element, optional
  115. `Colormap` colours are chosen from; only used if style='colour'
  116. unicol : HEX-colour
  117. Colour used with transparency; only used if style='alpha'
  118. lc, lw : optional
  119. Line colour and width, as in standard plots.
  120. Returns
  121. -------
  122. rct : list
  123. List of plotted polygon patches.
  124. """
  125. # Ravel values, and set NaN's to zero
  126. rval = values.ravel()
  127. rvalnan = np.isnan(rval)
  128. rval[rvalnan] = 0
  129. # Define colour depending on style
  130. if style == 'alpha':
  131. # Create RGB from HEX
  132. unicol = mpl.colors.colorConverter.to_rgb(unicol)
  133. # Repeat colour for all values,
  134. # filling the value into the transparency column
  135. colour = np.vstack((np.repeat(unicol, len(rval)).reshape(3, -1),
  136. rval)).transpose()
  137. else:
  138. # Split cmap into 101 points from 0 to 1
  139. cmcol = cmap(np.linspace(0, 1, 101))
  140. # Map the values onto these
  141. colour = cmcol[list(map(int, 100*rval))]
  142. # Set transparency to 0 for NaN's
  143. colour[rvalnan, -1] = 0
  144. # Draw all rectangles at once
  145. xxval = np.array([xval[:-1], xval[:-1], xval[1:], xval[1:]]).repeat(
  146. len(yval)-1, axis=1).reshape(4, -1)
  147. yyval = np.array([yval[:-1], yval[1:], yval[1:], yval[:-1]]).repeat(
  148. len(xval)-1, axis=0).reshape(4, -1)
  149. rct = mpl.pyplot.gca().fill(xxval, yyval, lw=lw, ec=lc)
  150. # Map the colour onto a list
  151. cls = list(map(mpl.colors.rgb2hex, colour))
  152. # Adjust colour and transparency for all cells
  153. for ind in range(len(rct)):
  154. rct[ind].set_facecolor(cls[ind])
  155. rct[ind].set_alpha(colour[ind, -1])
  156. return rct
  157. # In[5]:
  158. def checksize(fhndl, name, dsize, precision=0.01, extent=0.05, kwargs={}, _cf=False):
  159. """Print figure with 'name.pdf', check size, compare with dsize, and adjust if required
  160. Parameters
  161. ----------
  162. fhndl : figure-handle
  163. Figure handle of the figure to be saved.
  164. name : string
  165. Figure name.
  166. dsize : list of two floats
  167. Desired size of pdf in cm.
  168. precision : float, optional; <0.01>
  169. Desired precision in cm of the dimension, defaults to 1 mm.
  170. extent : float or list of floats, optional; <0.01>
  171. - If float, then bbox_inches is set to tight, and pad_inches=extent.
  172. - If it is an array of two numbers it sets the percentaged extent-width,
  173. `Bbox.expanded`.
  174. - If it is an array of four numbers it sets [x0, y0, x1, y1] of Bbox.
  175. kwargs : dict
  176. Other input arguments that will be passed on to `plt.savefig`; e.g. dpi or facecolor.
  177. _cf : Internal parameter for recursion and adjustment.
  178. """
  179. # Import PyPDF2
  180. from PyPDF2 import PdfFileReader
  181. # Check `extent` input and set bbox_inches and pad_inches accordingly
  182. if np.size(extent) == 1:
  183. bbox_inches = 'tight'
  184. pad_inches = extent
  185. else:
  186. fext = fhndl.gca().get_window_extent().transformed(
  187. fhndl.dpi_scale_trans.inverted())
  188. if np.size(extent) == 2:
  189. bbox_inches = fext.expanded(extent[0], extent[1])
  190. elif np.size(extent) == 4:
  191. fext.x0, fext.y0, fext.x1, fext.y1 = extent
  192. extent = [1, 1] # set extent to [1, 1] for recursion
  193. bbox_inches = fext
  194. pad_inches=0
  195. # Save the figure
  196. fhndl.savefig(name+'.pdf', bbox_inches=bbox_inches, pad_inches=pad_inches, **kwargs)
  197. # Get pdf-dimensions in cm
  198. pdffile = PdfFileReader(open(name+'.pdf', mode='rb'))
  199. pdfsize = np.array([float(pdffile.getPage(0).mediaBox[2]),
  200. float(pdffile.getPage(0).mediaBox[3])])
  201. pdfdim = pdfsize*2.54/72. # points to cm
  202. # Define `print`-precision on desired precision
  203. pprec = abs(int(('%.1e' % precision).split('e')[1]))+1
  204. # Get difference btw desired and actual size
  205. diff = dsize-pdfdim
  206. # If diff>precision, adjust, else finish
  207. if np.any(abs(diff) > precision):
  208. if not _cf:
  209. _cf = [1, 1]
  210. # Be verbose
  211. print(' resize...')
  212. # Adjust width
  213. if (abs(diff[0]) > precision):
  214. print(' X-diff:', np.round(diff[0], pprec), 'cm')
  215. # Set new factor to old factor times (desired size)/(actual size)
  216. _cf[0] = _cf[0]*dsize[0]/pdfdim[0]
  217. # Set new figure width
  218. fhndl.set_figwidth(_cf[0]*dsize[0]/2.54) # cm2in
  219. # Adjust height
  220. if (abs(diff[1]) > precision):
  221. print(' Y-diff:', np.round(diff[1], pprec), 'cm')
  222. # Set new factor to old factor times (desired size)/(actual size)
  223. _cf[1] = _cf[1]*dsize[1]/pdfdim[1]
  224. # Set new figure height
  225. fhndl.set_figheight(_cf[1]*dsize[1]/2.54) #cm2in
  226. # Call the function again, with new factor _cf
  227. figsize = checksize(fhndl, name, dsize, precision, extent, kwargs, _cf)
  228. return figsize
  229. else: # Print some info if the desired dimensions are reached
  230. # Print figure name and pdf dimensions
  231. print('Figure saved to '+name +'.pdf;',
  232. np.round(pdfdim[0], pprec), 'x',
  233. np.round(pdfdim[1], pprec), 'cm.')
  234. # Print the new figsize if it had to be adjusted
  235. if _cf:
  236. print(' => NEW FIG-SIZE: figsize=('+
  237. str(np.round(fhndl.get_size_inches()[0], 2*pprec))+', '+
  238. str(np.round(fhndl.get_size_inches()[1], 2*pprec))+')')
  239. # Return figsize
  240. return fhndl.get_size_inches()
  241. # In[6]:
  242. def cm2in(length, decimals=2):
  243. """Convert cm to inch.
  244. Parameters
  245. ----------
  246. length : scalar or vector
  247. Numbers to be converted.
  248. decimals : int, optional; <2>
  249. As in np.round, used to round the result.
  250. Returns
  251. -------
  252. cm2in : scalar or vector
  253. Converted numbers.
  254. Examples
  255. --------
  256. >>> from adashof import cm2in
  257. >>> cm2in(5)
  258. 1.97
  259. """
  260. # Test input
  261. try:
  262. length = np.array(length, dtype='float')
  263. decimals = int(decimals)
  264. except ValueError:
  265. print("{length} must be a number, {decimals} an integer")
  266. return np.round(length/2.54, decimals)