1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

import warnings 

 

import numpy as np 

 

import matplotlib 

from matplotlib import docstring, rcParams 

from matplotlib.artist import allow_rasterization 

import matplotlib.transforms as mtransforms 

import matplotlib.patches as mpatches 

import matplotlib.path as mpath 

 

 

class Spine(mpatches.Patch): 

"""an axis spine -- the line noting the data area boundaries 

 

Spines are the lines connecting the axis tick marks and noting the 

boundaries of the data area. They can be placed at arbitrary 

positions. See function:`~matplotlib.spines.Spine.set_position` 

for more information. 

 

The default position is ``('outward',0)``. 

 

Spines are subclasses of class:`~matplotlib.patches.Patch`, and 

inherit much of their behavior. 

 

Spines draw a line, a circle, or an arc depending if 

function:`~matplotlib.spines.Spine.set_patch_line`, 

function:`~matplotlib.spines.Spine.set_patch_circle`, or 

function:`~matplotlib.spines.Spine.set_patch_arc` has been called. 

Line-like is the default. 

 

""" 

def __str__(self): 

return "Spine" 

 

@docstring.dedent_interpd 

def __init__(self, axes, spine_type, path, **kwargs): 

""" 

- *axes* : the Axes instance containing the spine 

- *spine_type* : a string specifying the spine type 

- *path* : the path instance used to draw the spine 

 

Valid kwargs are: 

%(Patch)s 

""" 

super().__init__(**kwargs) 

self.axes = axes 

self.set_figure(self.axes.figure) 

self.spine_type = spine_type 

self.set_facecolor('none') 

self.set_edgecolor(rcParams['axes.edgecolor']) 

self.set_linewidth(rcParams['axes.linewidth']) 

self.set_capstyle('projecting') 

self.axis = None 

 

self.set_zorder(2.5) 

self.set_transform(self.axes.transData) # default transform 

 

self._bounds = None # default bounds 

self._smart_bounds = False 

 

# Defer initial position determination. (Not much support for 

# non-rectangular axes is currently implemented, and this lets 

# them pass through the spines machinery without errors.) 

self._position = None 

if not isinstance(path, matplotlib.path.Path): 

raise ValueError( 

"'path' must be an instance of 'matplotlib.path.Path'") 

self._path = path 

 

# To support drawing both linear and circular spines, this 

# class implements Patch behavior three ways. If 

# self._patch_type == 'line', behave like a mpatches.PathPatch 

# instance. If self._patch_type == 'circle', behave like a 

# mpatches.Ellipse instance. If self._patch_type == 'arc', behave like 

# a mpatches.Arc instance. 

self._patch_type = 'line' 

 

# Behavior copied from mpatches.Ellipse: 

# Note: This cannot be calculated until this is added to an Axes 

self._patch_transform = mtransforms.IdentityTransform() 

 

def set_smart_bounds(self, value): 

"""set the spine and associated axis to have smart bounds""" 

self._smart_bounds = value 

 

# also set the axis if possible 

if self.spine_type in ('left', 'right'): 

self.axes.yaxis.set_smart_bounds(value) 

elif self.spine_type in ('top', 'bottom'): 

self.axes.xaxis.set_smart_bounds(value) 

self.stale = True 

 

def get_smart_bounds(self): 

"""get whether the spine has smart bounds""" 

return self._smart_bounds 

 

def set_patch_arc(self, center, radius, theta1, theta2): 

"""set the spine to be arc-like""" 

self._patch_type = 'arc' 

self._center = center 

self._width = radius * 2 

self._height = radius * 2 

self._theta1 = theta1 

self._theta2 = theta2 

self._path = mpath.Path.arc(theta1, theta2) 

# arc drawn on axes transform 

self.set_transform(self.axes.transAxes) 

self.stale = True 

 

def set_patch_circle(self, center, radius): 

"""set the spine to be circular""" 

self._patch_type = 'circle' 

self._center = center 

self._width = radius * 2 

self._height = radius * 2 

# circle drawn on axes transform 

self.set_transform(self.axes.transAxes) 

self.stale = True 

 

def set_patch_line(self): 

"""set the spine to be linear""" 

self._patch_type = 'line' 

self.stale = True 

 

# Behavior copied from mpatches.Ellipse: 

def _recompute_transform(self): 

"""NOTE: This cannot be called until after this has been added 

to an Axes, otherwise unit conversion will fail. This 

makes it very important to call the accessor method and 

not directly access the transformation member variable. 

""" 

assert self._patch_type in ('arc', 'circle') 

center = (self.convert_xunits(self._center[0]), 

self.convert_yunits(self._center[1])) 

width = self.convert_xunits(self._width) 

height = self.convert_yunits(self._height) 

self._patch_transform = mtransforms.Affine2D() \ 

.scale(width * 0.5, height * 0.5) \ 

.translate(*center) 

 

def get_patch_transform(self): 

if self._patch_type in ('arc', 'circle'): 

self._recompute_transform() 

return self._patch_transform 

else: 

return super().get_patch_transform() 

 

def get_window_extent(self, renderer=None): 

# make sure the location is updated so that transforms etc are 

# correct: 

self._adjust_location() 

return super().get_window_extent(renderer=renderer) 

 

def get_path(self): 

return self._path 

 

def _ensure_position_is_set(self): 

if self._position is None: 

# default position 

self._position = ('outward', 0.0) # in points 

self.set_position(self._position) 

 

def register_axis(self, axis): 

"""register an axis 

 

An axis should be registered with its corresponding spine from 

the Axes instance. This allows the spine to clear any axis 

properties when needed. 

""" 

self.axis = axis 

if self.axis is not None: 

self.axis.cla() 

self.stale = True 

 

def cla(self): 

"""Clear the current spine""" 

self._position = None # clear position 

if self.axis is not None: 

self.axis.cla() 

 

def is_frame_like(self): 

"""return True if directly on axes frame 

 

This is useful for determining if a spine is the edge of an 

old style MPL plot. If so, this function will return True. 

""" 

self._ensure_position_is_set() 

position = self._position 

if isinstance(position, str): 

if position == 'center': 

position = ('axes', 0.5) 

elif position == 'zero': 

position = ('data', 0) 

if len(position) != 2: 

raise ValueError("position should be 2-tuple") 

position_type, amount = position 

if position_type == 'outward' and amount == 0: 

return True 

else: 

return False 

 

def _adjust_location(self): 

"""automatically set spine bounds to the view interval""" 

 

if self.spine_type == 'circle': 

return 

 

if self._bounds is None: 

if self.spine_type in ('left', 'right'): 

low, high = self.axes.viewLim.intervaly 

elif self.spine_type in ('top', 'bottom'): 

low, high = self.axes.viewLim.intervalx 

else: 

raise ValueError('unknown spine spine_type: %s' % 

self.spine_type) 

 

if self._smart_bounds: 

# attempt to set bounds in sophisticated way 

 

# handle inverted limits 

viewlim_low, viewlim_high = sorted([low, high]) 

 

if self.spine_type in ('left', 'right'): 

datalim_low, datalim_high = self.axes.dataLim.intervaly 

ticks = self.axes.get_yticks() 

elif self.spine_type in ('top', 'bottom'): 

datalim_low, datalim_high = self.axes.dataLim.intervalx 

ticks = self.axes.get_xticks() 

# handle inverted limits 

ticks = np.sort(ticks) 

datalim_low, datalim_high = sorted([datalim_low, datalim_high]) 

 

if datalim_low < viewlim_low: 

# Data extends past view. Clip line to view. 

low = viewlim_low 

else: 

# Data ends before view ends. 

cond = (ticks <= datalim_low) & (ticks >= viewlim_low) 

tickvals = ticks[cond] 

if len(tickvals): 

# A tick is less than or equal to lowest data point. 

low = tickvals[-1] 

else: 

# No tick is available 

low = datalim_low 

low = max(low, viewlim_low) 

 

if datalim_high > viewlim_high: 

# Data extends past view. Clip line to view. 

high = viewlim_high 

else: 

# Data ends before view ends. 

cond = (ticks >= datalim_high) & (ticks <= viewlim_high) 

tickvals = ticks[cond] 

if len(tickvals): 

# A tick is greater than or equal to highest data 

# point. 

high = tickvals[0] 

else: 

# No tick is available 

high = datalim_high 

high = min(high, viewlim_high) 

 

else: 

low, high = self._bounds 

 

if self._patch_type == 'arc': 

if self.spine_type in ('bottom', 'top'): 

try: 

direction = self.axes.get_theta_direction() 

except AttributeError: 

direction = 1 

try: 

offset = self.axes.get_theta_offset() 

except AttributeError: 

offset = 0 

low = low * direction + offset 

high = high * direction + offset 

if low > high: 

low, high = high, low 

 

self._path = mpath.Path.arc(np.rad2deg(low), np.rad2deg(high)) 

 

if self.spine_type == 'bottom': 

rmin, rmax = self.axes.viewLim.intervaly 

try: 

rorigin = self.axes.get_rorigin() 

except AttributeError: 

rorigin = rmin 

scaled_diameter = (rmin - rorigin) / (rmax - rorigin) 

self._height = scaled_diameter 

self._width = scaled_diameter 

 

else: 

raise ValueError('unable to set bounds for spine "%s"' % 

self.spine_type) 

else: 

v1 = self._path.vertices 

assert v1.shape == (2, 2), 'unexpected vertices shape' 

if self.spine_type in ['left', 'right']: 

v1[0, 1] = low 

v1[1, 1] = high 

elif self.spine_type in ['bottom', 'top']: 

v1[0, 0] = low 

v1[1, 0] = high 

else: 

raise ValueError('unable to set bounds for spine "%s"' % 

self.spine_type) 

 

@allow_rasterization 

def draw(self, renderer): 

self._adjust_location() 

ret = super().draw(renderer) 

self.stale = False 

return ret 

 

def _calc_offset_transform(self): 

"""calculate the offset transform performed by the spine""" 

self._ensure_position_is_set() 

position = self._position 

if isinstance(position, str): 

if position == 'center': 

position = ('axes', 0.5) 

elif position == 'zero': 

position = ('data', 0) 

assert len(position) == 2, "position should be 2-tuple" 

position_type, amount = position 

assert position_type in ('axes', 'outward', 'data') 

if position_type == 'outward': 

if amount == 0: 

# short circuit commonest case 

self._spine_transform = ('identity', 

mtransforms.IdentityTransform()) 

elif self.spine_type in ['left', 'right', 'top', 'bottom']: 

offset_vec = {'left': (-1, 0), 

'right': (1, 0), 

'bottom': (0, -1), 

'top': (0, 1), 

}[self.spine_type] 

# calculate x and y offset in dots 

offset_x = amount * offset_vec[0] / 72.0 

offset_y = amount * offset_vec[1] / 72.0 

self._spine_transform = ('post', 

mtransforms.ScaledTranslation( 

offset_x, 

offset_y, 

self.figure.dpi_scale_trans)) 

else: 

warnings.warn('unknown spine type "%s": no spine ' 

'offset performed' % self.spine_type) 

self._spine_transform = ('identity', 

mtransforms.IdentityTransform()) 

elif position_type == 'axes': 

if self.spine_type in ('left', 'right'): 

self._spine_transform = ('pre', 

mtransforms.Affine2D.from_values( 

# keep y unchanged, fix x at 

# amount 

0, 0, 0, 1, amount, 0)) 

elif self.spine_type in ('bottom', 'top'): 

self._spine_transform = ('pre', 

mtransforms.Affine2D.from_values( 

# keep x unchanged, fix y at 

# amount 

1, 0, 0, 0, 0, amount)) 

else: 

warnings.warn('unknown spine type "%s": no spine ' 

'offset performed' % self.spine_type) 

self._spine_transform = ('identity', 

mtransforms.IdentityTransform()) 

elif position_type == 'data': 

if self.spine_type in ('right', 'top'): 

# The right and top spines have a default position of 1 in 

# axes coordinates. When specifying the position in data 

# coordinates, we need to calculate the position relative to 0. 

amount -= 1 

if self.spine_type in ('left', 'right'): 

self._spine_transform = ('data', 

mtransforms.Affine2D().translate( 

amount, 0)) 

elif self.spine_type in ('bottom', 'top'): 

self._spine_transform = ('data', 

mtransforms.Affine2D().translate( 

0, amount)) 

else: 

warnings.warn('unknown spine type "%s": no spine ' 

'offset performed' % self.spine_type) 

self._spine_transform = ('identity', 

mtransforms.IdentityTransform()) 

 

def set_position(self, position): 

"""set the position of the spine 

 

Spine position is specified by a 2 tuple of (position type, 

amount). The position types are: 

 

* 'outward' : place the spine out from the data area by the 

specified number of points. (Negative values specify placing the 

spine inward.) 

 

* 'axes' : place the spine at the specified Axes coordinate (from 

0.0-1.0). 

 

* 'data' : place the spine at the specified data coordinate. 

 

Additionally, shorthand notations define a special positions: 

 

* 'center' -> ('axes',0.5) 

* 'zero' -> ('data', 0.0) 

 

""" 

if position in ('center', 'zero'): 

# special positions 

pass 

else: 

if len(position) != 2: 

raise ValueError("position should be 'center' or 2-tuple") 

if position[0] not in ['outward', 'axes', 'data']: 

raise ValueError("position[0] should be one of 'outward', " 

"'axes', or 'data' ") 

self._position = position 

self._calc_offset_transform() 

 

self.set_transform(self.get_spine_transform()) 

 

if self.axis is not None: 

self.axis.reset_ticks() 

self.stale = True 

 

def get_position(self): 

"""get the spine position""" 

self._ensure_position_is_set() 

return self._position 

 

def get_spine_transform(self): 

"""get the spine transform""" 

self._ensure_position_is_set() 

what, how = self._spine_transform 

 

if what == 'data': 

# special case data based spine locations 

data_xform = self.axes.transScale + \ 

(how + self.axes.transLimits + self.axes.transAxes) 

if self.spine_type in ['left', 'right']: 

result = mtransforms.blended_transform_factory( 

data_xform, self.axes.transData) 

elif self.spine_type in ['top', 'bottom']: 

result = mtransforms.blended_transform_factory( 

self.axes.transData, data_xform) 

else: 

raise ValueError('unknown spine spine_type: %s' % 

self.spine_type) 

return result 

 

if self.spine_type in ['left', 'right']: 

base_transform = self.axes.get_yaxis_transform(which='grid') 

elif self.spine_type in ['top', 'bottom']: 

base_transform = self.axes.get_xaxis_transform(which='grid') 

else: 

raise ValueError('unknown spine spine_type: %s' % 

self.spine_type) 

 

if what == 'identity': 

return base_transform 

elif what == 'post': 

return base_transform + how 

elif what == 'pre': 

return how + base_transform 

else: 

raise ValueError("unknown spine_transform type: %s" % what) 

 

def set_bounds(self, low, high): 

"""Set the bounds of the spine.""" 

if self.spine_type == 'circle': 

raise ValueError( 

'set_bounds() method incompatible with circular spines') 

self._bounds = (low, high) 

self.stale = True 

 

def get_bounds(self): 

"""Get the bounds of the spine.""" 

return self._bounds 

 

@classmethod 

def linear_spine(cls, axes, spine_type, **kwargs): 

""" 

(staticmethod) Returns a linear :class:`Spine`. 

""" 

# all values of 0.999 get replaced upon call to set_bounds() 

if spine_type == 'left': 

path = mpath.Path([(0.0, 0.999), (0.0, 0.999)]) 

elif spine_type == 'right': 

path = mpath.Path([(1.0, 0.999), (1.0, 0.999)]) 

elif spine_type == 'bottom': 

path = mpath.Path([(0.999, 0.0), (0.999, 0.0)]) 

elif spine_type == 'top': 

path = mpath.Path([(0.999, 1.0), (0.999, 1.0)]) 

else: 

raise ValueError('unable to make path for spine "%s"' % spine_type) 

result = cls(axes, spine_type, path, **kwargs) 

result.set_visible(rcParams['axes.spines.{0}'.format(spine_type)]) 

 

return result 

 

@classmethod 

def arc_spine(cls, axes, spine_type, center, radius, theta1, theta2, 

**kwargs): 

""" 

(classmethod) Returns an arc :class:`Spine`. 

""" 

path = mpath.Path.arc(theta1, theta2) 

result = cls(axes, spine_type, path, **kwargs) 

result.set_patch_arc(center, radius, theta1, theta2) 

return result 

 

@classmethod 

def circular_spine(cls, axes, center, radius, **kwargs): 

""" 

(staticmethod) Returns a circular :class:`Spine`. 

""" 

path = mpath.Path.unit_circle() 

spine_type = 'circle' 

result = cls(axes, spine_type, path, **kwargs) 

result.set_patch_circle(center, radius) 

return result 

 

def set_color(self, c): 

""" 

Set the edgecolor. 

 

Parameters 

---------- 

c : color or sequence of rgba tuples 

 

.. seealso:: 

 

:meth:`set_facecolor`, :meth:`set_edgecolor` 

For setting the edge or face color individually. 

""" 

# The facecolor of a spine is always 'none' by default -- let 

# the user change it manually if desired. 

self.set_edgecolor(c) 

self.stale = True