Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# axis3d.py, original mplot3d version by John Porter 

2# Created: 23 Sep 2005 

3# Parts rewritten by Reinier Heeres <reinier@heeres.eu> 

4 

5import numpy as np 

6 

7from matplotlib import ( 

8 artist, cbook, lines as mlines, axis as maxis, patches as mpatches, 

9 rcParams) 

10from . import art3d, proj3d 

11 

12 

13@cbook.deprecated("3.1") 

14def get_flip_min_max(coord, index, mins, maxs): 

15 if coord[index] == mins[index]: 

16 return maxs[index] 

17 else: 

18 return mins[index] 

19 

20 

21def move_from_center(coord, centers, deltas, axmask=(True, True, True)): 

22 """ 

23 For each coordinate where *axmask* is True, move *coord* away from 

24 *centers* by *deltas*. 

25 """ 

26 coord = np.asarray(coord) 

27 return coord + axmask * np.copysign(1, coord - centers) * deltas 

28 

29 

30def tick_update_position(tick, tickxs, tickys, labelpos): 

31 '''Update tick line and label position and style.''' 

32 

33 tick.label1.set_position(labelpos) 

34 tick.label2.set_position(labelpos) 

35 tick.tick1line.set_visible(True) 

36 tick.tick2line.set_visible(False) 

37 tick.tick1line.set_linestyle('-') 

38 tick.tick1line.set_marker('') 

39 tick.tick1line.set_data(tickxs, tickys) 

40 tick.gridline.set_data(0, 0) 

41 

42 

43class Axis(maxis.XAxis): 

44 """An Axis class for the 3D plots.""" 

45 # These points from the unit cube make up the x, y and z-planes 

46 _PLANES = ( 

47 (0, 3, 7, 4), (1, 2, 6, 5), # yz planes 

48 (0, 1, 5, 4), (3, 2, 6, 7), # xz planes 

49 (0, 1, 2, 3), (4, 5, 6, 7), # xy planes 

50 ) 

51 

52 # Some properties for the axes 

53 _AXINFO = { 

54 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2), 

55 'color': (0.95, 0.95, 0.95, 0.5)}, 

56 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2), 

57 'color': (0.90, 0.90, 0.90, 0.5)}, 

58 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1), 

59 'color': (0.925, 0.925, 0.925, 0.5)}, 

60 } 

61 

62 def __init__(self, adir, v_intervalx, d_intervalx, axes, *args, 

63 rotate_label=None, **kwargs): 

64 # adir identifies which axes this is 

65 self.adir = adir 

66 

67 # This is a temporary member variable. 

68 # Do not depend on this existing in future releases! 

69 self._axinfo = self._AXINFO[adir].copy() 

70 if rcParams['_internal.classic_mode']: 

71 self._axinfo.update( 

72 {'label': {'va': 'center', 

73 'ha': 'center'}, 

74 'tick': {'inward_factor': 0.2, 

75 'outward_factor': 0.1, 

76 'linewidth': rcParams['lines.linewidth']}, 

77 'axisline': {'linewidth': 0.75, 

78 'color': (0, 0, 0, 1)}, 

79 'grid': {'color': (0.9, 0.9, 0.9, 1), 

80 'linewidth': 1.0, 

81 'linestyle': '-'}, 

82 }) 

83 else: 

84 self._axinfo.update( 

85 {'label': {'va': 'center', 

86 'ha': 'center'}, 

87 'tick': {'inward_factor': 0.2, 

88 'outward_factor': 0.1, 

89 'linewidth': rcParams.get( 

90 adir + 'tick.major.width', 

91 rcParams['xtick.major.width'])}, 

92 'axisline': {'linewidth': rcParams['axes.linewidth'], 

93 'color': rcParams['axes.edgecolor']}, 

94 'grid': {'color': rcParams['grid.color'], 

95 'linewidth': rcParams['grid.linewidth'], 

96 'linestyle': rcParams['grid.linestyle']}, 

97 }) 

98 

99 maxis.XAxis.__init__(self, axes, *args, **kwargs) 

100 

101 # data and viewing intervals for this direction 

102 self.d_interval = d_intervalx 

103 self.v_interval = v_intervalx 

104 self.set_rotate_label(rotate_label) 

105 

106 def init3d(self): 

107 self.line = mlines.Line2D( 

108 xdata=(0, 0), ydata=(0, 0), 

109 linewidth=self._axinfo['axisline']['linewidth'], 

110 color=self._axinfo['axisline']['color'], 

111 antialiased=True) 

112 

113 # Store dummy data in Polygon object 

114 self.pane = mpatches.Polygon( 

115 np.array([[0, 0], [0, 1], [1, 0], [0, 0]]), 

116 closed=False, alpha=0.8, facecolor='k', edgecolor='k') 

117 self.set_pane_color(self._axinfo['color']) 

118 

119 self.axes._set_artist_props(self.line) 

120 self.axes._set_artist_props(self.pane) 

121 self.gridlines = art3d.Line3DCollection([]) 

122 self.axes._set_artist_props(self.gridlines) 

123 self.axes._set_artist_props(self.label) 

124 self.axes._set_artist_props(self.offsetText) 

125 # Need to be able to place the label at the correct location 

126 self.label._transform = self.axes.transData 

127 self.offsetText._transform = self.axes.transData 

128 

129 @cbook.deprecated("3.1") 

130 def get_tick_positions(self): 

131 majorLocs = self.major.locator() 

132 majorLabels = self.major.formatter.format_ticks(majorLocs) 

133 return majorLabels, majorLocs 

134 

135 def get_major_ticks(self, numticks=None): 

136 ticks = maxis.XAxis.get_major_ticks(self, numticks) 

137 for t in ticks: 

138 t.tick1line.set_transform(self.axes.transData) 

139 t.tick2line.set_transform(self.axes.transData) 

140 t.gridline.set_transform(self.axes.transData) 

141 t.label1.set_transform(self.axes.transData) 

142 t.label2.set_transform(self.axes.transData) 

143 return ticks 

144 

145 def set_pane_pos(self, xys): 

146 xys = np.asarray(xys) 

147 xys = xys[:, :2] 

148 self.pane.xy = xys 

149 self.stale = True 

150 

151 def set_pane_color(self, color): 

152 '''Set pane color to a RGBA tuple.''' 

153 self._axinfo['color'] = color 

154 self.pane.set_edgecolor(color) 

155 self.pane.set_facecolor(color) 

156 self.pane.set_alpha(color[-1]) 

157 self.stale = True 

158 

159 def set_rotate_label(self, val): 

160 ''' 

161 Whether to rotate the axis label: True, False or None. 

162 If set to None the label will be rotated if longer than 4 chars. 

163 ''' 

164 self._rotate_label = val 

165 self.stale = True 

166 

167 def get_rotate_label(self, text): 

168 if self._rotate_label is not None: 

169 return self._rotate_label 

170 else: 

171 return len(text) > 4 

172 

173 def _get_coord_info(self, renderer): 

174 mins, maxs = np.array([ 

175 self.axes.get_xbound(), 

176 self.axes.get_ybound(), 

177 self.axes.get_zbound(), 

178 ]).T 

179 centers = (maxs + mins) / 2. 

180 deltas = (maxs - mins) / 12. 

181 mins = mins - deltas / 4. 

182 maxs = maxs + deltas / 4. 

183 

184 vals = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] 

185 tc = self.axes.tunit_cube(vals, renderer.M) 

186 avgz = [tc[p1][2] + tc[p2][2] + tc[p3][2] + tc[p4][2] 

187 for p1, p2, p3, p4 in self._PLANES] 

188 highs = np.array([avgz[2*i] < avgz[2*i+1] for i in range(3)]) 

189 

190 return mins, maxs, centers, deltas, tc, highs 

191 

192 def draw_pane(self, renderer): 

193 renderer.open_group('pane3d', gid=self.get_gid()) 

194 

195 mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) 

196 

197 info = self._axinfo 

198 index = info['i'] 

199 if not highs[index]: 

200 plane = self._PLANES[2 * index] 

201 else: 

202 plane = self._PLANES[2 * index + 1] 

203 xys = [tc[p] for p in plane] 

204 self.set_pane_pos(xys) 

205 self.pane.draw(renderer) 

206 

207 renderer.close_group('pane3d') 

208 

209 @artist.allow_rasterization 

210 def draw(self, renderer): 

211 self.label._transform = self.axes.transData 

212 renderer.open_group('axis3d', gid=self.get_gid()) 

213 

214 ticks = self._update_ticks() 

215 

216 info = self._axinfo 

217 index = info['i'] 

218 

219 mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) 

220 

221 # Determine grid lines 

222 minmax = np.where(highs, maxs, mins) 

223 maxmin = np.where(highs, mins, maxs) 

224 

225 # Draw main axis line 

226 juggled = info['juggled'] 

227 edgep1 = minmax.copy() 

228 edgep1[juggled[0]] = maxmin[juggled[0]] 

229 

230 edgep2 = edgep1.copy() 

231 edgep2[juggled[1]] = maxmin[juggled[1]] 

232 pep = np.asarray( 

233 proj3d.proj_trans_points([edgep1, edgep2], renderer.M)) 

234 centpt = proj3d.proj_transform(*centers, renderer.M) 

235 self.line.set_data(pep[0], pep[1]) 

236 self.line.draw(renderer) 

237 

238 # Grid points where the planes meet 

239 xyz0 = np.tile(minmax, (len(ticks), 1)) 

240 xyz0[:, index] = [tick.get_loc() for tick in ticks] 

241 

242 # Draw labels 

243 # The transAxes transform is used because the Text object 

244 # rotates the text relative to the display coordinate system. 

245 # Therefore, if we want the labels to remain parallel to the 

246 # axis regardless of the aspect ratio, we need to convert the 

247 # edge points of the plane to display coordinates and calculate 

248 # an angle from that. 

249 # TODO: Maybe Text objects should handle this themselves? 

250 dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) - 

251 self.axes.transAxes.transform([pep[0:2, 0]]))[0] 

252 

253 lxyz = 0.5 * (edgep1 + edgep2) 

254 

255 # A rough estimate; points are ambiguous since 3D plots rotate 

256 ax_scale = self.axes.bbox.size / self.figure.bbox.size 

257 ax_inches = np.multiply(ax_scale, self.figure.get_size_inches()) 

258 ax_points_estimate = sum(72. * ax_inches) 

259 deltas_per_point = 48 / ax_points_estimate 

260 default_offset = 21. 

261 labeldeltas = ( 

262 (self.labelpad + default_offset) * deltas_per_point * deltas) 

263 axmask = [True, True, True] 

264 axmask[index] = False 

265 lxyz = move_from_center(lxyz, centers, labeldeltas, axmask) 

266 tlx, tly, tlz = proj3d.proj_transform(*lxyz, renderer.M) 

267 self.label.set_position((tlx, tly)) 

268 if self.get_rotate_label(self.label.get_text()): 

269 angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx))) 

270 self.label.set_rotation(angle) 

271 self.label.set_va(info['label']['va']) 

272 self.label.set_ha(info['label']['ha']) 

273 self.label.draw(renderer) 

274 

275 # Draw Offset text 

276 

277 # Which of the two edge points do we want to 

278 # use for locating the offset text? 

279 if juggled[2] == 2: 

280 outeredgep = edgep1 

281 outerindex = 0 

282 else: 

283 outeredgep = edgep2 

284 outerindex = 1 

285 

286 pos = move_from_center(outeredgep, centers, labeldeltas, axmask) 

287 olx, oly, olz = proj3d.proj_transform(*pos, renderer.M) 

288 self.offsetText.set_text(self.major.formatter.get_offset()) 

289 self.offsetText.set_position((olx, oly)) 

290 angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx))) 

291 self.offsetText.set_rotation(angle) 

292 # Must set rotation mode to "anchor" so that 

293 # the alignment point is used as the "fulcrum" for rotation. 

294 self.offsetText.set_rotation_mode('anchor') 

295 

296 #---------------------------------------------------------------------- 

297 # Note: the following statement for determining the proper alignment of 

298 # the offset text. This was determined entirely by trial-and-error 

299 # and should not be in any way considered as "the way". There are 

300 # still some edge cases where alignment is not quite right, but this 

301 # seems to be more of a geometry issue (in other words, I might be 

302 # using the wrong reference points). 

303 # 

304 # (TT, FF, TF, FT) are the shorthand for the tuple of 

305 # (centpt[info['tickdir']] <= pep[info['tickdir'], outerindex], 

306 # centpt[index] <= pep[index, outerindex]) 

307 # 

308 # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools 

309 # from the variable 'highs'. 

310 # --------------------------------------------------------------------- 

311 if centpt[info['tickdir']] > pep[info['tickdir'], outerindex]: 

312 # if FT and if highs has an even number of Trues 

313 if (centpt[index] <= pep[index, outerindex] 

314 and np.count_nonzero(highs) % 2 == 0): 

315 # Usually, this means align right, except for the FTT case, 

316 # in which offset for axis 1 and 2 are aligned left. 

317 if highs.tolist() == [False, True, True] and index in (1, 2): 

318 align = 'left' 

319 else: 

320 align = 'right' 

321 else: 

322 # The FF case 

323 align = 'left' 

324 else: 

325 # if TF and if highs has an even number of Trues 

326 if (centpt[index] > pep[index, outerindex] 

327 and np.count_nonzero(highs) % 2 == 0): 

328 # Usually mean align left, except if it is axis 2 

329 if index == 2: 

330 align = 'right' 

331 else: 

332 align = 'left' 

333 else: 

334 # The TT case 

335 align = 'right' 

336 

337 self.offsetText.set_va('center') 

338 self.offsetText.set_ha(align) 

339 self.offsetText.draw(renderer) 

340 

341 if self.axes._draw_grid and len(ticks): 

342 # Grid lines go from the end of one plane through the plane 

343 # intersection (at xyz0) to the end of the other plane. The first 

344 # point (0) differs along dimension index-2 and the last (2) along 

345 # dimension index-1. 

346 lines = np.stack([xyz0, xyz0, xyz0], axis=1) 

347 lines[:, 0, index - 2] = maxmin[index - 2] 

348 lines[:, 2, index - 1] = maxmin[index - 1] 

349 self.gridlines.set_segments(lines) 

350 self.gridlines.set_color(info['grid']['color']) 

351 self.gridlines.set_linewidth(info['grid']['linewidth']) 

352 self.gridlines.set_linestyle(info['grid']['linestyle']) 

353 self.gridlines.draw(renderer, project=True) 

354 

355 # Draw ticks 

356 tickdir = info['tickdir'] 

357 tickdelta = deltas[tickdir] 

358 if highs[tickdir]: 

359 ticksign = 1 

360 else: 

361 ticksign = -1 

362 

363 for tick in ticks: 

364 # Get tick line positions 

365 pos = edgep1.copy() 

366 pos[index] = tick.get_loc() 

367 pos[tickdir] = ( 

368 edgep1[tickdir] 

369 + info['tick']['outward_factor'] * ticksign * tickdelta) 

370 x1, y1, z1 = proj3d.proj_transform(*pos, renderer.M) 

371 pos[tickdir] = ( 

372 edgep1[tickdir] 

373 - info['tick']['inward_factor'] * ticksign * tickdelta) 

374 x2, y2, z2 = proj3d.proj_transform(*pos, renderer.M) 

375 

376 # Get position of label 

377 default_offset = 8. # A rough estimate 

378 labeldeltas = ( 

379 (tick.get_pad() + default_offset) * deltas_per_point * deltas) 

380 

381 axmask = [True, True, True] 

382 axmask[index] = False 

383 pos[tickdir] = edgep1[tickdir] 

384 pos = move_from_center(pos, centers, labeldeltas, axmask) 

385 lx, ly, lz = proj3d.proj_transform(*pos, renderer.M) 

386 

387 tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) 

388 tick.tick1line.set_linewidth(info['tick']['linewidth']) 

389 tick.draw(renderer) 

390 

391 renderer.close_group('axis3d') 

392 self.stale = False 

393 

394 # TODO: Get this to work properly when mplot3d supports 

395 # the transforms framework. 

396 def get_tightbbox(self, renderer): 

397 # Currently returns None so that Axis.get_tightbbox 

398 # doesn't return junk info. 

399 return None 

400 

401 @property 

402 def d_interval(self): 

403 return self.get_data_interval() 

404 

405 @d_interval.setter 

406 def d_interval(self, minmax): 

407 return self.set_data_interval(*minmax) 

408 

409 @property 

410 def v_interval(self): 

411 return self.get_view_interval() 

412 

413 @v_interval.setter 

414 def v_interval(self, minmax): 

415 return self.set_view_interval(*minmax) 

416 

417 

418# Use classes to look at different data limits 

419 

420 

421class XAxis(Axis): 

422 get_view_interval, set_view_interval = maxis._make_getset_interval( 

423 "view", "xy_viewLim", "intervalx") 

424 get_data_interval, set_data_interval = maxis._make_getset_interval( 

425 "data", "xy_dataLim", "intervalx") 

426 

427 

428class YAxis(Axis): 

429 get_view_interval, set_view_interval = maxis._make_getset_interval( 

430 "view", "xy_viewLim", "intervaly") 

431 get_data_interval, set_data_interval = maxis._make_getset_interval( 

432 "data", "xy_dataLim", "intervaly") 

433 

434 

435class ZAxis(Axis): 

436 get_view_interval, set_view_interval = maxis._make_getset_interval( 

437 "view", "zz_viewLim", "intervalx") 

438 get_data_interval, set_data_interval = maxis._make_getset_interval( 

439 "data", "zz_dataLim", "intervalx")